From fd3332c158fff6d7b9669872c0c85d7836dc8dbd Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 1 Oct 2020 15:59:26 -0400 Subject: [PATCH 1/2] Make keyArgs tolerant of optional arguments. The keyArgs:[...] configuration for field policies should be able to include arguments that are optional, while still specifying an ordering of all possible key arguments for serialization purposes. When an optional argument is not provided, it will simply not be included in the serialized storeKeyName suffix, and no exception will be thrown. Should address #6973. --- .../__tests__/__snapshots__/policies.ts.snap | 372 ++++++++++++++++++ src/cache/inmemory/__tests__/policies.ts | 186 ++++++++- src/cache/inmemory/policies.ts | 19 +- 3 files changed, 567 insertions(+), 10 deletions(-) diff --git a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap index 1ebbd93b8f7..296372a0cd1 100644 --- a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap +++ b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap @@ -839,6 +839,378 @@ Object { } `; +exports[`type policies field policies can include optional arguments in keyArgs 1`] = ` +Object { + "Author:{\\"name\\":\\"Nadia Eghbal\\"}": Object { + "__typename": "Author", + "name": "Nadia Eghbal", + "writings:{\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + }, + "ROOT_QUERY": Object { + "__typename": "Query", + "author": Object { + "__ref": "Author:{\\"name\\":\\"Nadia Eghbal\\"}", + }, + }, +} +`; + +exports[`type policies field policies can include optional arguments in keyArgs 2`] = ` +Object { + "Author:{\\"name\\":\\"Nadia Eghbal\\"}": Object { + "__typename": "Author", + "name": "Nadia Eghbal", + "writings:{\\"a\\":1,\\"b\\":2,\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + }, + "ROOT_QUERY": Object { + "__typename": "Query", + "author": Object { + "__ref": "Author:{\\"name\\":\\"Nadia Eghbal\\"}", + }, + }, +} +`; + +exports[`type policies field policies can include optional arguments in keyArgs 3`] = ` +Object { + "Author:{\\"name\\":\\"Nadia Eghbal\\"}": Object { + "__typename": "Author", + "name": "Nadia Eghbal", + "writings:{\\"a\\":1,\\"b\\":2,\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":1,\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + }, + "ROOT_QUERY": Object { + "__typename": "Query", + "author": Object { + "__ref": "Author:{\\"name\\":\\"Nadia Eghbal\\"}", + }, + }, +} +`; + +exports[`type policies field policies can include optional arguments in keyArgs 4`] = ` +Object { + "Author:{\\"name\\":\\"Nadia Eghbal\\"}": Object { + "__typename": "Author", + "name": "Nadia Eghbal", + "writings:{\\"a\\":1,\\"b\\":2,\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":1,\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + }, + "ROOT_QUERY": Object { + "__typename": "Query", + "author": Object { + "__ref": "Author:{\\"name\\":\\"Nadia Eghbal\\"}", + }, + }, +} +`; + +exports[`type policies field policies can include optional arguments in keyArgs 5`] = ` +Object { + "Author:{\\"name\\":\\"Nadia Eghbal\\"}": Object { + "__typename": "Author", + "name": "Nadia Eghbal", + "writings:{\\"a\\":1,\\"b\\":2,\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":1,\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":3}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + }, + "ROOT_QUERY": Object { + "__typename": "Query", + "author": Object { + "__ref": "Author:{\\"name\\":\\"Nadia Eghbal\\"}", + }, + }, +} +`; + +exports[`type policies field policies can include optional arguments in keyArgs 6`] = ` +Object { + "Author:{\\"name\\":\\"Nadia Eghbal\\"}": Object { + "__typename": "Author", + "name": "Nadia Eghbal", + "writings:{\\"a\\":1,\\"b\\":2,\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":1,\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":3}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + }, + "ROOT_QUERY": Object { + "__typename": "Query", + "author": Object { + "__ref": "Author:{\\"name\\":\\"Nadia Eghbal\\"}", + }, + }, +} +`; + +exports[`type policies field policies can include optional arguments in keyArgs 7`] = ` +Object { + "Author:{\\"name\\":\\"Nadia Eghbal\\"}": Object { + "__typename": "Author", + "name": "Nadia Eghbal", + "writings:{\\"a\\":1,\\"b\\":2,\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":1,\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":3}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"b\\":4}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + }, + "ROOT_QUERY": Object { + "__typename": "Query", + "author": Object { + "__ref": "Author:{\\"name\\":\\"Nadia Eghbal\\"}", + }, + }, +} +`; + +exports[`type policies field policies can include optional arguments in keyArgs 8`] = ` +Object { + "Author:{\\"name\\":\\"Nadia Eghbal\\"}": Object { + "__typename": "Author", + "name": "Nadia Eghbal", + "writings": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":1,\\"b\\":2,\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":1,\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"a\\":3}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"b\\":2}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"b\\":4}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{\\"type\\":\\"Book\\"}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + "writings:{}": Array [ + Object { + "__typename": "Book", + "isbn": "0578675862", + "title": "Working in Public: The Making and Maintenance of Open Source Software", + }, + ], + }, + "ROOT_QUERY": Object { + "__typename": "Query", + "author": Object { + "__ref": "Author:{\\"name\\":\\"Nadia Eghbal\\"}", + }, + }, +} +`; + exports[`type policies field policies read, merge, and modify functions can access options.storage 1`] = ` Object { "ROOT_QUERY": Object { diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts index dcf75babe1b..4b9c5ad0006 100644 --- a/src/cache/inmemory/__tests__/policies.ts +++ b/src/cache/inmemory/__tests__/policies.ts @@ -2,7 +2,7 @@ import gql from "graphql-tag"; import { InMemoryCache } from "../inMemoryCache"; import { ReactiveVar, makeVar } from "../reactiveVars"; -import { Reference, StoreObject, ApolloClient, NetworkStatus, TypedDocumentNode } from "../../../core"; +import { Reference, StoreObject, ApolloClient, NetworkStatus, TypedDocumentNode, DocumentNode } from "../../../core"; import { MissingFieldError } from "../.."; import { relayStylePagination } from "../../../utilities"; import { MockLink } from '../../../utilities/testing/mocking/mockLink'; @@ -742,6 +742,190 @@ describe("type policies", function () { }); }); + it("can include optional arguments in keyArgs", function () { + const cache = new InMemoryCache({ + typePolicies: { + Author: { + keyFields: ["name"], + fields: { + writings: { + keyArgs: ["a", "b", "type"] + }, + }, + }, + }, + }); + + const data = { + author: { + __typename: "Author", + name: "Nadia Eghbal", + writings: [{ + __typename: "Book", + isbn: "0578675862", + title: "Working in Public: The Making and Maintenance of " + + "Open Source Software", + }], + }, + }; + + function check( + query: DocumentNode | TypedDocumentNode, + variables?: TVars, + ) { + cache.writeQuery({ query, variables, data }); + expect(cache.readQuery({ query, variables })).toEqual(data); + } + + check(gql` + query { + author { + name + writings(type: "Book") { + ... on Book { + title + isbn + } + } + } + } + `); + expect(cache.extract()).toMatchSnapshot(); + + check(gql` + query { + author { + name + writings(type: "Book", b: 2, a: 1) { + ... on Book { + title + isbn + } + } + } + } + `); + expect(cache.extract()).toMatchSnapshot(); + + check(gql` + query { + author { + name + writings(b: 2, a: 1) { + ... on Book { + title + isbn + } + } + } + } + `); + expect(cache.extract()).toMatchSnapshot(); + + check(gql` + query { + author { + name + writings(b: 2) { + ... on Book { + title + isbn + } + } + } + } + `); + expect(cache.extract()).toMatchSnapshot(); + + check(gql` + query { + author { + name + writings(a: 3) { + ... on Book { + title + isbn + } + } + } + } + `); + expect(cache.extract()).toMatchSnapshot(); + + check(gql` + query { + author { + name + writings(unrelated: "oyez") { + ... on Book { + title + isbn + } + } + } + } + `); + expect(cache.extract()).toMatchSnapshot(); + + check(gql` + query AuthorWritings ($type: String) { + author { + name + writings(b: 4, type: $type, unrelated: "oyez") { + ... on Book { + title + isbn + } + } + } + } + `, { type: void 0 as any }); + expect(cache.extract()).toMatchSnapshot(); + + check(gql` + query { + author { + name + writings { + ... on Book { + title + isbn + } + } + } + } + `); + expect(cache.extract()).toMatchSnapshot(); + + const storeFieldNames: string[] = []; + + cache.modify({ + id: cache.identify({ + __typename: "Author", + name: "Nadia Eghbal", + }), + + fields: { + writings(value, { storeFieldName }) { + storeFieldNames.push(storeFieldName); + expect(value).toEqual(data.author.writings); + return value; + }, + }, + }) + + expect(storeFieldNames.sort()).toEqual([ + "writings", + 'writings:{"a":1,"b":2,"type":"Book"}', + 'writings:{"a":1,"b":2}', + 'writings:{"a":3}', + 'writings:{"b":2}', + 'writings:{"b":4}', + 'writings:{"type":"Book"}', + "writings:{}", + ]); + }); + it("can return KeySpecifier arrays from keyArgs functions", function () { const cache = new InMemoryCache({ typePolicies: { diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 8f2160ba9f5..b9dfc376a1b 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -882,7 +882,7 @@ function keyArgsFnFromSpecifier( ): KeyArgsFunction { return (args, context) => { return args ? `${context.fieldName}:${ - JSON.stringify(computeKeyObject(args, specifier)) + JSON.stringify(computeKeyObject(args, specifier, false)) }` : context.fieldName; }; } @@ -907,7 +907,7 @@ function keyFieldsFnFromSpecifier( } const keyObject = context.keyObject = - computeKeyObject(object, specifier, aliasMap); + computeKeyObject(object, specifier, true, aliasMap); return `${context.typename}:${JSON.stringify(keyObject)}`; }; @@ -959,6 +959,7 @@ function makeAliasMap( function computeKeyObject( response: Record, specifier: KeySpecifier, + strict: boolean, aliasMap?: AliasMap, ): Record { // The order of adding properties to keyObj affects its JSON serialization, @@ -971,17 +972,17 @@ function computeKeyObject( if (typeof prevKey === "string") { const subsets = aliasMap && aliasMap.subsets; const subset = subsets && subsets[prevKey]; - keyObj[prevKey] = computeKeyObject(response[prevKey], s, subset); + keyObj[prevKey] = computeKeyObject(response[prevKey], s, strict, subset); } } else { const aliases = aliasMap && aliasMap.aliases; const responseName = aliases && aliases[s] || s; - invariant( - hasOwn.call(response, responseName), - // TODO Make this appropriate for keyArgs as well - `Missing field '${responseName}' while computing key fields`, - ); - keyObj[prevKey = s] = response[responseName]; + if (hasOwn.call(response, responseName)) { + keyObj[prevKey = s] = response[responseName]; + } else { + invariant(!strict, `Missing field '${responseName}' while computing key fields`); + prevKey = void 0; + } } }); return keyObj; From db7255da768f03177b2b65bc3c6089efd287df45 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 1 Oct 2020 17:19:29 -0400 Subject: [PATCH 2/2] Mention PR #7109 in CHANGELOG.md. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9364ee534..c7d94bceb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ - The schema link package (`@apollo/client/link/schema`) will now validate incoming queries against its client-side schema, and return `errors` as a GraphQL server would.
[@amannn](https://github.com/amannn) in [#7094](https://github.com/apollographql/apollo-client/pull/7094) +- Allow optional arguments in `keyArgs: [...]` arrays for `InMemoryCache` field policies.
+ [@benjamn](https://github.com/benjamn) in [#7109](https://github.com/apollographql/apollo-client/pull/7109) + ## Apollo Client 3.2.2 ## Bug Fixes