diff --git a/.changeset/ten-kangaroos-march.md b/.changeset/ten-kangaroos-march.md new file mode 100644 index 0000000000..3f2aafa70b --- /dev/null +++ b/.changeset/ten-kangaroos-march.md @@ -0,0 +1,5 @@ +--- +'@urql/exchange-graphcache': patch +--- + +Update `cache` methods, for instance `cache.resolve`, to consistently accept the `parent` argument from `resolvers` and `updates` and alias it to the parent's key (which is usually found on `info.parentKey`). This usage of `cache.resolve(parent, ...)` was intuitive and is now supported as expected. diff --git a/docs/api/graphcache.md b/docs/api/graphcache.md index b502c22013..cd270a8190 100644 --- a/docs/api/graphcache.md +++ b/docs/api/graphcache.md @@ -71,6 +71,22 @@ A `Resolver` receives four arguments when it's called: `parent`, `args`, `cache` | `cache` | `Cache` | The cache using which data can be read or written. [See `Cache`.](#cache) | | `info` | `Info` | Additional metadata and information about the current operation and the current field. [See `Info`.](#info) | +We can use the arguments it receives to either return new data based on just the arguments and other +cache information, but we may also read information about the parent and return new data for the +current field. + +```js +{ + Todo: { + createdAt(parent, args, cache) { + // Read `createdAt` on the parent but return a Date instance + const date = cache.resolve(parent, 'createdAt'); + return new Date(date); + } + } +} +``` + [Read more about how to set up `resolvers` on the "Computed Queries" page.](../graphcache/computed-queries.md) @@ -262,6 +278,7 @@ differing arguments) that is known to the cache. The `FieldInfo` interface has t This works on any given entity. When calling this method the cache works in reverse on its data structure, by parsing the entity's individual field keys. +p ```js cache.inspectFields({ __typename: 'Query' }); @@ -444,16 +461,17 @@ This is a metadata object that is passed to every resolver and updater function. information about the current GraphQL document and query, and also some information on the current field that a given resolver or updater is called on. -| Argument | Type | Description | -| ---------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `parentTypeName` | `string` | The field's parent entity's typename | -| `parentKey` | `string` | The field's parent entity's cache key (if any) | -| `parentFieldKey` | `string` | The current key's cache key, which is the parent entity's key combined with the current field's key (This is mostly obsolete) | -| `fieldName` | `string` | The current field's name | -| `fragments` | `{ [name: string]: FragmentDefinitionNode }` | A dictionary of fragments from the current GraphQL document | -| `variables` | `object` | The current GraphQL operation's variables (may be an empty object) | -| `partial` | `?boolean` | This may be set to `true` at any point in time (by your custom resolver or by _Graphcache_) to indicate that some data is uncached and missing | -| `optimistic` | `?boolean` | This is only `true` when an optimistic mutation update is running | +| Argument | Type | Description | +| ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `parent` | `Data` | The field's parent entity's data, as it was written or read up until now, which means it may be incomplete. [Use `cache.resolve`](#resolve) to read from it. | +| `parentTypeName` | `string` | The field's parent entity's typename | +| `parentKey` | `string` | The field's parent entity's cache key (if any) | +| `parentFieldKey` | `string` | The current key's cache key, which is the parent entity's key combined with the current field's key (This is mostly obsolete) | +| `fieldName` | `string` | The current field's name | +| `fragments` | `{ [name: string]: FragmentDefinitionNode }` | A dictionary of fragments from the current GraphQL document | +| `variables` | `object` | The current GraphQL operation's variables (may be an empty object) | +| `partial` | `?boolean` | This may be set to `true` at any point in time (by your custom resolver or by _Graphcache_) to indicate that some data is uncached and missing | +| `optimistic` | `?boolean` | This is only `true` when an optimistic mutation update is running | > **Note:** Using `info` is regarded as a last resort. Please only use information from it if > there's no other solution to get to the metadata you need. We don't regard the `Info` API as diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index b8bd9425ef..d80e03b1cf 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -185,15 +185,9 @@ export const readFragment = ( } const typename = getFragmentTypeName(fragment); - if (typeof entity !== 'string' && !entity.__typename) { + if (typeof entity !== 'string' && !entity.__typename) entity.__typename = typename; - } - - const entityKey = - typeof entity !== 'string' - ? store.keyOfEntity({ __typename: typename, ...entity } as Data) - : entity; - + const entityKey = store.keyOfEntity(entity as Data); if (!entityKey) { warn( "Can't generate a key for readFragment(...).\n" + @@ -311,7 +305,7 @@ const readSelection = ( ) { // We have to update the information in context to reflect the info // that the resolver will receive - updateContext(ctx, typename, entityKey, key, fieldName); + updateContext(ctx, data, typename, entityKey, key, fieldName); // We have a resolver for this field. // Prepare the actual fieldValue, so that the resolver can use it diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts index 787e5c86b7..8a0aa75751 100644 --- a/exchanges/graphcache/src/operations/shared.ts +++ b/exchanges/graphcache/src/operations/shared.ts @@ -22,11 +22,14 @@ export interface Context { parentTypeName: string; parentKey: string; parentFieldKey: string; + parent: Data; fieldName: string; partial: boolean; optimistic: boolean; } +export const contextRef: { current: Context | null } = { current: null }; + export const makeContext = ( store: Store, variables: Variables, @@ -38,6 +41,7 @@ export const makeContext = ( store, variables, fragments, + parent: { __typename: typename }, parentTypeName: typename, parentKey: entityKey, parentFieldKey: '', @@ -48,11 +52,14 @@ export const makeContext = ( export const updateContext = ( ctx: Context, + data: Data, typename: string, entityKey: string, fieldKey: string, fieldName: string ) => { + contextRef.current = ctx; + ctx.parent = data; ctx.parentTypeName = typename; ctx.parentKey = entityKey; ctx.parentFieldKey = fieldKey; diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts index 950da5e68e..d45d2b1748 100644 --- a/exchanges/graphcache/src/operations/write.ts +++ b/exchanges/graphcache/src/operations/write.ts @@ -253,7 +253,7 @@ const writeSelection = ( if (!resolver) continue; // We have to update the context to reflect up-to-date ResolveInfo - updateContext(ctx, typename, typename, fieldKey, fieldName); + updateContext(ctx, data, typename, typename, fieldKey, fieldName); fieldValue = data[fieldAlias] = ensureData( resolver(fieldArgs || {}, ctx.store, ctx) ); @@ -286,6 +286,7 @@ const writeSelection = ( // We have to update the context to reflect up-to-date ResolveInfo updateContext( ctx, + data, typename, typename, joinKeys(typename, fieldKey), diff --git a/exchanges/graphcache/src/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts index 24276459e4..cb08f51c63 100644 --- a/exchanges/graphcache/src/store/store.test.ts +++ b/exchanges/graphcache/src/store/store.test.ts @@ -5,6 +5,7 @@ import { gql, maskTypename } from '@urql/core'; import { mocked } from 'ts-jest/utils'; import { Data, StorageAdapter } from '../types'; +import { makeContext, updateContext } from '../operations/shared'; import { query } from '../operations/query'; import { write, writeOptimistic } from '../operations/write'; import * as InMemoryData from './data'; @@ -344,6 +345,7 @@ describe('Store with ResolverConfig', () => { describe('Store with OptimisticMutationConfig', () => { let store; + let context; beforeEach(() => { store = new Store({ @@ -356,8 +358,8 @@ describe('Store with OptimisticMutationConfig', () => { }, }); + context = makeContext(store, {}, {}, 'Query', 'Query'); write(store, { query: Todos }, todosData); - InMemoryData.initDataState('read', store.data, null); }); @@ -377,6 +379,22 @@ describe('Store with OptimisticMutationConfig', () => { InMemoryData.clearDataState(); }); + it('should resolve current parent argument fields', () => { + const randomData = { __typename: 'Todo', id: 1, createdAt: '2020-12-09' }; + + updateContext( + context, + randomData, + 'Todo', + 'Todo:1', + 'Todo:1.createdAt', + 'createdAt' + ); + + expect(store.keyOfEntity(randomData)).toBe(context.parentKey); + expect(store.keyOfEntity({})).not.toBe(context.parentKey); + }); + it('should resolve with a key as first argument', () => { const authorResult = store.resolve('Author:0', 'name'); expect(authorResult).toBe('Jovi'); diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts index c44198431c..81e576a2bf 100644 --- a/exchanges/graphcache/src/store/store.ts +++ b/exchanges/graphcache/src/store/store.ts @@ -17,6 +17,7 @@ import { } from '../types'; import { invariant } from '../helpers/help'; +import { contextRef } from '../operations/shared'; import { read, readFragment } from '../operations/query'; import { writeFragment, startWrite } from '../operations/write'; import { invalidateEntity } from '../operations/invalidate'; @@ -103,13 +104,17 @@ export class Store implements Cache { keyOfField = keyOfField; - keyOfEntity(data: Data) { + keyOfEntity(data: Data | null | string) { + if (data == null || typeof data === 'string') return data || null; const { __typename: typename, id, _id } = data; - if (!typename) { - return null; - } else if (this.rootNames[typename] !== undefined) { - return typename; - } + if (!typename) return null; + if (this.rootNames[typename]) return typename; + + // In resolvers and updaters we may have a specific parent + // object available that can be used to skip to a specific parent + // key directly without looking at its incomplete properties + if (contextRef.current && data === contextRef.current.parent) + return contextRef.current!.parentKey; let key: string | null | void; if (this.keys[typename]) { @@ -124,11 +129,8 @@ export class Store implements Cache { } resolveFieldByKey(entity: Data | string | null, fieldKey: string): DataField { - const entityKey = - entity !== null && typeof entity !== 'string' - ? this.keyOfEntity(entity) - : entity; - if (entityKey === null) return null; + const entityKey = this.keyOfEntity(entity); + if (!entityKey) return null; const fieldValue = InMemoryData.readRecord(entityKey, fieldKey); if (fieldValue !== undefined) return fieldValue; const link = InMemoryData.readLink(entityKey, fieldKey); @@ -143,9 +145,8 @@ export class Store implements Cache { return this.resolveFieldByKey(entity, keyOfField(field, args)); } - invalidate(entity: Data | string, field?: string, args?: Variables) { - const entityKey = - typeof entity === 'string' ? entity : this.keyOfEntity(entity); + invalidate(entity: Data | string | null, field?: string, args?: Variables) { + const entityKey = this.keyOfEntity(entity); invariant( entityKey, @@ -162,12 +163,8 @@ export class Store implements Cache { } inspectFields(entity: Data | string | null): FieldInfo[] { - const entityKey = - entity !== null && typeof entity !== 'string' - ? this.keyOfEntity(entity) - : entity; - - return entityKey !== null ? InMemoryData.inspectFields(entityKey) : []; + const entityKey = this.keyOfEntity(entity); + return entityKey ? InMemoryData.inspectFields(entityKey) : []; } updateQuery( diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index 0b226c6558..6f025cce4f 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -58,6 +58,7 @@ export interface OperationRequest { } export interface ResolveInfo { + parent: Data; parentTypeName: string; parentKey: string; parentFieldKey: string; @@ -75,7 +76,7 @@ export interface QueryInput { export interface Cache { /** keyOfEntity() returns the key for an entity or null if it's unkeyable */ - keyOfEntity(data: Data): string | null; + keyOfEntity(data: Data | null | string): string | null; /** keyOfField() returns the key for a field */ keyOfField( @@ -97,7 +98,11 @@ export interface Cache { inspectFields(entity: Data | string | null): FieldInfo[]; /** invalidate() invalidates an entity or a specific field of an entity */ - invalidate(entity: Data | string, fieldName?: string, args?: Variables): void; + invalidate( + entity: Data | string | null, + fieldName?: string, + args?: Variables + ): void; /** updateQuery() can be used to update the data of a given query using an updater function */ updateQuery(