10

I've got a type called Article in my schema:

type Article {
  id: ID!
  updated: DateTime
  headline: String
  subline: String
}

For updates to it, there's a corresponding input type that is used by a updateArticle(id: ID!, article: ArticleInput!) mutation:

input ArticleInput {
  headline: String
  subline: String
}

The mutation itself looks like this:

mutation updateArticle($id: ID!, $article: ArticleInput!) {
  updateArticle(id: $id, article: $article) {
    id
    updated
    headline
    subline
  }
}

The article is always saved as a whole (not individual fields one by one) and so when I pass an article to that mutation that I've previously fetched, it throws errors like Unknown field. In field "updated", Unknown field. In field "__typename" and Unknown field. In field "id". These have the root cause, that those fields aren't defined on the input type.

This is correct behaviour according to the spec:

(…) This unordered map should not contain any entries with names not defined by a field of this input object type, otherwise an error should be thrown.

Now my question is what a good way to deal these kinds of scenarios is. Should I list all properties that are allowed on the input type in my app code?

If possible I'd like to avoid this and maybe have a utility function slice them off for me which knows about the input type. However, since the client doesn't know about the schema, this would have to happen on the server side. Thus, the unnecessary properties would be transferred there, which I suppose is the reason why they shouldn't be transferred in the first place.

Is there a better way than maintaining a list of properties?

I'm using apollo-client, react-apollo and graphql-server-express.

amann
  • 5,449
  • 4
  • 38
  • 46
  • Can you please show the actual mutation that you're calling? It sounds like something else is amiss here. – marktani Mar 06 '17 at 17:22
  • Thanks for the quick response @marktani! I've added the mutation code to the question. – amann Mar 06 '17 at 17:36
  • Do you have the possibility to run a "raw" mutation against your server? That is, using GraphiQL or a HTTP client like curl or fetch. I want to understand if this is an error thrown by Apollo or thrown by your server. – marktani Mar 06 '17 at 17:43
  • 1
    I've just simulated the request with a curl. The error is definitely thrown by the server. Removing the fields that are unknown to the input type fixes the problem. – amann Mar 06 '17 at 17:51
  • So looks like your payload for that mutation just doesn't offer `id`, `updated` or `updated`. That it doesn't offer `__typename` is troublesome, as that's [part of the spec](https://facebook.github.io/graphql/#sec-Type-Name-Introspection). If you can share the endpoint that would be helpful, otherwise check the docs generated by GraphiQL. – marktani Mar 06 '17 at 17:53
  • 1
    Exactly, the root cause seems to be that the mutation doesn't know about those fields. My preferred solution would have been that they are ignored, but that would involve transferring them to the server, which I guess is the reason this isn't supported. Sadly I can't share the endpoint with you, but I guess every GraphQL endpoint will respond in the same way when a mutation is called with an input type that includes extra fields. – amann Mar 06 '17 at 18:00
  • Again, I don't think the input arguments are the problem, rather the items you include in the query. – marktani Mar 06 '17 at 20:06
  • Sorry, I think I overlooked that. I just tried again with a mutation that only queries fields on the input type after the mutation but that fails with the same error. – amann Mar 07 '17 at 09:47

2 Answers2

10

You can use a fragment for the query, which includes all mutable fields of the data. That fragment can be used by a filter utility to remove all unwanted data before the mutation happens.

The gist is:

const ArticleMutableFragment = gql`
fragment ArticleMutable on Article {
  headline
  subline
  publishing {
    published
    time
  }
}
`

const ArticleFragment = gql`
fragment Article on Article {
  ...ArticleMutable
  id
  created
  updated
}
${ArticleMutableFragment}
`;

const query = gql`
query Article($id: ID!) {
  article(id: $id) {
    ...Article
  }
}
${ArticleFragment}
`;

const articleUpdateMutation = gql`
mutation updateArticle($id: ID!, $article: ArticleInput!) {
  updateArticle(id: $id, article: $article) {
    ...Article
  }
}
${ArticleFragment}
`;

...

import filterGraphQlFragment from 'graphql-filter-fragment';

...

graphql(articleUpdateMutation, {
  props: ({mutate}) => ({
    onArticleUpdate: (id, article) =>
      // Filter for properties the input type knows about
      mutate({variables: {id, article: filterGraphQlFragment(ArticleMutableFragment, article)}})
  })
})

...

The ArticleMutable fragment can now also be reused for creating new articles.

amann
  • 5,449
  • 4
  • 38
  • 46
  • Yeah GraphQL currently doesn't include a lot of nice-to-have stuff for CRUD operations, like utilities for saving an entire object via an input object. It kind of assumes you are calling each mutation with exactly the data you mean to pass in. Which might be good because if someone added a new field to the input type, then you will end up sending more fields than you expected. – stubailo Mar 07 '17 at 07:03
  • That's true. On the other hand, when new fields are added to `Article` that are queried and updatable with the view, I might forget to add them to the whitelist – so that's something to keep in mind. But good to know that I'm not missing anything obvious – thanks! – amann Mar 07 '17 at 09:36
  • 1
    Actually, this might be solved in a good way with the filter capabilities of your [graphql-anywhere](https://github.com/apollographql/graphql-anywhere#example-filter-a-nested-object) package. That way I can write a query that filters the data before a mutation. That query can be placed right next to the query that fetches the data and so it might be easier to keep those in sync. – amann Mar 07 '17 at 09:37
  • Or even better a shared fragment between the query and the filter query for the mutation. I'll give that another try. – amann Mar 07 '17 at 09:48
  • Ooh, I like this a lot because it still lets you specify exactly the fields you are trying to send! – stubailo Mar 07 '17 at 16:15
0

I've personally had same idea and took @amann 's approach earlier, but after some time the conceptual flaw of using query fragments on input types became evident. You would'n have an option to pick input type field that isn't present in (corresponding) object type - is there even any?

Currently I'm describing my input data by typesafe-joi schemas and using it's stripUnknown option to filter out my form data.

Invalid data never leaves form so valid data can be statically typed.

In a sense, creating joi schema is same activity as defining "input fragment" so no code duplication takes place and your code can be type-safe.

Jiří Brabec
  • 300
  • 2
  • 7