Repository: FormidableLabs/urql (ts)

Issue:
(graphcache) - cache.resolve(parent, ...) doesn't seem to work
Hi guys,

After upgrade to `@urql/exchange-graphcache@3.3.1` `cache.resolve` doesn't seem to work always return `null` value.

```js
export const cache = cacheExchange({
  resolvers: {
    City: {
      name(parent, args, cache) {
        const id = cache.resolve(parent, "id");
        const name = cache.resolve(parent, "name");

        console.log({ id, name }); // null , null

        return `ID: ${id} - ` + name;
      }
    }
  }
});
```
`urql: 1.11.4`
`@urql/exchange-graphcache: 3.3.1`

**Steps to reproduce**
Check log tab
https://codesandbox.io/s/headless-wildflower-ynjsk

**The repo older version still work**
https://codesandbox.io/s/prod-monad-gmvyk


Pull request description:
(graphcache) - Alias resolvers/updates parent to its parentKey in cache methods
Resolves #1207

## Summary

This updates the shared context object to be provided on a global ref, which is then used in various cache methods, via `cache.keyOfEntity`, to alias the `parent` argument, which can be found on resolvers and updaters, to `info.parentKey`. This is done by adding (publicly) `info.parent`, which is the current parent's data as it is. (This avoids us having to share it on a separate object)

This further refactors `store.keyOfEntity` to take care of all key normalisation that has been done repeatedly in separate methods before, which further ensures that `cache[method](parent)` is going to work consistently as expected.

The reasoning here is that writing resolvers with `parent` as the first argument to further cache methods is so intuitive that we even did it ourselves in the docs, without thinking about it, although its support wasn't guaranteed as #1185 demonstrates. As an example:

```js
cacheExchange({
  resolvers: {
    Todo: {
      createdAt(parent, args, cache) {
        const date = cache.resolve(parent, 'createdAt');
        return new Date(date);
      }
    }
  },
})
```

## Set of changes

- Add `parent` field to `Context` and `ResolveInfo`
- Add global ref for the current `Context` to be exposed
- Add `store.keyOfEntity` alias if `entity === ctx.parent` to `ctx.parentKey`
- Refactor store methods to decrease repetition
- Update API docs


Patch:
diff --git a/.changeset/ten-kangaroos-march.md b/.changeset/ten-kangaroos-march.md
new file mode 100644
index 00000000..3f2aafa7
--- /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 b502c220..cd270a81 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 b8bd9425..d80e03b1 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 787e5c86..8a0aa757 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 950da5e6..d45d2b17 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.ts b/exchanges/graphcache/src/store/store.ts
index c4419843..81e576a2 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<T = Data, V = Variables>(
diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts
index 0b226c65..6f025cce 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<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(
@@ -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>(


Test patch:
diff --git a/exchanges/graphcache/src/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts
index 24276459..cb08f51c 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');