From dd2ce7687ae9afa399e950a523fc7330284c25fe Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 16 Oct 2023 17:35:19 +0200 Subject: [PATCH] `cache.modify`: less strict types & dev runtime warnings (#11206) --- .api-reports/api-report-cache.md | 11 +- .api-reports/api-report-core.md | 11 +- .api-reports/api-report-react.md | 21 +- .api-reports/api-report-react_components.md | 21 +- .api-reports/api-report-react_context.md | 21 +- .api-reports/api-report-react_hoc.md | 21 +- .api-reports/api-report-react_hooks.md | 21 +- .api-reports/api-report-react_ssr.md | 21 +- .api-reports/api-report-testing.md | 21 +- .api-reports/api-report-testing_core.md | 21 +- .api-reports/api-report-utilities.md | 11 +- .api-reports/api-report.md | 11 +- .changeset/weak-worms-fetch.md | 5 + .size-limit.cjs | 4 +- src/cache/core/__tests__/cache.ts | 6 +- src/cache/core/types/common.ts | 12 +- src/cache/inmemory/__tests__/cache.ts | 220 ++++++++++++++++++++ src/cache/inmemory/entityStore.ts | 45 ++++ src/utilities/graphql/storeUtils.ts | 18 ++ src/utilities/index.ts | 1 + 20 files changed, 459 insertions(+), 64 deletions(-) create mode 100644 .changeset/weak-worms-fetch.md diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index e2135907143..34278f0c522 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -75,6 +75,13 @@ export type ApolloReducerConfig = { addTypename?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) type BroadcastOptions = Pick, "optimistic" | "onWatchUpdated">; @@ -875,8 +882,10 @@ export interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) export type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index c5a2b4ae963..bd0acbce3df 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -271,6 +271,13 @@ export type ApolloReducerConfig = { addTypename?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) interface Body_2 { // (undocumented) @@ -2005,8 +2012,10 @@ export interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) export type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 1a92b739f41..8a6f21665bd 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -337,6 +337,13 @@ type ApolloQueryResult = { partial?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // Warning: (ae-forgotten-export) The symbol "WatchQueryFetchPolicy" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1849,8 +1856,10 @@ interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -2184,11 +2193,11 @@ interface WatchQueryOptions = { partial?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) interface BaseMutationOptions = ApolloCache> extends Omit, "mutation"> { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts @@ -1584,8 +1591,10 @@ interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -1722,11 +1731,11 @@ interface WatchQueryOptions = { partial?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) interface BaseQueryOptions extends Omit, "query"> { // (undocumented) @@ -1519,8 +1526,10 @@ interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -1620,11 +1629,11 @@ interface WatchQueryOptions = { partial?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) interface BaseMutationOptions = ApolloCache> extends Omit, "mutation"> { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts @@ -1540,8 +1547,10 @@ interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -1664,11 +1673,11 @@ export function withSubscription = { partial?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // Warning: (ae-forgotten-export) The symbol "WatchQueryFetchPolicy" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1742,8 +1749,10 @@ interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -2075,11 +2084,11 @@ interface WatchQueryOptions = { partial?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) interface BaseQueryOptions extends Omit, "query"> { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts @@ -1506,8 +1513,10 @@ interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -1607,11 +1616,11 @@ interface WatchQueryOptions = { partial?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) namespace Cache_2 { // (undocumented) @@ -1538,8 +1545,10 @@ interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -1657,11 +1666,11 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // // src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:95:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:96:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:97:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:98:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:99:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:96:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:97:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:98:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:99:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/ApolloClient.ts:47:3 - (ae-forgotten-export) The symbol "UriFunction" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 6dd0aaf3bae..f5de976f0c6 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -297,6 +297,13 @@ type ApolloQueryResult = { partial?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) namespace Cache_2 { // (undocumented) @@ -1494,8 +1501,10 @@ interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -1613,11 +1622,11 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // // src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:95:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:96:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:97:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:98:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:99:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:96:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:97:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:98:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:99:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/ApolloClient.ts:47:3 - (ae-forgotten-export) The symbol "UriFunction" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index f10def0a20a..742cf060348 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -329,6 +329,13 @@ type ApolloReducerConfig = { // @public (undocumented) export function argumentsObjectFromField(field: FieldNode | DirectiveNode, variables?: Record): Object | null; +// @public (undocumented) +export type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) export function asyncMap(observable: Observable, mapFn: (value: V) => R | PromiseLike, catchFn?: (error: any) => R | PromiseLike): Observable; @@ -2291,7 +2298,7 @@ export interface StoreObject { } // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) export type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; @@ -2515,7 +2522,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:205:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/storeUtils.ts:202:12 - (ae-forgotten-export) The symbol "stringify" needs to be exported by the entry point index.d.ts +// src/utilities/graphql/storeUtils.ts:220:12 - (ae-forgotten-export) The symbol "stringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index f30c67954a9..e3f49b900c0 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -310,6 +310,13 @@ export type ApolloReducerConfig = { addTypename?: boolean; }; +// @public (undocumented) +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + // @public (undocumented) export type BackgroundQueryHookFetchPolicy = Extract; @@ -2452,8 +2459,10 @@ export interface StoreObject { __typename?: string; } +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type StoreObjectValueMaybeReference = StoreVal extends Record[] ? Readonly | readonly Reference[] : StoreVal extends Record ? StoreVal | Reference : StoreVal; +type StoreObjectValueMaybeReference = StoreVal extends Array> ? ReadonlyArray | Reference> : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; // @public (undocumented) export type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; diff --git a/.changeset/weak-worms-fetch.md b/.changeset/weak-worms-fetch.md new file mode 100644 index 00000000000..ac65f76dd98 --- /dev/null +++ b/.changeset/weak-worms-fetch.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`cache.modify`: Less strict types & new dev runtime warnings. diff --git a/.size-limit.cjs b/.size-limit.cjs index 223a3dce7ae..d63784ccf10 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "37960", + limit: "37956", }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "31970", + limit: "32017", }, ...[ "ApolloProvider", diff --git a/src/cache/core/__tests__/cache.ts b/src/cache/core/__tests__/cache.ts index f456695c7b5..6ca7e77e8b6 100644 --- a/src/cache/core/__tests__/cache.ts +++ b/src/cache/core/__tests__/cache.ts @@ -374,7 +374,7 @@ describe.skip("Cache type tests", () => { }, children(field) { expectTypeOf(field).toEqualTypeOf< - ReadonlyArray<{ anotherObject: false }> | ReadonlyArray + ReadonlyArray<{ anotherObject: false } | Reference> >(); return field; }, @@ -440,7 +440,7 @@ describe.skip("Cache type tests", () => { id: "foo", fields(field) { expectTypeOf(field).toEqualTypeOf< - boolean | symbol | readonly OtherChildEntry[] | readonly Reference[] + boolean | symbol | ReadonlyArray >(); return field; }, @@ -477,7 +477,7 @@ describe.skip("Cache type tests", () => { id: "foo", fields(field) { expectTypeOf(field).toEqualTypeOf< - boolean | symbol | readonly OtherChildEntry[] | readonly Reference[] + boolean | symbol | ReadonlyArray >(); return field; }, diff --git a/src/cache/core/types/common.ts b/src/cache/core/types/common.ts index 052844014a0..19200d4845e 100644 --- a/src/cache/core/types/common.ts +++ b/src/cache/core/types/common.ts @@ -5,6 +5,7 @@ import type { StoreObject, StoreValue, isReference, + AsStoreObject, } from "../../../utilities/index.js"; import type { StorageType } from "../../inmemory/policies.js"; @@ -104,13 +105,12 @@ export type Modifier = ( details: ModifierDetails ) => T | DeleteModifier | InvalidateModifier; -type StoreObjectValueMaybeReference = StoreVal extends Record< - string, - any ->[] - ? Readonly | readonly Reference[] +type StoreObjectValueMaybeReference = StoreVal extends Array< + infer Item extends Record +> + ? ReadonlyArray | Reference> : StoreVal extends Record - ? StoreVal | Reference + ? AsStoreObject | Reference : StoreVal; export type AllFieldsModifier> = Modifier< diff --git a/src/cache/inmemory/__tests__/cache.ts b/src/cache/inmemory/__tests__/cache.ts index 498936c1e31..a7289cd2c64 100644 --- a/src/cache/inmemory/__tests__/cache.ts +++ b/src/cache/inmemory/__tests__/cache.ts @@ -18,6 +18,7 @@ import { StoreReader } from "../readFromStore"; import { StoreWriter } from "../writeToStore"; import { ObjectCanon } from "../object-canon"; import { TypePolicies } from "../policies"; +import { spyOnConsole } from "../../../testing/internal"; disableFragmentWarnings(); @@ -3454,6 +3455,225 @@ describe("InMemoryCache#modify", () => { expect(cache.extract()).toEqual(snapshot); }); + + it("warns if `modify` returns a mixed array of objects and references", () => { + const cache = new InMemoryCache(); + const query = gql` + query { + me { + id + books { + id + title + } + } + } + `; + + interface Book { + __typename: "Book"; + id: string; + title: string; + } + + const book1: Book = { __typename: "Book", id: "1", title: "1984" }; + const book2: Book = { __typename: "Book", id: "2", title: "The Odyssey" }; + const book3: Book = { __typename: "Book", id: "3", title: "The Hobbit" }; + const book4: Book = { __typename: "Book", id: "4", title: "The Swarm" }; + + cache.writeQuery({ + query, + data: { + me: { + __typename: "User", + id: "42", + books: [book1, book2, book3], + }, + }, + }); + + expect(cache.readQuery({ query })).toEqual({ + me: { + __typename: "User", + books: [book1, book2, book3], + id: "42", + }, + }); + + { + using consoleSpy = spyOnConsole("warn"); + cache.modify<{ books: Book[] }>({ + id: cache.identify({ __typename: "User", id: "42" }), + fields: { + books(existingBooks, { toReference }) { + return [toReference(existingBooks[2])!, book4]; + }, + }, + }); + expect(consoleSpy.warn).toHaveBeenLastCalledWith( + "cache.modify: Writing an array with a mix of both References and Objects will not result in the Objects being normalized correctly.\n" + + "Please convert the object instance %o to a Reference before writing it to the cache by calling `toReference(object, true)`.", + book4 + ); + } + }); + + it("warns if `modify` returns a Reference that is not part of the store as part of an array", () => { + const cache = new InMemoryCache(); + const query = gql` + query { + me { + id + books { + id + title + } + } + } + `; + + type Book = { + __typename: "Book"; + id: string; + title: string; + }; + + const book1: Book = { __typename: "Book", id: "1", title: "1984" }; + const book2: Book = { __typename: "Book", id: "2", title: "The Odyssey" }; + const book3: Book = { __typename: "Book", id: "3", title: "The Hobbit" }; + const book4: Book = { __typename: "Book", id: "4", title: "The Swarm" }; + + cache.writeQuery({ + query, + data: { + me: { + __typename: "User", + id: "42", + books: [book1, book2, book3], + }, + }, + }); + + expect(cache.readQuery({ query })).toEqual({ + me: { + __typename: "User", + books: [book1, book2, book3], + id: "42", + }, + }); + + { + using consoleSpy = spyOnConsole("warn"); + cache.modify<{ books: Book[] }>({ + id: cache.identify({ __typename: "User", id: "42" }), + fields: { + books(existingBooks, { toReference }) { + return [...existingBooks, toReference(book4)!]; + }, + }, + }); + expect(consoleSpy.warn).toHaveBeenLastCalledWith( + "cache.modify: You are trying to write a Reference that is not part of the store: %o\n" + + "Please make sure to set the `mergeIntoStore` parameter to `true` when creating a Reference that is not part of the store yet:\n" + + "`toReference(object, true)`", + { __ref: "Book:4" } + ); + } + + // reading the cache *looks* good to the user + expect(cache.readQuery({ query })).toEqual({ + me: { + __typename: "User", + // this is what we're warning about - book 4 is not in the store + books: [book1, book2, book3], + id: "42", + }, + }); + expect(cache.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", me: { __ref: "User:42" } }, + "Book:1": book1, + "Book:2": book2, + "Book:3": book3, + // no Book:4 + "User:42": { + __typename: "User", + id: "42", + // Book:4 here is a dead ref + books: [ + { __ref: "Book:1" }, + { __ref: "Book:2" }, + { __ref: "Book:3" }, + { __ref: "Book:4" }, + ], + }, + }); + }); + + it("warns if `modify` returns a Reference that is not part of the store", () => { + const cache = new InMemoryCache(); + const query = gql` + query { + me { + id + } + } + `; + + type User = { + __typename: string; + id: string; + }; + + cache.writeQuery({ + query, + data: { + me: { + __typename: "User", + id: "42", + }, + }, + }); + + expect(cache.readQuery({ query })).toEqual({ + me: { + __typename: "User", + id: "42", + }, + }); + + { + using consoleSpy = spyOnConsole("warn"); + cache.modify<{ me: User }>({ + id: "ROOT_QUERY", + fields: { + me(existingUser, { toReference }) { + return toReference({ + __typename: "User", + id: "43", + })!; + }, + }, + }); + expect(consoleSpy.warn).toHaveBeenLastCalledWith( + "cache.modify: You are trying to write a Reference that is not part of the store: %o\n" + + "Please make sure to set the `mergeIntoStore` parameter to `true` when creating a Reference that is not part of the store yet:\n" + + "`toReference(object, true)`", + { __ref: "User:43" } + ); + } + + // reading the cache returns `null` + expect(cache.readQuery({ query })).toEqual(null); + expect(cache.extract()).toEqual({ + // User:43 is a dead ref + ROOT_QUERY: { __typename: "Query", me: { __ref: "User:43" } }, + "User:42": { + __typename: "User", + id: "42", + }, + // no User:43 + }); + }); }); describe("ReactiveVar and makeVar", () => { diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index 024e694b220..ddb03b274da 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -257,6 +257,51 @@ export abstract class EntityStore implements NormalizedCache { changedFields[storeFieldName] = newValue; needToMerge = true; fieldValue = newValue; + + if (__DEV__) { + const checkReference = (ref: Reference) => { + if (this.lookup(ref.__ref) === undefined) { + invariant.warn( + "cache.modify: You are trying to write a Reference that is not part of the store: %o\n" + + "Please make sure to set the `mergeIntoStore` parameter to `true` when creating a Reference that is not part of the store yet:\n" + + "`toReference(object, true)`", + ref + ); + return true; + } + }; + if (isReference(newValue)) { + checkReference(newValue); + } else if (Array.isArray(newValue)) { + // Warn about writing "mixed" arrays of Reference and non-Reference objects + let seenReference: boolean = false; + let someNonReference: unknown; + for (const value of newValue) { + if (isReference(value)) { + seenReference = true; + if (checkReference(value)) break; + } else { + // Do not warn on primitive values, since those could never be represented + // by a reference. This is a valid (albeit uncommon) use case. + if (typeof value === "object" && !!value) { + const [id] = this.policies.identify(value); + // check if object could even be referenced, otherwise we are not interested in it for this warning + if (id) { + someNonReference = value; + } + } + } + if (seenReference && someNonReference !== undefined) { + invariant.warn( + "cache.modify: Writing an array with a mix of both References and Objects will not result in the Objects being normalized correctly.\n" + + "Please convert the object instance %o to a Reference before writing it to the cache by calling `toReference(object, true)`.", + someNonReference + ); + break; + } + } + } + } } } } diff --git a/src/utilities/graphql/storeUtils.ts b/src/utilities/graphql/storeUtils.ts index 606c3667f00..439a47359ea 100644 --- a/src/utilities/graphql/storeUtils.ts +++ b/src/utilities/graphql/storeUtils.ts @@ -55,6 +55,24 @@ export interface StoreObject { [storeFieldName: string]: StoreValue; } +/** + * Workaround for a TypeScript quirk: + * types per default have an implicit index signature that makes them + * assignable to `StoreObject`. + * interfaces do not have that implicit index signature, so they cannot + * be assigned to `StoreObject`. + * This type just maps over a type or interface that is passed in, + * implicitly adding the index signature. + * That way, the result can be assigned to `StoreObject`. + * + * This is important if some user-defined interface is used e.g. + * in cache.modify, where the `toReference` method expects a + * `StoreObject` as input. + */ +export type AsStoreObject = { + [K in keyof T]: T[K]; +}; + export function isDocumentNode(value: any): value is DocumentNode { return ( isNonNullObject(value) && diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 6462f639fea..5f120fd4290 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -39,6 +39,7 @@ export { print } from "./graphql/print.js"; export type { StoreObject, + AsStoreObject, Reference, StoreValue, Directives,