Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
2 tasks done
martip opened this issue Jun 28, 2023 · 3 comments
Assignees
Labels
bug Something isn't working DataStore p2 transferred Issue has been transferred from another Amplify repository

Comments

@martip
Copy link

martip commented Jun 28, 2023

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

  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:

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:

  • I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
  • I have removed any sensitive information from my code snippets and submission.
@martip martip added the pending-triage Issues that need further discussion to determine label Jun 28, 2023
@josefaidt
Copy link
Contributor

Hey @martip 👋 thanks for raising this! I'm going to transfer this over to our API repo for better assistance with DataStore 🙂

@josefaidt josefaidt transferred this issue from aws-amplify/amplify-cli Jun 29, 2023
@josefaidt josefaidt added the transferred Issue has been transferred from another Amplify repository label Jun 29, 2023
@chrisbonifacio
Copy link

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.

@chrisbonifacio chrisbonifacio added pending-response and removed pending-triage Issues that need further discussion to determine labels Jul 3, 2023
@martip
Copy link
Author

martip commented Jul 4, 2023

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?

@alharris-at alharris-at transferred this issue from aws-amplify/amplify-category-api Jul 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working DataStore p2 transferred Issue has been transferred from another Amplify repository
Projects
None yet
Development

No branches or pull requests

7 participants