diff --git a/packages/sync/package.json b/packages/sync/package.json index 8d1caa52..24245089 100644 --- a/packages/sync/package.json +++ b/packages/sync/package.json @@ -62,6 +62,7 @@ "graphql-tag": "2.10.1", "graphql-tools": "4.0.4", "idb-localstorage": "0.2.0", - "subscriptions-transport-ws": "0.9.16" + "subscriptions-transport-ws": "0.9.16", + "util": "^0.12.0" } } diff --git a/packages/sync/src/cache/createMutationOptions.ts b/packages/sync/src/cache/createMutationOptions.ts index ae28876f..85f63674 100644 --- a/packages/sync/src/cache/createMutationOptions.ts +++ b/packages/sync/src/cache/createMutationOptions.ts @@ -4,11 +4,12 @@ import { CacheOperation } from "./CacheOperation"; import { createOptimisticResponse } from "./createOptimisticResponse"; import { Query } from "./CacheUpdates"; import { getOperationFieldName, deconstructQuery } from "../utils/helpers"; +import { isArray } from "util"; export interface MutationHelperOptions { mutation: DocumentNode; variables: OperationVariables; - updateQuery: Query; + updateQuery: Query | Query[]; typeName: string; operationType?: CacheOperation; idField?: string; @@ -33,14 +34,31 @@ export const createMutationOptions = (options: MutationHelperOptions): MutationO typeName }); - const update = getUpdateFunction(operationName, idField, updateQuery, operationType); + const update: MutationUpdaterFn = (cache, { data }) => { + if (isArray(updateQuery)) { + for (const query of updateQuery) { + const updateFunction = getUpdateFunction(operationName, idField, query, operationType); + updateFunction(cache, { data }); + } + } else { + const updateFunction = getUpdateFunction(operationName, idField, updateQuery, operationType); + updateFunction(cache, { data }); + } + }; + return { mutation, variables, optimisticResponse, update }; }; -// returns the update function used to update the cache by the client -// ignores the scenario where the cache operation is an update as this is handled automatically -// from Apollo client 2.5 onwards. -const getUpdateFunction = ( +/** + * Generate the update function to update the cache for a given operation and query. + * Ignores the scenario where the cache operation is an update as this is handled automatically + * from Apollo Client 2.5 onwards. + * @param operation The title of the operation being performed + * @param idField The id field the item keys off + * @param updateQuery The Query to update in the cache + * @param opType The type of operation being performed + */ +export const getUpdateFunction = ( operation: string, idField: string, updateQuery: Query, diff --git a/packages/sync/src/cache/createSubscriptionOptions.ts b/packages/sync/src/cache/createSubscriptionOptions.ts index f0618dff..82c4bda9 100644 --- a/packages/sync/src/cache/createSubscriptionOptions.ts +++ b/packages/sync/src/cache/createSubscriptionOptions.ts @@ -11,6 +11,11 @@ export interface SubscriptionHelperOptions { idField?: string; } +/** + * Helper function which can be used to call subscribeToMore for multiple SubscriptionHelperOptions + * @param observableQuery the query which you would like to call subscribeToMore on. + * @param arrayOfHelperOptions the array of `SubscriptionHelperOptions` + */ export const subscribeToMoreHelper = (observableQuery: ObservableQuery, arrayOfHelperOptions: SubscriptionHelperOptions[]) => { for (const option of arrayOfHelperOptions) { @@ -18,6 +23,11 @@ export const subscribeToMoreHelper = (observableQuery: ObservableQuery, } }; +/** + * Creates a SubscribeToMoreOptions object which can be used with Apollo Client's subscribeToMore function + * on an observable query. + * @param options see `SubscriptionHelperOptions` + */ export const createSubscriptionOptions = (options: SubscriptionHelperOptions): SubscribeToMoreOptions => { const { subscriptionQuery, @@ -53,6 +63,11 @@ export const createSubscriptionOptions = (options: SubscriptionHelperOptions): S }; }; +/** + * Generate the standard update function to update the cache for a given operation type and query. + * @param opType The type of operation being performed + * @param idField The id field the item keys off + */ export const getUpdateQueryFunction = (opType: CacheOperation, idField = "id"): UpdateFunction => { let updateFunction: UpdateFunction; diff --git a/packages/sync/test/CacheHelpersTest.ts b/packages/sync/test/CacheHelpersTest.ts index cb0f7690..4c39c55e 100644 --- a/packages/sync/test/CacheHelpersTest.ts +++ b/packages/sync/test/CacheHelpersTest.ts @@ -1,12 +1,16 @@ import { expect, should } from "chai"; -import { MutationHelperOptions, CacheOperation, createMutationOptions, createSubscriptionOptions } from "../src/cache"; +import { CacheOperation, createMutationOptions, createSubscriptionOptions } from "../src/cache"; import { CREATE_ITEM, GET_ITEMS, + GET_LISTS, DELETE_ITEM, ITEM_CREATED_SUB, ITEM_DELETED_SUB, - ITEM_UPDATED_SUB + ITEM_UPDATED_SUB, + CREATE_LIST, + DOESNT_EXIST, + GET_NON_EXISTENT } from "./mock/mutations"; import { NormalizedCacheObject } from "apollo-cache-inmemory"; import { OfflineClient } from "../src"; @@ -39,6 +43,26 @@ describe("CacheHelpers", () => { operationType: CacheOperation.DELETE, idField: "id" }); + const builtOptionsWithArray = createMutationOptions({ + mutation: CREATE_LIST, + variables: { + "title": "new list" + }, + updateQuery: [{ query: GET_ITEMS, variables: {} }, { query: GET_LISTS, variables: {} }], + typeName: "List", + operationType: CacheOperation.ADD, + idField: "id" + }); + const builtNonExistent = createMutationOptions({ + mutation: DOESNT_EXIST, + variables: { + "title": "new list" + }, + updateQuery: { query: GET_NON_EXISTENT, variables: {} }, + typeName: "Something", + operationType: CacheOperation.ADD, + idField: "id" + }); const builtSubCreateOptions = createSubscriptionOptions({ subscriptionQuery: ITEM_CREATED_SUB, cacheUpdateQuery: GET_ITEMS, @@ -70,6 +94,34 @@ describe("CacheHelpers", () => { } }; + const createList = (id: string) => { + if (builtOptionsWithArray && builtOptionsWithArray.update) { + builtOptionsWithArray.update(client.cache, { + data: { + "createList": { + "id": id, + "title": "new list" + id, + "__typename": "Item" + } + } + }); + } + }; + + const createNonExistent = (id: string) => { + if (builtNonExistent && builtNonExistent.update) { + builtNonExistent.update(client.cache, { + data: { + "somethingFake": { + "id": id, + "title": "new list" + id, + "__typename": "Item" + } + } + }); + } + }; + const remove = (id: string) => { if (builtDeleteOptions && builtDeleteOptions.update) { builtDeleteOptions.update(client.cache, { @@ -96,6 +148,12 @@ describe("CacheHelpers", () => { "allItems": [] } }); + client.writeQuery({ + query: GET_LISTS, + data: { + "allLists": [] + } + }); }); it("ensures built mutation options include a function", () => { @@ -109,6 +167,13 @@ describe("CacheHelpers", () => { expect(client.readQuery({ query: GET_ITEMS }).allItems).to.have.length(1); }); + it("add item to cache with multiple", () => { + expect(client.readQuery({ query: GET_LISTS }).allLists).to.have.length(0); + createList("1"); + expect(client.readQuery({ query: GET_LISTS }).allLists).to.have.length(1); + expect(client.readQuery({ query: GET_ITEMS }).allItems).to.have.length(1); + }); + it("delete item from cache", async () => { expect(client.readQuery({ query: GET_ITEMS }).allItems).to.have.length(0); create("1"); @@ -232,7 +297,7 @@ describe("CacheHelpers", () => { { subscriptionData: { data: { - itemUpdated: { } + itemUpdated: {} } } }); diff --git a/packages/sync/test/mock/mutations.ts b/packages/sync/test/mock/mutations.ts index 6b3f5b5e..20f7cc62 100644 --- a/packages/sync/test/mock/mutations.ts +++ b/packages/sync/test/mock/mutations.ts @@ -8,6 +8,14 @@ mutation createItem($title: String!){ } `; +export const CREATE_LIST = gql` +mutation createList($title: String!){ + createList(title: $title){ + title + } + } +`; + export const DELETE_ITEM = gql` mutation deleteItem($id: ID!){ deleteItem(id: $id){ @@ -16,6 +24,14 @@ mutation deleteItem($id: ID!){ } `; +export const DOESNT_EXIST = gql` +mutation somethingFake($id: ID!){ + somethingFake(id: $id){ + title + } + } +`; + export const GET_ITEMS = gql` query allItems($first: Int) { allItems(first: $first) { @@ -25,6 +41,24 @@ export const GET_ITEMS = gql` } `; +export const GET_LISTS = gql` + query allLists($first: Int) { + allLists(first: $first) { + id + title + } +} +`; + +export const GET_NON_EXISTENT = gql` + query somethingFake($first: Int) { + somethingFake(first: $first) { + id + title + } +} +`; + export const ITEM_CREATED_SUB = gql` subscription itemCreated { itemCreated {