Relay: Passing variables to getFatQuery and what about using dotObject in getConfigs? [reduce code changing on the backend with mutations]

Created on 14 Apr 2016  路  24Comments  路  Source: facebook/relay

Right now I almost finished rewrite own module, like graffiti-mongoose, which generates GraphQL types and schemas from the existing mongoose models. I didn't found any problems with querying in Relay, but found some lacks in Mutations. I suppose that same problems have clients of reindex.io (@freiksenet can you aprove this?)

1) Passing variables to query in getFatQuery method. GraphQL support such behaviour, but in Relay.mutation I didn't find such ability.

Eg. user add new note, and this change some summary in the article with $articleId.
Of course we can change payload for mutation and add article there. But in my case I want get it via viewer. May be tomorrow somebody want add another model for fetching after mutation on the client. So I try obtain some features for auto-generated schemas, and eventually write less code on the backend:

  getMutation() {
    return Relay.QL`mutation { createNote }`;
  }
  getFatQuery() {
    return Relay.QL`
      fragment on CreateNotePayload {
        viewer {
          article(id: $articleId) {   // <--- by design I want get such data on client, after adding the note 
            summary
          }
        }
        changedNoteEdge {
          node
        }
      }
    `;
  }

2) Problem with nesting in getConfigs. It will be cool if we can write fieldNames via dotNotation for nested nodes (see https://github.com/mariocasciaro/object-path or https://github.com/rhalff/dot-object). So for example above, I would like to write such config:

  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: { 
        changedNoteEdge: this.props.refer.id, 
        'viewer.article': this.props.articleId,  // <---- Wow, I point to nested object via dot
      },
    }];
  }

Also this should work with ADD_RANGE and others.

So this two things can not so dusty reduce changing code on the backends, when frontend wants more and more new data after mutations.

Most helpful comment

Closing this due new Mutation API which does not contain FatQuery and perfectly works with additional variables:

I highly recommend migrating to new mutation API. It allows writing mutation inside your React component (avoid creating Mutation files). And in many cases do not provide CONFIGS for updating data in the Relay store. No more fatigue for me with Relay Mutations. 馃帀馃帀馃帀

Of course, you may make API better by writing your own wrapper above Relay.GraphQLMutation. Eg. https://gist.github.com/nodkz/03f28aaa1b66195b517ecf06e92487bf
So code in your React component may become like this (example with an additional variable in delete method):

class Order extends Component {
  // ...

  create(formState) {
    relayStore.mutate({
      query: Relay.QL`mutation {
        orderCreate(input: $input) {
          recordId
          record {
            email
            id
          }
          orderConnection {
            ${List.getFragment('orderConnection')}
          }
        }
      }`,
      // variables: {
      //   input {
      //     record: minimize(formState.getValue()),
      //   }
      // },
      // short `variables` definition due autowrapping with `input` https://gist.github.com/nodkz/03f28aaa1b66195b517ecf06e92487bf#file-relaystore-js-L24-L32
      variables: {
        record: minimize(formState.getValue()),
      },
      onSuccess: (res) => {
        browserHistory.push({
          pathname: `/orders/${res.orderCreate.recordId}`,
        });
      },
      onError: (transaction) => {
        console.log('mutation onFailure', transaction);
      },
      onStart: () => {
        this.setState({ isSaving: true });
      },
      onEnd: () => {
        this.setState({ isSaving: false });
      },
    });
  }

  changeStatus(status, cb) {
    relayStore.mutate({
      query: Relay.QL`mutation {
        orderUpdate(input: $input) {
          record {
            status
            id
          }
        }
      }`,
      variables: {
        record: {
          _id: this.props.order._id,
          status,
        },
      },
      optimisticResponse: {
        record: {
          id: this.props.order.id,
          status: 'updating...',
        },
      },
      onSuccess: () => {
        this.setState({
          isSaved: true,
        });
        if (cb) cb();
      },
      onError: (transaction) => {
        console.log(transaction.getError());
        const e = transaction.getError();
        const errMsg = e ? e.message.toString() : null;
        this.setState({ errMsg });
      },
      onStart: () => {
        this.setState({
          isSaved: false,
          isSaving: true,
          errMsg: null,
        });
      },
      onEnd: () => {
        this.setState({
          isSaving: false,
        });
      },
    });
  }

  delete() {
    relayStore.mutate({
      query: Relay.QL`mutation {
        orderRemove(input: $input) {
          nodeId
          viewer {
            orderConnection(first: $first) {
              edges {
                node {
                  _id
                }
              }
            }
          }
        }
      }`,
      variables: {
        input: {
          _id: this.props.order._id,
        },
        first: 666,
      },
      onSuccess: () => {
        browserHistory.goLevelUp();
      },
      onError: (transaction) => {
        alert(transaction.getError());
      },
      onStart: () => {
        this.setState({ isSaving: true });
      },
      onEnd: () => {
        this.setState({ isSaving: false });
      },
    });
  }
}

All 24 comments

Also forgot write about supporting of assigning the result into a variable.

If you investigate payloads on main page of reindex.io via graphiql, you may see that all payloads contains viewer. Also it would be great if they will contain node(ID).

So with assignment to variable, I can construct such queries:

  getFatQuery() {
    return Relay.QL`
      fragment on CreateNotePayload {
        myArticle: node(id: $articleId) { 
           summary
        }
        yetAnotherArticle: node(id: $articleId2) { 
           summary
        }
        changedNoteEdge {
          node
        }
      }
    `;
  }
  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: { 
        changedNoteEdge: this.props.refer.id, 
        myArticle: this.props.articleId,
        yetAnotherArticle: this.props.articleId2,
      },
    }];
  }

In native graphQL mutations I have such ability, but Relay does not cover this.

That is indeed not possible in Relay. In Reindex we solve it by providing all objects that can have a connection with item changed in the payload. We do it automatically through introspection. I think you can solve it the same way in graffiti-mongoose.

Here is how the payload would look for the note:

type NotePayload {
  clientMutationId: String
  id: ID
  changedNote: Note
  # For Relay RANGE mutations
  changedNoteEdge: NoteEdge user

  # Viewer is added becaus it contains `all` listing connections, like `allNotes` in this case
  viewer: ReindexViewer

  # For all the objects that has a connection to Note, we include it here
  article: Article,
  user: User
}

The above approach works if you only allow one object and one connection to be changed at once, so, eg, you can't add note to multiple articles or create multiple notes. I think it might work correctly also for multiple mutations if you return lists of changed items and maybe lists of their connections, not sure about that. For mutations that modify many-to-many connections, we just return both changed object.

type NoteArticleNotesPayload {
  clientMutationId: String
  changedNote: Note,
  changedNoteEdge: NoteEdge,
  changedArticle: Article,
  changedArticleEdge: ArticleEdge,
}

I saw how you add referenced models to payload, and for now it is the best hack for Relay.

But I think, without variables in payload we can not get full power of graphql, which teased me to get all needed data with one request.

There is some talk on how to improve mutations here https://github.com/facebook/relay/issues/538, it might make fat query unnecessary or simpler.

I'm agreed - without variables in getFatQuery it will look dirty:

  getFatQuery() {
    let articlesFragment
    if (this.props.filter === 'PREVIEW') {
      articlesFragment = Relay.QL`
        fragment on User {
          articles(first: 100, filter: PREVIEW) {
            edges {
              node {
                title
              }
            }
          }
        }
      `
    } else {
      articlesFragment = Relay.QL`
        fragment on User {
          articles(first: 100, filter: SUBSCRIBED) {
            edges {
              node {
                title
              }
            }
          }
        }
      `
    }
    return Relay.QL`
      fragment on LikeArticleMutationPayload {
        article {
          id
        }
        articleEdge
        user {
          ${articlesFragment}
        }
      }
    `
  }

@DenisIzmaylov any idea on how to pass variable using your method like user which takes id argument.

You can set variables like the following using template variables. I found this to be working inside getFatQuery, but it did not work elsewhere. (weird).

 getFatQuery() {
    return Relay.QL`
      fragment on CreateNotePayload {
        viewer {
          article(id: "${articleId}") {   // <--- by design I want get such data on client, after adding the note 
            summary
          }
        }
        changedNoteEdge {
          node
        }
      }
    `;
  }

What I want to know is how to have a wild card variables that matches any values, hopefully something like the following:

 getFatQuery() {
    return Relay.QL`
      fragment on CreateNotePayload {
        viewer {
          article(id: *) {              // * <-- It will be great to have a '*' as a wild card that matches all ids on the store.
            summary
          }
        }
        changedNoteEdge {
          node
        }
      }
    `;
  }

@joonhocho Did you try using that template variable. I tried in my mutation's getFatQuery but it was not working.

@shahankit hmm.. I guess this working for me inside getFatQuery is maybe not a feature, but a bug. It works for me only inside getFatQuery in my mutations. I see it working for me even now.

Okay it's also working for me now. Had an bug in passing them. Thanks for pointing this out.

Glad it helped.

Hey @joonhocho can you expand a bit more on how you set the variables the fat query is using? You said this works for you:

 getFatQuery() {
    return Relay.QL`
      fragment on CreateNotePayload {
        viewer {
          article(id: "${articleId}") {
            note 
            summary
          }
        }
        changedNoteEdge {
          node
        }
      }
    `;
  }

But where do you set the value of articleId? I tried the following but I get an error:

  getFatQuery() {
    const previousStageId = this.props.candiate.currentStage.recordId;
    return Relay.QL`
      fragment on ChangeCandidateStagePayload @relay(pattern: true) {
        job {
          candidates(first: 100, currentStagesIds: [${previousStageId}]) // <-- I need 'currentStagesids: [2]' here but this fails.
        }
      }
    `;
  }

... but I get this error:

change-candidate-stage-mutation.js:83 Uncaught Error: Relay transform error ``Syntax Error change (3:53) Unexpected ...

2:         job {
3:           candidates(first: 100, currentStagesIds: [...RQL_0])
                                                       ^
4:           stagesWithCount {

I'm running on [email protected] so it might be a version thing, but wanted to ask to see if you had any insights on this or see anything wrong with my code sample.

@josercruz01 What you are doing with previousStageId (setting a local variable) is what I did for my fatQueries, but as I mentioned before it doesn't seem to work everytime. I honestly don't know why. I also got the same error as yours when I used the interpolation trick in other places than getFatQuery. At this point, I feel like it should always not work, even in getFatQuery.

Oh I see. Yeah it is strange, thanks a lot for the info :)

falling into the same problem. I'll probably have to hack like @DenisIzmaylov did..

I am also getting the same issue...

Closing this due new Mutation API which does not contain FatQuery and perfectly works with additional variables:

I highly recommend migrating to new mutation API. It allows writing mutation inside your React component (avoid creating Mutation files). And in many cases do not provide CONFIGS for updating data in the Relay store. No more fatigue for me with Relay Mutations. 馃帀馃帀馃帀

Of course, you may make API better by writing your own wrapper above Relay.GraphQLMutation. Eg. https://gist.github.com/nodkz/03f28aaa1b66195b517ecf06e92487bf
So code in your React component may become like this (example with an additional variable in delete method):

class Order extends Component {
  // ...

  create(formState) {
    relayStore.mutate({
      query: Relay.QL`mutation {
        orderCreate(input: $input) {
          recordId
          record {
            email
            id
          }
          orderConnection {
            ${List.getFragment('orderConnection')}
          }
        }
      }`,
      // variables: {
      //   input {
      //     record: minimize(formState.getValue()),
      //   }
      // },
      // short `variables` definition due autowrapping with `input` https://gist.github.com/nodkz/03f28aaa1b66195b517ecf06e92487bf#file-relaystore-js-L24-L32
      variables: {
        record: minimize(formState.getValue()),
      },
      onSuccess: (res) => {
        browserHistory.push({
          pathname: `/orders/${res.orderCreate.recordId}`,
        });
      },
      onError: (transaction) => {
        console.log('mutation onFailure', transaction);
      },
      onStart: () => {
        this.setState({ isSaving: true });
      },
      onEnd: () => {
        this.setState({ isSaving: false });
      },
    });
  }

  changeStatus(status, cb) {
    relayStore.mutate({
      query: Relay.QL`mutation {
        orderUpdate(input: $input) {
          record {
            status
            id
          }
        }
      }`,
      variables: {
        record: {
          _id: this.props.order._id,
          status,
        },
      },
      optimisticResponse: {
        record: {
          id: this.props.order.id,
          status: 'updating...',
        },
      },
      onSuccess: () => {
        this.setState({
          isSaved: true,
        });
        if (cb) cb();
      },
      onError: (transaction) => {
        console.log(transaction.getError());
        const e = transaction.getError();
        const errMsg = e ? e.message.toString() : null;
        this.setState({ errMsg });
      },
      onStart: () => {
        this.setState({
          isSaved: false,
          isSaving: true,
          errMsg: null,
        });
      },
      onEnd: () => {
        this.setState({
          isSaving: false,
        });
      },
    });
  }

  delete() {
    relayStore.mutate({
      query: Relay.QL`mutation {
        orderRemove(input: $input) {
          nodeId
          viewer {
            orderConnection(first: $first) {
              edges {
                node {
                  _id
                }
              }
            }
          }
        }
      }`,
      variables: {
        input: {
          _id: this.props.order._id,
        },
        first: 666,
      },
      onSuccess: () => {
        browserHistory.goLevelUp();
      },
      onError: (transaction) => {
        alert(transaction.getError());
      },
      onStart: () => {
        this.setState({ isSaving: true });
      },
      onEnd: () => {
        this.setState({ isSaving: false });
      },
    });
  }
}

@nodkz Thanks for that information 馃憤 In your mutations above you have (input: $input) in all of them, but only one of them defines input as in variables - was that a typo or does $input mean something different than a thing from variables?

@benjie nope this is not a typo. This is a short definition (added via my wrapper https://gist.github.com/nodkz/03f28aaa1b66195b517ecf06e92487bf#file-relaystore-js-L24-L32).
For Relay mutation input is a required arg. So for saving two lines of code (input: { and }) for every my mutation I just check if input not present in variables then wrap they with input and pass to Relay mutation.

So in delete method I'll need pass additional root argument (first), so in this case, I define input explicitly.

PS. Added a note to the example above about autowrapping of variables.

@nodkz
Thanks a lot for this example.

@nodkz Hey, thanks for a great example! I'm using this approach but still there're some cases where it's not clear how to configure mutation. I've created stackoverflow question, would really appreciate help with it.

@valerybugakov sorry I can not help you with RANGE_ADD. I do not use it at all, had some problems with it (do not remember what exactly). So I just reload all connection via this.props.relay.forceFetch().

@nodkz huh, kinda solution :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

HsuTing picture HsuTing  路  3Comments

derekdowling picture derekdowling  路  3Comments

luongthanhlam picture luongthanhlam  路  3Comments

amccloud picture amccloud  路  3Comments

johntran picture johntran  路  3Comments