typePolicies seem to be invoked regardless of "@client" directive

This is not a browser cache issue.

I have a typePolicies defined on the ApolloClient:

const client = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(
      {
        typePolicies: policyMap
      }
    ),
});

These are implemented in a function that takes a field argument:

const Opening2 = (field) => {
    const data = {
        "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1": {
            eco: "B00",
            moves: "1. e4",
            name: "King's Pawn Game",
            next: [
                {
                    name: "King's Pawn Game",
                    moves: "1. e4 e5",
                    score: 0.43,
                    eco: "C20",
                },
            ],
            from: [],
            aliases: ["King's Pawn", "King's Pawn"],
            score: 0.44,
        },
    };

    return {
        read(_, { variables }) {
            const variableString = Object.values(variables).join();
            const dataForFen = data[variableString];
            return dataForFen[field];
        },
    };
};

const policyMap = {
    Opening2: {
        fields: {
            eco: Opening2("eco"),
            name: Opening2("name"),
            moves: Opening2("moves"),
            next: Opening2("next"),
            from: Opening2("from"),
            aliases: Opening2("aliases"),
            score: Opening2("score"),
        },
    },
};

From the documentation, I had the understanding that these field policies only get invoked if and @client directive was attached to the field(s):

const client = "@client"

const GET_OPENING = gql`
    query getOpening($fen: String!) {
        getOpeningForFenFull(fen: $fen) {
            eco 
            name ${client}
            moves ${client}
            next  {
                name
                moves
                score
                eco
            }
            from ${client} {
                name
                moves
            }
            aliases ${client}
            score ${client}
        }
    }
`;

But it seems if I remove the client directives, the values from the policyMap still apply (I get local variables). Is this correct behavior?

Hi @Jeff_Lowery :wave: yes, this is expected behavior. typePolicies are valid for any field name, regardless of what directives are applied. If you’d like to make your read function behave in the manner you’re describing, you could use the hasDirectives utility function:

import { hasDirectives } from 'src/utilities/graphql/directives.ts';

function readFieldWithClientDirective(field, variables) {
  if (hasDirectives(['client'], field)) {
    // Your read function logic here
  } else {
    return field;
  }
}

const cache = new InMemoryCache({
  typePolicies: {
    MyType: {
      fields: {
        myField: {
          read(existing, { readField }) {
            return readFieldWithClientDirective(readField('myField'), {});
          },
        },
      },
    },
  },
});

Hope that helps!

Thanks, I will give it a try.

Hi Jeff,

I’m stuck on how to import hasDirectives. Where is this method exported?

I found this, but I can’t import hasDirectives from it:

The apollo-utilities package has been removed, but you can access the utilities themselves from the @apollo/client/utilities entry point:

import { isReference, isInlineFragment } from ‘@apollo/client/utilities’;

Okay, I think I have it figured out:

import { hasDirectives } from "@apollo/client/utilities/graphql/directives.js";

@Jeff_Lowery thanks for calling this out, you’re absolutely right and I forgot that hasDirectives isn’t exported :grimacing: here’s a sketch of how you could achieve the same thing, it’s pretty simple so I might be missing something, lmk if this helps:

function hasClientDirective(field) {
  return field.directives && field.directives.some(directive => directive.name.value === 'client');
}

And I believe you’d need an isReference check in the read function to avoid unexpected errors, though I’m not certain:

const cache = new InMemoryCache({
  typePolicies: {
    MyType: {
      fields: {
        myField: {
          read(existing, { readField, toReference, isReference }) {
            const field = readField('myField');
            if (isReference(field) && hasClientDirective(field)) {
              // Your read function logic here
            } else {
              return field;
            }
          },
        },
      },
    },
  },
});
1 Like

Okay, I think I have it figured out:

Glad this worked! Typed up my reply before I saw this. Good to know this is able to be imported in userspace!

Well…

Here’s what I got:

const Opening2 = (fieldName) => { ...

    function hasClientDirective(field) {
        return (
            field.directives &&
            field.directives.some(
                (directive) => directive.name.value === "client"
            )
        );
    }

    return {
        read(_, { readField, isReference, variables }) {
            const field = readField(fieldName);
            if (isReference(field) && hasClientDirective(field)) {
                const variableString = Object.values(variables).join();
                const dataForFen = data[variableString];
                return dataForFen[field];
            } else {
                return field;
            }
        },
    };

const policyMap = {
    Opening2: {
        fields: {
            eco: Opening2("eco"),
            name: Opening2("name"),
            moves: Opening2("moves"),
            next: Opening2("next"),
            from: Opening2("from"),
            aliases: Opening2("aliases"),
            score: Opening2("score"),
        },
    },
};

This compiles and runs, but I get a stack overflow on this line:

            const field = readField(fieldName);

Browser error with partial call stack:

It appears that calling readField() in this context causes recursion, if I’m interpreting it right.

Unfortunately the import from directives.js gets an AST node error at compile time, which appears to be a variable scope issue.

Alright, I got it working (or hacked, maybe):

    return {
        read(cachedValue, args) {
            const { isReference, variables, field } = args;
            // const field = readField(fieldName);
            if (!isReference(field) && hasClientDirective(field)) {
                const variableString = Object.values(variables).join();
                const dataForFen = data[variableString];
                return dataForFen[fieldName];
            } else {
                return cachedValue;
            }
        },
    };

Turns out that the field is passed in as an argument to read. I’m not sure about the cachedValue, though.