1
votes

I have 2 models, company & user. From a db perspective a company has many users. When creating a single user, I want to take advantage of the power of graphQL by returning the company associated with the user. However, this only works when doing a query. When attempting a mutation, the object is mutated, but the requested relational data always returns null

In the Model we declare a one -> many relationship and include the company model schema in our user model schema to access the data

User Model Schema

  type User {
    clients: [Client!]
    company: Company         <------- Company Relation
    companyId: UUID
    confirmed: Boolean
    defaultPortfolioSize: Int
    email: String!
    firstName: String!
    lastLogin: String!
    lastName: String!
    id: UUID!
    isActive: Boolean
    isStaff: Boolean
    isSuperuser: Boolean
    password: String
    phoneNumber: String
    priceNotification: Boolean
    priceThreshold: Float
    sentimentNotification: Boolean
    sentimentThreshold: Float
    token: String

    clientCount: Int
    notificationCount: Int
    portfolioCount: Int
    stockAverageCount: Float
    totalValue: Float
    stockList: [PortfolioStock!]
  }

In the user mutation, we pass a company id which we use to connect the user to the associated company object

User Mutation

    user(
      companyId: UUID               <---- Company ID for relation
      confirmed: Boolean
      defaultPortfolioSize: Int
      delete: Boolean
      email: String
      firstName: String
      lastName: String
      id: UUID
      isActive: Boolean
      isStaff: Boolean
      isSuperuser: Boolean
      password: String
      phoneNumber: String
      priceNotification: Boolean
      priceThreshold: Float
      sentimentNotification: Boolean
      sentimentThreshold: Float
      username: String
    ): User!

The resolver is pretty straightforward. We verify authorization and then continue the request.

User Mutation Resolver

  user: async (_, params, { user }) => {
    if (params.id) {
      await authorize(user, Permission.MODIFY_USER, { userId: params.id });
    } else {
      // Anyone can register
    }

    return await userDataLoader.upsertUser(user, params);
  },

The dataloader is where the magic happens. We call upsertUser to create, update, and delete any object. Here we successfully create a user and can verify in the db the creation.

User Dataloader

upsertUser: async (user, params) => {
    ...

    /* Register  */

    if (!params.companyId) {
      throw new UserInputError("Missing 'companyId' parameter");
    }

    if (!params.password) {
      throw new UserInputError("Missing 'password' parameter");
    }

    let newUser = new User({
      billingAddressId: 0,
      dateJoined: new Date(),
      defaultPortfolioSize: 45,
      isActive: true,
      isStaff: false,
      isSuperuser: false,
      lastLogin: new Date(),
      phoneNumber: '',
      priceNotification: false,
      priceThreshold: 0,
      sentimentNotification: false,
      sentimentThreshold: 0,
      subscriptionStatus: false,
      ...params,
    });

    newUser = await newUser.save();
    newUser.token = getJWT(newUser.email, newUser.id);

    EmailManager(
      EmailTemplate.CONFIRM_ACCOUNT,
      `${config.emailBaseUrl}authentication/account-confirmation/?key=${
        newUser.token
      }`,
      newUser.email
    );

    return newUser;
  },

  // Including the users query dataloader for reference
  users: async params => {
    return await User.findAll(get({ ...defaultParams(), ...params }));
  },

Here is an example mutation where we create a user object and request a response with the nested company relation.

Example Mutation

mutation {
  user(
    companyId: "16a94e71-d023-4332-8263-3feacf1ad4dc",
    firstName: "Test"
    lastName: "User"
    email: "[email protected]"
    password: "PleaseWork"
  ) {
    id
    company {
      id
      name
    }
    email
    firstName
    lastName
  }
}

However, when requesting the relation to be included in the response object, the api returns null rather than the object.

Example Response

ACTUAL:
{
  "data": {
    "user": {
      "id": "16a94e71-d023-4332-8263-3feacf1ad4dc",
      "company": null,
      "email": "[email protected]",
      "firstName": "Test",
      "lastName": "User"
    }
  }
}

EXPECTED:
{
  "data": {
    "user": {
      "id": "16a94e71-d023-4332-8263-3feacf1ad4dc",
      "company": {
        "id": "16a94e71-d023-4332-8263-3feacf1ad4dc",
        "name": "Test Company",
      },
      "email": "[email protected]",
      "firstName": "Test",
      "lastName": "User"
    }
  }
}

I suppose I am slightly confused as to why graphQL cannot graph my nested object during a mutation, but can do so via a query.

1
Daniel Reardan will probably answer this with this link to his explanation: stackoverflow.com/questions/56319137/…. I've learned the GraphQL message are very general and the problem could be one of many issues. Best to start with Daniel's solution.Preston
@Preston thanks for your response I will check it outwnamen
You say you successfully retrieve the company info in case of a query. How do you retrieve the company data for a given user? If you're doing it with a standard DB join, then it's possible that your newUser data is incomplete and doesn't hold the company information. Ideally, you'd want to have a specific resolver for the company field on the User type and implement your association that way ; this would guarantee that the resolver would always run whenever you return a type User (if the company field is queried, of course).Thomas Hennes

1 Answers

0
votes

The problem was with Sequelize. Since mutations to a table are not shared with their associations, the mutated object does not contain said associations like a typical query may have. Therefore, any association requested from the mutated object will return null because that object does not exist directly in the model.

That being said, there were a few ways to supplement this issue...

  1. Creation - When inserting a new row you can specifically include the associations with either of Sequelize's create or build methods. Like so:
let client = new Client(
      {
        ...params
      },
      { include: [ClientGroup] }
    );

return client.save()

Using the options parameter in the method, we can pass the include argument with the associated models. This will return with the associations.

  1. Updating - This one is a little trickier because, again, the associations are not within the model that's being mutated. So the object that is returned will not contain those associations. Moreover, Sequelize's update method does not provide the option to include the model associations like when we first created object. Here is a quick solution:
await Client.update(params, {
          // @ts-ignore: Unknown property 'plain'
          plain: true,
          returning: true,
          where: { id: params.id },
        });

        return await Client.findOne({
          include: [ClientGroup],
          where: { id: params.id },
        });

First, we use the update method to mutate the object. Once updated, we use the findOne method to fetch the mutated object with the associations.

Though this solves the problem, there are definitely other ways to go about this. Especially, if you you want to mutate those associations through this model directly. If that is the case, I recommend checking out Sequelize's transactions.