diff --git a/.changeset/wise-hairs-pump.md b/.changeset/wise-hairs-pump.md new file mode 100644 index 0000000000..640d4a0bf3 --- /dev/null +++ b/.changeset/wise-hairs-pump.md @@ -0,0 +1,12 @@ +--- +'@urql/exchange-request-policy': patch +'@urql/exchange-graphcache': patch +'@urql/exchange-persisted': patch +'@urql/exchange-populate': patch +'@urql/exchange-context': patch +'@urql/exchange-execute': patch +'@urql/exchange-refocus': patch +'@urql/exchange-retry': patch +--- + +Add TSDocs for all exchanges, documenting API internals. diff --git a/exchanges/context/src/context.ts b/exchanges/context/src/context.ts index a56f304fee..0b8dc6f2ee 100644 --- a/exchanges/context/src/context.ts +++ b/exchanges/context/src/context.ts @@ -6,12 +6,64 @@ import { } from '@urql/core'; import { fromPromise, fromValue, mergeMap, pipe } from 'wonka'; +/** Input parameters for the {@link contextExchange}. */ export interface ContextExchangeArgs { - getContext: ( + /** Returns a new {@link OperationContext}, optionally wrapped in a `Promise`. + * + * @remarks + * `getContext` is called for every {@link Operation} the `contextExchange` + * receives and must return a new {@link OperationContext} or a `Promise` + * of it. + * + * The new `OperationContext` will be used to update the `Operation`'s + * context before it's forwarded to the next exchange. + */ + getContext( operation: Operation - ) => OperationContext | Promise; + ): OperationContext | Promise; } +/** Exchange factory modifying the {@link OperationContext} per incoming `Operation`. + * + * @param options - A {@link ContextExchangeArgs} configuration object. + * @returns the created context {@link Exchange}. + * + * @remarks + * The `contextExchange` allows the {@link OperationContext` to be easily + * modified per `Operation`. This may be useful to dynamically change the + * `Operation`’s parameters, even when we need to do so asynchronously. + * + * You must define a {@link ContextExchangeArgs.getContext} function, + * which may return a `Promise` or `OperationContext`. + * + * Hint: If the `getContext` function passed to this exchange returns a + * `Promise` it must be placed _after_ all synchronous exchanges, such as + * a `cacheExchange`. + * + * @example + * ```ts + * import { Client, cacheExchange, fetchExchange } from '@urql/core'; + * import { contextExchange } from '@urql/exchange-context'; + * + * const client = new Client({ + * url: '', + * exchanges: [ + * cacheExchange, + * contextExchange({ + * async getContext(operation) { + * const url = await loadDynamicUrl(); + * return { + * ...operation.context, + * url, + * }; + * }, + * }), + * fetchExchange, + * ], + * }); + * ``` + */ + export const contextExchange = ({ getContext }: ContextExchangeArgs): Exchange => ({ forward }) => { diff --git a/exchanges/execute/src/execute.ts b/exchanges/execute/src/execute.ts index 0a7c7948de..731887605f 100644 --- a/exchanges/execute/src/execute.ts +++ b/exchanges/execute/src/execute.ts @@ -30,10 +30,20 @@ import { OperationResult, } from '@urql/core'; +/** Input parameters for the {@link executeExchange}. + * @see {@link ExecutionArgs} which this interface mirrors. */ export interface ExecuteExchangeArgs { + /** GraphQL Schema definition that `Operation`s are execute against. */ schema: GraphQLSchema; + /** Context object or a factory function creating a `context` object. + * + * @remarks + * The `context` that is passed to the `schema` may either be passed + * or created from an incoming `Operation`, which also allows it to + * be recreated per `Operation`. + */ + context?: ((operation: Operation) => any) | any; rootValue?: any; - context?: ((op: Operation) => any) | any; fieldResolver?: GraphQLFieldResolver; typeResolver?: GraphQLTypeResolver; subscribeFieldResolver?: GraphQLFieldResolver; @@ -108,16 +118,18 @@ const makeExecuteSource = ( }); }; -/** Exchange for executing queries locally on a schema using graphql-js. */ +/** Exchange factory that executes operations against a GraphQL schema. + * + * @param options - A {@link ExecuteExchangeArgs} configuration object. + * @returns the created execute {@link Exchange}. + * + * @remarks + * The `executeExchange` executes GraphQL operations against the `schema` + * that it’s passed. As such, its options mirror the options that GraphQL.js’ + * {@link execute} function accepts. + */ export const executeExchange = - ({ - schema, - rootValue, - context, - fieldResolver, - typeResolver, - subscribeFieldResolver, - }: ExecuteExchangeArgs): Exchange => + (options: ExecuteExchangeArgs): Exchange => ({ forward }) => { return ops$ => { const sharedOps$ = share(ops$); @@ -139,7 +151,9 @@ export const executeExchange = ); const contextValue = - typeof context === 'function' ? context(operation) : context; + typeof options.context === 'function' + ? options.context(operation) + : options.context; // Filter undefined values from variables before calling execute() // to support default values within directives. @@ -162,15 +176,15 @@ export const executeExchange = return pipe( makeExecuteSource(operation, { - schema, + schema: options.schema, document: operation.query, - rootValue, + rootValue: options.rootValue, contextValue, variableValues, operationName, - fieldResolver, - typeResolver, - subscribeFieldResolver, + fieldResolver: options.fieldResolver, + typeResolver: options.typeResolver, + subscribeFieldResolver: options.subscribeFieldResolver, }), takeUntil(teardown$) ); diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts index 516bc1a7cc..9cdc77f2f5 100644 --- a/exchanges/graphcache/src/cacheExchange.ts +++ b/exchanges/graphcache/src/cacheExchange.ts @@ -38,6 +38,23 @@ type ResultMap = Map; type OptimisticDependencies = Map; type DependentOperations = Map; +/** Exchange factory that creates a normalized cache exchange. + * + * @param opts - A {@link CacheExchangeOpts} configuration object. + * @returns the created normalized cache {@link Exchange}. + * + * @remarks + * Graphcache is a normalized cache, enabled by using the `cacheExchange` + * in place of `@urql/core`’s. A normalized GraphQL cache uses typenames + * and key fields in the result to share a single copy for each unique + * entity across all queries. + * + * The `cacheExchange` may be passed a {@link CacheExchangeOpts} object + * to define custom resolvers, custom updates for mutations, + * optimistic updates, or to add custom key fields per type. + * + * @see {@link https://urql.dev/goto/docs/graphcache} for the full Graphcache docs. + */ export const cacheExchange = >(opts?: C): Exchange => ({ forward, client, dispatchDebug }) => { diff --git a/exchanges/graphcache/src/default-storage/index.ts b/exchanges/graphcache/src/default-storage/index.ts index 3e62b180c3..7f9baf1bd1 100644 --- a/exchanges/graphcache/src/default-storage/index.ts +++ b/exchanges/graphcache/src/default-storage/index.ts @@ -23,14 +23,34 @@ const getTransactionPromise = (transaction: IDBTransaction): Promise => { }; export interface StorageOptions { + /** Name of the IndexedDB database that will be used. + * @defaultValue `'graphcache-v4'` + */ idbName?: string; + /** Maximum age of cache entries (in days) after which data is discarded. + * @defaultValue `7` days + */ maxAge?: number; } +/** Sample storage adapter persisting to IndexedDB. */ export interface DefaultStorage extends StorageAdapter { + /** Clears the entire IndexedDB storage. */ clear(): Promise; } +/** Creates a default {@link StorageAdapter} which uses IndexedDB for storage. + * + * @param opts - A {@link StorageOptions} configuration object. + * @returns the created {@link StorageAdapter}. + * + * @remarks + * The default storage uses IndexedDB to persist the normalized cache for + * offline use. It demonstrates that the cache can be chunked by timestamps. + * + * Note: We have no data on stability of this storage and our Offline Support + * for large APIs or longterm use. Proceed with caution. + */ export const makeDefaultStorage = (opts?: StorageOptions): DefaultStorage => { if (!opts) opts = {}; diff --git a/exchanges/graphcache/src/extras/relayPagination.ts b/exchanges/graphcache/src/extras/relayPagination.ts index 26f5a9339c..0419330a9d 100644 --- a/exchanges/graphcache/src/extras/relayPagination.ts +++ b/exchanges/graphcache/src/extras/relayPagination.ts @@ -3,7 +3,17 @@ import { Cache, Resolver, Variables, NullArray } from '../types'; export type MergeMode = 'outwards' | 'inwards'; +/** Input parameters for the {@link relayPagination} factory. */ export interface PaginationParams { + /** Flip between inwards and outwards pagination. + * + * @remarks + * This is only relevant if you’re querying pages using forwards and + * backwards pagination at the same time. + * When set to `'inwards'`, its default, pages that have been queried + * forward are placed in front of all pages that were queried backwards. + * When set to `'outwards'`, the two sets are merged in reverse. + */ mergeMode?: MergeMode; } @@ -182,6 +192,25 @@ const getPage = ( return page; }; +/** Creates a {@link Resolver} that combines pages that comply to the Relay pagination spec. + * + * @param params - A {@link PaginationParams} configuration object. + * @returns the created Relay pagination {@link Resolver}. + * + * @remarks + * `relayPagination` is a factory that creates a {@link Resolver} that can combine + * multiple pages on a field that complies to the Relay pagination spec into a single, + * combined list for infinite scrolling. + * + * This resolver will only work on fields that return a `Connection` GraphQL object + * type, according to the Relay pagination spec. + * + * Hint: It's not recommended to use this when you can handle infinite scrolling + * in your UI code instead. + * + * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers#relay-pagination} for more information. + * @see {@link https://urql.dev/goto/docs/basics/ui-patterns/#infinite-scrolling} for an alternate approach. + */ export const relayPagination = ( params: PaginationParams = {} ): Resolver => { diff --git a/exchanges/graphcache/src/extras/simplePagination.ts b/exchanges/graphcache/src/extras/simplePagination.ts index 7c3abc45ad..e4b055bfa4 100644 --- a/exchanges/graphcache/src/extras/simplePagination.ts +++ b/exchanges/graphcache/src/extras/simplePagination.ts @@ -3,12 +3,38 @@ import { Resolver, Variables, NullArray } from '../types'; export type MergeMode = 'before' | 'after'; +/** Input parameters for the {@link simplePagination} factory. */ export interface PaginationParams { + /** The name of the field argument used to define the page’s offset. */ offsetArgument?: string; + /** The name of the field argument used to define the page’s length. */ limitArgument?: string; + /** Flip between forward and backwards pagination. + * + * @remarks + * When set to `'after'`, its default, pages are merged forwards and in order. + * When set to `'before'`, pages are merged in reverse, putting later pages + * in front of earlier ones. + */ mergeMode?: MergeMode; } +/** Creates a {@link Resolver} that combines pages of a primitive pagination field. + * + * @param options - A {@link PaginationParams} configuration object. + * @returns the created pagination {@link Resolver}. + * + * @remarks + * `simplePagination` is a factory that creates a {@link Resolver} that can combine + * multiple lists on a paginated field into a single, combined list for infinite + * scrolling. + * + * Hint: It's not recommended to use this when you can handle infinite scrolling + * in your UI code instead. + * + * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers#simple-pagination} for more information. + * @see {@link https://urql.dev/goto/docs/basics/ui-patterns/#infinite-scrolling} for an alternate approach. + */ export const simplePagination = ({ offsetArgument = 'skip', limitArgument = 'limit', diff --git a/exchanges/graphcache/src/offlineExchange.ts b/exchanges/graphcache/src/offlineExchange.ts index 0270731558..60ad3bf0e7 100644 --- a/exchanges/graphcache/src/offlineExchange.ts +++ b/exchanges/graphcache/src/offlineExchange.ts @@ -26,6 +26,7 @@ import { OptimisticMutationConfig, Variables, CacheExchangeOpts, + StorageAdapter, } from './types'; import { cacheExchange } from './cacheExchange'; @@ -57,13 +58,49 @@ const isOptimisticMutation = ( return false; }; +/** Input parameters for the {@link offlineExchange}. + * @remarks + * This configuration object extends the {@link CacheExchangeOpts} + * as the `offlineExchange` extends the regular {@link cacheExchange}. + */ export interface OfflineExchangeOpts extends CacheExchangeOpts { + /** Configures an offline storage adapter for Graphcache. + * + * @remarks + * A {@link StorageAdapter} allows Graphcache to write data to an external, + * asynchronous storage, and hydrate data from it when it first loads. + * This allows you to preserve normalized data between restarts/reloads. + * + * @see {@link https://urql.dev/goto/docs/graphcache/offline} for the full Offline Support docs. + */ + storage: StorageAdapter; + /** Predicate function to determine whether a {@link CombinedError} hints at a network error. + * + * @remarks + * Not ever {@link CombinedError} means that the device is offline and by default + * the `offlineExchange` will check for common network error messages and check + * `navigator.onLine`. However, when `isOfflineError` is passed it can replace + * the default offline detection. + */ isOfflineError?( error: undefined | CombinedError, result: OperationResult ): boolean; } +/** Exchange factory that creates a normalized cache exchange in Offline Support mode. + * + * @param opts - A {@link OfflineExchangeOpts} configuration object. + * @returns the created normalized, offline cache {@link Exchange}. + * + * @remarks + * The `offlineExchange` is a wrapper around the regular {@link cacheExchange} + * which adds logic via the {@link OfflineExchangeOpts.storage} adapter to + * recognize when it’s offline, when to retry failed mutations, and how + * to handle longer periods of being offline. + * + * @see {@link https://urql.dev/goto/docs/graphcache/offline} for the full Offline Support docs. + */ export const offlineExchange = (opts: C): Exchange => input => { diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 6f4b41585b..496f994c59 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -59,6 +59,9 @@ export interface QueryResult { data: null | Data; } +/** Reads a GraphQL query from the cache. + * @internal + */ export const query = ( store: Store, request: OperationRequest, diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts index 515d5e6cf2..5222ec9e7d 100644 --- a/exchanges/graphcache/src/operations/write.ts +++ b/exchanges/graphcache/src/operations/write.ts @@ -53,7 +53,9 @@ export interface WriteResult { dependencies: Dependencies; } -/** Writes a request given its response to the store */ +/** Writes a GraphQL response to the cache. + * @internal + */ export const write = ( store: Store, request: OperationRequest, diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts index f5516a7ad0..4dbcbb60b7 100644 --- a/exchanges/graphcache/src/store/store.ts +++ b/exchanges/graphcache/src/store/store.ts @@ -37,6 +37,9 @@ import { type RootField = 'query' | 'mutation' | 'subscription'; +/** Implementation of the {@link Cache} interface as created internally by the {@link cacheExchange}. + * @internal + */ export class Store< C extends Partial = Partial > implements Cache diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index df11155474..1eaa7981f8 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -2,151 +2,645 @@ import { AnyVariables, TypedDocumentNode } from '@urql/core'; import { GraphQLError, DocumentNode, FragmentDefinitionNode } from 'graphql'; import { IntrospectionData } from './ast'; -// Helper types +/** Nullable GraphQL list types of `T`. + * + * @remarks + * Any GraphQL list of a given type `T` that is nullable is + * expected to contain nullable values. Nested lists are + * also taken into account in Graphcache. + */ export type NullArray = Array>; +/** Dictionary of GraphQL Fragment definitions by their names. + * + * @remarks + * A map of {@link FragmentDefinitionNode | FragmentDefinitionNodes} by their + * fragment names from the original GraphQL document that Graphcache is + * executing. + */ export interface Fragments { [fragmentName: string]: void | FragmentDefinitionNode; } -// Scalar types are not entities as part of response data +/** Non-object JSON values as serialized by a GraphQL API + * @see {@link https://spec.graphql.org/October2021/#sel-DAPJDHAAEJHAKmzP} for the + * GraphQL spec’s serialization format. + */ export type Primitive = null | number | boolean | string; +/** Any GraphQL scalar object + * + * @remarks + * A GraphQL schema may define custom scalars that are resolved + * and serialized as objects. These objects could also be turned + * on the client-side into a non-JSON object, e.g. a `Date`. + * + * @see {@link https://spec.graphql.org/October2021/#sec-Scalars} for the + * GraphQL spec’s information on custom scalars. + */ export interface ScalarObject { constructor?: Function; [key: string]: any; } +/** GraphQL scalar value + * @see {@link https://spec.graphql.org/October2021/#sec-Scalars} for the GraphQL + * spec’s definition of scalars + */ export type Scalar = Primitive | ScalarObject; +/** Fields that Graphcache expects on GraphQL object (“entity”) results. + * + * @remarks + * Any object that comes back from a GraphQL API will have + * a `__typename` field from GraphQL Object types. + * + * The `__typename` field must be present as Graphcache updates + * GraphQL queries with type name introspection. + * Furthermore, Graphcache always checks for its default key + * fields, `id` and `_id` to be present. + */ export interface SystemFields { + /** GraphQL Object type name as returned by Type Name Introspection. + * @see {@link https://spec.graphql.org/October2021/#sec-Type-Name-Introspection} for + * more information on GraphQL’s Type Name introspection. + */ __typename: string; _id?: string | number | null; id?: string | number | null; } +/** Scalar values are stored separately from relations between entities. + * @internal + */ export type EntityField = undefined | Scalar | NullArray; + +/** Values on GraphQL object (“entity”) results. + * + * @remarks + * Any field that comes back from a GraphQL API will have + * values that are scalars, other objects, or arrays + * of scalars or objects. + */ export type DataField = Scalar | Data | NullArray | NullArray; +/** Definition of GraphQL object (“entity”) fields. + * + * @remarks + * Any object that comes back from a GraphQL API will have + * values that are scalars, other objects, or arrays + * of scalars or objects, i.e. the {@link DataField} type. + */ export interface DataFields { [fieldName: string]: DataField; } +/** Definition of GraphQL variables objects. + * @remarks + * Variables, as passed to GraphQL queries, can only contain scalar values. + * + * @see {@link https://spec.graphql.org/October2021/#sec-Coercing-Variable-Values} for the + * GraphQL spec’s coercion of GraphQL variables. + */ export interface Variables { [name: string]: Scalar | Scalar[] | Variables | NullArray; } +/** Definition of GraphQL objects (“entities”). + * + * @remarks + * An entity is expected to consist of a `__typename` + * fields, optionally the default `id` or `_id` key + * fields, and scalar values or other entities + * otherwise. + */ export type Data = SystemFields & DataFields; + +/** An entity, a key of an entity, or `null` + * + * @remarks + * When Graphcache accepts a reference to an entity, you may pass it a key of an entity, + * 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; + +/** A key of an entity, or `null`; or a list of keys. + * + * @remarks + * When Graphcache accepts a reference to one or more entities, you may pass it a + * key, an entity, or a list of entities or keys. This is often passed to {@link Cache.link} + * to update a field pointing to other GraphQL objects. + */ export type Link = null | Key | NullArray; -export type Connection = [Variables, string]; + +/** Arguments passed to a Graphcache field resolver. + * + * @remarks + * Arguments a field receives are similar to variables and can + * only contain scalars or other arguments objects. This + * is equivalent to the {@link Variables} type. + * + * @see {@link https://spec.graphql.org/October2021/#sec-Coercing-Field-Arguments} for the + * GraphQL spec’s coercion of field arguments. + */ export type FieldArgs = Variables | null | undefined; +/** Metadata about an entity’s cached field. + * + * @remarks + * As returned by {@link Cache.inspectFields}, `FieldInfo` specifies an entity’s cached field, + * split into the field’s key itself and the field’s original name and arguments. + */ export interface FieldInfo { + /** The field’s key which combines `fieldName` and `arguments`. */ fieldKey: string; + /** The field’s name, as defined on a GraphQL Object type. */ fieldName: string; + /** The arguments passed to the field as found on the cache. */ arguments: Variables | null; } +/** A key to an entity field split back into the entity’s key and the field’s key part. + * @internal + */ export interface KeyInfo { entityKey: string; fieldKey: string; } -// This is an input operation +/** Abstract type for GraphQL requests. + * + * @remarks + * Similarly to `@urql/core`’s `GraphQLRequest` type, `OperationRequest` + * requires the minimum fields that Grapcache requires to execute a + * GraphQL operation: its query document and variables. + */ export interface OperationRequest { query: DocumentNode | TypedDocumentNode; variables?: any; } +/** Metadata object passed to all resolver functions. + * + * @remarks + * `ResolveInfo`, similar to GraphQL.js’ `GraphQLResolveInfo` object, + * gives your resolvers a global state of the current GraphQL + * document traversal. + * + * `parent`, `parenTypeName`, `parentKey`, and `parentFieldKey` + * are particularly useful to make reusable resolver functions that + * must know on which field and type they’re being called on. + */ export interface ResolveInfo { + /** The parent GraphQL object. + * + * @remarks + * The GraphQL object that the resolver has been called on. Because this is + * a reference to raw GraphQL data, this may be incomplete or contain + * aliased fields! + */ parent: Data; + /** The parent object’s typename that the resolver has been called on. */ parentTypeName: string; + /** The parent object’s entity key that the resolver has been called on. */ parentKey: string; + /** Current field’s key that the resolver has been called on. */ parentFieldKey: string; + /** Current field that the resolver has been called on. */ fieldName: string; + /** Map of fragment definitions from the {@link DocumentNode}. */ fragments: Fragments; + /** Full original {@link Variables} object on the {@link OperationRequest}. */ variables: Variables; + /** Error that occurred for the current field, if any. + * + * @remarks + * If a {@link GraphQLError.path} points at the current field, the error + * will be set and provided here. This can be useful to recover from an + * error on a specific field. + */ error: GraphQLError | undefined; + /** Flag used to indicate whether the current GraphQL query is only partially cached. + * + * @remarks + * When Graphcache has {@link CacheExchangeOpts.schema} introspection information, + * it can automatically generate partial results and trigger a full API request + * in the background. + * Hence, this field indicates whether any data so far has only been partially + * resolved from the cache, and is only in use on {@link Resolver | Resolvers}. + * + * However, you can also flip this flag to `true` manually to indicate to + * the {@link cacheExchange} that it should still make a network request. + */ partial?: boolean; + /** Flag used to indicate whether the current GraphQL mutation is optimistically executed. + * + * @remarks + * An {@link UpdateResolver} is called for both API mutation responses and + * optimistic mutation reuslts, as generated by {@link OptimisticMutationResolver}. + * + * Since an update sometimes needs to perform different actions if it’s run + * optimistically, this flag is set to `true` during optimisti cupdates. + */ optimistic?: boolean; + /** Internal state used by Graphcache. + * @internal + */ __internal?: unknown; } +/** GraphQL document and variables that should be queried against the cache. + * + * @remarks + * `QueryInput` is a generic GraphQL request that should be executed against + * cached data, as accepted by {@link cache.readQuery}. + */ export interface QueryInput { query: string | DocumentNode | TypedDocumentNode; variables?: V; } +/** Interface to interact with cached data, which resolvers receive. */ export interface Cache { - /** keyOfEntity() returns the key for an entity or null if it's unkeyable */ + /** Returns the cache key for a given entity or `null` if it’s unkeyable. + * + * @param entity - the {@link Entity} to generate a key for. + * @returns the entity’s key or `null`. + * + * @remarks + * `cache.keyOfEntity` may be called with a partial GraphQL object (“entity”) + * and generates a key for it. It uses your {@link KeyingConfig} and otherwise + * defaults to `id` and `_id` fields. + * + * 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; - /** keyOfField() returns the key for a field */ + /** Returns the cache key for a field. + * + * @param fieldName - the field’s name. + * @param args - the field’s arguments, if any. + * @returns the field key + * + * @remarks + * `cache.keyOfField` is used to create a field’s cache key from a given + * field name and its arguments. This is used internally by {@link cache.resolve} + * to combine an entity key and a field key into a path that normalized data is + * accessed on in Graphcache’s internal data structure. + */ keyOfField(fieldName: string, args?: FieldArgs): string | null; - /** resolve() retrieves the value (or link) of a field on any entity, given a partial/keyable entity or an entity key */ + /** Returns a cached value on a given entity’s field. + * + * @param entity - a GraphQL object (“entity”) or an entity key. + * @param fieldName - the field’s name. + * @param args - the field’s arguments, if any. + * @returns the field’s value or the entity key(s) this field is pointing at. + * + * @remarks + * `cache.resolve` is used to retrieve either the cached value of a field, or + * to get the relation of the field (“link”). When a cached field points at + * another normalized entity, this method will return the related entity key + * (or a list, if it’s pointing at a list of entities). + * + * As such, if you’re accessing a nested field, you may have to call + * `cache.resolve` again and chain its calls. + * + * Hint: If you have a field key from {@link FieldInfo} or {@link cache.keyOfField}, + * you may pass it as a second argument. + * + * @example + * ```ts + * const authorName = cache.resolve( + * cache.resolve({ __typename: 'Book', id }, 'author'), + * 'name' + * ); + * ``` + */ resolve(entity: Entity, fieldName: string, args?: FieldArgs): DataField; - /** @deprecated use resolve() instead */ + /** 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; - /** inspectFields() retrieves all known fields for a given entity */ + /** Returns a list of cached fields for a given GraphQL object (“entity”). + * + * @param entity - a GraphQL object (“entity”) or an entity key. + * @returns a list of {@link FieldInfo} objects. + * + * @remarks + * `cache.inspectFields` can be used to list out all known fields + * of a given entity. This can be useful in an {@link UpdateResolver} + * if you have a `Query` field that accepts many different arguments, + * for instance a paginated field. + * + * The returned list of fields are all fields that the cache knows about, + * and you may have to filter them by name or arguments to find only which + * ones you need. + * + * Hint: This method is theoretically a slower operation than simple + * cache lookups, as it has to decode field keys. It’s only recommended + * to be used in updaters. + */ inspectFields(entity: Entity): FieldInfo[]; - /** invalidate() invalidates an entity or a specific field of an entity */ + /** Deletes a cached entity or an entity’s field. + * + * @param entity - a GraphQL object (“entity”) or an entity key. + * @param fieldName - optionally, a field name. + * @param args - optionally, the field’s arguments, if any. + * + * @remarks + * `cache.invalidate` can be used in updaters to delete data from + * the cache. This will cause the {@link cacheExchange} to reexecute + * queries that contain the deleted data. + * + * If you only pass its first argument, the entire entity is deleted. + * 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; - /** updateQuery() can be used to update the data of a given query using an updater function */ + /** Updates a GraphQL query‘s cached data. + * + * @param input - a {@link QueryInput}, which is a GraphQL query request. + * @param updater - a function called with the query’s result or `null` in case of a cache miss, which + * may return updated data, which is written to the cache using the query. + * + * @remarks + * `cache.updateQuery` can be used to update data for an entire GraphQL query document. + * When it's passed a GraphQL query request, it calls the passed `updater` function + * with the cached result for this query. You may then modify and update the data and + * return it, after which it’s written back to the cache. + * + * Hint: While this allows for large updates at once, {@link cache.link}, + * {@link cache.resolve}, and {@link cache.writeFragment} are often better + * choices for more granular and compact updater code. + * + * @example + * ```ts + * cache.updateQuery({ query: TodoList }, data => { + * data.todos.push(newTodo); + * return data; + * }); + * ``` + */ updateQuery( input: QueryInput, updater: (data: T | null) => T | null ): void; - /** readQuery() retrieves the data for a given query */ + /** Returns a GraphQL query‘s cached result. + * + * @param input - a {@link QueryInput}, which is a GraphQL query request. + * @returns the cached data result of the query or `null`, in case of a cache miss. + * + * @remarks + * `cache.readQuery` can be used to read an entire query’s data all at once + * from the cache. + * + * This can be useful when typing out many {@link cache.resolve} + * calls is too tedious. + * + * @example + * ```ts + * const data = cache.readQuery({ + * query: TodosQuery, + * variables: { from: 0, limit: 10 } + * }); + * ``` + */ readQuery(input: QueryInput): T | null; - /** readFragment() retrieves the data for a given fragment, given a partial/keyable entity or an entity key */ + /** Returns a GraphQL fragment‘s cached result. + * + * @param fragment - a {@link DocumentNode} containing a fragment definition. + * @param entity - a GraphQL object (“entity”) or an entity key to read the fragment on. + * @param variables - optionally, GraphQL variables, if the fragments use any. + * @returns the cached data result of the fragment or `null`, in case of a cache miss. + * + * @remarks + * `cache.readFragment` can be used to read an entire query’s data all at once + * from the cache. + * + * It attempts to read the fragment starting from the `entity` that’s passed to it. + * If the entity can’t be resolved or has mismatching types, `null` is returned. + * + * This can be useful when typing out many {@link cache.resolve} + * calls is too tedious. + * + * @example + * ```ts + * const data = cache.readFragment( + * gql`fragment _ on Todo { id, text }`, + * { id: '123' } + * ); + * ``` + */ readFragment( fragment: DocumentNode | TypedDocumentNode, entity: string | Data | T, variables?: V ): T | null; - /** writeFragment() can be used to update the data of a given fragment, given an entity that is supposed to be written using the fragment */ + /** Writes a GraphQL fragment to the cache. + * + * @param fragment - a {@link DocumentNode} containing a fragment definition. + * @param data - a GraphQL object to be written with the given fragment. + * @param variables - optionally, GraphQL variables, if the fragments use any. + * + * @remarks + * `cache.writeFragment` can be used to write an entity to the cache. + * The method will generate a key for the `data` it’s passed, and start writing + * it using the fragment. + * + * This method is used when writing scalar values to the cache. + * Since it's rare for an updater to write values to the cache, {@link cache.link} + * only allows relations (“links”) to be updated, and `cache.writeFragment` is + * instead used when writing multiple scalars. + * + * @example + * ```ts + * const data = cache.writeFragment( + * gql`fragment _ on Todo { id, text }`, + * { id: '123', text: 'New Text' } + * ); + * ``` + */ writeFragment( fragment: DocumentNode | TypedDocumentNode, data: T, variables?: V ): void; - /** link() can be used to update a given entity field to link to another entity or entities */ + /** Updates the relation (“link”) from an entity’s field to another entity. + * + * @param entity - a GraphQL object (“entity”) or an entity key. + * @param fieldName - the field’s name. + * @param args - optionally, the field’s arguments, if any. + * @param link - the GraphQL object(s) that should be set on this field. + * + * @remarks + * The normalized cache stores relations between GraphQL objects separately. + * As such, a field can be updated using `cache.link` to point to a new entity, + * or a list of entities. + * + * In other words, `cache.link` is used to set a field to point to another + * entity or a list of entities. + * + * @example + * ```ts + * const todos = cache.resolve('Query', 'todos'); + * cache.link('Query', 'todos', [...todos, newTodo]); + * ``` + */ link( entity: Entity, field: string, args: FieldArgs, link: Link ): void; - /** link() can be used to update a given entity field to link to another entity or entities */ link(entity: Entity, field: string, value: Link): void; } -type ResolverResult = +/** Values a {@link Resolver} may return. + * + * @remarks + * A resolver may return any value that a GraphQL object may contain. + * + * Additionally however, a resolver may return `undefined` to indicate that data + * isn’t available from the cache, i.e. to trigger a cache miss. + */ +export type ResolverResult = | DataField | (DataFields & { __typename?: string }) | null | undefined; +/** Input parameters for the {@link cacheExchange}. */ export type CacheExchangeOpts = { + /** Configures update functions which are called when the mapped fields are written to the cache. + * + * @remarks + * `updates` are commonly used to define additional changes to the cache for + * mutation or subscription fields. It may commonly be used to invalidate + * cached data or to modify lists after mutations. + * This is a map of types to fields to {@link UpdateResolver} functions. + * + * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates} for the full updates docs. + */ updates?: UpdatesConfig; + /** Configures resolvers which replace cached reuslts with custom values. + * + * @remarks + * `resolvers` is a map of types to fields to {@link Resolver} functions. + * These functions allow us to replace cached field values with a custom + * result, either to replace values on GraphQL results, or to resolve + * entities from the cache for queries that haven't been sent to the API + * yet. + * + * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs. + */ resolvers?: ResolverConfig; + /** Configures optimistic updates to react to mutations instantly before an API response. + * + * @remarks + * `optimistic` is a map of mutation fields to {@link OptimisticMutationResolver} functions. + * These functions allow us to return result data for mutations to optimistically apply them. + * Optimistic updates are temporary updates to the cache’s data which allow an app to + * instantly reflect changes that a mutation will make. + * + * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates/#optimistic-updates} for the + * full optimistic updates docs. + */ optimistic?: OptimisticMutationConfig; + /** Configures keying functions for GraphQL types. + * + * @remarks + * `keys` is a map of GraphQL object type names to {@link KeyGenerator} functions. + * If a type in your API has no key field or a key field that isn't the default + * `id` or `_id` fields, you may define a custom key generator for the type. + * + * Hint: Graphcache will log warnings when it finds objects that have no keyable + * fields, which will remind you to define these functions gradually for every + * type that needs them. + * + * @see {@link https://urql.dev/goto/docs/graphcache/normalized-caching/#custom-keys-and-non-keyable-entities} for + * the full keys docs. + */ keys?: KeyingConfig; + /** Configures Graphcache with Schema Introspection data. + * + * @remarks + * Passing a `schema` to Graphcache enables it to do non-heuristic fragment + * matching, and be certain when a fragment matches against a union or interface + * on your schema. + * + * It also enables a mode called “Schema Awareness”, which allows Graphcache to + * return partial GraphQL results, `null`-ing out fields that aren’t in the cache + * that are nullable on your schema, while requesting the full API response in + * the background. + * + * @see {@link https://urql.dev/goto/urql/docs/graphcache/schema-awareness} for + * the full keys docs on Schema Awareness. + */ schema?: IntrospectionData; + /** Configures an offline storage adapter for Graphcache. + * + * @remarks + * A {@link StorageAdapter} allows Graphcache to write data to an external, + * asynchronous storage, and hydrate data from it when it first loads. + * This allows you to preserve normalized data between restarts/reloads. + * + * Hint: If you’re trying to use Graphcache’s Offline Support, you may + * want to swap out the `cacheExchange` with the {@link offlineExchange}. + * + * @see {@link https://urql.dev/goto/docs/graphcache/offline} for the full Offline Support docs. + */ storage?: StorageAdapter; }; -// Cache resolvers are user-defined to overwrite an entity field result +/** Cache Resolver, which may resolve or replace data during cache reads. + * + * @param parent - The GraphQL object that is currently being constructed from cache data. + * @param args - This field’s arguments. + * @param cache - {@link Cache} interface. + * @param info - {@link ResolveInfo} interface. + * @returns a {@link ResolverResult}, which is an updated value, partial entity, or entity key + * + * @remarks + * A `Resolver`, as defined on the {@link ResolverConfig}, is called for + * a field’s type during cache reads, and can be used to deserialize or replace + * scalar values, or to resolve an entity from cached data, even if the + * current field hasn’t been cached from an API response yet. + * + * For instance, if you have a `Query.picture(id: ID!)` field, you may define + * a resolver that returns `{ __typename: 'Picture', id: args.id }`, since you + * know the key fields of the GraphQL object. + * + * @example + * ```ts + * cacheExchange({ + * resolvers: { + * Query: { + * // resolvers can be used to resolve cached entities without API requests + * todo: (_parent, args) => ({ __typename: 'Todo', id: args.id }), + * }, + * Todo: { + * // resolvers can also be used to replace/deserialize scalars + * updatedAt: parent => new Date(parent.updatedAt), + * }, + * }, + * }); + * ``` + * + * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs. + */ export type Resolver< ParentData = DataFields, Args = Variables, @@ -160,12 +654,55 @@ export type Resolver< ): Result; }['bivarianceHack']; +/** Configures resolvers which replace cached reuslts with custom values. + * + * @remarks + * A `ResolverConfig` is a map of types to fields to {@link Resolver} functions. + * These functions allow us to replace cached field values with a custom + * result, either to replace values on GraphQL results, or to resolve + * entities from the cache for queries that haven't been sent to the API + * yet. + * + * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs. + */ export type ResolverConfig = { [typeName: string]: { [fieldName: string]: Resolver | void; } | void; }; +/** Cache Updater, which defines additional cache updates after cache writes. + * + * @param parent - The GraphQL object that is currently being written to the cache. + * @param args - This field’s arguments. + * @param cache - {@link Cache} interface. + * @param info - {@link ResolveInfo} interface. + * + * @remarks + * An `UpdateResolver` (“updater”), as defined on the {@link UpdatesConfig}, is + * called for a field’s type during cache writes, and can be used to instruct + * the {@link Cache} to perform other cache updates at the same time. + * + * This is often used, for instance, to update lists or invalidate entities + * after a mutation response has come back from the API. + * + * @example + * ```ts + * cacheExchange({ + * updates: { + * Mutation: { + * // updaters can invalidate data from the cache + * deleteAuthor: (_parent, args, cache) => { + * cache.invalidate({ __typename: 'Author', id: args.id }); + * }, + * }, + * }, + * }); + * ``` + * + * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates} for the + * full cache updates docs. + */ export type UpdateResolver = { bivarianceHack( parent: ParentData, @@ -175,65 +712,239 @@ export type UpdateResolver = { ): void; }['bivarianceHack']; +/** A key functon, which is called to create a cache key for a GraphQL object (“entity”). + * + * @param data - The GraphQL object that a key is generated for. + * @returns a key `string` or `null` or unkeyable objects. + * + * @remarks + * By default, Graphcache will use an object’s `__typename`, and `id` or `_id` fields + * to generate a key for an object. However, not all GraphQL objects will have a unique + * field, and some objects don’t have a key at all. + * + * When one of your GraphQL object types has a different key field, you may define a + * function on the {@link KeyingConfig} to return its key field. + * You may also have objects that don’t have keys, like “Edge” objects, or scalar-like + * objects. For these, you can define a function that returns `null`, which tells + * Graphcache that it’s an embedded object, which only occurs on its parent and is + * globally unique. + * + * @see {@link https://urql.dev/goto/docs/graphcache/normalized-caching/#custom-keys-and-non-keyable-entities} for + * the full keys docs. + * + * @example + * ```ts + * cacheExchange({ + * keys: { + * Image: data => data.url, + * LatLng: () => null, + * }, + * }); + * ``` + */ export type KeyGenerator = { bivarianceHack(data: Data): string | null; }['bivarianceHack']; +/** Configures update functions which are called when the mapped fields are written to the cache. + * + * @remarks + * `UpdatesConfig` is a map of types to fields to {@link UpdateResolver} functions. + * These update functions are defined to instruct the cache to make additional changes + * when a field is written to the cache. + * + * As changes are often made after a mutation or subscription, the `typeName` is + * often set to `'Mutation'` or `'Subscription'`. + * + * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates} for the full updates docs. + * + * @example + * ```ts + * const updates = { + * Mutation: { + * deleteAuthor(_parent, args, cache) { + * // Delete the Author from the cache when Mutation.deleteAuthor is sent + * cache.invalidate({ __typename: 'Author', id: args.id }); + * }, + * }, + * }; + */ export type UpdatesConfig = { [typeName: string | 'Query' | 'Mutation' | 'Subscription']: { [fieldName: string]: UpdateResolver | void; } | void; }; +/** Remaps result type to allow for nested optimistic mutation resolvers. + * + * @remarks + * An {@link OptimisticMutationResolver} can not only return partial, nested + * mutation result data, but may also contain more optimistic mutation resolvers + * for nested fields, which allows fields with arguments to optimistically be + * resolved to dynamic values. + * + * @see {@link OptimisticMutationConfig} for more information. + */ export type MakeFunctional = T extends { __typename: string } ? WithTypename<{ [P in keyof T]?: MakeFunctional; }> : OptimisticMutationResolver | T; +/** Optimistic mutation resolver, which may return data that a mutation response will return. + * + * @param args - This field’s arguments. + * @param cache - {@link Cache} interface. + * @param info - {@link ResolveInfo} interface. + * @returns the field’s optimistic data + * + * @remarks + * Graphcache can update its cache optimistically via the {@link OptimisticMutationConfig}. + * An `OptimisticMutationResolver` should return partial data that a mutation will return + * once it completes and we receive its result. + * + * For instance, it could return the data that a deletion mutation may return + * optimistically, which might allow an updater to run early and your UI to update + * instantly. + * + * The result that this function returns may miss some fields that your mutation may return, + * especially if it contains GraphQL object that are already cached. It may also contain + * other, nested resolvers, which allows you to handle fields that accept arguments. + */ export type OptimisticMutationResolver< Args = Variables, Result = Link | Scalar > = { bivarianceHack( - vars: Args, + args: Args, cache: Cache, info: ResolveInfo ): MakeFunctional; }['bivarianceHack']; +/** Configures optimistic result functions which are called to get a mutation’s optimistic result. + * + * @remarks + * `OptimisticMutationConfig` is a map of mutation fields to {@link OptimisticMutationResolver} + * functions, which return result data for mutations to optimistically apply them. + * Optimistic updates are temporary updates to the cache’s data which allow an app to + * instantly reflect changes that a mutation will make. + * + * Hint: Results returned from optimistic functions may be partial, and may contain functions. + * If the returned optimistic object contains functions on fields, these are executed as nested + * optimistic resolver functions. + * + * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates/#optimistic-updates} for the + * full optimistic updates docs. + * + * @example + * ```ts + * const optimistic = { + * updateProfile: (args) => ({ + * __typename: 'UserProfile', + * id: args.id, + * name: args.newName, + * }), + * }; + */ export type OptimisticMutationConfig = { [mutationFieldName: string]: OptimisticMutationResolver; }; +/** Configures keying functions for GraphQL types. + * + * @remarks + * `KeyingConfig` is a map of GraphQL object type names to {@link KeyGenerator} functions. + * If a type in your API has no key field or a key field that isn't the default + * `id` or `_id` fields, you may define a custom key generator for the type. + * + * Keys are important to a normalized cache, because they’re the identity of the object + * that is shared across the cache, and helps the cache recognize shared/normalized data. + * + * Hint: Graphcache will log warnings when it finds objects that have no keyable + * fields, which will remind you to define these functions gradually for every + * type that needs them. + * + * @see {@link https://urql.dev/goto/docs/graphcache/normalized-caching/#custom-keys-and-non-keyable-entities} for + * the full keys docs. + * + * @example + * ```ts + * const keys = { + * Image: data => data.url, + * LatLng: () => null, + * }; + * ``` + */ export type KeyingConfig = { [typename: string]: KeyGenerator; }; -export type SerializedEntry = EntityField | Connection[] | Link; - +/** Serialized normalized caching data. */ export interface SerializedEntries { [key: string]: string | undefined; } +/** A serialized GraphQL request for offline storage. */ export interface SerializedRequest { query: string; variables: AnyVariables; } +/** Interface for a storage adapter, used by the {@link offlineExchange} for Offline Support. + * @see {@link https://urql.dev/goto/docs/graphcache/offline} for the full Offline Support docs. + * @see `@urql/exchange-graphcache/default-storage` for an example implementation using IndexedDB. + */ export interface StorageAdapter { + /** Called to rehydrate data when the {@link cacheExchange} first loads. + * @remarks + * `readData` is called when Graphcache first starts up, and loads cache entries + * using which it'll repopulate its normalized cache data. + */ readData(): Promise; + /** Called by the {@link cacheExchange} to write new data to the offline storage. + * @remarks + * `writeData` is called when Graphcache updated its cached data and wishes to + * persist this data to the offline storage. The data is a partial object and + * Graphcache does not write all its data at once. + */ writeData(delta: SerializedEntries): Promise; + /** Called to rehydrate metadata when the {@link offlineExchange} first loads. + * @remarks + * `readMetadata` is called when Graphcache first starts up, and loads + * metadata informing it of pending mutations that failed while the device + * was offline. + */ readMetadata?(): Promise; + /** Called by the {@link offlineExchange} to persist failed mutations. + * @remarks + * `writeMetadata` is called when a mutation failed to persist a queue + * of failed mutations to the offline storage that must be retried when + * the application is reloaded. + */ writeMetadata?(json: SerializedRequest[]): void; + /** Called to register a callback called when the device is back online. + * @remarks + * `onOnline` is called by the {@link offlineExchange} with a callback. + * This callback must be called when the device comes back online and + * will cause all failed mutations in the queue to be retried. + */ onOnline?(cb: () => void): any; } +/** Set of keys that have been modified or accessed. + * @internal + */ export type Dependencies = Set; -/** The type of cache operation being executed. */ +/** The type of cache operation being executed. + * @internal + */ export type OperationType = 'read' | 'write'; +/** Casts a given object type to have a required typename field. + * @internal + */ export type WithTypename = T & { __typename: NonNullable; }; diff --git a/exchanges/persisted/src/persistedExchange.ts b/exchanges/persisted/src/persistedExchange.ts index 8b5e9953f9..6f9a2040de 100644 --- a/exchanges/persisted/src/persistedExchange.ts +++ b/exchanges/persisted/src/persistedExchange.ts @@ -29,13 +29,89 @@ const isPersistedMiss = (error: CombinedError): boolean => const isPersistedUnsupported = (error: CombinedError): boolean => error.graphQLErrors.some(x => x.message === 'PersistedQueryNotSupported'); +/** Input parameters for the {@link persistedExchange}. */ export interface PersistedExchangeOptions { + /** Enforces GET method requests to be made for Persisted Queries. + * + * @remarks + * When enabled, the `persistedExchange` will set + * `OperationContext.preferGetMethod` to `'force'` on persisted queries, + * which will force requests to be made using a GET request. + * + * This is frequently used to make GraphQL requests more cacheable + * on CDNs. + * + * @defaultValue `true` - enabled + */ preferGetForPersistedQueries?: boolean; + /** Enforces non-automatic persisted queries by ignoring APQ errors. + * + * @remarks + * When enabled, the `persistedExchange` will ignore `PersistedQueryNotFound` + * and `PersistedQueryNotSupported` errors and assume that all persisted + * queries are already known to the API. + * + * This is used to switch from Automatic Persisted Queries to + * Persisted Queries. This is commonly used to obfuscate GraphQL + * APIs. + */ enforcePersistedQueries?: boolean; - generateHash?: (query: string, document: DocumentNode) => Promise; + /** Custom hashing function for persisted queries. + * + * @remarks + * By default, `persistedExchange` will create a SHA-256 hash for + * persisted queries automatically. If you're instead generating + * hashes at compile-time, or need to use a custom SHA-256 function, + * you may pass one here. + * + * Hint: The default SHA-256 function uses the WebCrypto API. This + * API is unavailable on React Native, which may require you to + * pass a custom function here. + */ + generateHash?(query: string, document: DocumentNode): Promise; + /** Enables persisted queries to be used for mutations. + * + * @remarks + * When enabled, the `persistedExchange` will also use the persisted queries + * logic for mutation operations. + * + * This is disabled by default, but often used on APIs that obfuscate + * their GraphQL APIs. + */ enableForMutation?: boolean; } +/** Exchange factory that adds support for Persisted Queries. + * + * @param options - A {@link PersistedExchangeOptions} configuration object. + * @returns the created persisted queries {@link Exchange}. + * + * @remarks + * The `persistedExchange` adds support for (Automatic) Persisted Queries + * to any `fetchExchange`, `subscriptionExchange`, or other API exchanges + * following it. + * + * It does so by adding the `persistedQuery` extensions field to GraphQL + * requests and handles `PersistedQueryNotFound` and + * `PersistedQueryNotSupported` errors. + * + * @example + * ```ts + * import { Client, cacheExchange, fetchExchange } from '@urql/core'; + * import { persistedExchange } from '@urql/exchange-persisted'; + * + * const client = new Client({ + * url: 'URL', + * exchanges: [ + * cacheExchange, + * persistedExchange({ + * preferGetForPersistedQueries: true, + * }), + * fetchExchange + * ], + * }); + * ``` + */ export const persistedExchange = (options?: PersistedExchangeOptions): Exchange => ({ forward }) => { diff --git a/exchanges/populate/src/populateExchange.ts b/exchanges/populate/src/populateExchange.ts index 0c508d85dc..320bf1ac99 100644 --- a/exchanges/populate/src/populateExchange.ts +++ b/exchanges/populate/src/populateExchange.ts @@ -20,13 +20,43 @@ import { Exchange, Operation, stringifyVariables } from '@urql/core'; import { getName, GraphQLFlatType, unwrapType } from './helpers/node'; import { traverse } from './helpers/traverse'; -interface Options { +/** Configuration options for the {@link populateExchange}'s behaviour */ +export interface Options { + /** Prevents populating fields for matching types. + * + * @remarks + * `skipType` may be set to a regular expression that, when matching, + * prevents fields to be added automatically for the given type by the + * `populateExchange`. + * + * @defaultValue `/^PageInfo|(Connection|Edge)$/` - Omit Relay pagination fields + */ skipType?: RegExp; + /** Specifies a maximum depth for populated fields. + * + * @remarks + * `maxDepth` may be set to a maximum depth at which fields are populated. + * This may prevent the `populateExchange` from adding infinitely deep + * recursive fields or simply too many fields. + * + * @defaultValue `2` - Omit fields past a depth of 2. + */ maxDepth?: number; } -interface PopulateExchangeOpts { +/** Input parameters for the {@link populateExchange}. */ +export interface PopulateExchangeOpts { + /** Introspection data for an API’s schema. + * + * @remarks + * `schema` must be passed Schema Introspection data for the GraphQL API + * this exchange is applied for. + * You may use the `@urql/introspection` package to generate this data. + * + * @see {@link https://spec.graphql.org/October2021/#sec-Schema-Introspection} for the Schema Introspection spec. + */ schema: IntrospectionQuery; + /** Configuration options for the {@link populateExchange}'s behaviour */ options?: Options; } @@ -50,6 +80,8 @@ const SKIP_COUNT_TYPE = /^PageInfo|(Connection|Edge)$/; /** Creates an `Exchange` handing automatic mutation selection-set population based on the * query selection-sets seen. * + * @param options - A {@link PopulateExchangeOpts} configuration object. + * @returns the created populate {@link Exchange}. * * @remarks * The `populateExchange` will create an exchange that monitors queries and @@ -62,11 +94,17 @@ const SKIP_COUNT_TYPE = /^PageInfo|(Connection|Edge)$/; * * @example * ```ts - * populateExchange({ schema, options: { maxDepth: 3, skipType: /Todo/ }}) + * populateExchange({ + * schema, + * options: { + * maxDepth: 3, + * skipType: /Todo/ + * }, + * }); * * const query = gql` * mutation { addTodo @popualte } - * ` + * `; * ``` */ export const populateExchange = diff --git a/exchanges/refocus/src/refocusExchange.ts b/exchanges/refocus/src/refocusExchange.ts index 4be61981e7..bc0bdf5bbe 100644 --- a/exchanges/refocus/src/refocusExchange.ts +++ b/exchanges/refocus/src/refocusExchange.ts @@ -1,6 +1,19 @@ import { pipe, tap } from 'wonka'; import { Exchange, Operation } from '@urql/core'; +/** Exchange factory that reexecutes operations after a user returns to the tab. + * + * @returns a new refocus {@link Exchange}. + * + * @remarks + * The `refocusExchange` will reexecute `Operation`s with the `cache-and-network` + * policy when a user switches back to your application's browser tab. This can + * effectively update all on-screen data when a user has stayed inactive for a + * long time. + * + * The `cache-and-network` policy will refetch data in the background, but will + * only refetch queries that are currently active. + */ export const refocusExchange = (): Exchange => { return ({ client, forward }) => ops$ => { diff --git a/exchanges/request-policy/src/requestPolicyExchange.ts b/exchanges/request-policy/src/requestPolicyExchange.ts index 4259cf1ed4..55d653fe6e 100644 --- a/exchanges/request-policy/src/requestPolicyExchange.ts +++ b/exchanges/request-policy/src/requestPolicyExchange.ts @@ -11,24 +11,25 @@ const defaultTTL = 5 * 60 * 1000; /** Input parameters for the {@link requestPolicyExchange}. */ export interface Options { /** Predicate allowing you to selectively not upgrade `Operation`s. - - @remarks - When `shouldUpgrade` is set, it may be used to selectively return a boolean - per `Operation`. This allows certain `Operation`s to not be upgraded to a - `cache-and-network` policy, when `false` is returned. - - By default, all `Operation`s are subject to be upgraded. - * operation to "cache-and-network". */ + * + * @remarks + * When `shouldUpgrade` is set, it may be used to selectively return a boolean + * per `Operation`. This allows certain `Operation`s to not be upgraded to a + * `cache-and-network` policy, when `false` is returned. + * + * By default, all `Operation`s are subject to be upgraded. + * operation to "cache-and-network". + */ shouldUpgrade?: (op: Operation) => boolean; /** The time-to-live (TTL) for which a request policy won't be upgraded. - - @remarks - The `ttl` defines the time frame in which the `Operation` won't be updated - with a `cache-and-network` request policy. If an `Operation` is sent again - and the `ttl` time period has expired, the policy is upgraded. - - @defaultValue `300_000` - 5min - */ + * + * @remarks + * The `ttl` defines the time frame in which the `Operation` won't be updated + * with a `cache-and-network` request policy. If an `Operation` is sent again + * and the `ttl` time period has expired, the policy is upgraded. + * + * @defaultValue `300_000` - 5min + */ ttl?: number; } @@ -48,10 +49,10 @@ export interface Options { * @example * ```ts * requestPolicyExchange({ - * // Upgrade when we haven't seen this operation for 1 second - * ttl: 1000, - * // and only upgrade operations that query the `todos` field. - * shouldUpgrade: op => op.kind === 'query' && op.query.definitions[0].name?.value === 'todos' + * // Upgrade when we haven't seen this operation for 1 second + * ttl: 1000, + * // and only upgrade operations that query the `todos` field. + * shouldUpgrade: op => op.kind === 'query' && op.query.definitions[0].name?.value === 'todos' * }); * ``` */ diff --git a/exchanges/retry/src/retryExchange.ts b/exchanges/retry/src/retryExchange.ts index d58a728b21..12d0a00b4b 100644 --- a/exchanges/retry/src/retryExchange.ts +++ b/exchanges/retry/src/retryExchange.ts @@ -19,32 +19,104 @@ import { OperationResult, } from '@urql/core'; +/** Input parameters for the {@link retryExchange}. */ export interface RetryExchangeOptions { + /** Specify the minimum time to wait until retrying. + * + * @remarks + * `initialDelayMs` specifies the minimum time (in milliseconds) to wait + * until a failed operation is retried. + * + * @defaultValue `1_000` - one second + */ initialDelayMs?: number; + /** Specifies the maximum time to wait until retrying. + * + * @remarks + * `maxDelayMs` specifies the maximum time (in milliseconds) to wait + * until a failed operation is retried. While `initialDelayMs` + * specifies the minimum amount of time, `randomDelay` may cause + * the delay to increase over multiple attempts. + * + * @defaultValue `15_000` - 15 seconds + */ maxDelayMs?: number; + /** Enables a random exponential backoff to increase the delay over multiple retries. + * + * @remarks + * `randomDelay`, unless disabled, increases the time until a failed + * operation is retried over multiple attempts. It increases the time + * starting at `initialDelayMs` by 1.5x with an added factor of 0–1, + * until `maxDelayMs` is reached. + * + * @defaultValue `true` - enables random exponential backoff + */ randomDelay?: boolean; + /** Specifies the maximum times an operation should be sent to the API. + * + * @remarks + * `maxNumberAttempts` defines the number of attempts an operation should + * be retried until it's considered failed. + * + * @defaultValue `2` - Retry once, i.e. two attempts + */ maxNumberAttempts?: number; - /** Conditionally determine whether an error should be retried */ - retryIf?: (error: CombinedError, operation: Operation) => boolean; - /** Conditionally update operations as they're retried (retryIf can be replaced with this) */ - retryWith?: ( + /** Predicate allowing you to selectively not retry `Operation`s. + * + * @remarks + * `retryIf` is called with a {@link CombinedError} and the {@link Operation} that + * failed. If this function returns false the failed `Operation` is not retried. + * + * @defaultValue `(error) => !!error.networkError` - retries only on network errors. + */ + retryIf?(error: CombinedError, operation: Operation): boolean; + /** Transform function allowing you to selectively replace a retried `Operation` or return nullish value. + * + * @remarks + * `retryWhen` is called with a {@link CombinedError} and the {@link Operation} that + * failed. If this function returns an `Operation`, `retryExchange` will replace the + * failed `Operation` and retry. It won't retry the `Operation` if a nullish value + * is returned. + * + * The `retryIf` function, if defined, takes precedence and overrides this option. + */ + retryWith?( error: CombinedError, operation: Operation - ) => Operation | null | undefined; + ): Operation | null | undefined; } -export const retryExchange = ({ - initialDelayMs, - maxDelayMs, - randomDelay, - maxNumberAttempts, - retryIf, - retryWith, -}: RetryExchangeOptions): Exchange => { - const MIN_DELAY = initialDelayMs || 1000; - const MAX_DELAY = maxDelayMs || 15000; - const MAX_ATTEMPTS = maxNumberAttempts || 2; - const RANDOM_DELAY = randomDelay !== undefined ? !!randomDelay : true; +/** Exchange factory that retries failed operations. + * + * @param options - A {@link RetriesExchangeOptions} configuration object. + * @returns the created retry {@link Exchange}. + * + * @remarks + * The `retryExchange` retries failed operations with specified delays + * and exponential backoff. + * + * You may define a {@link RetryExchangeOptions.retryIf} or + * {@link RetryExchangeOptions.retryWhen} function to only retry + * certain kinds of operations, e.g. only queries. + * + * @example + * ```ts + * retryExchange({ + * initialDelayMs: 1000, + * maxDelayMs: 15000, + * randomDelay: true, + * maxNumberAttempts: 2, + * retryIf: err => err && err.networkError, + * }); + * ``` + */ +export const retryExchange = (options: RetryExchangeOptions): Exchange => { + const { retryIf, retryWith } = options; + const MIN_DELAY = options.initialDelayMs || 1000; + const MAX_DELAY = options.maxDelayMs || 15000; + const MAX_ATTEMPTS = options.maxNumberAttempts || 2; + const RANDOM_DELAY = + options.randomDelay != null ? !!options.randomDelay : true; return ({ forward, dispatchDebug }) => ops$ => { diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index e31ab66c38..c2f410eeb9 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -115,7 +115,7 @@ export interface ClientOptions { * To easily get started you should consider using the {@link dedupExchange}, {@link cacheExchange} and {@link fetchExchange} * these are all exported from the core package. * - * @see {@link https://formidable.com/open-source/urql/docs/architecture/#the-client-and-exchanges} for more information + * @see {@link https://urql.dev/goto/docs/architecture/#the-client-and-exchanges} for more information * on what `Exchange`s are and how they work. */ exchanges: Exchange[]; @@ -198,7 +198,7 @@ export interface ClientOptions { * creating operations, managing consumers of active operations, sharing results for operations, * and more tasks as a “central hub”. * - * @see {@link https://formidable.com/open-source/urql/docs/architecture/#requests-and-operations-on-the-client} for more information + * @see {@link https://urql.dev/goto/docs/architecture/#requests-and-operations-on-the-client} for more information * on what the `Client` is and does. */ export interface Client { diff --git a/packages/core/src/exchanges/cache.ts b/packages/core/src/exchanges/cache.ts index f43c313fe9..41c64d97b6 100755 --- a/packages/core/src/exchanges/cache.ts +++ b/packages/core/src/exchanges/cache.ts @@ -35,7 +35,7 @@ const shouldSkip = ({ kind }: Operation) => * {@link OperationContext.additionalTypenames} for queries and mutations that * should invalidate one another. * - * @see {@link https://formidable.com/open-source/urql/docs/basics/document-caching/} for more information on this cache. + * @see {@link https://urql.dev/goto/docs/basics/document-caching} for more information on this cache. */ export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => { const resultCache: ResultCache = new Map(); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c418e879de..f53e9ad258 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -515,7 +515,7 @@ export interface OperationContext { * The {@link ExchangeIO} type describes how {@link Exchange | Exchanges} receive `Operation`s and return * `OperationResults`, using `teardown` `Operation`s to cancel ongoing operations. * - * @see {@link https://formidable.com/open-source/urql/docs/architecture/#the-client-and-exchanges} for more information + * @see {@link https://urql.dev/goto/docs/architecture/#the-client-and-exchanges} for more information * on the flow of Exchanges. */ export interface Operation< @@ -668,8 +668,8 @@ export interface ExchangeInput { * Like middleware, exchanges are composed, calling each other in a pipeline-like fashion, which is facilitated by exchanges * calling {@link ExchangeInput.forward}, which is set to the next exchange's {@link ExchangeIO} function in the pipeline. * - * @see {@link https://formidable.com/open-source/urql/docs/architecture/#the-client-and-exchanges} for more information on Exchanges. - * @see {@link https://formidable.com/open-source/urql/docs/advanced/authoring-exchanges/} on how Exchanges are authored. + * @see {@link https://urql.dev/goto/docs/architecture/#the-client-and-exchanges} for more information on Exchanges. + * @see {@link https://urql.dev/goto/docs/advanced/authoring-exchanges} on how Exchanges are authored. */ export type Exchange = (input: ExchangeInput) => ExchangeIO; @@ -686,7 +686,7 @@ export type Exchange = (input: ExchangeInput) => ExchangeIO; * Generally, the stream of `OperationResult` returned by {@link ExchangeInput.forward} is always merged and combined with * the `Exchange`'s own stream of results if the `Exchange` creates and delivers results of its own. * - * @see {@link https://formidable.com/open-source/urql/docs/advanced/authoring-exchanges/} on how Exchanges are authored. + * @see {@link https://urql.dev/goto/docs/advanced/authoring-exchanges} on how Exchanges are authored. */ export type ExchangeIO = (ops$: Source) => Source; diff --git a/packages/core/src/utils/error.ts b/packages/core/src/utils/error.ts index f0dd162a46..3777f9f9d9 100644 --- a/packages/core/src/utils/error.ts +++ b/packages/core/src/utils/error.ts @@ -44,7 +44,7 @@ const rehydrateGraphQlError = (error: any): GraphQLError => { * is set to this error, the `CombinedError` abstracts all errors, making it easier to handle only * a subset of error cases. * - * @see {@link https://formidable.com/open-source/urql/docs/basics/errors/} for more information on handling + * @see {@link https://urql.dev/goto/docs/basics/errors} for more information on handling * GraphQL errors and the `CombinedError`. */ export class CombinedError extends Error {