From 124d12b5b71302e843318f68d6255a535b1432ca Mon Sep 17 00:00:00 2001 From: Gianluca Date: Tue, 23 Jun 2020 15:43:11 +0100 Subject: [PATCH] Cache pagination (#524) --- docs/ref-cache.md | 41 +++++++++++++- .../offix-cache/src/createMutationOptions.ts | 55 +++++++++++-------- .../src/createSubscriptionOptions.ts | 40 +++++++++----- 3 files changed, 98 insertions(+), 38 deletions(-) diff --git a/docs/ref-cache.md b/docs/ref-cache.md index b055c7c82..06c0c5838 100644 --- a/docs/ref-cache.md +++ b/docs/ref-cache.md @@ -76,9 +76,30 @@ const options = createMutationOptions(mutationOptions); apolloClient.mutate(options); ``` -> NOTE: Cache helpers currently support only GraphQL Queries that return arrays. -> For example `getUsers():[User]`. -> When working with single objects returned by Queries we usually do not need use any helper as Query will be updated automatically on every update +### Pagination and relationships + +Offix by default assumes that the object returned by mutations contains just your data. In situations when you need to wrap your data into some container to provide pagination and other information, Offix will require an additional parameter. For example when the API returns TaskPage etc. the actual data might be returned over `items`. + +When this query is cached, it may be necessary to update a field within the query and not the entire query, for example with relationships. In order to do this, it is necessary to provide the name of the field being updated with the `returnField` parameter. + +```javascript +const mutationOptions = { + mutation: ADD_COMMENT, + variables: { + title: 'comment title' + }, + updateQuery: { + query: GET_TASK, + variables: { + filterBy: 'some filter' + } + }, + returnType: 'Comment', + operationType: CacheOperation.ADD, + idField: 'id', + returnField: 'comments +}; +``` ## Subscription Helpers @@ -121,6 +142,20 @@ query.subscribeToMore(subscriptionOptions); The cache will now be kept up to date with automatic data deduplication being performed. +### Pagination and relationships + +Similarly to the mutation cache update helpers, it is necessary to provide the `returnField` parameter to specify the name of the field to update within the query. + +```javascript +export const editSubscriptionOptions = createSubscriptionOptions({ + subscriptionQuery: NEW_COMMENT, + cacheUpdateQuery: GET_TASK, + operationType: CacheOperation.ADD, + idField: 'id', + returnField: 'comments' +}); +``` + ### Multiple Subscriptions `offix-cache` also provides the ability to automatically call `subscribeToMore` on your `ObservableQuery`. This can be useful in a situation where you may have multiple subscriptions which can affect one single query. For example, if you have a `TaskAdded`, `TaskDeleted` and a `TaskUpdated` subscription you would need three separate `subscribeToMore` function calls. This can become tedious as your number of subscriptions grow. To combat this, we can use the `subscribeToMoreHelper` function from offix-cache to automatically handle this for us by passing it an array of subscriptions and their corresponding queries which need to be updated. diff --git a/packages/offix-cache/src/createMutationOptions.ts b/packages/offix-cache/src/createMutationOptions.ts index 830feed1e..390ea2488 100644 --- a/packages/offix-cache/src/createMutationOptions.ts +++ b/packages/offix-cache/src/createMutationOptions.ts @@ -69,6 +69,14 @@ interface CacheUpdateHelperOptions { * @default uses `id` field on the type */ idField?: string; + + /** + * String value for possible nested return type, primarily used + * with pagination or relationships. + * + * @default uses null + */ + returnField?: string | null; } /** @@ -173,7 +181,7 @@ export const getUpdateFunction = (options: CacheUpdateOptions): MutationUpdaterF * Generic cache update function that adds an item to a query that contains a list of items * Might be exported in the future */ -function addItemToQuery({ mutationName, updateQuery, idField = "id" }: CacheUpdateHelperOptions): MutationUpdaterFn { +function addItemToQuery({ mutationName, updateQuery, idField = "id", returnField = null }: CacheUpdateHelperOptions): MutationUpdaterFn { return (cache, { data }) => { const { query, variables } = deconstructQuery(updateQuery); const queryField = getOperationFieldName(query); @@ -186,29 +194,29 @@ function addItemToQuery({ mutationName, updateQuery, idField = "id" }: CacheUpda queryResult = {}; } - const result = queryResult[queryField]; + const result = (returnField) + ? queryResult[queryField][returnField] + : queryResult[queryField]; + if (result && operationData) { // FIXME deduplication should happen on subscriptions // We do that every time no matter if we have subscription - if (result.find) { + if (result instanceof Array) { const foundItem = !result.find((item: any) => { return item[idField] === operationData[idField]; }); if (foundItem) { result.push(operationData); } - - } else if (result.items && result.items.find) { - const foundItem = !result.items.find((item: any) => { - return item[idField] === operationData[idField]; - }); - if (foundItem) { - result.items.push(operationData); - } } } else { - queryResult[queryField] = [operationData]; + if (!returnField) { + queryResult[queryField] = [operationData]; + } else { + queryResult[queryField][returnField] = [operationData]; + } } + try { cache.writeQuery({ query, @@ -226,7 +234,7 @@ function addItemToQuery({ mutationName, updateQuery, idField = "id" }: CacheUpda * Generic cache update function that removes an item from a query that contains a list of items * Might be exported in the future */ -function deleteItemFromQuery({ mutationName, updateQuery, idField = "id" }: CacheUpdateHelperOptions): MutationUpdaterFn { +function deleteItemFromQuery({ mutationName, updateQuery, idField = "id", returnField = null }: CacheUpdateHelperOptions): MutationUpdaterFn { return (cache, { data }) => { const { query, variables } = deconstructQuery(updateQuery); const queryField = getOperationFieldName(query); @@ -242,18 +250,21 @@ function deleteItemFromQuery({ mutationName, updateQuery, idField = "id" }: Cach toBeRemoved = operationData; } let newData: any; - if (typeof queryResult[queryField].filter === "function") { - newData = queryResult[queryField].filter((item: any) => { - return toBeRemoved[idField] !== item[idField]; - }); - } else if (queryResult[queryField].items) { - const newItems = queryResult[queryField].items.filter((item: any) => { + + const prev = (returnField) + ? queryResult[queryField][returnField] + : queryResult[queryField]; + + if (prev instanceof Array) { + newData = prev.filter((item: any) => { return toBeRemoved[idField] !== item[idField]; }); - newData = queryResult[queryField]; - newData.items = newItems; } else { - newData = queryResult[queryField]; + if (!returnField) { + newData = queryResult[queryField]; + } else { + newData = queryResult[queryField][returnField]; + } } queryResult[queryField] = newData; diff --git a/packages/offix-cache/src/createSubscriptionOptions.ts b/packages/offix-cache/src/createSubscriptionOptions.ts index 2f29fe8f8..cb443c21a 100644 --- a/packages/offix-cache/src/createSubscriptionOptions.ts +++ b/packages/offix-cache/src/createSubscriptionOptions.ts @@ -10,6 +10,7 @@ export interface SubscriptionHelperOptions { cacheUpdateQuery: CacheUpdatesQuery; operationType: CacheOperation; idField?: string; + returnField?: string; } /** @@ -34,7 +35,8 @@ export const createSubscriptionOptions = (options: SubscriptionHelperOptions): S subscriptionQuery, cacheUpdateQuery, operationType, - idField = "id" + idField = "id", + returnField = null } = options; const document = (subscriptionQuery && (subscriptionQuery as QueryWithVariables).query) || (subscriptionQuery as DocumentNode); @@ -53,12 +55,27 @@ export const createSubscriptionOptions = (options: SubscriptionHelperOptions): S const mutadedItem = data[key]; const optype = operationType; - const obj = prev[queryField]; + + // necessary for relationships + // i.e. comments on a task field + const obj = (returnField) + ? prev[queryField][returnField] + : prev[queryField]; const updater = getUpdateQueryFunction(optype, idField); const result = updater(obj, mutadedItem); + + if (!returnField) { + return { + [queryField]: result + }; + } + return { - [queryField]: result + [queryField]: { + ...prev[queryField], + [returnField]: result + } }; } }; @@ -91,12 +108,11 @@ const getUpdateQueryFunction = (operationType: CacheOperation, idField = "id"): function addSubscriptionItem({ idField }: { idField: string }) { return (prev: [CacheItem], newItem: CacheItem | undefined) => { if (!newItem) { - return [...prev]; - } else { - return [...prev.filter(item => { - return item[idField] !== newItem[idField]; - }), newItem]; + return prev; } + return [...prev.filter(item => { + return item[idField] !== newItem[idField]; + }), newItem]; }; }; @@ -108,9 +124,8 @@ function deleteSubscriptionItem({ idField }: { idField: string }) { return (prev: [CacheItem], newItem: CacheItem | undefined) => { if (!newItem) { return []; - } else { - return prev.filter((item: any) => item[idField] !== newItem[idField]); } + return prev.filter((item: any) => item[idField] !== newItem[idField]); }; }; @@ -121,9 +136,8 @@ function deleteSubscriptionItem({ idField }: { idField: string }) { function updateSubscriptionItem({ idField }: { idField: string }) { return (prev: [CacheItem], newItem: CacheItem | undefined) => { if (!newItem) { - return [...prev]; - } else { - return prev.map((item: any) => item[idField] === newItem[idField] ? newItem : item); + return prev; } + return prev.map((item: any) => item[idField] === newItem[idField] ? newItem : item); }; };