Skip to content

Commit

Permalink
feat(graphcache): Allow cache.resolve to return undefined for uncache…
Browse files Browse the repository at this point in the history
…d fields (#3333)
  • Loading branch information
kitten authored Jul 25, 2023
1 parent a0741aa commit 68d02c1
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-badgers-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': minor
---

Allow `cache.resolve` to return `undefined` when a value is not cached to make it easier to cause a cache miss in resolvers. **Reminder:** Returning `undefined` from a resolver means a field is uncached, while returning `null` means that a field’s value is `null` without causing a cache miss.
41 changes: 27 additions & 14 deletions docs/graphcache/local-resolvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ A resolver may be attached to any type's field and accepts four positional argum
docs](../api/graphcache.md#info).

The local resolvers may return any value that fits the query document's shape, however we must
ensure that what we return matches the types of our schema. It for instance isn't possible to turn a
ensure that what we return matches the types of our schema. It, for instance, isn't possible to turn a
record field into a link, i.e. replace a scalar with an entity. Instead, local resolvers are useful
to transform records, like dates in our previous example, or to imitate server-side logic to allow
Graphcache to retrieve more data from its cache without sending a query to our API.
Expand All @@ -70,6 +70,15 @@ methods to read from our cache, only ["Cache Updates"](./cache-updates.md) get t
the cache. If you call `cache.updateQuery`, `cache.writeFragment`, or `cache.link` in resolvers,
you‘ll get an error, since it‘s not possible to update the cache while reading from it.

When writing a resolver you’ll mostly use `cache.resolve`, which can be chained, to read field
values from the cache. When a field points to another entity we may get a key, but resolvers are
allowed to return keys or partial entities containing keys.

> **Note:** This essentially means that resolvers can return either scalar values for fields without
> selection sets, and either partial entities or keys for fields with selection sets, i.e.
> links / relations. When we return `null`, this will be interpreted a the literal GraphQL Null scalar,
> while returning `undefined` will cause a cache miss.
## Transforming Records

As we've explored in the ["Normalized Caching" page's section on
Expand Down Expand Up @@ -222,13 +231,12 @@ link can return a partial entity [or a key](#resolving-by-keys).

However sometimes we'll need to resolve data from other fields in our resolvers.

For records, if the other field is on the same `parent` entity, it may seem logical to access it on
`parent[otherFieldName]` as well, however the `parent` object will only be sparsely populated with
fields that the cache has already queried prior to reaching the resolver.

In the previous example, where we've created a resolver for `Todo.updatedAt` and accessed
`parent.updatedAt` to transform its value the `parent.updatedAt` field is essentially a shortcut
that allows us to get to the record quickly.
> **Note:** For records, if the other field is on the same `parent` entity, it may seem logical to access it on
> `parent[otherFieldName]` as well, however the `parent` object will only be sparsely populated with
> fields that the cache has already queried prior to reaching the resolver.
> In the previous example, where we've created a resolver for `Todo.updatedAt` and accessed
> `parent.updatedAt` to transform its value the `parent.updatedAt` field is essentially a shortcut
> that allows us to get to the record quickly.
Instead we can use [the `cache.resolve` method](../api/graphcache.md#resolve). This method
allows us to access Graphcache's cached data directly. It is used to resolve records or links on any
Expand Down Expand Up @@ -261,9 +269,10 @@ cacheExchange({

When we call `cache.resolve(parent, "updatedAt")`, the cache will look up the `"updatedAt"` field on
the `parent` entity, i.e. on the current `Todo` entity.
We've also previously learned that `parent` may not contain all fields that the entity may have and
may hence be missing its keyable fields, like `id`, so why does this then work?
It works because `cache.resolve(parent)` is a shortcut for `cache.resolve(info.parentKey)`.

> **Note:** We've also previously learned that `parent` may not contain all fields that the entity may have and
> may hence be missing its keyable fields, like `id`, so why does this then work?
> It works because `cache.resolve(parent)` is a shortcut for `cache.resolve(info.parentKey)`.
Like the `info.fieldName` property `info.parentKey` gives us information about the current state of
Graphcache's query operation. In this case, `info.parentKey` tells us what the parent's key is.
Expand Down Expand Up @@ -308,9 +317,13 @@ may return records for fields without selection sets, in other cases it may give
other entities ("links") instead. It can even give you arrays of keys or records when the field's
value contains a list.

It's a pretty flexible method that allows us to access arbitrary values from our cache, however, we
have to be careful about what value will be resolved by it, since the cache can't know itself what
type of value it may return.
When a value is not present in the cache, `cache.resolve` will instead return `undefined` to signal
that a value is uncached. Similarly, a resolver may return `undefined` to tell Graphcache that the
field isn’t cached and that a call to the API is necessary.

`cache.resolve` is a pretty flexible method that allows us to access arbitrary values from our cache,
however, we have to be careful about what value will be resolved by it, since the cache can't know
itself what type of value it may return.

The last trick this method allows you to apply is to access arbitrary fields on the root `Query`
type. If we call `cache.resolve("Query", ...)` then we're also able to access arbitrary fields
Expand Down
8 changes: 7 additions & 1 deletion docs/graphcache/normalized-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ Since it can access the field's arguments from the GraphQL query document, we ca
object is keyable, it will tell _Graphcache_ what the key of the returned entity is. In other words,
we've told it how to get to a `Todo` from the `Query.todo` field.

This mechanism is immensely more powerful than this example. We have two other use-cases that
This mechanism is immensely more powerful than this example. We have other use-cases that
resolvers may be used for:

- Resolvers can be applied to fields with records, which means that it can be used to change or
Expand All @@ -402,6 +402,12 @@ resolvers may be used for:
- Resolvers can return deeply nested results, which will be layered on top of the in-memory
relational cached data of _Graphcache_, which means that it can emulate infinite pagination and
other complex behaviour.
- Resolvers can change when a cache miss or hit occurs. Returning `null` means that a field’s value
is literally `null`, which will not cause a cache miss, while returning `undefined` will mean
a field’s value is uncached.
- Resolvers can return either partial entities or keys, so we can chain `cache.resolve` calls to
read fields from the cache, even when a field is pointing at another entity, since we can return
keys to the other entity directly.

[Read more about resolvers on the following page about "Local Resolvers".](./local-resolvers.md)

Expand Down
4 changes: 2 additions & 2 deletions exchanges/graphcache/src/operations/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,8 @@ export const ensureData = (x: DataField): Data | NullArray<Data> | null =>
x == null ? null : (x as Data | NullArray<Data>);

export const ensureLink = (store: Store, ref: Link<Entity>): Link => {
if (ref == null) {
return ref;
if (!ref) {
return ref || null;
} else if (Array.isArray(ref)) {
const link = new Array(ref.length);
for (let i = 0, l = link.length; i < l; i++)
Expand Down
10 changes: 4 additions & 6 deletions exchanges/graphcache/src/store/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,12 +422,10 @@ export const gc = () => {
};

const updateDependencies = (entityKey: string, fieldKey?: string) => {
if (fieldKey !== '__typename') {
if (entityKey !== currentData!.queryRootKey) {
currentDependencies!.add(entityKey);
} else if (fieldKey !== undefined) {
currentDependencies!.add(joinKeys(entityKey, fieldKey));
}
if (entityKey !== currentData!.queryRootKey) {
currentDependencies!.add(entityKey);
} else if (fieldKey !== undefined && fieldKey !== '__typename') {
currentDependencies!.add(joinKeys(entityKey, fieldKey));
}
};

Expand Down
33 changes: 18 additions & 15 deletions exchanges/graphcache/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,20 @@ export class Store<
return globalID || !key ? key : `${typename}:${key}`;
}

resolve(entity: Entity, field: string, args?: FieldArgs): DataField {
const fieldKey = keyOfField(field, args);
resolve(
entity: Entity,
field: string,
args?: FieldArgs
): DataField | undefined {
let fieldValue: DataField | undefined = 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);
return link || null;
if (entityKey) {
const fieldKey = keyOfField(field, args);
fieldValue = InMemoryData.readRecord(entityKey, fieldKey);
if (fieldValue === undefined)
fieldValue = InMemoryData.readLink(entityKey, fieldKey);
}
return fieldValue;
}

resolveFieldByKey(entity: Entity, field: string, args?: FieldArgs) {
Expand Down Expand Up @@ -248,15 +254,12 @@ export class Store<
link(
entity: Entity,
field: string,
argsOrLink: FieldArgs | Link<Entity>,
maybeLink?: Link<Entity>
...rest: [FieldArgs, Link<Entity>] | [Link<Entity>]
): void {
const args = (maybeLink !== undefined ? argsOrLink : null) as FieldArgs;
const link = (
maybeLink !== undefined ? maybeLink : argsOrLink
) as Link<Entity>;
const entityKey = ensureLink(this, entity);
if (typeof entityKey === 'string') {
const args = rest.length === 2 ? rest[0] : null;
const link = rest.length === 2 ? rest[1] : rest[0];
const entityKey = this.keyOfEntity(entity);
if (entityKey) {
InMemoryData.writeLink(
entityKey,
keyOfField(field, args),
Expand Down
21 changes: 16 additions & 5 deletions exchanges/graphcache/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export type Data = SystemFields & DataFields;
* as retrieved for instance by {@link Cache.keyOfEntity} or a partial GraphQL object
* (i.e. an object with a `__typename` and key field).
*/
export type Entity = null | Data | string;
export type Entity = undefined | null | Data | string;

/** A key of an entity, or `null`; or a list of keys.
*
Expand Down Expand Up @@ -284,7 +284,7 @@ export interface Cache {
* If it’s passed a `string` or `null`, it will simply return what it’s been passed.
* Objects that lack a `__typename` field will return `null`.
*/
keyOfEntity(entity: Entity): string | null;
keyOfEntity(entity: Entity | undefined): string | null;

/** Returns the cache key for a field.
*
Expand Down Expand Up @@ -327,14 +327,21 @@ export interface Cache {
* );
* ```
*/
resolve(entity: Entity, fieldName: string, args?: FieldArgs): DataField;
resolve(
entity: Entity | undefined,
fieldName: string,
args?: FieldArgs
): DataField | undefined;

/** Returns a cached value on a given entity’s field by its field key.
*
* @deprecated
* Use {@link cache.resolve} instead.
*/
resolveFieldByKey(entity: Entity, fieldKey: string): DataField;
resolveFieldByKey(
entity: Entity | undefined,
fieldKey: string
): DataField | undefined;

/** Returns a list of cached fields for a given GraphQL object (“entity”).
*
Expand Down Expand Up @@ -372,7 +379,11 @@ export interface Cache {
* However, if a field name (and optionally, its arguments) are passed,
* only a single field is erased.
*/
invalidate(entity: Entity, fieldName?: string, args?: FieldArgs): void;
invalidate(
entity: Entity | undefined,
fieldName?: string,
args?: FieldArgs
): void;

/** Updates a GraphQL query‘s cached data.
*
Expand Down

0 comments on commit 68d02c1

Please sign in to comment.