57

I have an object that my GraphQL client requests.

It's a reasonably simple object:

type Element {
    content: [ElementContent]
    elementId: String
    name: String
    notes: String
    type: String
    createdAt: String
    updatedAt: String
  }

With the special type ElementContent, which is tiny and looks like this:

  type ElementContent {
    content: String
    locale: String
  }

Now, when I query this on the clientside, both the top level object and the lower level object has additional properties (which interfere with updating the object if I attempt to clone the body exactly-as-is);

Notably, GraphQL seems to supply a __typename property in the parent object, and in the child objects, they have typename and a Symbol(id) property as well.

enter image description here

I'd love to copy this object to state, update in state, then clone the state and ship it to my update mutation. However, I get roadblocked because of unknown properties that GraphQL itself supplies.

I've tried doing:

delete element.__typename to good effect, but then I also need to loop through the children (a dynamic array of objects), and likely have to remove those properties as well.

I'm not sure if I'm missing something during this equation, or I should just struggle through the code and loop + delete (I received errors attempting to do a forEach loop initially). Is there a better strategy for what I'm attempting to do? Or am I on the right path and just need some good loop code to clean unwanted properties?

ilrein
  • 3,833
  • 4
  • 31
  • 48

7 Answers7

155

There are three ways of doing this

First way

Update the client parameter like this it will omit the unwanted fields in graphql.

apollo.create({
  link: http,
  cache: new InMemoryCache({
    addTypename: false
  })
});

Second Way

By using the omit-deep package and use it as a middleware

const cleanTypeName = new ApolloLink((operation, forward) => {
  if (operation.variables) {

    operation.variables = omitDeep(operation.variables,'__typename')
  }
  return forward(operation).map((data) => {
    return data;
  });
});

Third Way

Creating a custom middleware and inject in the apollo

const cleanTypeName = new ApolloLink((operation, forward) => {
  if (operation.variables) {
    const omitTypename = (key, value) => (key === '__typename' ? undefined : value);
    operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename);
  }
  return forward(operation).map((data) => {
    return data;
  });
});

and inject the middleware

const httpLinkWithErrorHandling = ApolloLink.from([
  cleanTypeName,
  retry,
  error,
  http,
]);

If you use fragments with the queries/mutations Second Way & Third Way is recommended.

Preferred method is Third Way Because it does not have any third pary dependency and no cache performance issues

Rigin Oommen
  • 3,060
  • 2
  • 20
  • 29
  • 1
    Sadly, it doesn't work for me, the `__typename` is not within `operation.variables`. – Qwerty Sep 18 '19 at 16:33
  • 3
    Why do you use the incidence map `data => data`? – Qwerty Sep 18 '19 at 16:36
  • @qwerty that also works. I will update it accordingly – Rigin Oommen Sep 23 '19 at 07:49
  • @qwerty. I reverified the function this works fine also can you please share the apollo config with me – Rigin Oommen Sep 23 '19 at 07:55
  • 8
    Sorry for confusion, I was asking why do you use the `.map` that maps `data` to itself as in `.map((data) => { return data })`- I have seen it in almost every example, but I don't understand it. It does not manipulate anything in any way. – Qwerty Sep 25 '19 at 14:25
  • 1
    The first way worked for me w/ Angular8 client. Thanks!! – Scott Byers Jan 29 '20 at 19:41
  • The second way shouldn't be used. `omit-deep` inserts an empty object to fields with `undefined` value which can break requests. Having a variable with `undefined` is useful when you take many values from a form with empty values and they are not required. See the [existing 2 years old PR](https://github.com/jonschlinkert/omit-deep/issues/8) and my [working example](https://repl.it/@Murillo2380/omit-deep-error) – Murillo Ferreira Apr 08 '20 at 13:32
  • First way fails for me. If you use fragment, you get an error, cause __typename is used to match node types for fragments... Any suggestions? – Giuseppe Capoluongo Jun 26 '20 at 08:58
  • Please try second and third ways too.that resolves your issue. – Rigin Oommen Jun 26 '20 at 09:41
  • 7
    Unfortunately this answer is not applicable to many real-life cases. First of all, the question was about GQL *responses*, and Option 2 and 3 only modify operation variables (which are sent to the server), not responses. Only option 1 would cover both (probably), but then there's another problem: when schema is deeply nested and/or uses fragments, each option breaks cache, which relies on `__typename`. Summing up, there is no good cover-all solution here. Some options may work for your case, but if you use fragments and/or cache, most surely you just need to live with `__typename`. – piotr.d Jul 14 '20 at 10:25
  • `Cannot delete property '__typename' of #` Not working for me :( – Mateusz Piguła Feb 03 '22 at 10:59
  • Can you please share the code snippet you use – Rigin Oommen Feb 04 '22 at 05:00
  • 1
    @piotr.d Have you tried something like this, where you keep the `__typename` field but just make it non-enumerable?: `const typeName = data.__typename; delete data.__typename; if (typeName) Object.defineProperty(data, "__typename", {value: typeName}); // defining it this way, makes the property non-enumerable` – Venryx Mar 22 '22 at 06:48
5

If you want to wipe up __typename from GraphQL response (from the root and its children), you can use graphql-anywhere package.

Something like: const wipedData = filter(inputFragment, rcvData);

  • inputFragment is a fragment defines the fields (You can see details here)
  • rcvData is the received data from GraphQL query

By using the filter function, the wipedData includes only required fields you need to pass as mutation input.

Alireza Hariri
  • 119
  • 2
  • 4
2

Here's what I did, to support file uploads as well. It's a merge of multiple suggestions I found on the Github thread here: Feature idea: Automatically remove __typename from mutations

import { parse, stringify } from 'flatted';

const cleanTypename = new ApolloLink((operation, forward) => {
    const omitTypename = (key, value) => (key === '__typename' ? undefined : value);

    if ((operation.variables && !operation.getContext().hasUpload)) {
        operation.variables = parse(stringify(operation.variables), omitTypename);
    }

    return forward(operation);
});

Hooking up the rest of my client.tsx file, simplified:

import { InMemoryCache } from 'apollo-cache-inmemory';
import { createUploadLink } from 'apollo-upload-client';
import { ApolloClient } from 'apollo-client';
import { setContext } from 'apollo-link-context';
import { ApolloLink } from 'apollo-link';

const authLink = setContext((_, { headers }) => {
    const token = localStorage.getItem(AUTH_TOKEN);
    return {
        headers: {
        ...headers,
        authorization: token ? `Bearer ${ token }` : '',
        },
    };
});



const httpLink = ApolloLink.from([
    cleanTypename,
    authLink.concat(upLoadLink),
]);

const client = new ApolloClient({
    link: httpLink,
    cache,
});

export default client;

Now when I call mutations that are of type upload, I simply set the context hasUpload to true, as shown here:

UpdateStation({variables: { input: station }, context: {hasUpload: true }}).then()
Asamoah
  • 459
  • 6
  • 9
2

For those looking for a TypeScript solution:

import cloneDeepWith from "lodash/cloneDeepWith";

export const omitTypenameDeep = (
  variables: Record<string, unknown>
): Record<string, unknown> =>
  cloneDeepWith(variables, (value) => {
    if (value && value.__typename) {
      const { __typename, ...valWithoutTypename } = value;
      return valWithoutTypename;
    }

    return undefined;
  });
const removeTypename = new ApolloLink((operation, forward) => {
  const newOperation = operation;
  newOperation.variables = omitTypenameDeep(newOperation.variables);
  return forward(newOperation);
});

// ...

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: ApolloLink.from([removeTypename, httpLink]),
});
gabo bernal
  • 721
  • 6
  • 5
  • 1
    Your function `omitTypenameDeep` is not correct because it won't do a deep clone (recursively). Correction: ```ts const omitTypenameDeep = ( variables: Record ): Record => cloneDeepWith(variables, (value) => { if (value?.__typename) { const { __typename, ...valWithoutTypename } = value return omitTypenameDeep(valWithoutTypename) } return undefined }) ``` – M. M Aug 27 '21 at 00:53
2

Below approach worked for me so far in my use case.

const {
        loading,
        error,
        data,
      } = useQuery(gqlRead, {
        variables: { id },
        fetchPolicy: 'network-only',
        onCompleted: (data) => {
          const { someNestedData } = data;
          const filteredData = removeTypeNameFromGQLResult(someNestedData);
          //Do sth with filteredData
        },
      });
    
    //in helper
    export const removeTypeNameFromGQLResult = (result: Record<string, any>) => {
      return JSON.parse(
        JSON.stringify(result, (key, value) => {
          if (key === '__typename') return;
          return value;
        })
      );
    };
  • Using addTypename: false can cause perfomance and duplicate items in your cache so better leave it alone. Just get the data and normalize it i believe is the best way. – theocikos May 18 '23 at 14:45
1

I've just published graphql-filter-fragment to help with this use case. I've wrote in a bit more detail about the CRUD use case I had that led me to this approach.

Example:

import {filterGraphQlFragment} from 'graphql-filter-fragment';
import {gql} from '@apollo/client/core';

const result = filterGraphQlFragment(
  gql`
    fragment museum on Museum {
      name
      address {
        city
      }
    }
  `,
  {
    __typename: 'Museum',
    name: 'Museum of Popular Culture',
    address: {
      __typename: 'MuseumAddress',
      street: '325 5th Ave N',
      city: 'Seattle'
    }
  }
);

expect(result).toEqual({
  name: 'Museum of Popular Culture',
  address: {
    city: 'Seattle'
  }
});
amann
  • 5,449
  • 4
  • 38
  • 46
1

Here is my solution. Vanilla JS, recursive, and does not mutate the original object:

const removeAllTypenamesNoMutate = (item) => {
  if (!item) return;

  const recurse = (source, obj) => {
    if (!source) return;

    if (Array.isArray(source)) {
      for (let i = 0; i < source.length; i++) {
        const item = source[i];
        if (item !== undefined && item !== null) {
          source[i] = recurse(item, item);
        }
      }
      return obj;
    } else if (typeof source === 'object') {
      for (const key in source) {
        if (key === '__typename') continue;
        const property = source[key];
        if (Array.isArray(property)) {
          obj[key] = recurse(property, property);
        } else if (!!property && typeof property === 'object') {
          const { __typename, ...rest } = property;
          obj[key] = recurse(rest, rest);
        } else {
          obj[key] = property;
        }
      }
      const { __typename, ...rest } = obj;

      return rest;
    } else {
      return obj;
    }
  };

   return recurse(JSON.parse(JSON.stringify(item)), {});
};
Jordan Daniels
  • 4,896
  • 1
  • 19
  • 29