Adding a custom GraphQL mutation to Parse-Server

I am having success using the built-in schema for basic CRUD tasks on individual classes. Thank you for adding this remarkable graphql feature set to parse-server!

I have been reading here and here about creating custom schema to use with my app. I’ve also had a look at Moumouls’ next-atomic-gql-server repo. Unfortunately, I’m not gleaning enough from these resources to move forward on my own so I’m looking for some help.

I have these browser classes on my parse-server:

  1. User - has typical fields plus a cart field having a ‘relation’ with Item class.
  2. Item - has typical fields (title, description, price, …) plus user field having a pointer to User class.
  3. CartItem - has similar field to Item class plus user field having a pointer to User class and an item field having a pointer to Item class.

I would like to create an addToCart mutation:

# from schema.graphql
type Mutation {
  addToCart(id: ID!): CartItem
}
# in the client
mutation ADD_TO_CART_MUTATION ($id: ID!) {
  addToCart(id: $id) {
    cartItem {
      id
      quantity
    }
  }
}

As an example, I have a mutation resolver example from GraphQL Yoga (using express.js) that I’d like to emulate:

async addToCart(parent, args, ctx, info) {
  // 1. query the user's current cart
  const [existingCartItem] = await ctx.db.query.cartItems({
    where: {
      user: { id: userId },
      item: { id: args.id },
    },
  })
  // 2. check if that item is already in their cart and increment by one if it is
  if (existingCartItem) {
    return ctx.db.mutation.updateCartItem({
      where: { id: existingCartItem.id },
      data: { quantity: existingCartItem.quantity + 1 },
    }, info);
  }
  // 3. if it's not, create a fresh cartItem for that user
  return ctx.db.mutation.createCartItem({
    data: {
      item: {
        connect: { id: args.id },
      },
      user: {
        connect: { id: userId },
      },
    },
  }, info);
},

How is a custom mutation like addToCart, that involves first calling another query, then doing a conditional check before branching to one of two other mutations, created in parse-server?

Hi @185driver,

First of all you need to create a new GraphQL schema that will be merged with the auto generated one.

example here (from ParseGraphQLServer.spec.js):

          parseGraphQLServer = new ParseGraphQLServer(parseServer, {
            graphQLPath: '/graphql',
            graphQLCustomTypeDefs: new GraphQLSchema({
              query: new GraphQLObjectType({
                name: 'Query',
                fields: {
                  customQuery: {
                    type: new GraphQLNonNull(GraphQLString),
                    args: {
                      message: { type: new GraphQLNonNull(GraphQLString) },
                    },
                    resolve: (p, { message }) => message,
                  },
                  customQueryWithAutoTypeReturn: {
                    type: SomeClassType,
                    args: {
                      id: { type: new GraphQLNonNull(GraphQLString) },
                    },
                    resolve: async (p, { id }) => {
                      const obj = new Parse.Object('SomeClass');
                      obj.id = id;
                      await obj.fetch();
                      return obj.toJSON();
                    },
                  },
                },
              }),
              types: [
                new GraphQLInputObjectType({
                  name: 'CreateSomeClassFieldsInput',
                  fields: {
                    type: { type: TypeEnum },
                  },
                }),
                new GraphQLInputObjectType({
                  name: 'UpdateSomeClassFieldsInput',
                  fields: {
                    type: { type: TypeEnum },
                  },
                }),
                SomeClassType,
              ],
            }),
          });

        parseGraphQLServer.applyGraphQL(expressApp);

Then in your resolver feel free to query data as you want, an internal graphql client, Parse REST API or Parse JS SDK. To allow Parse to resolve correctly your auto generated type (CartItem) at the end of your custom resolver just ensure to foward all needed data to child resolvers.

On Parse JS SDK Query you can use: query.includeAll()
On Parse JS SDK Object you can use: object.fetchWithInclude('aPointerField','anotherPointerField')

Here the code example that you probably search:

                  customQueryWithAutoTypeReturn: {
                    type: SomeClassType,
                    args: {
                      id: { type: new GraphQLNonNull(GraphQLString) },
                    },
                    resolve: async (p, { id }) => {
                      const obj = new Parse.Object('SomeClass');
                      obj.id = id;
                      await obj.fetch() // or fetchWithInclude() if your object have some pointers;
                      return obj.toJSON();
                    },
                  },

Note: You can do this with Next Atomic GQL Server, you will have the benefit of pre Created GraphQL schema with auto migration, a ready to run Parse Server built on TS, and Nexus GraphQL schema system (more easier to maintain than a graphql js schema). :slight_smile:

Hi @Moumouls,

Thank you for taking the time to respond. I wish I understood how to use your code snippet more fully, but alas, I do not. As for your Next Atomic GQL Server repo, it has been quite helpful to me and the code is both well done and gorgeously formatted. Very professional!

Upon further research, I did come across the solution which I think is a better fit for my coding level and I’d like to go with it.

It keeps the client side graphql mutation and the mutation schema the same, but it uses Cloud code for the resolver. MyaddToCart Cloud function mimics my example resolver above almost perfectly so I am pretty happy with it.

Parse.Cloud.define('addToCart', async (req) => {
  const { user, params: { id: itemId } } = req;

  // 1. make sure they are signed in
  if (!user) {
    throw new Error('You must be signed in.');
  }

  // 2. query the user's current cart
  const userQuery = new Parse.Query(user);
  const itemQuery = new Parse.Query('Item');
  itemQuery.equalTo('id', itemId);
  const cartItemQuery = new Parse.Query('CartItem');
  cartItemQuery.matchesQuery('item', itemQuery);
  cartItemQuery.matchesQuery('user', userQuery);
  const [existingCartItem] = await cartItemQuery.find();

  // 3. check if that item is already in their cart and, if so, increment by one
  if (existingCartItem) {
    const quantity = await existingCartItem.get('quantity');
    return existingCartItem.save({ quantity: quantity + 1 });
  }

  // 4. if it's not, create a fresh cartItem for that user!
  const CartItem = Parse.Object.extend('CartItem');
  const cartItem = new CartItem();
  const item = await itemQuery.get(itemId);
  const savedCartItem = await cartItem.save({ quantity: 1, item, user });
  const relation = user.relation('cart');
  relation.add(savedCartItem);
  // Throws a "Cannot modify user" error unless the masterKey is used.
  await user.save(null, { useMasterKey: true });
  return savedCartItem;
});

Unfortunately, it’s still not quite right. The one problem I haven’t been able to solve is that the Cloud code is not handling an id property. If I use the id prop, the resolver throws Error: {"message":"Object not found.","code":101}.

So, if I call this mutation:

export const ADD_TO_CART_MUTATION = gql`
  mutation ADD_TO_CART_MUTATION ($id: ID!) {
    addToCart(id: $id) {
      id
      quantity
    }
  }
`;

The id variable is passed to the Cloud function as params":{"id":"SXRlbTpEbDVjZmFWclRI"}, but that id doesn’t exist in my Item class. Instead, there is an objectId key of Dl5cfaVrTH.

I thought that Parse now handled ids and objectIds the same, but apparently not yet in this area?

If I could get past this objectId vs id issue, I think I would be on my way. Any help would be most appreciated. Thanks.

Hi @185driver, you are right, Parse GraphQL API follow the relay spec where id is a base64 of the class:ObjectId, IMPORTANT: id is not the objectId

So here you have 2 solutions:

  • Decode the id in the cloud function (it’s just a base64 concatened with the classname)
  • Or in the front app, in your GraphQL queries, just select the objectID and the Id too, and send the objectId to your mutation !

Then you should be right !

Copy that. Thanks for that good info. It looks like I still have one issue remaining.

Going with option #1, I decoded my id variable and it works:

  const binary = Buffer.from(id, 'base64').toString();
  const itemId = binary.split(':')[1];

But it appears I now have a similar problem in reverse when I return the savedCartItem to the client. The mutation should return the cartItem id and quantity props, but only quantity is returning correctly. id throws an error:

"message": "Cannot return null for non-nullable field CartItem.id."

If I return the objectId instead, then it works.

{
  "data": {
    "addToCart": {
      "objectId": "3jFvY7Sgda",
      "quantity": 1
    }
  }
}

I tried encoding the objectId to base64 and injecting it into savedCartItem as an id prop, but it fails the same way (it does correctly encode the objectId value, however).

const concattedObjectId = `CartItem:${cartItemObjectId}`;
const cartItemId = Buffer.from(concattedObjectId).toString('base64');
Object.assign(savedCartItem, { id: cartItemId });

How can I return a base64 id prop to my client mutation when only objectId seems to exist? Many thanks.

Add the end of your cloud function can you just make attempt the following code to ensure that sub resolvers avec access to all fields correctly.
Note: Sub resolvers are designed to deal with a REST response of Parse API. The following should works correctly for you !

// Ensure to return a full object, use withInclude if the object have some pointer
await savedCartItem.fetch({sessionToken: TheUserSessionTokenFromAuth})
// Needed since parse graphql api do not use the JS SDK, but REST like responses
return savedCartItem.toJSON()

This should work !

Wonderful! Thank you. I had tried many things, but returning the JSON representation of the object was not something I thought could be done. Just for reference, here’s the final section of my Cloud code resolver now (as included above), starting where I save the user relation:

await user.save(null, { useMasterKey: true });

// Encodes the CartItem objectId to a base64 id string for graphql use.
const cartItemObjectId = savedCartItem.id;
const concattedObjectId = `CartItem:${cartItemObjectId}`;
const cartItemId = Buffer.from(concattedObjectId).toString('base64');

const jsonCartItem = savedCartItem.toJSON();
Object.assign(jsonCartItem, { id: cartItemId });
return jsonCartItem;

I ended up not using your suggestion to fetch the object before returning it, but I had somewhat of an opposite experience. If I fetch, I didn’t get the full object, as the item and user props were truncated somewhat (for example):

{ __type: 'Pointer', className: '_User', objectId: 'zFruc41tmW' }

Whereas, using const jsonCartItem = savedCartItem.toJSON() provided the full object for me, including nested objects. I don’t need them here, but it’s nice to know they are available.

Thanks again for all your help with this! Such good info.

@185driver
Since you have been confronted with an interesting use case, would you be interested in sending a PR on the GraphQL doc so that you can help other developers with your feedback on custom resolvers?

Repo here : https://github.com/parse-community/docs

I had been thinking about that too. I’ll see what I can do. Thanks for reaching out.