aws-amplify / aws-amplify/amplify-codegen

Swift and JS DataStore subscriptions are inconsistent, if @belongsTo field is marked as required

Open
#626 3 comments 0 reactions 2 assignees Assigned to @manueliglesias View on GitHub
bug DataStore p2 transferred
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.