2
votes

I'm diving into GraphQL and Relay. So far, everything has been relatively smooth and easy, for me, to comprehend. I've got a GraphQL schema going with Accounts and Teams. There is no relationships between the two, yet. I've got some Relay-specific GraphQL adjustments for connections, for both the accounts and teams. Here's an example query for those two connections ...

{
  viewer {
    teams {
      edges {
        node {
          id,
          name
        }
      }
    }
    accounts {
      edges {
        node {
          id,
          username
        }
      }
    }
  }
}

I've got a GraphQL mutation ready to go that creates a new account. Here's the GraphQL representation of that ...

type Mutation {
  newAccount(input: NewAccountInput!): NewAccountPayload
}

input NewAccountInput {
  username: String!
  password: String!
  clientMutationId: String!
}

type NewAccountPayload {
  account: Account
  clientMutationId: String!
}

type Account implements Node {
  id: ID!
  username: String!
  date_created: String!
}

I'm now trying to create my client-side Relay mutation that uses this GraphQL mutation. I'm thoroughly confused as to how to do this correctly, though. I've followed examples and nothing I come up with even seems to run correctly. I tend to get errors relating to fragment composition.

If I were writing a Relay mutation that uses this GraphQL mutation, what would the appropriate mutator configuration be? Should I be using RANGE_ADD?

2

2 Answers

6
votes

For your client side mutation, you can use something like this:

export default class AddAccountMutation extends Relay.Mutation {
   static fragments = {
      viewer: () => Relay.QL`
         fragment on Viewer {
            id,
         }
       `,
   };

   getMutation() {
      return Relay.QL`mutation{addAccount}`;
   }

   getVariables() {
      return {
         newAccount: this.props.newAccount,
      };
   }

   getFatQuery() {
      return Relay.QL`
         fragment on AddAccountPayload {
            accountEdge,
            viewer {
               accounts,
            },
         }
       `;
    }

   getConfigs() {
      return [{
         type: 'RANGE_ADD',
         parentName: 'viewer',
         parentID: this.props.viewer.id,
         connectionName: 'accounts',
         edgeName: 'accountEdge',
         rangeBehaviors: {
            '': 'append',
         },
      }];
   }

   getOptimisticResponse() {
      return {
         accountEdge: {
            node: {
               userName: this.props.newAccount.userName,
            },
         },
         viewer: {
            id: this.props.viewer.id,
         },
      };
   }
}

Then, in your GraphQL schema, you'll need to return the newly created edge, as well as the cursor:

var GraphQLAddAccountMutation = mutationWithClientMutationId({
   name: 'AddAccount',
   inputFields: {
      newAccount: { type: new GraphQLNonNull(NewAccountInput) } 
   },
   outputFields: {
      accountEdge: {
         type: GraphQLAccountEdge,
         resolve: async ({localAccountId}) => {
            var account = await getAccountById(localAccountId);
            var accounts = await getAccounts();
            return {
              cursor: cursorForObjectInConnection(accounts, account)
              node: account,
            };
         }
      },
      viewer: {
        type: GraphQLViewer,
        resolve: () => getViewer()
      },
   },
   mutateAndGetPayload: async ({ newAccount }) => {
      var localAccountId = await createAccount(newAccount);
      return {localAccountId};
   }
});

var {
   connectionType: AccountsConnection,
   edgeType: GraphQLAccountEdge,
} = connectionDefinitions({
   name: 'Account',
   nodeType: Account,
});

You'll need to substitute the getAccounts(), getAccountById() and createAccount method calls to whatever your server/back-end uses.

There may be a better way to calculate the cursor without having to do multiple server trips, but keep in mind the Relay helper cursorForObjectInConnection does not do any kind of deep comparison of objects, so in case you need to find the account by an id in the list, you may need to do a custom comparison:

function getCursor(dataList, item) {
   for (const i of dataList) {
      if (i._id.toString() === item._id.toString()) {
         let cursor = cursorForObjectInConnection(dataList, i);
         return cursor;
      } 
   }
}

Finally, add the GraphQL mutation as 'addAccount' to your schema mutation fields, which is referenced by the client side mutation.

4
votes

Right now, I'm following a roughly 5 step process to define mutations:

  1. Define the input variables based on what portion of the graph you are targeting - in your case, it's a new account, so you just need the new data
  2. Name the mutation based on #1 - for you, that's AddAccountMutation
  3. Define the fat query based on what is affected by the mutation - for you, it's just the accounts connection on viewer, but in the future I'm sure your schema will become more complex
  4. Define the mutation config based on how you can intersect the fat query with your local graph
  5. Define the mutation fragments you need to satisfy the requirements of #1, #3 and #4

Generally speaking, step #4 is the one people find the most confusing. That's because it's confusing. It's hard to summarize in a Stack Overflow answer why I feel this is good advice but... I recommend you use FIELDS_CHANGE for all your mutations*. It's relatively easy to explain and reason about - just tell Relay how to look up the nodes corresponding to the top level fields in your mutation payload. Relay will then use the mutation config to build a "tracked query" representing everything you've requested so far, and intersect that with the "fat query" representing everything that could change. In your case, you want the intersected query to be something like viewer { accounts(first: 10) { edges { nodes { ... } } }, so that means you're going to want to make sure you've requested the existing accounts somewhere already. But you almost certainly have, and if you haven't... maybe you don't actually need to make any changes locally for this mutation!

Make sense?

EDIT: For clarity, here's what I mean for the fat query & configs.

getConfigs() {
  return [
  {
    type: "FIELDS_CHANGE",
    fieldIDs: {
      viewer: this.props.viewer.id
    }
  }]
}

getFatQuery() {
  return Relay.QL`
    fragment on AddAccountMutation {
      viewer {
        accounts
      }
    }
  `
}

*addendum: I currently believe there are only one or two reasons not to use FIELDS_CHANGE. The first is that you can't reliably say what fields are changing, so you want to just manipulate the graph directly. The second is because you decide you need the query performance optimizations afforded by the more specific variants of FIELDS_CHANGE like NODE_DELETE, RANGE_ADD, etc.