diff --git a/.changeset/thin-badgers-return.md b/.changeset/thin-badgers-return.md new file mode 100644 index 0000000000..59bb8c3f90 --- /dev/null +++ b/.changeset/thin-badgers-return.md @@ -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. diff --git a/docs/graphcache/local-resolvers.md b/docs/graphcache/local-resolvers.md index fdf19a4497..7a2af025e5 100644 --- a/docs/graphcache/local-resolvers.md +++ b/docs/graphcache/local-resolvers.md @@ -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. @@ -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 @@ -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 @@ -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. @@ -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 diff --git a/docs/graphcache/normalized-caching.md b/docs/graphcache/normalized-caching.md index 64f648c65f..930829cb73 100644 --- a/docs/graphcache/normalized-caching.md +++ b/docs/graphcache/normalized-caching.md @@ -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 @@ -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) diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts index 57198ef153..d964fd4712 100644 --- a/exchanges/graphcache/src/operations/shared.ts +++ b/exchanges/graphcache/src/operations/shared.ts @@ -223,8 +223,8 @@ export const ensureData = (x: DataField): Data | NullArray | null => x == null ? null : (x as Data | NullArray); export const ensureLink = (store: Store, ref: Link): 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++) diff --git a/exchanges/graphcache/src/store/data.ts b/exchanges/graphcache/src/store/data.ts index 7346eb7457..7ad2dd6781 100644 --- a/exchanges/graphcache/src/store/data.ts +++ b/exchanges/graphcache/src/store/data.ts @@ -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)); } }; diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts index 9c6fbfb00a..07390779fb 100644 --- a/exchanges/graphcache/src/store/store.ts +++ b/exchanges/graphcache/src/store/store.ts @@ -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) { @@ -248,15 +254,12 @@ export class Store< link( entity: Entity, field: string, - argsOrLink: FieldArgs | Link, - maybeLink?: Link + ...rest: [FieldArgs, Link] | [Link] ): void { - const args = (maybeLink !== undefined ? argsOrLink : null) as FieldArgs; - const link = ( - maybeLink !== undefined ? maybeLink : argsOrLink - ) as Link; - 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), diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index dfeb6aab85..f4c299bc44 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -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. * @@ -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. * @@ -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”). * @@ -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. *