soosap
soosap

Reputation: 1445

How to tackle server-side input validation using Relay?

I have the following GraphQL operation that works:

GraphQL Mutation:

mutation CreateUserMutation ($input: CreateUserInput!) {
  createUser(input: $input) {
    clientMutationId
    userEdge {
      node {
        email
        username
      }
    },
    validationErrors {
      id
      email
    }
  }
}

GraphQL mutation response:

{
  "data": {
    "createUser": {
      "clientMutationId": "112",
      "userEdge": {
        "node": {
          "email": "[email protected]",
          "username": "soosap112"
        }
      },
      "validationErrors": {
        "id": "create-user-validation-errors",
        "email": [
          "Email looks not so good."
        ]
      }
    }
  }
}

So far so good, my GraphQL response's validationErrors is an object of key-value pairs, where the value is always an array of input validation error messages from the server w/ respect to a particular input field (i.e. {'email': ['email is taken', 'email is on blacklist']}).

Next step (and this is where I need help) - how to consume that data in the Relay client store? In other words, what to do to have the validationErrors available in my component as this.props.validationErrors?

CreateUserMutation.js

import Relay from 'react-relay';

export class CreateUserMutation extends Relay.Mutation {
  getMutation() {
    return Relay.QL`
      mutation {
        createUser
      }
    `;
  }

  getVariables() {
    return {
      email: this.props.email,
      username: this.props.username,
      password: this.props.password,
    };
  }

  getFatQuery() {
    return Relay.QL`
      fragment on CreateUserPayload @relay(pattern: true) {
        userEdge,
        validationErrors,
        viewer { userConnection }
      }
    `;
  }

  getConfigs() {
    return [
      {
        type: 'FIELDS_CHANGE',
        fieldIDs: {
          validationErrors: 'create-user-validation-errors',
        },
      },
      {
        type: 'RANGE_ADD',
        parentName: 'viewer',
        parentID: this.props.viewer.id,
        connectionName: 'userConnection',
        edgeName: 'userEdge',
        rangeBehaviors: {
          // When the ships connection is not under the influence
          // of any call, append the ship to the end of the connection
          '': 'append',
          // Prepend the ship, wherever the connection is sorted by age
          // 'orderby(newest)': 'prepend',
        },
      },
    ];
  }
}

Here is my attempt: First of all I am able to consume user edges into my Relay client store by using the getConfigs RANGE_ADD. Since my validationErrors object does not implement the connection model, the FIELDS_CHANGE seemed to be the only reasonable type in my case. I am trying to mock the dataID that Relay seems to require to populate the client store using 'create-user-validation-errors' as a unique id.

Here is a snippet from my React component to make the example complete.

class App extends React.Component {
  static propTypes = {
    limit: React.PropTypes.number,
    viewer: React.PropTypes.object,
    validationErrors: React.PropTypes.object,
  };

  static defaultProps = {
    limit: 5,
    validationErrors: {
      id: 'create-user-validation-errors',
    },
  };

  handleUserSubmit = (e) => {
    e.preventDefault();

    Relay.Store.commitUpdate(
      new CreateUserMutation({
        email: this.refs.newEmail.value,
        username: this.refs.newUsername.value,
        password: this.refs.newPassword.value,
        viewer: this.props.viewer,
      })
    );
  };

Upvotes: 3

Views: 1245

Answers (1)

Ahmad Ferdous
Ahmad Ferdous

Reputation: 3399

A mutation either succeeds or fails. I usually design the client-side mutation's fat query and configs, keeping the success case in mind. To handle failure case, the mutation's callback functions are sufficient.

How do I consume a non-connection model based piece of information from a GraphQL response in a React component using Relay?

There are two ways that I know of.

1) Using FIELDS_CHANGE type in getConfigs function of client-side mutation code: When we need to update data in Relay store in response to the mutation's result, this is usually used. The code looks like the following:

getFatQuery() {
  return Relay.QL`
    fragment on CreateUserPayload {
      ...
      ...
      viewer { 
        userCount,
      }
    }
  `;
}

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

In your case, if you want to update validationErrors as part of the Relay store, pass it as a prop and have validationErrors: this.props.validationErrors.id.

2) Using mutation's callback functions: When we just need some information that shouldn't affect the data in Relay store, this is a good option. The code looks like below:

const mutation = new CreateUserMutation({
  email: this.state.email,
  username: this.state.username,
  password: this.state.password,
  viewer: this.props.viewer,
});
const onSuccess = (response) => {
  // If you have both successful data update and some other extra info, you
  // can have, say `response.warnings`. The server sends `warnings` in the
  // response object.
};
const onFailure = (transaction) => {
  // This is the most appropriate place for dealing with errors.
  var error = transaction.getError();
  // Get the errors!
};
Relay.Store.commitUpdate(mutation, {onSuccess, onFailure});

Do you know of a better way of doing server-side input validation using GraphQL and Relay?

The elementary input validation supported by GraphQL and Relay are currently limited to using GraphQLNonNull and the intended types, for example, names: new GraphQLNonNull(GraphQLString). So, if we pass an integer for name, the mutation will fail and appropriate error message will be provided.

For other types of validation, mutateAndGetPayload function in server-side mutation is a good place, where we can apply our own logic.

Konstantin Tarkus has an excellent article on input validation in GraphQL mutations.

Upvotes: 2

Related Questions