Saving nested models in Android may raise GraphQL error for iOS devices that subscribe to the same models
This issue is caused by that platform libraries generating incompatible selection set making mutation requests. In most cases, especially when nested models are involved, iOS platform cannot receive subscription updates from mutations made in other platforms, such as amplify-js (Amplify Studio), amplify-android.
Describe the bug Saving nested models in Android may raise GraphQL error for iOS devices that subscribe to the same models. It happens when saving nested models in Android, and receiving subscription message in iOS.
Expand for details
This is due to Android doesn't populate nested models by default, when saving object, service side GraphQL validation may fail due to missing fields in the request sent from Android. The error may be emitted to all model subscriptions in different devices.
Two possible causes:
- In Android, nested models (has-one, belongs-to) are not populated with data
- When saving an object with creating association with other models, complete objects of other models see to be required, but no associating only by
idfield
Android sent mutation that caused the error
{
"query": "mutation CreateComment($input: CreateCommentInput!) {\n createComment(input: $input) {\n _deleted\n _lastChangedAt\n _version\n content\n id\n post {\n id\n }\n }\n}\n",
"variables": {
"input": {
"id": "2a7693b3-0591-4941-901e-fb355c962f72",
"postID": "a4930282-77ea-468d-88e2-c042a8e575a5",
"content": "tetetet"
}
}
}
To Reproduce Steps to reproduce the behavior:
- Run
amplify_datastorewith API sync in 1 Android emulator and 1 iOS simulator - Save a new "Comment" in Android emulator
- Observe iOS simulator logs
- See error
2021-06-22 12:59:28.065189-0700 Runner[5863:11863431] [StarscreamAdapter] websocketDidReceiveMessage: - {"id":"13C8F47B-5693-4925-9818-9A404031CC15","type":"data","payload":{"data":{"onCreateComment":{"id":"8360f342-5515-4a49-98e1-906211efd408","content":"Test","post":null,"__typename":"Comment","_version":1,"_deleted":null,"_lastChangedAt":1624391967947}},"errors":[{"message":"Cannot return null for non-nullable type: 'Int' within parent 'Post' (/onCreateComment/post/rating)","path":["onCreateComment","post","rating"]},{"message":"Cannot return null for non-nullable type: 'String' within parent 'Post' (/onCreateComment/post/title)","path":["onCreateComment","post","title"]},{"message":"Cannot return null for non-nullable type: 'Person' within parent 'Post' (/onCreateComment/post/author)","path":["onCreateComment","post","author"]},{"message":"Cannot return null for non-nullable type: 'Int' within parent 'Post' (/onCreateComment/post/_version)","path":["onCreateComment","post","_version"]},{"message":"Cannot return null for non-nullable type: 'AWSTimestamp' within parent 'Post' (/onCreateComment/post/_lastChangedAt)","path":["onCreateComment","post","_lastChangedAt"]}]}}
Expected behavior iOS should receive correct subscription message.
Platform Amplify Flutter current supports iOS and Android. This issue is reproducible in (check all that apply): [ x ] Android [ x ] iOS
Discovered that amplify-android has a bug that the SelectionSet class never generates GraphQL document correctly, as the getModelFields for Flutter uses ModelSchemaRegistry as singleton but it actually is not a singleton so it cannot retrieve related model schema.
Selection set incompatibility.
| amplify-js | amplify-ios | amplify-android | amplify-flutter | amplify-flutter | |
|---|---|---|---|---|---|
| iOS platform | Android platform | ||||
| Create mutation selection set | createPost(input: $input) { id title rating createdAt updatedAt _version _lastChangedAt _deleted blog { id _deleted } } | createPost(input: $input) { id createdAt rating title updatedAt blog { id createdAt name updatedAt __typename _version _deleted _lastChangedAt } __typename _version _deleted _lastChangedAt } | createPost(input: $input) { _deleted _lastChangedAt _version blog { id } comments { items { id } nextToken startedAt } createdAt id rating title updatedAt } | Same as amplify-ios | Same as amplify-android |
| Update mutation selection set | updatePost(input: $input) { id title rating createdAt updatedAt _version _lastChangedAt _deleted blog { id _deleted } } | updatePost(input: $input) { id createdAt rating title updatedAt blog { id createdAt name updatedAt __typename _version _deleted _lastChangedAt } __typename _version _deleted _lastChangedAt } | updatePost(input: $input) { _deleted _lastChangedAt _version blog { id } comments { items { id } nextToken startedAt } createdAt id rating title updatedAt } | Same as amplify-ios | Same as amplify-android |
| Delete mutation selection set | deletePost(input: $input) { id title rating createdAt updatedAt _version _lastChangedAt _deleted blog { id _deleted } } | deletePost(input: $input) { id createdAt rating title updatedAt blog { id createdAt name updatedAt __typename _version _deleted _lastChangedAt } __typename _version _deleted _lastChangedAt } | deletePost(input: $input) { _deleted _lastChangedAt _version blog { id } comments { items { id } nextToken startedAt } createdAt id rating title updatedAt } | Same as amplify-ios | Same as amplify-android |
| onCreate subscription selection set | onCreatePost { id title rating createdAt updatedAt _version _lastChangedAt _deleted blog { id _deleted } } | onCreatePost { id createdAt rating title updatedAt blog { id createdAt name updatedAt __typename _version _deleted _lastChangedAt } __typename _version _deleted _lastChangedAt } | onCreatePost { _deleted _lastChangedAt _version blog { id } comments { items { id } nextToken startedAt } createdAt id rating title updatedAt } | Same as amplify-ios | Same as amplify-android |
| onUpdate subscription selection set | onUpdatePost { id title rating createdAt updatedAt _version _lastChangedAt _deleted blog { id _deleted } } | onUpdatePost { id createdAt rating title updatedAt blog { id createdAt name updatedAt __typename _version _deleted _lastChangedAt } __typename _version _deleted _lastChangedAt } | onUpdatePost { _deleted _lastChangedAt _version blog { id } comments { items { id } nextToken startedAt } createdAt id rating title updatedAt } | Same as amplify-ios | Same as amplify-android |
| onDelete subscription selection set | onDeletePost { id title rating createdAt updatedAt _version _lastChangedAt _deleted blog { id _deleted } } | onDeletePost { id createdAt rating title updatedAt blog { id createdAt name updatedAt __typename _version _deleted _lastChangedAt } __typename _version _deleted _lastChangedAt } | onDeletePost { _deleted _lastChangedAt _version blog { id } comments { items { id } nextToken startedAt } createdAt id rating title updatedAt } | Same as amplify-ios | Same as amplify-android |
any update on this? I have the same problem but only when I add record from ios , I got error in Android. but when I add record from android, ios is fine and update record ..
I am SMH here, because in my dev environment it works with no issues.
the issue was that my dev database was recreated so fields like created at and updated at was shipped in models being generated ... now issue fixed. Thank you and ignore my comment above.
Thank you for the feedback, @yazoonic!
Hey @HuiSF I have nested models for example this is my model schema:
type User @model @aws_cognito_user_pools @aws_iam @auth(rules: [{ allow: owner ,identityClaim: "sub",operations: [create,read,update]}, { allow: private, provider: iam }]){
id: ID! @primaryKey
email: String!
phone_number: String!
transactions: [Transaction] @hasMany(indexName: "TransactionsByUser", fields: ["id"])
}
type Transaction @model @aws_cognito_user_pools @aws_iam @auth(rules: [{ allow: owner, identityClaim: "sub" , operations: [create,read]}, { allow: private, provider: iam }]){
id: ID! @primaryKey
status: String!
userID: ID! @index(name: "TransactionsByUser", sortKeyFields: ["created_at"])
user: User @belongsTo(fields: ["userID"])
}
A mutation to update the User would be something like this:
mutation UpdateUser($input: UpdateUserInput!) {
updateUser(input: $input) {
id
email
phone_number
_deleted
_lastChangedAt
_version
createdAt
owner
__typename
transactions{
items{
id
status
userID
_deleted
_lastChangedAt
_version
createdAt
owner
__typename
user{
id
email
owner
phone_number
_deleted
_lastChangedAt
_version
createdAt
__typename
}
}
}
}
}
When I mutate User to trigger a subscription on Amplify-iOS I have to return the user in transactions nested field correct ?
this is an infinite loop because i have to select the user in the transactions again and again correct ? what should I do in this case and is there a solution for the correct selection set for this case ? could please give me an example on this , thanks in advance.
The mutation is created with an appsync client with boto3 python aws sdk.
@HuiSF any further updates regarding this? we will moving towards multi device(s)
Any update??? Such an important request not fixed in two years. Abandon amplify and try firebase.
Any update??? Such an important request not fixed in two years. Abandon amplify and try firebase.
This is the best thing I did to my app..
Hey folks - There is a draft PR up in Amplify-iOS (linked above) to change the selection set, which should resolve this issue. That PR is being reviewed/tested. Once it is merged and released, it will be brought into Amplify Flutter.
whats the update ? how long will take to be fixed for amplify flutter ?
We are actively working with amplify-swift maintainers to fix the underlying issue, we will update the progress here. Thank you for your patience.
Fix for this has been released with both v1 https://pub.dev/packages/amplify_datastore/versions/1.2.0-supports-only-mobile+1/changelog and v0 (0.6.14) so closing this now.