Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove the clientMutationId requirement by creating a new root id for each executed mutation #2349

Closed
wants to merge 4 commits into from

Conversation

robrichard
Copy link
Contributor

@robrichard robrichard commented Feb 26, 2018

Fixes #1734
Fixes #2333
Fixes #825

Related to relayjs/relay-examples#30
When a mutation is executed, it's payload is added to the store using a key of the serialized arguments. In the Todo Modern example this looks like this:

{
  'client:root:addTodo(input:{"text":"a"})': { ... } // added by mutation response
  'client:VXNlcjptZQ==:__TodoList_todos_connection': {
    edges: [
      __refs: [
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:0',
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:1',
        'client:root:addTodo(input:{"text":"a"}):todoEdge', // added by mutation imperative updater function
      ]
    ]
  }
}

If you execute a second addTodo mutation with the same arguments, the client:root:addTodo(input:{"text":"a"}) field in the store will get overwritten with the second mutation and the store will look like this:

{
  'client:root:addTodo(input:{"text":"a"})': { ... }
  'client:VXNlcjptZQ==:__TodoList_todos_connection': {
    edges: [
      __refs: [
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:0',
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:1',
        'client:root:addTodo(input:{"text":"a"}):todoEdge',
        'client:root:addTodo(input:{"text":"a"}):todoEdge',
      ]
    ]
  }
}

When React renders the data, you will have the latter todo in the connection array twice and will likely get an error along the lines of Encountered two children with the same key, <id>. Child keys must be unique; when two children share a key, only the first child will be used.

This was fixed in relay-examples by adding a clientMutationId to the mutation arguments. I don't think this is ideal since

  1. it requires the developer to manually add this to each mutation
  2. it places an additional constraint on the GraphQL schema to support this field.

The approach I took to fix this, was to use a new root dataID (instead of the static client:root) for results on each mutation. This will namespace results from each mutation so they can never overwrite one another.

With these changes, the relay store now looks like this after completing the same series of mutations:

{
  'client:mutationRoot0:addTodo(input:{"text":"a"})': { ... },
  'client:mutationRoot1:addTodo(input:{"text":"a"})': { ... },
  'client:VXNlcjptZQ==:__TodoList_todos_connection': {
    edges: [
      __refs: [
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:0',
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:1',
        'client:mutationRoot0:addTodo(input:{"text":"a"}):todoEdge',
        'client:mutationRoot1:addTodo(input:{"text":"a"}):todoEdge'
      ]
    ]
  }
}

I tested this and it works to solve this issue, but I'm unsure if there could be any unintended consequences by this change.

@alloy
Copy link
Contributor

alloy commented Mar 1, 2018

This PR addresses the issue described in #2333. And while this does not remove the ID, it is related to #2077.

@alloy
Copy link
Contributor

alloy commented Mar 1, 2018

@steida Mind testing this PR for your situation? (And is your situation a production app?)

@steida
Copy link

steida commented Mar 2, 2018

@alloy My situation is https://github.com/este/este. Not a production :(

@jstejada What do you think, should I test? Will Relay team merge it? Thank you.

@steida
Copy link

steida commented Mar 3, 2018

@kassens What do you think? Should I test it? Is there any chance it will be merged in, say, next six months?

@alloy
Copy link
Contributor

alloy commented Mar 3, 2018

@steida We should help the Relay team by testing them preemptively so that when they have time they will know it was already tested by at least somebody.

@taion
Copy link
Contributor

taion commented Apr 10, 2018

Is there a constructive reason for the old behavior, incidentally? It does seem odd to essentially assume idempotency for all mutations.

Copy link
Contributor

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jstejada has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@dblock
Copy link

dblock commented Jul 10, 2018

@AndrewIngram mentions in https://graphql.slack.com/archives/C0BEXJLKG/p1531176878000009 that another work-around until this is merged is to clone the data that comes back. Anyone has such code to share here that, hopefully, avoids the long boilerplate in https://facebook.github.io/relay/docs/en/mutations.html#updater-configs?

@taion
Copy link
Contributor

taion commented Aug 31, 2018

BTW, the same problem arises for subscriptions. In this case the fix needs to be applied in this manner, as there's no place to inject a payload sequence ID (as far as I'm aware).

That said, @AndrewIngram's workaround is pretty satisfactory in practice.

@sibelius
Copy link
Contributor

@jstejada @kassens can we try to import this again ?

we could also merge this one #2401 when this has landed

@jstejada
Copy link
Contributor

thanks for the ping @sibelius! apologies again for the delay in getting to this. We still need to discuss internally a little bit what the best approach would be here. From a short discussion, it seems like we could address separately the problem of ensuring that we don't overwrite mutation payloads vs removing the requirement for a clientMutationID. cc @josephsavona @kassens

@taion
Copy link
Contributor

taion commented Feb 27, 2019

I just ran into something like this. We've switched our mutations to return unions between the payload type and various typed errors as their root types. When running a mutation with the same inputs leads to a different concrete return type (e.g. an error instead of a payload), we get an invalid record update due to the type mismatch, because the root field gets reused.

@tlvenn
Copy link
Contributor

tlvenn commented Oct 23, 2019

@jstejada do you have any update on this particular issue ? Thanks in advance.

@jstejada
Copy link
Contributor

i think @tyao1 already fixed this

@robrichard
Copy link
Contributor Author

@jstejada @tyao1 I'm pretty sure this is still an issue. I just tested in relay-examples todo with relay v7 and was able to reproduce

@kassens
Copy link
Member

kassens commented Dec 10, 2019

This has been a long time coming, thanks @robrichard for the initial PR and @tyao1 for picking this back up!

@facebook-github-bot
Copy link
Contributor

@tyao1 merged this pull request in c988815.

facebook-github-bot pushed a commit that referenced this pull request Dec 19, 2019
…t practices and not purely a relay spec (#2603)

Summary:
Better late than never, roughly a year ago in the GraphQL Working Group 3 we took it on Artsy to start moving the Relay specs to be considered "GraphQL Best Practices" ([link](https://github.com/graphql/graphql-wg/blob/master/notes/2018-02-01.md#discuss-moving-connectionglobal-id-specs-from-relay-into-best-practices-repo))

This starts that, beginning with first minimizing the use of Relay as being the sole client that conforms to the connection and object identifier spec.

Main notes:

- When referring to relay on the client I switched "Relay" to "Spec-compliant client" or "client"
- When referring to relay's compatibility on the server side, I oriented it to just be "spec-compliant"
- I mostly left the Mutation input spec alone, now that it's only for Relay classic and is close to being removed completely from Relay #2349
- This is step one, once this is merged then I'd like to move both object identification and connection to be on the graphql repos instead of relay and I can handle link updating etc

Happy to change wording etc

/cc leebyron in case you don't watch this repo too closely.
Pull Request resolved: #2603

Reviewed By: josephsavona

Differential Revision: D19071019

Pulled By: jstejada

fbshipit-source-id: cf5a7dfb93cfd565d4f178a51cdff7cd0caec1a0
jstejada pushed a commit that referenced this pull request Dec 19, 2019
…or each executed mutation (#2349)

Summary:
Fixes #1734
Fixes #2333

Related to relayjs/relay-examples#30
When a mutation is executed, it's payload is added to the store using a key of the serialized arguments. In the Todo Modern example this looks like this:

```js
{
  'client:root:addTodo(input:{"text":"a"})': { ... } // added by mutation response
  'client:VXNlcjptZQ==:__TodoList_todos_connection': {
    edges: [
      __refs: [
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:0',
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:1',
        'client:root:addTodo(input:{"text":"a"}):todoEdge', // added by mutation imperative updater function
      ]
    ]
  }
}
```

If you execute a second `addTodo` mutation with the same arguments, the `client:root:addTodo(input:{"text":"a"})` field in the store will get overwritten with the second mutation and the store will look like this:

```js
{
  'client:root:addTodo(input:{"text":"a"})': { ... }
  'client:VXNlcjptZQ==:__TodoList_todos_connection': {
    edges: [
      __refs: [
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:0',
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:1',
        'client:root:addTodo(input:{"text":"a"}):todoEdge',
        'client:root:addTodo(input:{"text":"a"}):todoEdge',
      ]
    ]
  }
}
```

When React renders the data, you will have the latter `todo` in the connection array twice and will likely get an error along the lines of `Encountered two children with the same key, <id>. Child keys must be unique; when two children share a key, only the first child will be used`.

This was [fixed in relay-examples](relayjs/relay-examples@ed95594#diff-4b42725e6eb503f56da8e3f637e9a4e3) by adding a `clientMutationId` to the mutation arguments. I don't think this is ideal since
1. it requires the developer to manually add this to each mutation
2. it places an additional constraint on the GraphQL schema to support this field.

The approach I took to fix this, was to use a new root `dataID` (instead of the static `client:root`) for results on each mutation. This will namespace results from each mutation so they can never overwrite one another.

With these changes, the relay store now looks like this after completing the same series of mutations:

```js
{
  'client:mutationRoot0:addTodo(input:{"text":"a"})': { ... },
  'client:mutationRoot1:addTodo(input:{"text":"a"})': { ... },
  'client:VXNlcjptZQ==:__TodoList_todos_connection': {
    edges: [
      __refs: [
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:0',
        'client:VXNlcjptZQ==:__TodoList_todos_connection:edges:1',
        'client:mutationRoot0:addTodo(input:{"text":"a"}):todoEdge',
        'client:mutationRoot1:addTodo(input:{"text":"a"}):todoEdge'
      ]
    ]
  }
}
```

I tested this and it works to solve this issue, but I'm unsure if there could be any unintended consequences by this change.
Closes #2349

Reviewed By: josephsavona

Differential Revision: D7631345

Pulled By: tyao1

fbshipit-source-id: fc53638536860855f271f436f810caa0fc60bcb0
jstejada pushed a commit that referenced this pull request Dec 19, 2019
…t practices and not purely a relay spec (#2603)

Summary:
Better late than never, roughly a year ago in the GraphQL Working Group 3 we took it on Artsy to start moving the Relay specs to be considered "GraphQL Best Practices" ([link](https://github.com/graphql/graphql-wg/blob/master/notes/2018-02-01.md#discuss-moving-connectionglobal-id-specs-from-relay-into-best-practices-repo))

This starts that, beginning with first minimizing the use of Relay as being the sole client that conforms to the connection and object identifier spec.

Main notes:

- When referring to relay on the client I switched "Relay" to "Spec-compliant client" or "client"
- When referring to relay's compatibility on the server side, I oriented it to just be "spec-compliant"
- I mostly left the Mutation input spec alone, now that it's only for Relay classic and is close to being removed completely from Relay #2349
- This is step one, once this is merged then I'd like to move both object identification and connection to be on the graphql repos instead of relay and I can handle link updating etc

Happy to change wording etc

/cc leebyron in case you don't watch this repo too closely.
Pull Request resolved: #2603

Reviewed By: josephsavona

Differential Revision: D19071019

Pulled By: jstejada

fbshipit-source-id: cf5a7dfb93cfd565d4f178a51cdff7cd0caec1a0
@ersinakinci
Copy link

Can someone please comment with respect to the relevance of this PR to the following, taken from the "GraphQL Server Specification" section of the documentation?

Relay uses a common pattern for mutations, where there are root fields on the mutation type with a single argument, input, and where the input and output both contain a client mutation identifier used to reconcile requests and responses.

Is it correct to say that this documentation is incorrect because client mutation identifiers no longer exist? Or do they exist but they're an internal API (e.g., auto-generated)? Or something else...?

@sibelius
Copy link
Contributor

It is still a common pattern, but not a requirement anymore

@pvgoddijn
Copy link

any guidance on how to implement the mutation identifier when writing a compatible server?

@taion
Copy link
Contributor

taion commented Aug 7, 2020

If you're using JS, graphql-relay provides primitives for this. Otherwise, many other frameworks have their own equivalents. It just depends on which server you're using.

@sibelius
Copy link
Contributor

sibelius commented Aug 7, 2020

clientMutationId is like an idempotence key (https://stripe.com/docs/api/idempotent_requests)

so your server can validate that user hasn't sent the same mutation twice

@taion
Copy link
Contributor

taion commented Aug 7, 2020

not exactly – it was originally meant for clients to be able to correlate mutation payloads with mutations, without assuming request/response semantics on the network transport

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet