Amplify-js: Many-to-Many returns null when using Amplify GraphQL client but not in Appsync Console's query tool.

Created on 5 Dec 2019  路  14Comments  路  Source: aws-amplify/amplify-js

Which Category is your question related to?
GraphQL API, specifically using Amplify GraphQL client.

What AWS Services are you utilizing?
Cognito, AWS AppSync, DynamoDB

Provide additional details e.g. code snippets

{
 "expo": "^35.0.0",
 "@aws-amplify/api": "^1.2.4",
 "@aws-amplify/auth": "^1.5.0",
 "@aws-amplify/core": "^1.2.4"
}

I'm getting different results with the same user credentials when I run a query in Appsync console and when I try the exact query it on my client side. The issue seems to be with the many-to-many connection. I'm not sure if this is a bug or I'm not correctly settings things up.

I've got a react native expo project and when run a custom getUser query in appsync console I get the correct results back no issues. The user has 4 jobs assigned to it, however when I run the following code in my app, I get and error but the error shows 4 null results for the job. The number of nulls seems to match the jobs assigned to the user.

type User
  @model
  @key(fields: ["tenantId", "userId"]) {
  tenantId: String!
  userId: ID!
  first_name: String!
  last_name: String!
  phone: AWSPhone
  jobs: [Assignment] @connection(name: "UserJobs", keyField: "userId")
}

type Job
  @model
  @key(fields: ["tenantId", "jobId"]) {
  tenantId: String!
  jobId: ID!
  identifier: String!
  note: String
  assignedTo: [Assignment] @connection(name: "JobUsers", keyField: "jobId")
}

# many-to-many connection
type Assignment @model(queries: null) @key(fields: ["tenantId", "id"]) {
  tenantId: String!
  id: ID!
  job: Job! @connection(name: "JobUsers", keyField: "jobId")
  user: User! @connection(name: "UserJobs", keyField: "userId")
}

A simple API call on screen load to test and see what I get back:

useEffect(() => {
    const loadData = async () => {
      try {
        const data = await API.graphql(
          graphqlOperation(getUser, {
            tenantId,
            userId
          })
        );
        console.log(data);
      } catch (error) {
        console.log(error);
      }
    };
    loadData();
  }, []);

And the resulting console log (outputting the caught error):

{
  "data": {
    "getUser": {
      "tenantId": "a5e7333e-c22a-4926-9654-bf88fab55c36",
      "userId": "984f7aa4-9f30-4ae8-8322-5d7e52b9a570",
      "first_name": "John",
      "last_name": "Smith",
      "phone": "+12225556789",
      "jobs": {
        "items": [
          null,
          null,
          null,
          null,
        ],
        "nextToken": null,
      },
    },
  },
  "errors": [
    {
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'Job' within parent 'Assignment' (/getUser/jobs/items[0]/job)",
      "path": [
        "getUser",
        "jobs",
        "items",
        0,
        "job",
      ],
    },
    {
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'Job' within parent 'Assignment' (/getUser/jobs/items[1]/job)",
      "path": [
        "getUser",
        "jobs",
        "items",
        1,
        "job",
      ],
    },
    {
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'Job' within parent 'Assignment' (/getUser/jobs/items[2]/job)",
      "path": [
        "getUser",
        "jobs",
        "items",
        2,
        "job",
      ],
    },
    {
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'Job' within parent 'Assignment' (/getUser/jobs/items[3]/job)",
      "path": [
        "getUser",
        "jobs",
        "items",
        3,
        "job",
      ],
    },
  ],
}

Here's what my query looks like:

export const getUser = `query GetUser($tenantId: String!, $userId: ID!) {
    getUser(tenantId: $tenantId, userId: $userId) {
      tenantId
      userId
      first_name
      last_name
      phone
      jobs {
        items {
          job {
            jobId
            identifier
            note
          }
        }
        nextToken
      }
    }
  }
`;
DataStore question

Most helpful comment

@amirmishani Not at the moment. We have a Feature request open #4652 and is part of our roadmap for Datastore. I would suggest you track the updates in the mentioned issue :)

All 14 comments

Have you checked to see if the Job records have the correct foreign key fields set? Speaking of which, is it currently possible in Amplify to insert nested connection records (I.e. update multiple tables with a single mutation)?

@jkeys-ecg-nmsu Yes, I've checked the table and all the fields are there. gsi-UserJobs partition is userId and I've also checked the generated resolvers User.jobs.req.vtl to make sure they have the correct connectionAttribute which is also userId.

Also it looks like things have changes a bit since two weeks ago: many-to-many #91

Based on the latest docs regarding many-to-many connections it seems like I should change my schema to to the following and add two additional @key directives like so:

# many-to-many connection
type Assignment @model(queries: null) {
  @key(fields: ["tenantId", "jobId"])
  @key(name: "byJob", fields: ["jobId", "userId"])
  @key(name: "byUser", fields: ["editorID", "postID"]) {
  tenantId: ID!
  id: ID!
  jobId: ID!
  userId: ID!
  job: Job! @connection(name: "JobUsers", keyField: "jobId")
  user: User! @connection(name: "UserJobs", keyField: "userId")
}

But before I change my schema I'm still having trouble understanding why my current schema works fine in the AWS Appsync query console and not on the client side!

@jkeys-ecg-nmsu as far as I can tell, the simple answer to your question about using a single mutation via amplify is no it's not possible. Currently the docs under the many-to-many section say you have to update with multiple mutations, however if you really need to give your frontend devs a single mutation you have a few workaround options.

UPDATE: was able to confirm the issue has to do with using the Amplify GraphQL client. When I use Appsync SDK I get the expected results back. I was lead to believe that the only difference is offline capabilities however it seems like it cannot handle my setup for many-to-many connections.

@amirmishani I was led to believe this too. I'm going to ask my product owner to put unit testing currently unused API access patterns at the top of our backlog. These issues make me really nervous about relying on codegen for production websites.

@amirmishani : are you using cli to generate the queries and mutation?

@ashika01 yes I'm using the cli but in this case the getUser query is modified. The generated query does not go deep enough to get the jobs so I've added that myself.

@amirmishani I modified your schema to the following to get the many:many going.

type User @model @key(fields: ["tenantId", "userId"]) {
  tenantId: ID!
  userId: ID!
  first_name: String!
  last_name: String!
  phone: AWSPhone
  jobs: [Assignment] @connection(keyName: "byUser", fields: ["userId"])
}

type Job @model @key(fields: ["tenantId", "jobId"]) {
  tenantId: ID!
  jobId: ID!
  identifier: String!
  note: String
  assignedTo: [Assignment] @connection(keyName: "byJob", fields: ["jobId"])
}

# many-to-many connection
type Assignment
  @model(queries: null)
  @key(name: "byJob", fields: ["assignmentJobId", "assignmentUserId"])
  @key(name: "byUser", fields: ["assignmentUserId", "assignmentJobId"]) {
  id: ID!
  assignmentJobId: ID!
  assignmentUserId: ID!
  job: Job! @connection(fields: ["assignmentJobId"])
  user: User! @connection(fields: ["assignmentUserId"])
}

And I set the depth to 3 or 4. I am yet to work on this. But maybe this helps to get moving forward..

Depth in my current project has me stumped... i.e. in your case, User has a Job, which is an assignment, which is connected to a Job.

If I traversed that in my GraphQL Schema mutation call as I intend to use it... I get errors because Users would come back with Jobs with Assignments with Jobs inside. Assignments does not technically have a field matching 'Job' ... because it's a connection so it actually throw me an error if I called User, because Jobs, gets Assignment which is it's intention but also gets Jobs inside Assignment, which is also it's intention...

I believe the way the code works is either not explained well, or broken on the AWS side.

My current solution is to work everything around a --max-depth 2 ... otherwise it errors on every single mutation/query call.

@amirmishani Do you think that answered your question? Are we good to close the issue?

@michaelcuneo I can point you to our implementation in the code - https://github.com/aws-amplify/amplify-js/blob/master/packages/datastore/src/storage/adapter/indexeddb.ts

This is the implementation code for indexeddb. Its not clear on what exactly you are trying to achieve with Datastore. Do you have an issue open already for this?

@ashika01 My problem turned out to be a version difference between two amplify-cli's and I missed the change from --max-depth as a terminal command to --max-depth being included in the actual config.

@ashika01 in your setup is there any way to sort the jobs under getUser by Job.createdAt date? I don't mean date on the Assignment model but the Job model itself. Thanks.

@amirmishani Not at the moment. We have a Feature request open #4652 and is part of our roadmap for Datastore. I would suggest you track the updates in the mentioned issue :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

karlmosenbacher picture karlmosenbacher  路  3Comments

benevolentprof picture benevolentprof  路  3Comments

oste picture oste  路  3Comments

DougWoodCDS picture DougWoodCDS  路  3Comments

rygo6 picture rygo6  路  3Comments