Skip to content

Commit

Permalink
(graphcache) - Alias resolvers/updates parent to its parentKey in cac…
Browse files Browse the repository at this point in the history
…he methods (#1208)

* Add Context.parent info field and provide context globally

* Alias current context.parent in resolve to context.parentKey

* Move entity key aliasing to store.keyOfEntity

* Add ctx.parent to docs and affirm cache.resolve(parent, ...) support

* Add changeset

* Fix accidental type mismatch

* Add quick test for cache.keyOfEntity(parent)

* Update usage of store.keyOfEntity in query operation
  • Loading branch information
kitten authored Dec 9, 2020
1 parent a30a923 commit eea7d7f
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-kangaroos-march.md
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 28 additions & 10 deletions docs/api/graphcache.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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' });
Expand Down Expand Up @@ -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
Expand Down
12 changes: 3 additions & 9 deletions exchanges/graphcache/src/operations/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" +
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions exchanges/graphcache/src/operations/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,6 +41,7 @@ export const makeContext = (
store,
variables,
fragments,
parent: { __typename: typename },
parentTypeName: typename,
parentKey: entityKey,
parentFieldKey: '',
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion exchanges/graphcache/src/operations/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
Expand Down Expand Up @@ -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),
Expand Down
20 changes: 19 additions & 1 deletion exchanges/graphcache/src/store/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -344,6 +345,7 @@ describe('Store with ResolverConfig', () => {

describe('Store with OptimisticMutationConfig', () => {
let store;
let context;

beforeEach(() => {
store = new Store({
Expand All @@ -356,8 +358,8 @@ describe('Store with OptimisticMutationConfig', () => {
},
});

context = makeContext(store, {}, {}, 'Query', 'Query');
write(store, { query: Todos }, todosData);

InMemoryData.initDataState('read', store.data, null);
});

Expand All @@ -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');
Expand Down
37 changes: 17 additions & 20 deletions exchanges/graphcache/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]) {
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -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<T = Data, V = Variables>(
Expand Down
9 changes: 7 additions & 2 deletions exchanges/graphcache/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface OperationRequest {
}

export interface ResolveInfo {
parent: Data;
parentTypeName: string;
parentKey: string;
parentFieldKey: string;
Expand All @@ -75,7 +76,7 @@ export interface QueryInput<T = Data, V = Variables> {

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(
Expand All @@ -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<T = Data, V = Variables>(
Expand Down

0 comments on commit eea7d7f

Please sign in to comment.