aws-amplify / aws-amplify/amplify-codegen
Swift and JS DataStore subscriptions are inconsistent, if @belongsTo field is marked as required
- Dominant language
- TypeScript
- Stars
- 59
- Forks
- 64
- PR merge metrics
- No merged PRs in 30d
Description
### How did you install the Amplify CLI?
pnpm
### If applicable, what version of Node.js are you using?
v19.2.0
### Amplify CLI Version
12.0.2
### What operating system are you using?
Mac
### Did you make any manual changes to the cloud resources managed by Amplify? Please describe the changes made.
No
### Describe the bug
I've been struggling with this for a while in a big project. So I created a small, simple project with only two models:
~~~graphql
enum ChannelStatus {
ACTIVE
INACTIVE
}
enum MessageStatus {
READ
UNREAD
}
type Channel @model {
id: ID!
name: String!
status: ChannelStatus!
messages: [Message] @hasMany
}
type Message @model {
id: ID!
title: String!
content: String
status: MessageStatus!
channelMessagesId: ID! @index(name: "byChannel")
channel: Channel @belongsTo(fields: ["channelMessagesId"])
}
~~~
I set the relationship field `channelMessagesId` explicitly, because in the big project I need it for sorting purposes.
Then I created two different small clients, **Swift** and **JavaScript**, that only use the DataStore (no API).
**_In my `cli.json` the flag `generateModelsForLazyLoadAndCustomSelectionSet` was set to `false`._**
So I had to change it to `true`, push the backend again (using `--force` because there was nothing to update) and pull it back from my clients.
I wrote two simple functions to create a message:
createMessage (Swift)
```swift
func createMessage (title: String) async {
do {
let channel = try await Amplify.DataStore.query(Channel.self, byId: CHANNEL_ID)
if (channel != nil) {
let message = Message(
title: title,
status: MessageStatus.unread,
channel: channel
)
let savedMessage = try await Amplify.DataStore.save(message)
print("SAVED MESSAGE: \(String(describing: savedMessage))")
}
} catch let error as DataStoreError {
print("Failed with error \(error)")
} catch {
print("Unexpected error \(error)")
}
}
```
createMessage (JavaScript)
```javascript
const createMessage = async (title) => {
const channel = await DataStore.query(Channel, CHANNEL_ID);
const message = new Message({
title,
status: MessageStatus.UNREAD,
channel,
});
if (channel) {
try {
const savedMessage = await DataStore.save(message);
console.log('SAVED MESSAGE: ' + JSON.stringify(savedMessage, null, 2));
} catch (error) {
console.log(error);
}
}
};
```
Finally, I set both clients in _listening mode_, by observing the Message type:
subscribeToMessages (Swift)
```swift
var messagesSubscription: AmplifyAsyncThrowingSequence?
func subscribeToMessages() async {
let subscription = Amplify.DataStore.observe(Message.self)
messagesSubscription = subscription
do {
for try await changes in subscription {
print("Subscription received mutation: \(changes)")
}
} catch {
print("Subscription received error: \(error)")
}
}
```
subscribeToMessages (JavaScript)
```javascript
let subscription;
const subscribeToMessages = () => {
subscription = DataStore.observe(Message).subscribe((msg) => {
console.log(msg.model, msg.opType, msg.element);
});
};
```
The mutation that is executed when creating a message on the Swift client generates a warning on the JavaScript client, preventing the subscription to succeed:
~~~
[WARN] 04:42.541 DataStore - Skipping incoming subscription. Messages: Cannot return null for non-nullable type: 'ID' within parent 'Message' (/onCreateMessage/channelMessagesId)
~~~
The subscription on the Swift client receives the mutation without issues:
~~~
Subscription received mutation: MutationEvent(id: ...
~~~
The message is created correctly on the backend.
Next, I tried the other way around: creating a message on the JavaScript client.
Both the JavaScript and the Swift clients receive the mutation without issues.
### Expected behavior
As I discovered, the problem lies in the schema (see below).
What I expected, though, is that the DataStore would have behaved consistently between the Amplify implementations.
### Reproduction steps
1. Create a simple project, with the provided schema (Channel, Message)
2. Check that the flag `generateModelsForLazyLoadAndCustomSelectionSet` is set to `true` in `cli.json`
3. Create two clients (Swift and JavaScript), enabling the DataStore on both
4. Configure the two clients to observe the Message type
5. Save a new message to the DataStore from the Swift client
6. Verify that the Swift client receives the mutation, while the JavaScript client doesn't
7. Save a new message to the DataStore from the JavaScript client
8. Verify that both the JavaScript and the Swift clients receive the mutation
### Project Identifier
_No response_
### Log output
_No response_
### Additional information
And now for the best part: I managed to spot the root of the problem.
**_Remember: it's crucial that the flag `generateModelsForLazyLoadAndCustomSelectionSet` is set to `true` in `cli.json`!_**
In the GraphQL schema, I switched the **required** flag (ie the exclamation mark) from `channelMessagesId` to `channel`:
~~~graphql
type Message @model {
id: ID!
title: String!
content: String
status: MessageStatus!
channelMessagesId: ID @index(name: "byChannel")
channel: Channel! @belongsTo(fields: ["channelMessagesId"])
}
~~~
and now (after pushing, pulling and rebuilding, of course) both clients behave as expected.
I created this issue to highlight that maybe the documentation should be explicit on all this.
The examples provided for the `@belongsTo` directive, with the usage of the `fields` argument (https://docs.amplify.aws/cli/graphql/data-modeling/#belongs-to-relationship), can lead to the anomaly that I described.
It took me **_a lot of time_** to figure out the solution; I hope this helps anyone facing the same problem!
### Before submitting, please confirm:
- [X] I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
- [X] I have removed any sensitive information from my code snippets and submission.
Contributor guide
Assessment
This issue has not been assessed yet.