amplify-codegen
amplify-codegen copied to clipboard
Swift and JS DataStore subscriptions are inconsistent, if @belongsTo field is marked as required
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:
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)
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)
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)
var messagesSubscription: AmplifyAsyncThrowingSequence<MutationEvent>?
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)
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
- Create a simple project, with the provided schema (Channel, Message)
- Check that the flag
generateModelsForLazyLoadAndCustomSelectionSetis set totrueincli.json - Create two clients (Swift and JavaScript), enabling the DataStore on both
- Configure the two clients to observe the Message type
- Save a new message to the DataStore from the Swift client
- Verify that the Swift client receives the mutation, while the JavaScript client doesn't
- Save a new message to the DataStore from the JavaScript client
- 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:
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.
Hey @martip :wave: thanks for raising this! I'm going to transfer this over to our API repo for better assistance with DataStore 🙂
Hi @martip can you share the graphql string for the mutation from Swift and the subscription from JavaScript?
It's possible that the channelMessagesId field is missing from the Swift selection set and the JavaScript client is including it in the subscription, causing this error.
Hi @chrisbonifacio, thank you for taking time to look into this.
I'm pretty sure, as you are, that the problem is due to field differences between the selection sets and the subscriptions...
but I didn't write any GraphQL!
Maybe I'm missing your point, but I raised the issue exactly for this reason: the differences are somewhere in the GraphQL, generated at runtime by the DataStore libraries.
I'm not aware of a simple way to inspect the exact GraphQL that gets created by the following functions (if you know one, please let me know!):
// Swift
let savedMessage = try await Amplify.DataStore.save(message) // <- this creates the GraphQL mutation
let subscription = Amplify.DataStore.observe(Message.self) // <- this creates the GraphQL subscription
// JavaScript
const savedMessage = await DataStore.save(message); // <- this creates the GraphQL mutation
const subscription = DataStore.observe(Message).subscribe((msg) => { ... }); // <- this creates the GraphQL subscription
Am I missing something?