From 03f7f7676ed35d6e6ba291e4efcc7b18ab7c2f70 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Thu, 9 Feb 2017 15:51:04 -0500 Subject: [PATCH 01/29] add read method to ApolloClient --- src/ApolloClient.ts | 50 ++++++++++++++++++++++ src/data/readFromStore.ts | 4 +- test/readFromStore.ts | 87 +++++++++++++++++++++++++++++++++++++++ test/tests.ts | 2 +- 4 files changed, 141 insertions(+), 2 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index fd0abd90e24..0d9eb9f97a6 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -11,6 +11,7 @@ import { SelectionSetNode, /* tslint:enable */ + DocumentNode, } from 'graphql'; import { @@ -58,6 +59,14 @@ import { storeKeyNameFromFieldNameAndArgs, } from './data/storeUtils'; +import { + readQueryFromStore, +} from './data/readFromStore'; + +import { + getQueryDefinition, +} from './queries/getFromAST'; + import { version, } from './version'; @@ -336,6 +345,47 @@ export default class ApolloClient { return this.queryManager.startGraphQLSubscription(realOptions); } + /** + * Tries to read some data from the store without making a network request. + * + * By default we start reading from the root query, but if an `id` is + * provided then we will start reading from that location. + */ + public read ({ + selection, + variables, + id, + returnPartialData, + }: { + selection: DocumentNode, + variables?: Object, + id?: string, + returnPartialData?: boolean, + }): QueryType { + this.initStore(); + + // Throw the right validation error by trying to find a query in the document. + // We also run some extra validation below on the operation. + const operation = getQueryDefinition(selection); + + // Make sure that the query is in the format `{ ... }` vs. `query Name { ... }`. + // We don’t want our users to think they are running an actual query. + // Especially if they provide an `id`. + if (operation.operation !== 'query' || operation.name || (operation.variableDefinitions || []).length !== 0) { + throw new Error('Can only use a nameless query with no variable definitions to read from the store.'); + } + + const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; + + return readQueryFromStore({ + store: reduxRootSelector(this.store.getState()).data, + query: selection, + variables, + rootId: id, + returnPartialData, + }); + } + /** * Returns a reducer function configured according to the `reducerConfig` instance variable. */ diff --git a/src/data/readFromStore.ts b/src/data/readFromStore.ts index e1bd3dae6f8..5891e5d315f 100644 --- a/src/data/readFromStore.ts +++ b/src/data/readFromStore.ts @@ -56,6 +56,7 @@ export type ReadQueryOptions = { variables?: Object, returnPartialData?: boolean, previousResult?: any, + rootId?: string, config?: ApolloReducerConfig, }; @@ -247,6 +248,7 @@ export function diffQueryAgainstStore({ variables, returnPartialData = true, previousResult, + rootId = 'ROOT_QUERY', config, }: ReadQueryOptions): DiffResult { // Throw the right validation error by trying to find a query in the document @@ -264,7 +266,7 @@ export function diffQueryAgainstStore({ const rootIdValue = { type: 'id', - id: 'ROOT_QUERY', + id: rootId, previousResult, }; diff --git a/test/readFromStore.ts b/test/readFromStore.ts index 891e4dd41ac..51878fbc5ed 100644 --- a/test/readFromStore.ts +++ b/test/readFromStore.ts @@ -692,4 +692,91 @@ describe('reading from the store', () => { computedField: 'This is a string!5bit', }); }); + + it('will read from an arbitrary root id', () => { + const result: any = { + id: 'abcd', + stringField: 'This is a string!', + numberField: 5, + nullField: null, + nestedObj: { + id: 'abcde', + stringField: 'This is a string too!', + numberField: 6, + nullField: null, + } as StoreObject, + deepNestedObj: { + stringField: 'This is a deep string', + numberField: 7, + nullField: null, + } as StoreObject, + nullObject: null, + __typename: 'Item', + }; + + const store = { + 'ROOT_QUERY': assign({}, assign({}, omit(result, 'nestedObj', 'deepNestedObj')), { + __typename: 'Query', + nestedObj: { + type: 'id', + id: 'abcde', + generated: false, + }, + }) as StoreObject, + abcde: assign({}, result.nestedObj, { + deepNestedObj: { + type: 'id', + id: 'abcdef', + generated: false, + }, + }) as StoreObject, + abcdef: result.deepNestedObj as StoreObject, + } as NormalizedCache; + + const queryResult1 = readQueryFromStore({ + store, + rootId: 'abcde', + query: gql` + { + stringField + numberField + nullField + deepNestedObj { + stringField + numberField + nullField + } + } + `, + }); + + const queryResult2 = readQueryFromStore({ + store, + rootId: 'abcdef', + query: gql` + { + stringField + numberField + nullField + } + `, + }); + + assert.deepEqual(queryResult1, { + stringField: 'This is a string too!', + numberField: 6, + nullField: null, + deepNestedObj: { + stringField: 'This is a deep string', + numberField: 7, + nullField: null, + }, + }); + + assert.deepEqual(queryResult2, { + stringField: 'This is a deep string', + numberField: 7, + nullField: null, + }); + }); }); diff --git a/test/tests.ts b/test/tests.ts index 5fa1ffe9cde..ca4512f36d7 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -52,4 +52,4 @@ import './customResolvers'; import './isEqual'; import './cloneDeep'; import './assign'; -import './environment' +import './environment'; From f5f2ba6e75748d6ef092335c85010e19319223e7 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Thu, 9 Feb 2017 16:25:21 -0500 Subject: [PATCH 02/29] add tests for ApolloClient --- test/ApolloClient.ts | 179 +++++++++++++++++++++++++++++++++++++++++++ test/tests.ts | 1 + 2 files changed, 180 insertions(+) create mode 100644 test/ApolloClient.ts diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts new file mode 100644 index 00000000000..b105f6577ce --- /dev/null +++ b/test/ApolloClient.ts @@ -0,0 +1,179 @@ +import { assert } from 'chai'; +import gql from 'graphql-tag'; +import { Store } from '../src/store'; +import ApolloClient from '../src/ApolloClient'; + +describe('ApolloClient', () => { + describe('read', () => { + it('will read some data from state', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + }, + }, + }, + }, + }); + + assert.deepEqual(client.read({ selection: gql`{ a }` }), { a: 1 }); + assert.deepEqual(client.read({ selection: gql`{ b c }` }), { b: 2, c: 3 }); + assert.deepEqual(client.read({ selection: gql`{ a b c }` }), { a: 1, b: 2, c: 3 }); + }); + + it('will read some deeply nested data from state', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: 'foo', + generated: false, + }, + }, + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }, + }, + }, + }); + + assert.deepEqual( + client.read({ selection: gql`{ a d { e } }` }), + { a: 1, d: { e: 4 } }, + ); + assert.deepEqual( + client.read({ selection: gql`{ a d { e h { i } } }` }), + { a: 1, d: { e: 4, h: { i: 7 } } }, + ); + assert.deepEqual( + client.read({ selection: gql`{ a b c d { e f g h { i j k } } }` }), + { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, + ); + }); + + it('will read some deeply nested data from state at any id', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: 'foo', + generated: false, + }, + }, + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }, + }, + }, + }); + + assert.deepEqual( + client.read({ selection: gql`{ e h { i } }`, id: 'foo' }), + { e: 4, h: { i: 7 } }, + ); + assert.deepEqual( + client.read({ selection: gql`{ e f g h { i j k } }`, id: 'foo' }), + { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, + ); + assert.deepEqual( + client.read({ selection: gql`{ i }`, id: 'bar' }), + { i: 7 }, + ); + assert.deepEqual( + client.read({ selection: gql`{ i j k }`, id: 'bar' }), + { i: 7, j: 8, k: 9 }, + ); + }); + + it('will read some data from state with variables', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }, + }, + }, + }); + + assert.deepEqual(client.read({ + selection: gql`{ + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + }`, + variables: { + literal: false, + value: 42, + }, + }), { a: 1, b: 2 }); + }); + + it('will read some parital data from state', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + }, + }, + }, + }, + }); + + assert.deepEqual(client.read({ selection: gql`{ a }`, returnPartialData: true }), { a: 1 }); + assert.deepEqual(client.read({ selection: gql`{ b c }`, returnPartialData: true }), { b: 2, c: 3 }); + assert.deepEqual(client.read({ selection: gql`{ a b c }`, returnPartialData: true }), { a: 1, b: 2, c: 3 }); + assert.deepEqual(client.read({ selection: gql`{ a d }`, returnPartialData: true }), { a: 1 }); + assert.deepEqual(client.read({ selection: gql`{ b c d }`, returnPartialData: true }), { b: 2, c: 3 }); + assert.deepEqual(client.read({ selection: gql`{ a b c d }`, returnPartialData: true }), { a: 1, b: 2, c: 3 }); + }); + }); +}); diff --git a/test/tests.ts b/test/tests.ts index ca4512f36d7..ad65fa99680 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -53,3 +53,4 @@ import './isEqual'; import './cloneDeep'; import './assign'; import './environment'; +import './ApolloClient'; From c1acb420d02728aacfb0d9f5d2415f77586431d2 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Fri, 10 Feb 2017 15:31:45 -0500 Subject: [PATCH 03/29] rename test variable --- test/readFromStore.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/readFromStore.ts b/test/readFromStore.ts index 51878fbc5ed..0d37a47dc50 100644 --- a/test/readFromStore.ts +++ b/test/readFromStore.ts @@ -694,7 +694,7 @@ describe('reading from the store', () => { }); it('will read from an arbitrary root id', () => { - const result: any = { + const data: any = { id: 'abcd', stringField: 'This is a string!', numberField: 5, @@ -715,7 +715,7 @@ describe('reading from the store', () => { }; const store = { - 'ROOT_QUERY': assign({}, assign({}, omit(result, 'nestedObj', 'deepNestedObj')), { + 'ROOT_QUERY': assign({}, assign({}, omit(data, 'nestedObj', 'deepNestedObj')), { __typename: 'Query', nestedObj: { type: 'id', @@ -723,14 +723,14 @@ describe('reading from the store', () => { generated: false, }, }) as StoreObject, - abcde: assign({}, result.nestedObj, { + abcde: assign({}, data.nestedObj, { deepNestedObj: { type: 'id', id: 'abcdef', generated: false, }, }) as StoreObject, - abcdef: result.deepNestedObj as StoreObject, + abcdef: data.deepNestedObj as StoreObject, } as NormalizedCache; const queryResult1 = readQueryFromStore({ From f496df23a49ba780eeff976154785587ba6349ba Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Fri, 10 Feb 2017 15:36:02 -0500 Subject: [PATCH 04/29] move assertion --- test/readFromStore.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/readFromStore.ts b/test/readFromStore.ts index 0d37a47dc50..07ea322cb04 100644 --- a/test/readFromStore.ts +++ b/test/readFromStore.ts @@ -750,6 +750,17 @@ describe('reading from the store', () => { `, }); + assert.deepEqual(queryResult1, { + stringField: 'This is a string too!', + numberField: 6, + nullField: null, + deepNestedObj: { + stringField: 'This is a deep string', + numberField: 7, + nullField: null, + }, + }); + const queryResult2 = readQueryFromStore({ store, rootId: 'abcdef', @@ -762,17 +773,6 @@ describe('reading from the store', () => { `, }); - assert.deepEqual(queryResult1, { - stringField: 'This is a string too!', - numberField: 6, - nullField: null, - deepNestedObj: { - stringField: 'This is a deep string', - numberField: 7, - nullField: null, - }, - }); - assert.deepEqual(queryResult2, { stringField: 'This is a deep string', numberField: 7, From d246ca5514e2f7018b470d7ce065bf81d3ac9de6 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Fri, 10 Feb 2017 18:12:35 -0500 Subject: [PATCH 05/29] split read into readQuery and readFragment --- src/ApolloClient.ts | 96 ++++++++++++++++++------ test/ApolloClient.ts | 173 ++++++++++++++++++++++++++++++++----------- 2 files changed, 201 insertions(+), 68 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index 0d9eb9f97a6..1a15c41e553 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -12,6 +12,7 @@ import { /* tslint:enable */ DocumentNode, + FragmentDefinitionNode, } from 'graphql'; import { @@ -63,10 +64,6 @@ import { readQueryFromStore, } from './data/readFromStore'; -import { - getQueryDefinition, -} from './queries/getFromAST'; - import { version, } from './version'; @@ -347,41 +344,92 @@ export default class ApolloClient { /** * Tries to read some data from the store without making a network request. - * - * By default we start reading from the root query, but if an `id` is - * provided then we will start reading from that location. + * This method will start at the root query. To start at a a specific id in + * the store then use `readFragment`. */ - public read ({ - selection, + public readQuery ({ + query, variables, - id, returnPartialData, }: { - selection: DocumentNode, + query: DocumentNode, variables?: Object, - id?: string, returnPartialData?: boolean, }): QueryType { this.initStore(); - // Throw the right validation error by trying to find a query in the document. - // We also run some extra validation below on the operation. - const operation = getQueryDefinition(selection); - - // Make sure that the query is in the format `{ ... }` vs. `query Name { ... }`. - // We don’t want our users to think they are running an actual query. - // Especially if they provide an `id`. - if (operation.operation !== 'query' || operation.name || (operation.variableDefinitions || []).length !== 0) { - throw new Error('Can only use a nameless query with no variable definitions to read from the store.'); - } - const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; return readQueryFromStore({ store: reduxRootSelector(this.store.getState()).data, - query: selection, + query, variables, + returnPartialData, + }); + } + + /** + * Tries to read some data from the store without making a network request. + * This method will read a GraphQL fragment from any arbitrary id in the + * store. Unlike `readQuery` which will only read from the root query. + */ + public readFragment ({ + id, + fragment, + fragmentName, + returnPartialData, + }: { + id: string, + fragment: DocumentNode, + fragmentName?: string, + returnPartialData?: boolean, + }): FragmentType { + this.initStore(); + + const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; + let actualFragmentName = fragmentName; + + // If the user did not give us a fragment name then let us try to get a + // name from a single fragment in the definition. + if (typeof actualFragmentName === 'undefined') { + const fragments = fragment.definitions.filter(({ kind }) => kind === 'FragmentDefinition') as Array; + if (fragments.length !== 1) { + throw new Error(`Found ${fragments.length} fragments when exactly 1 was expected because \`fragmentName\` was not provided.`); + } + actualFragmentName = fragments[0].name.value; + } + + // Generate a query document with an operation that simply spreads the + // fragment inside of it. This is necessary to be compatible with our + // current store implementation, but we want users to write fragments to + // support GraphQL tooling everywhere. + const query: DocumentNode = { + ...fragment, + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { + kind: 'Name', + value: actualFragmentName, + }, + }, + ], + }, + }, + ...fragment.definitions, + ], + }; + + return readQueryFromStore({ rootId: id, + store: reduxRootSelector(this.store.getState()).data, + query, returnPartialData, }); } diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index b105f6577ce..8c02fb0ceed 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -4,7 +4,7 @@ import { Store } from '../src/store'; import ApolloClient from '../src/ApolloClient'; describe('ApolloClient', () => { - describe('read', () => { + describe('readQuery', () => { it('will read some data from state', () => { const client = new ApolloClient({ initialState: { @@ -20,9 +20,9 @@ describe('ApolloClient', () => { }, }); - assert.deepEqual(client.read({ selection: gql`{ a }` }), { a: 1 }); - assert.deepEqual(client.read({ selection: gql`{ b c }` }), { b: 2, c: 3 }); - assert.deepEqual(client.read({ selection: gql`{ a b c }` }), { a: 1, b: 2, c: 3 }); + assert.deepEqual(client.readQuery({ query: gql`{ a }` }), { a: 1 }); + assert.deepEqual(client.readQuery({ query: gql`{ b c }` }), { b: 2, c: 3 }); + assert.deepEqual(client.readQuery({ query: gql`{ a b c }` }), { a: 1, b: 2, c: 3 }); }); it('will read some deeply nested data from state', () => { @@ -61,25 +61,77 @@ describe('ApolloClient', () => { }); assert.deepEqual( - client.read({ selection: gql`{ a d { e } }` }), + client.readQuery({ query: gql`{ a d { e } }` }), { a: 1, d: { e: 4 } }, ); assert.deepEqual( - client.read({ selection: gql`{ a d { e h { i } } }` }), + client.readQuery({ query: gql`{ a d { e h { i } } }` }), { a: 1, d: { e: 4, h: { i: 7 } } }, ); assert.deepEqual( - client.read({ selection: gql`{ a b c d { e f g h { i j k } } }` }), + client.readQuery({ query: gql`{ a b c d { e f g h { i j k } } }` }), { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, ); }); + it('will read some data from state with variables', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }, + }, + }, + }); + + assert.deepEqual(client.readQuery({ + query: gql`query ($literal: Boolean, $value: Int) { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + }`, + variables: { + literal: false, + value: 42, + }, + }), { a: 1, b: 2 }); + }); + + it('will read some parital data from state', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + }, + }, + }, + }, + }); + + assert.deepEqual(client.readQuery({ query: gql`{ a }`, returnPartialData: true }), { a: 1 }); + assert.deepEqual(client.readQuery({ query: gql`{ b c }`, returnPartialData: true }), { b: 2, c: 3 }); + assert.deepEqual(client.readQuery({ query: gql`{ a b c }`, returnPartialData: true }), { a: 1, b: 2, c: 3 }); + assert.deepEqual(client.readQuery({ query: gql`{ a d }`, returnPartialData: true }), { a: 1 }); + assert.deepEqual(client.readQuery({ query: gql`{ b c d }`, returnPartialData: true }), { b: 2, c: 3 }); + assert.deepEqual(client.readQuery({ query: gql`{ a b c d }`, returnPartialData: true }), { a: 1, b: 2, c: 3 }); + }); + }); + + describe('readFragment', () => { it('will read some deeply nested data from state at any id', () => { const client = new ApolloClient({ initialState: { apollo: { data: { 'ROOT_QUERY': { + __typename: 'Type1', a: 1, b: 2, c: 3, @@ -90,6 +142,7 @@ describe('ApolloClient', () => { }, }, 'foo': { + __typename: 'Type2', e: 4, f: 5, g: 6, @@ -100,6 +153,7 @@ describe('ApolloClient', () => { }, }, 'bar': { + __typename: 'Type3', i: 7, j: 8, k: 9, @@ -110,47 +164,37 @@ describe('ApolloClient', () => { }); assert.deepEqual( - client.read({ selection: gql`{ e h { i } }`, id: 'foo' }), + client.readFragment({ fragment: gql`fragment foo on Foo { e h { i } }`, id: 'foo' }), { e: 4, h: { i: 7 } }, ); assert.deepEqual( - client.read({ selection: gql`{ e f g h { i j k } }`, id: 'foo' }), + client.readFragment({ fragment: gql`fragment foo on Foo { e f g h { i j k } }`, id: 'foo' }), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - client.read({ selection: gql`{ i }`, id: 'bar' }), + client.readFragment({ fragment: gql`fragment bar on Bar { i }`, id: 'bar' }), { i: 7 }, ); assert.deepEqual( - client.read({ selection: gql`{ i j k }`, id: 'bar' }), + client.readFragment({ fragment: gql`fragment bar on Bar { i j k }`, id: 'bar' }), + { i: 7, j: 8, k: 9 }, + ); + assert.deepEqual( + client.readFragment({ + fragment: gql`fragment foo on Foo { e f g h { i j k } } fragment bar on Bar { i j k }`, + id: 'foo', + fragmentName: 'foo', + }), + { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, + ); + assert.deepEqual( + client.readFragment({ + fragment: gql`fragment foo on Foo { e f g h { i j k } } fragment bar on Bar { i j k }`, + id: 'bar', + fragmentName: 'bar', + }), { i: 7, j: 8, k: 9 }, ); - }); - - it('will read some data from state with variables', () => { - const client = new ApolloClient({ - initialState: { - apollo: { - data: { - 'ROOT_QUERY': { - 'field({"literal":true,"value":42})': 1, - 'field({"literal":false,"value":42})': 2, - }, - }, - }, - }, - }); - - assert.deepEqual(client.read({ - selection: gql`{ - a: field(literal: true, value: 42) - b: field(literal: $literal, value: $value) - }`, - variables: { - literal: false, - value: 42, - }, - }), { a: 1, b: 2 }); }); it('will read some parital data from state', () => { @@ -158,7 +202,8 @@ describe('ApolloClient', () => { initialState: { apollo: { data: { - 'ROOT_QUERY': { + 'x': { + __typename: 'Type1', a: 1, b: 2, c: 3, @@ -168,12 +213,52 @@ describe('ApolloClient', () => { }, }); - assert.deepEqual(client.read({ selection: gql`{ a }`, returnPartialData: true }), { a: 1 }); - assert.deepEqual(client.read({ selection: gql`{ b c }`, returnPartialData: true }), { b: 2, c: 3 }); - assert.deepEqual(client.read({ selection: gql`{ a b c }`, returnPartialData: true }), { a: 1, b: 2, c: 3 }); - assert.deepEqual(client.read({ selection: gql`{ a d }`, returnPartialData: true }), { a: 1 }); - assert.deepEqual(client.read({ selection: gql`{ b c d }`, returnPartialData: true }), { b: 2, c: 3 }); - assert.deepEqual(client.read({ selection: gql`{ a b c d }`, returnPartialData: true }), { a: 1, b: 2, c: 3 }); + assert.deepEqual( + client.readFragment({ fragment: gql`fragment y on Y { a }`, returnPartialData: true, id: 'x' }), + { a: 1 }, + ); + assert.deepEqual( + client.readFragment({ fragment: gql`fragment y on Y { b c }`, returnPartialData: true, id: 'x' }), + { b: 2, c: 3 }, + ); + assert.deepEqual( + client.readFragment({ fragment: gql`fragment y on Y { a b c }`, returnPartialData: true, id: 'x' }), + { a: 1, b: 2, c: 3 }, + ); + assert.deepEqual( + client.readFragment({ fragment: gql`fragment y on Y { a d }`, returnPartialData: true, id: 'x' }), + { a: 1 }, + ); + assert.deepEqual( + client.readFragment({ fragment: gql`fragment y on Y { b c d }`, returnPartialData: true, id: 'x' }), + { b: 2, c: 3 }, + ); + assert.deepEqual( + client.readFragment({ fragment: gql`fragment y on Y { a b c d }`, returnPartialData: true, id: 'x' }), + { a: 1, b: 2, c: 3 }, + ); + }); + + it('will throw an error when there is no fragment', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.readFragment({ id: 'x', fragment: gql`query { a b c }` }); + }, 'Found 0 fragments when exactly 1 was expected because `fragmentName` was not provided.'); + assert.throws(() => { + client.readFragment({ id: 'x', fragment: gql`schema { query: Query }` }); + }, 'Found 0 fragments when exactly 1 was expected because `fragmentName` was not provided.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b }` }); + }, 'Found 2 fragments when exactly 1 was expected because `fragmentName` was not provided.'); + assert.throws(() => { + client.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }` }); + }, 'Found 3 fragments when exactly 1 was expected because `fragmentName` was not provided.'); }); }); }); From bce014a9c9a6d24d848aa75a6a9c1a795b93d811 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Mon, 13 Feb 2017 10:47:58 -0500 Subject: [PATCH 06/29] add variables to readFragment --- src/ApolloClient.ts | 3 +++ test/ApolloClient.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index 1a15c41e553..db13add11a0 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -377,11 +377,13 @@ export default class ApolloClient { id, fragment, fragmentName, + variables, returnPartialData, }: { id: string, fragment: DocumentNode, fragmentName?: string, + variables?: Object, returnPartialData?: boolean, }): FragmentType { this.initStore(); @@ -430,6 +432,7 @@ export default class ApolloClient { rootId: id, store: reduxRootSelector(this.store.getState()).data, query, + variables, returnPartialData, }); } diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index 8c02fb0ceed..dd951463488 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -260,5 +260,35 @@ describe('ApolloClient', () => { client.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }` }); }, 'Found 3 fragments when exactly 1 was expected because `fragmentName` was not provided.'); }); + + it('will read some data from state with variables', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'foo': { + __typename: 'Type1', + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }, + }, + }, + }); + + assert.deepEqual(client.readFragment({ + id: 'foo', + fragment: gql` + fragment foo on Foo { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + } + `, + variables: { + literal: false, + value: 42, + }, + }), { a: 1, b: 2 }); + }); }); }); From fec70031540aabef03d1b6078f856734e5762d37 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Fri, 17 Feb 2017 10:33:32 -0500 Subject: [PATCH 07/29] style --- src/ApolloClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index db13add11a0..7677456cd30 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -347,7 +347,7 @@ export default class ApolloClient { * This method will start at the root query. To start at a a specific id in * the store then use `readFragment`. */ - public readQuery ({ + public readQuery({ query, variables, returnPartialData, @@ -373,7 +373,7 @@ export default class ApolloClient { * This method will read a GraphQL fragment from any arbitrary id in the * store. Unlike `readQuery` which will only read from the root query. */ - public readFragment ({ + public readFragment({ id, fragment, fragmentName, From 33d60fe70dfdf392a481844b0d5cbd90e50867e1 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Fri, 17 Feb 2017 11:35:02 -0500 Subject: [PATCH 08/29] add imperative write methods to client --- src/ApolloClient.ts | 135 +++++------ src/actions.ts | 19 +- src/data/store.ts | 15 ++ src/queries/getFromAST.ts | 63 +++++ test/ApolloClient.ts | 479 ++++++++++++++++++++++++++++++-------- 5 files changed, 545 insertions(+), 166 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index 7677456cd30..4a4a7e48a06 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -64,6 +64,10 @@ import { readQueryFromStore, } from './data/readFromStore'; +import { + getFragmentQuery, +} from './queries/getFromAST'; + import { version, } from './version'; @@ -344,96 +348,97 @@ export default class ApolloClient { /** * Tries to read some data from the store without making a network request. - * This method will start at the root query. To start at a a specific id in - * the store then use `readFragment`. + * This method will start at the root query. To start at a specific id + * returned by `dataIdFromObject` use `readFragment`. */ - public readQuery({ - query, - variables, - returnPartialData, - }: { + public readQuery( query: DocumentNode, variables?: Object, - returnPartialData?: boolean, - }): QueryType { + ): QueryType { this.initStore(); - const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; - return readQueryFromStore({ + rootId: 'ROOT_QUERY', store: reduxRootSelector(this.store.getState()).data, query, variables, - returnPartialData, + returnPartialData: false, }); } /** * Tries to read some data from the store without making a network request. - * This method will read a GraphQL fragment from any arbitrary id in the - * store. Unlike `readQuery` which will only read from the root query. + * This method will read a GraphQL fragment from any arbitrary id that is + * currently cached. Unlike `readQuery` which will only read from the root + * query. + * + * You must pass in a GraphQL document with a single fragment or a document + * with multiple fragments that represent what you are writing. If you pass + * in a document with multiple fragments then you must also specify a + * `fragmentName`. */ - public readFragment({ - id, - fragment, - fragmentName, - variables, - returnPartialData, - }: { + public readFragment( id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - returnPartialData?: boolean, - }): FragmentType { + ): FragmentType { this.initStore(); - const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; - let actualFragmentName = fragmentName; - - // If the user did not give us a fragment name then let us try to get a - // name from a single fragment in the definition. - if (typeof actualFragmentName === 'undefined') { - const fragments = fragment.definitions.filter(({ kind }) => kind === 'FragmentDefinition') as Array; - if (fragments.length !== 1) { - throw new Error(`Found ${fragments.length} fragments when exactly 1 was expected because \`fragmentName\` was not provided.`); - } - actualFragmentName = fragments[0].name.value; - } - - // Generate a query document with an operation that simply spreads the - // fragment inside of it. This is necessary to be compatible with our - // current store implementation, but we want users to write fragments to - // support GraphQL tooling everywhere. - const query: DocumentNode = { - ...fragment, - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'FragmentSpread', - name: { - kind: 'Name', - value: actualFragmentName, - }, - }, - ], - }, - }, - ...fragment.definitions, - ], - }; - return readQueryFromStore({ rootId: id, store: reduxRootSelector(this.store.getState()).data, - query, + query: getFragmentQuery(fragment, fragmentName), variables, - returnPartialData, + returnPartialData: false, + }); + } + + /** + * Writes some data to the store without that data being the result of a + * network request. This method will start at the root query. To start at a a + * specific id returned by `dataIdFromObject` then use `writeFragment`. + */ + public writeQuery( + data: any, + query: DocumentNode, + variables?: Object, + ) { + this.initStore(); + this.store.dispatch({ + type: 'APOLLO_WRITE', + rootId: 'ROOT_QUERY', + result: data, + document: query, + variables: variables || {}, + }); + } + + /** + * Writes some data to the store without that data being the result of a + * network request. This method will write to a GraphQL fragment from any + * arbitrary id that is currently cached. Unlike `writeQuery` which will only + * write from the root query. + * + * You must pass in a GraphQL document with a single fragment or a document + * with multiple fragments that represent what you are writing. If you pass + * in a document with multiple fragments then you must also specify a + * `fragmentName`. + */ + public writeFragment( + data: any, + id: string, + fragment: DocumentNode, + fragmentName?: string, + variables?: Object, + ) { + this.initStore(); + this.store.dispatch({ + type: 'APOLLO_WRITE', + rootId: id, + result: data, + document: getFragmentQuery(fragment, fragmentName), + variables: variables || {}, }); } diff --git a/src/actions.ts b/src/actions.ts index 27454b6d063..984f19a1ae6 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -138,7 +138,7 @@ export function isStoreResetAction(action: ApolloAction): action is StoreResetAc return action.type === 'APOLLO_STORE_RESET'; } -export type SubscriptionResultAction = { +export interface SubscriptionResultAction { type: 'APOLLO_SUBSCRIPTION_RESULT'; result: ExecutionResult; subscriptionId: number; @@ -146,12 +146,24 @@ export type SubscriptionResultAction = { document: DocumentNode; operationName: string; extraReducers?: ApolloReducer[]; -}; +} export function isSubscriptionResultAction(action: ApolloAction): action is SubscriptionResultAction { return action.type === 'APOLLO_SUBSCRIPTION_RESULT'; } +export interface WriteAction { + type: 'APOLLO_WRITE'; + rootId: string; + result: any; + document: DocumentNode; + variables: Object; +} + +export function isWriteAction(action: ApolloAction): action is WriteAction { + return action.type === 'APOLLO_WRITE'; +} + export type ApolloAction = QueryResultAction | QueryErrorAction | @@ -163,4 +175,5 @@ export type ApolloAction = MutationErrorAction | UpdateQueryResultAction | StoreResetAction | - SubscriptionResultAction; + SubscriptionResultAction | + WriteAction; diff --git a/src/data/store.ts b/src/data/store.ts index 97c5402a7dc..5c0899a7ff3 100644 --- a/src/data/store.ts +++ b/src/data/store.ts @@ -5,6 +5,7 @@ import { isUpdateQueryResultAction, isStoreResetAction, isSubscriptionResultAction, + isWriteAction, } from '../actions'; import { @@ -198,6 +199,20 @@ export function data( // If we are resetting the store, we no longer need any of the data that is currently in // the store so we can just throw it all away. return {}; + } else if (isWriteAction(action)) { + const clonedState = { ...previousState } as NormalizedCache; + + // Simply write our result to the store for this action. + const newState = writeResultToStore({ + result: action.result, + dataId: action.rootId, + document: action.document, + variables: action.variables, + store: clonedState, + dataIdFromObject: config.dataIdFromObject, + }); + + return newState; } return previousState; diff --git a/src/queries/getFromAST.ts b/src/queries/getFromAST.ts index 58f3ba57ccb..87d3d36eead 100644 --- a/src/queries/getFromAST.ts +++ b/src/queries/getFromAST.ts @@ -147,3 +147,66 @@ export function createFragmentMap(fragments: FragmentDefinitionNode[] = []): Fra return symTable; } + +/** + * Returns a query document which adds a single query operation that only + * spreads the target fragment inside of it. + * + * So for example a document of: + * + * ```graphql + * fragment foo on Foo { a b c } + * ``` + * + * Turns into: + * + * ```graphql + * { ...foo } + * + * fragment foo on Foo { a b c } + * ``` + * + * The target fragment will either be the only fragment in the document, or a + * fragment specified by the provided `fragmentName`. If there is more then one + * fragment, but a `fragmentName` was not defined then an error will be thrown. + */ +export function getFragmentQuery(document: DocumentNode, fragmentName?: string): DocumentNode { + let actualFragmentName = fragmentName; + + // If the user did not give us a fragment name then let us try to get a + // name from a single fragment in the definition. + if (typeof actualFragmentName === 'undefined') { + const fragments = document.definitions.filter(({ kind }) => kind === 'FragmentDefinition') as Array; + if (fragments.length !== 1) { + throw new Error(`Found ${fragments.length} fragments. \`fragmentName\` must be provided when there are more then 1 fragments.`); + } + actualFragmentName = fragments[0].name.value; + } + + // Generate a query document with an operation that simply spreads the + // fragment inside of it. + const query: DocumentNode = { + ...document, + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { + kind: 'Name', + value: actualFragmentName, + }, + }, + ], + }, + }, + ...document.definitions, + ], + }; + + return query; +} diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index dd951463488..3d356da68d8 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -3,7 +3,7 @@ import gql from 'graphql-tag'; import { Store } from '../src/store'; import ApolloClient from '../src/ApolloClient'; -describe('ApolloClient', () => { +describe.only('ApolloClient', () => { describe('readQuery', () => { it('will read some data from state', () => { const client = new ApolloClient({ @@ -20,9 +20,9 @@ describe('ApolloClient', () => { }, }); - assert.deepEqual(client.readQuery({ query: gql`{ a }` }), { a: 1 }); - assert.deepEqual(client.readQuery({ query: gql`{ b c }` }), { b: 2, c: 3 }); - assert.deepEqual(client.readQuery({ query: gql`{ a b c }` }), { a: 1, b: 2, c: 3 }); + assert.deepEqual(client.readQuery(gql`{ a }`), { a: 1 }); + assert.deepEqual(client.readQuery(gql`{ b c }`), { b: 2, c: 3 }); + assert.deepEqual(client.readQuery(gql`{ a b c }`), { a: 1, b: 2, c: 3 }); }); it('will read some deeply nested data from state', () => { @@ -61,15 +61,15 @@ describe('ApolloClient', () => { }); assert.deepEqual( - client.readQuery({ query: gql`{ a d { e } }` }), + client.readQuery(gql`{ a d { e } }`), { a: 1, d: { e: 4 } }, ); assert.deepEqual( - client.readQuery({ query: gql`{ a d { e h { i } } }` }), + client.readQuery(gql`{ a d { e h { i } } }`), { a: 1, d: { e: 4, h: { i: 7 } } }, ); assert.deepEqual( - client.readQuery({ query: gql`{ a b c d { e f g h { i j k } } }` }), + client.readQuery(gql`{ a b c d { e f g h { i j k } } }`), { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, ); }); @@ -88,39 +88,16 @@ describe('ApolloClient', () => { }, }); - assert.deepEqual(client.readQuery({ - query: gql`query ($literal: Boolean, $value: Int) { + assert.deepEqual(client.readQuery( + gql`query ($literal: Boolean, $value: Int) { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) }`, - variables: { + { literal: false, value: 42, }, - }), { a: 1, b: 2 }); - }); - - it('will read some parital data from state', () => { - const client = new ApolloClient({ - initialState: { - apollo: { - data: { - 'ROOT_QUERY': { - a: 1, - b: 2, - c: 3, - }, - }, - }, - }, - }); - - assert.deepEqual(client.readQuery({ query: gql`{ a }`, returnPartialData: true }), { a: 1 }); - assert.deepEqual(client.readQuery({ query: gql`{ b c }`, returnPartialData: true }), { b: 2, c: 3 }); - assert.deepEqual(client.readQuery({ query: gql`{ a b c }`, returnPartialData: true }), { a: 1, b: 2, c: 3 }); - assert.deepEqual(client.readQuery({ query: gql`{ a d }`, returnPartialData: true }), { a: 1 }); - assert.deepEqual(client.readQuery({ query: gql`{ b c d }`, returnPartialData: true }), { b: 2, c: 3 }); - assert.deepEqual(client.readQuery({ query: gql`{ a b c d }`, returnPartialData: true }), { a: 1, b: 2, c: 3 }); + ), { a: 1, b: 2 }); }); }); @@ -164,131 +141,437 @@ describe('ApolloClient', () => { }); assert.deepEqual( - client.readFragment({ fragment: gql`fragment foo on Foo { e h { i } }`, id: 'foo' }), + client.readFragment('foo', gql`fragment fragmentFoo on Foo { e h { i } }`), { e: 4, h: { i: 7 } }, ); assert.deepEqual( - client.readFragment({ fragment: gql`fragment foo on Foo { e f g h { i j k } }`, id: 'foo' }), + client.readFragment('foo', gql`fragment fragmentFoo on Foo { e f g h { i j k } }`), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - client.readFragment({ fragment: gql`fragment bar on Bar { i }`, id: 'bar' }), + client.readFragment('bar', gql`fragment fragmentBar on Bar { i }`), { i: 7 }, ); assert.deepEqual( - client.readFragment({ fragment: gql`fragment bar on Bar { i j k }`, id: 'bar' }), + client.readFragment('bar', gql`fragment fragmentBar on Bar { i j k }`), { i: 7, j: 8, k: 9 }, ); assert.deepEqual( - client.readFragment({ - fragment: gql`fragment foo on Foo { e f g h { i j k } } fragment bar on Bar { i j k }`, - id: 'foo', - fragmentName: 'foo', - }), + client.readFragment( + 'foo', + gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + 'fragmentFoo', + ), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - client.readFragment({ - fragment: gql`fragment foo on Foo { e f g h { i j k } } fragment bar on Bar { i j k }`, - id: 'bar', - fragmentName: 'bar', - }), + client.readFragment( + 'bar', + gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + 'fragmentBar', + ), { i: 7, j: 8, k: 9 }, ); }); - it('will read some parital data from state', () => { + it('will throw an error when there is no fragment', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.readFragment('x', gql`query { a b c }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.readFragment('x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + + it('will read some data from state with variables', () => { const client = new ApolloClient({ initialState: { apollo: { data: { - 'x': { + 'foo': { __typename: 'Type1', - a: 1, - b: 2, - c: 3, + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, }, }, }, }, }); - assert.deepEqual( - client.readFragment({ fragment: gql`fragment y on Y { a }`, returnPartialData: true, id: 'x' }), - { a: 1 }, + assert.deepEqual(client.readFragment( + 'foo', + gql` + fragment foo on Foo { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + } + `, + undefined, + { + literal: false, + value: 42, + }, + ), { a: 1, b: 2 }); + }); + }); + + describe('writeQuery', () => { + it('will write some data to the state', () => { + const client = new ApolloClient(); + + client.writeQuery({ a: 1 }, gql`{ a }`); + + assert.deepEqual(client.store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + }, + }); + + client.writeQuery({ b: 2, c: 3 }, gql`{ b c }`); + + assert.deepEqual(client.store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + }, + }); + + client.writeQuery({ a: 4, b: 5, c: 6 }, gql`{ a b c }`); + + assert.deepEqual(client.store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 4, + b: 5, + c: 6, + }, + }); + }); + + it('will write some deeply nested data to state', () => { + const client = new ApolloClient(); + + client.writeQuery( + { a: 1, d: { e: 4 } }, + gql`{ a d { e } }`, ); - assert.deepEqual( - client.readFragment({ fragment: gql`fragment y on Y { b c }`, returnPartialData: true, id: 'x' }), - { b: 2, c: 3 }, + + assert.deepEqual(client.store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + }, + }); + + client.writeQuery( + { a: 1, d: { h: { i: 7 } } }, + gql`{ a d { h { i } } }`, ); - assert.deepEqual( - client.readFragment({ fragment: gql`fragment y on Y { a b c }`, returnPartialData: true, id: 'x' }), - { a: 1, b: 2, c: 3 }, + + assert.deepEqual(client.store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + }, + }); + + client.writeQuery( + { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, + gql`{ a b c d { e f g h { i j k } } }`, ); - assert.deepEqual( - client.readFragment({ fragment: gql`fragment y on Y { a d }`, returnPartialData: true, id: 'x' }), - { a: 1 }, + + assert.deepEqual(client.store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + j: 8, + k: 9, + }, + }); + }); + + it('will write some data to state with variables', () => { + const client = new ApolloClient(); + + client.writeQuery( + { + a: 1, + b: 2, + }, + gql` + query ($literal: Boolean, $value: Int) { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + } + `, + { + literal: false, + value: 42, + }, ); - assert.deepEqual( - client.readFragment({ fragment: gql`fragment y on Y { b c d }`, returnPartialData: true, id: 'x' }), - { b: 2, c: 3 }, + + assert.deepEqual(client.store.getState().apollo.data, { + 'ROOT_QUERY': { + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }); + }); + }); + + describe('writeFragment', () => { + it('will write some deeply nested data into state at any id', () => { + const client = new ApolloClient({ + dataIdFromObject: (o: any) => o.id, + }); + + client.writeFragment( + { e: 4, h: { id: 'bar', i: 7 } }, + 'foo', + gql`fragment fragmentFoo on Foo { e h { i } }`, ); - assert.deepEqual( - client.readFragment({ fragment: gql`fragment y on Y { a b c d }`, returnPartialData: true, id: 'x' }), - { a: 1, b: 2, c: 3 }, + + assert.deepEqual(client.store.getState().apollo.data, { + 'foo': { + e: 4, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + }, + }); + + client.writeFragment( + { f: 5, g: 6, h: { id: 'bar', j: 8, k: 9 } }, + 'foo', + gql`fragment fragmentFoo on Foo { f g h { j k } }`, ); + + assert.deepEqual(client.store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }); + + client.writeFragment( + { i: 10 }, + 'bar', + gql`fragment fragmentBar on Bar { i }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 10, + j: 8, + k: 9, + }, + }); + + client.writeFragment( + { j: 11, k: 12 }, + 'bar', + gql`fragment fragmentBar on Bar { j k }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 10, + j: 11, + k: 12, + }, + }); + + client.writeFragment( + { e: 4, f: 5, g: 6, h: { id: 'bar', i: 7, j: 8, k: 9 } }, + 'foo', + gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, + 'fooFragment', + ); + + assert.deepEqual(client.store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }); + + client.writeFragment( + { i: 10, j: 11, k: 12 }, + 'bar', + gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, + 'barFragment', + ); + + assert.deepEqual(client.store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 10, + j: 11, + k: 12, + }, + }); }); it('will throw an error when there is no fragment', () => { const client = new ApolloClient(); assert.throws(() => { - client.readFragment({ id: 'x', fragment: gql`query { a b c }` }); - }, 'Found 0 fragments when exactly 1 was expected because `fragmentName` was not provided.'); + client.writeFragment({}, 'x', gql`query { a b c }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); assert.throws(() => { - client.readFragment({ id: 'x', fragment: gql`schema { query: Query }` }); - }, 'Found 0 fragments when exactly 1 was expected because `fragmentName` was not provided.'); + client.writeFragment({}, 'x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); }); it('will throw an error when there is more than one fragment but no fragment name', () => { const client = new ApolloClient(); assert.throws(() => { - client.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b }` }); - }, 'Found 2 fragments when exactly 1 was expected because `fragmentName` was not provided.'); + client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); assert.throws(() => { - client.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }` }); - }, 'Found 3 fragments when exactly 1 was expected because `fragmentName` was not provided.'); + client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); }); - it('will read some data from state with variables', () => { - const client = new ApolloClient({ - initialState: { - apollo: { - data: { - 'foo': { - __typename: 'Type1', - 'field({"literal":true,"value":42})': 1, - 'field({"literal":false,"value":42})': 2, - }, - }, - }, - }, - }); + it('will write some data to state with variables', () => { + const client = new ApolloClient(); - assert.deepEqual(client.readFragment({ - id: 'foo', - fragment: gql` + client.writeFragment( + { + a: 1, + b: 2, + }, + 'foo', + gql` fragment foo on Foo { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) } `, - variables: { + undefined, + { literal: false, value: 42, }, - }), { a: 1, b: 2 }); + ); + + assert.deepEqual(client.store.getState().apollo.data, { + 'foo': { + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }); }); }); }); From edfadae87ea83c4319bf3e1cd3c14a1a225636d0 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Fri, 17 Feb 2017 19:18:30 -0500 Subject: [PATCH 09/29] add optimistic writes --- src/ApolloClient.ts | 82 ++++++ src/actions.ts | 26 +- src/optimistic-data/store.ts | 123 +++++++-- test/ApolloClient.ts | 501 ++++++++++++++++++++++++++++++++++- 4 files changed, 701 insertions(+), 31 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index 4a4a7e48a06..9d87df7fc19 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -110,6 +110,7 @@ export default class ApolloClient { public queryDeduplication: boolean; private devToolsHookCb: Function; + private optimisticWriteId: number; /** * Constructs an instance of {@link ApolloClient}. @@ -242,6 +243,8 @@ export default class ApolloClient { } this.version = version; + + this.optimisticWriteId = 1; } /** @@ -442,6 +445,85 @@ export default class ApolloClient { }); } + /** + * Writes some data to the store without that data being the result of a + * network request. This method will start at the root query. To start at a a + * specific id returned by `dataIdFromObject` then use + * `writeFragmentOptimistically`. + * + * Unlike `writeQuery`, the data written with this method will be stored in + * the optimistic portion of the cache and so will not be persisted. This + * optimistic write may also be rolled back with the `rollback` function that + * was returned. + */ + public writeQueryOptimistically( + data: any, + query: DocumentNode, + variables?: Object, + ): { + rollback: () => void, + } { + const optimisticWriteId = (this.optimisticWriteId++).toString(); + this.initStore(); + this.store.dispatch({ + type: 'APOLLO_WRITE_OPTIMISTIC', + optimisticWriteId, + rootId: 'ROOT_QUERY', + result: data, + document: query, + variables: variables || {}, + }); + return { + rollback: () => this.store.dispatch({ + type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK', + optimisticWriteId, + }), + }; + } + + /** + * Writes some data to the store without that data being the result of a + * network request. This method will write to a GraphQL fragment from any + * arbitrary id that is currently cached. Unlike `writeQueryOptimistically` + * which will only write from the root query. + * + * You must pass in a GraphQL document with a single fragment or a document + * with multiple fragments that represent what you are writing. If you pass + * in a document with multiple fragments then you must also specify a + * `fragmentName`. + * + * Unlike `writeFragment`, the data written with this method will be stored in + * the optimistic portion of the cache and so will not be persisted. This + * optimistic write may also be rolled back with the `rollback` function that + * was returned. + */ + public writeFragmentOptimistically( + data: any, + id: string, + fragment: DocumentNode, + fragmentName?: string, + variables?: Object, + ): { + rollback: () => void, + } { + const optimisticWriteId = (this.optimisticWriteId++).toString(); + this.initStore(); + this.store.dispatch({ + type: 'APOLLO_WRITE_OPTIMISTIC', + optimisticWriteId, + rootId: id, + result: data, + document: getFragmentQuery(fragment, fragmentName), + variables: variables || {}, + }); + return { + rollback: () => this.store.dispatch({ + type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK', + optimisticWriteId, + }), + }; + } + /** * Returns a reducer function configured according to the `reducerConfig` instance variable. */ diff --git a/src/actions.ts b/src/actions.ts index 984f19a1ae6..a65cbf1ff6c 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -164,6 +164,28 @@ export function isWriteAction(action: ApolloAction): action is WriteAction { return action.type === 'APOLLO_WRITE'; } +export interface WriteActionOptimistic { + type: 'APOLLO_WRITE_OPTIMISTIC'; + optimisticWriteId: string; + rootId: string; + result: any; + document: DocumentNode; + variables: Object; +} + +export function isWriteOptimisticAction(action: ApolloAction): action is WriteActionOptimistic { + return action.type === 'APOLLO_WRITE_OPTIMISTIC'; +} + +export interface WriteActionOptimisticRollback { + type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK'; + optimisticWriteId: string; +} + +export function isWriteOptimisticRollbackAction(action: ApolloAction): action is WriteActionOptimisticRollback { + return action.type === 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK'; +} + export type ApolloAction = QueryResultAction | QueryErrorAction | @@ -176,4 +198,6 @@ export type ApolloAction = UpdateQueryResultAction | StoreResetAction | SubscriptionResultAction | - WriteAction; + WriteAction | + WriteActionOptimistic | + WriteActionOptimisticRollback; diff --git a/src/optimistic-data/store.ts b/src/optimistic-data/store.ts index 07d2d63ff35..a3fe545f8d5 100644 --- a/src/optimistic-data/store.ts +++ b/src/optimistic-data/store.ts @@ -1,8 +1,11 @@ import { MutationResultAction, + WriteAction, isMutationInitAction, isMutationResultAction, isMutationErrorAction, + isWriteOptimisticAction, + isWriteOptimisticRollbackAction, } from '../actions'; import { @@ -28,11 +31,14 @@ import { import { assign } from '../util/assign'; -// a stack of patches of new or changed documents -export type OptimisticStore = { - mutationId: string, +export type OptimisticStoreItem = { + mutationId?: string, + optimisticWriteId?: string, data: NormalizedCache, -}[]; +}; + +// a stack of patches of new or changed documents +export type OptimisticStore = OptimisticStoreItem[]; const optimisticDefaultState: any[] = []; @@ -62,11 +68,10 @@ export function optimistic( updateQueries: action.updateQueries, }; - const fakeStore = { + const optimisticData = getDataWithOptimisticResults({ ...store, optimistic: previousState, - }; - const optimisticData = getDataWithOptimisticResults(fakeStore); + }); const patch = getOptimisticDataPatch( optimisticData, @@ -87,29 +92,50 @@ export function optimistic( return newState; } else if ((isMutationErrorAction(action) || isMutationResultAction(action)) && previousState.some(change => change.mutationId === action.mutationId)) { - // Create a shallow copy of the data in the store. - const optimisticData = assign({}, store.data); - - const newState = previousState - // Throw away optimistic changes of that particular mutation - .filter(change => change.mutationId !== action.mutationId) - // Re-run all of our optimistic data actions on top of one another. - .map(change => { - const patch = getOptimisticDataPatch( - optimisticData, - change.action, - store.queries, - store.mutations, - config, - ); - assign(optimisticData, patch); - return { - ...change, - data: patch, - }; - }); + return rollbackOptimisticData( + change => change.mutationId === action.mutationId, + previousState, + store, + config, + ); + } else if (isWriteOptimisticAction(action)) { + const fakeWriteAction: WriteAction = { + type: 'APOLLO_WRITE', + rootId: action.rootId, + result: action.result, + document: action.document, + variables: action.variables, + }; + + const optimisticData = getDataWithOptimisticResults({ + ...store, + optimistic: previousState, + }); + + const patch = getOptimisticDataPatch( + optimisticData, + fakeWriteAction, + store.queries, + store.mutations, + config, + ); + + const optimisticState = { + action: fakeWriteAction, + data: patch, + optimisticWriteId: action.optimisticWriteId, + }; + + const newState = [...previousState, optimisticState]; return newState; + } else if (isWriteOptimisticRollbackAction(action)) { + return rollbackOptimisticData( + change => change.optimisticWriteId === action.optimisticWriteId, + previousState, + store, + config, + ); } return previousState; @@ -117,7 +143,7 @@ export function optimistic( function getOptimisticDataPatch ( previousData: NormalizedCache, - optimisticAction: MutationResultAction, + optimisticAction: MutationResultAction | WriteAction, queries: QueryStore, mutations: MutationStore, config: ApolloReducerConfig, @@ -140,3 +166,42 @@ function getOptimisticDataPatch ( return patch; } + +/** + * Rolls back some optimistic data items depending on the provided filter + * function. In rolling back these items we also re-apply the other optimistic + * data patches to make sure our optimistic data is up to date. + * + * The filter function should return true for all items that we want to + * rollback. + */ +function rollbackOptimisticData ( + filterFn: (item: OptimisticStoreItem) => boolean, + previousState = optimisticDefaultState, + store: any, + config: any, +): OptimisticStore { + // Create a shallow copy of the data in the store. + const optimisticData = assign({}, store.data); + + const newState = previousState + // Throw away optimistic changes of that particular mutation + .filter(item => !filterFn(item)) + // Re-run all of our optimistic data actions on top of one another. + .map(change => { + const patch = getOptimisticDataPatch( + optimisticData, + change.action, + store.queries, + store.mutations, + config, + ); + assign(optimisticData, patch); + return { + ...change, + data: patch, + }; + }); + + return newState; +} diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index 3d356da68d8..81d92538ba8 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -3,7 +3,7 @@ import gql from 'graphql-tag'; import { Store } from '../src/store'; import ApolloClient from '../src/ApolloClient'; -describe.only('ApolloClient', () => { +describe('ApolloClient', () => { describe('readQuery', () => { it('will read some data from state', () => { const client = new ApolloClient({ @@ -574,4 +574,503 @@ describe.only('ApolloClient', () => { }); }); }); + + describe('writeQueryOptimistically', () => { + function getOptimisticData (client: ApolloClient) { + return client.store.getState().apollo.optimistic.map((optimistic: any) => optimistic.data); + } + + it('will write some data to the state that can be rolled back', () => { + const client = new ApolloClient(); + + const optimistic1 = client.writeQueryOptimistically({ a: 1 }, gql`{ a }`); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + a: 1, + }, + }, + ]); + + const optimistic2 = client.writeQueryOptimistically({ b: 2, c: 3 }, gql`{ b c }`); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + a: 1, + }, + }, + { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + }, + }, + ]); + + optimistic1.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + b: 2, + c: 3, + }, + }, + ]); + + const optimistic3 = client.writeQueryOptimistically({ a: 4, b: 5, c: 6 }, gql`{ a b c }`); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + b: 2, + c: 3, + }, + }, + { + 'ROOT_QUERY': { + a: 4, + b: 5, + c: 6, + }, + }, + ]); + + optimistic3.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + b: 2, + c: 3, + }, + }, + ]); + + optimistic2.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), []); + }); + + it('will write some deeply nested data to state and roll it back', () => { + const client = new ApolloClient(); + + const optimistic1 = client.writeQueryOptimistically( + { a: 1, d: { e: 4 } }, + gql`{ a d { e } }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + a: 1, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + }, + }, + ]); + + const optimistic2 = client.writeQueryOptimistically( + { d: { h: { i: 7 } } }, + gql`{ d { h { i } } }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + a: 1, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + }, + }, + { + 'ROOT_QUERY': { + a: 1, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + }, + }, + ]); + + optimistic1.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + }, + }, + ]); + + const optimistic3 = client.writeQueryOptimistically( + { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, + gql`{ a b c d { e f g h { i j k } } }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + }, + }, + { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + j: 8, + k: 9, + }, + }, + ]); + + optimistic3.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'ROOT_QUERY': { + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + }, + }, + ]); + + optimistic2.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), []); + }); + }); + + describe('writeFragmentOptimistically', () => { + function getOptimisticData (client: ApolloClient) { + return client.store.getState().apollo.optimistic.map((optimistic: any) => optimistic.data); + } + + it('will write some deeply nested data into state at any id and roll it back', () => { + const client = new ApolloClient({ + dataIdFromObject: (o: any) => o.id, + }); + + const optimistic1 = client.writeFragmentOptimistically( + { e: 4, h: { id: 'bar', i: 7 } }, + 'foo', + gql`fragment fragmentFoo on Foo { e h { i } }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'foo': { + e: 4, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + }, + }, + ]); + + const optimistic2 = client.writeFragmentOptimistically( + { f: 5, g: 6, h: { id: 'bar', j: 8, k: 9 } }, + 'foo', + gql`fragment fragmentFoo on Foo { f g h { j k } }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'foo': { + e: 4, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + }, + }, + { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }, + ]); + + optimistic1.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'foo': { + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + j: 8, + k: 9, + }, + }, + ]); + + const optimistic3 = client.writeFragmentOptimistically( + { i: 10 }, + 'bar', + gql`fragment fragmentBar on Bar { i }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'foo': { + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + j: 8, + k: 9, + }, + }, + { + 'bar': { + i: 10, + j: 8, + k: 9, + }, + }, + ]); + + const optimistic4 = client.writeFragmentOptimistically( + { j: 11, k: 12 }, + 'bar', + gql`fragment fragmentBar on Bar { j k }`, + ); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'foo': { + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + j: 8, + k: 9, + }, + }, + { + 'bar': { + j: 8, + k: 9, + i: 10, + }, + }, + { + 'bar': { + i: 10, + j: 11, + k: 12, + }, + }, + ]); + + optimistic3.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'foo': { + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + j: 8, + k: 9, + }, + }, + { + 'bar': { + j: 11, + k: 12, + }, + }, + ]); + + optimistic2.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), [ + { + 'bar': { + j: 11, + k: 12, + }, + }, + ]); + + optimistic4.rollback(); + + assert.deepEqual(client.store.getState().apollo.data, {}); + assert.deepEqual(getOptimisticData(client), []); + }); + + it('will throw an error when there is no fragment', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.writeFragmentOptimistically({}, 'x', gql`query { a b c }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.writeFragmentOptimistically({}, 'x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + }); }); From bd6a00f8732f5f5cf0bee71ebbf02846ea006bf1 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Fri, 17 Feb 2017 19:34:30 -0500 Subject: [PATCH 10/29] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71b8278ade7..6140774cf81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Expect active development and potentially significant breaking changes in the `0 ### vNEXT - Prefer stale data over partial data in cases where a user would previously get an error. [PR #1306](https://github.com/apollographql/apollo-client/pull/1306) +- Add imperative read and write methods to provide the user the power to interact directly with the GraphQL data cache. [PR #1310](https://github.com/apollographql/apollo-client/pull/1310) - ... ### 0.8.6 From baff038f5840b429b85142ddd219ca8b2dad838c Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 22 Feb 2017 10:16:49 -0500 Subject: [PATCH 11/29] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5a012d8fd..34cc8d68b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Expect active development and potentially significant breaking changes in the `0 ### vNEXT - Prefer stale data over partial data in cases where a user would previously get an error. [PR #1306](https://github.com/apollographql/apollo-client/pull/1306) -- Add imperative read and write methods to provide the user the power to interact directly with the GraphQL data cache. [PR #1310](https://github.com/apollographql/apollo-client/pull/1310) +- Add direct cache manipulation read and write methods to provide the user the power to interact with Apollo’s GraphQL data representation outside of mutations. [PR #1310](https://github.com/apollographql/apollo-client/pull/1310) - ... ### 0.8.7 From 37cd7b9127c2f7ae502f5237be3c2df6f1061164 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 22 Feb 2017 10:17:16 -0500 Subject: [PATCH 12/29] Update ApolloClient.ts --- src/ApolloClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index 9d87df7fc19..bf2c48d7bfb 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -376,7 +376,7 @@ export default class ApolloClient { * query. * * You must pass in a GraphQL document with a single fragment or a document - * with multiple fragments that represent what you are writing. If you pass + * with multiple fragments that represent what you are reading. If you pass * in a document with multiple fragments then you must also specify a * `fragmentName`. */ From b716e19d9f48be32bae2e03ddb09398ff1c17ecb Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 22 Feb 2017 10:32:53 -0500 Subject: [PATCH 13/29] return null when data from an id cannot be found --- src/ApolloClient.ts | 16 +++++++++++++--- test/ApolloClient.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index bf2c48d7bfb..07aaf322449 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -385,13 +385,23 @@ export default class ApolloClient { fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): FragmentType { + ): FragmentType | null { this.initStore(); + + const query = getFragmentQuery(fragment, fragmentName); const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; + const store = reduxRootSelector(this.store.getState()).data; + + // If we could not find an item in the store with the provided id then we + // just return `null`. + if (typeof store[id] === 'undefined') { + return null; + } + return readQueryFromStore({ rootId: id, - store: reduxRootSelector(this.store.getState()).data, - query: getFragmentQuery(fragment, fragmentName), + store, + query, variables, returnPartialData: false, }); diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index 81d92538ba8..40d182e0110 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -226,6 +226,32 @@ describe('ApolloClient', () => { }, ), { a: 1, b: 2 }); }); + + it('will return null when an id that can’t be found is provided', () => { + const client1 = new ApolloClient(); + const client2 = new ApolloClient({ + initialState: { + apollo: { + data: { + 'bar': { __typename: 'Type1', a: 1, b: 2, c: 3 }, + }, + }, + }, + }); + const client3 = new ApolloClient({ + initialState: { + apollo: { + data: { + 'foo': { __typename: 'Type1', a: 1, b: 2, c: 3 }, + }, + }, + }, + }); + + assert.equal(client1.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); + assert.equal(client2.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); + assert.deepEqual(client3.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), { a: 1, b: 2, c: 3 }); + }); }); describe('writeQuery', () => { From 793c59db50027f8238cf8ec3d6249e23a5a648c7 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 22 Feb 2017 10:47:09 -0500 Subject: [PATCH 14/29] reword docs and add argument docs --- src/ApolloClient.ts | 100 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index 07aaf322449..ed7f934f8bd 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -350,9 +350,14 @@ export default class ApolloClient { } /** - * Tries to read some data from the store without making a network request. - * This method will start at the root query. To start at a specific id - * returned by `dataIdFromObject` use `readFragment`. + * Tries to read some data from the store in the shape of the provided + * GraphQL query without making a network request. This method will start at + * the root query. To start at a specific id returned by `dataIdFromObject` + * use `readFragment`. + * + * @param query The GraphQL query shape to be used. + * + * @param variables Any variables that the GraphQL query may depend on. */ public readQuery( query: DocumentNode, @@ -370,15 +375,30 @@ export default class ApolloClient { } /** - * Tries to read some data from the store without making a network request. - * This method will read a GraphQL fragment from any arbitrary id that is - * currently cached. Unlike `readQuery` which will only read from the root - * query. + * Tries to read some data from the store in the shape of the provided + * GraphQL fragment without making a network request. This method will read a + * GraphQL fragment from any arbitrary id that is currently cached, unlike + * `readQuery` which will only read from the root query. * * You must pass in a GraphQL document with a single fragment or a document * with multiple fragments that represent what you are reading. If you pass * in a document with multiple fragments then you must also specify a * `fragmentName`. + * + * @param id The root id to be used. This id should take the same form as the + * value returned by your `dataIdFromObject` function. If a value with your + * id does not exist in the store, `null` will be returned. + * + * @param fragment A GraphQL document with one or more fragments the shape of + * which will be used. If you provide more then one fragments then you must + * also specify the next argument, `fragmentName`, to select a single + * fragment to use when reading. + * + * @param fragmentName The name of the fragment in your GraphQL document to + * be used. Pass `undefined` if there is only one fragment and you want to + * use that. + * + * @param variables Any variables that your GraphQL fragments depend on. */ public readFragment( id: string, @@ -408,9 +428,15 @@ export default class ApolloClient { } /** - * Writes some data to the store without that data being the result of a - * network request. This method will start at the root query. To start at a a + * Writes some data in the shape of the provided GraphQL query directly to + * the store. This method will start at the root query. To start at a a * specific id returned by `dataIdFromObject` then use `writeFragment`. + * + * @param data The data you will be writing to the store. + * + * @param query The GraphQL query shape to be used. + * + * @param variables Any variables that the GraphQL query may depend on. */ public writeQuery( data: any, @@ -428,15 +454,31 @@ export default class ApolloClient { } /** - * Writes some data to the store without that data being the result of a - * network request. This method will write to a GraphQL fragment from any - * arbitrary id that is currently cached. Unlike `writeQuery` which will only - * write from the root query. + * Writes some data in the shape of the provided GraphQL fragment directly to + * the store. This method will write to a GraphQL fragment from any arbitrary + * id that is currently cached, unlike `writeQuery` which will only write + * from the root query. * * You must pass in a GraphQL document with a single fragment or a document * with multiple fragments that represent what you are writing. If you pass * in a document with multiple fragments then you must also specify a * `fragmentName`. + * + * @param data The data you will be writing to the store. + * + * @param id The root id to be used. This id should take the same form as the + * value returned by your `dataIdFromObject` function. + * + * @param fragment A GraphQL document with one or more fragments the shape of + * which will be used. If you provide more then one fragments then you must + * also specify the next argument, `fragmentName`, to select a single + * fragment to use when reading. + * + * @param fragmentName The name of the fragment in your GraphQL document to + * be used. Pass `undefined` if there is only one fragment and you want to + * use that. + * + * @param variables Any variables that your GraphQL fragments depend on. */ public writeFragment( data: any, @@ -456,8 +498,8 @@ export default class ApolloClient { } /** - * Writes some data to the store without that data being the result of a - * network request. This method will start at the root query. To start at a a + * Writes some data in the shape of the provided GraphQL query directly to + * the store. This method will start at the root query. To start at a * specific id returned by `dataIdFromObject` then use * `writeFragmentOptimistically`. * @@ -465,6 +507,12 @@ export default class ApolloClient { * the optimistic portion of the cache and so will not be persisted. This * optimistic write may also be rolled back with the `rollback` function that * was returned. + * + * @param data The data you will be writing to the store. + * + * @param query The GraphQL query shape to be used. + * + * @param variables Any variables that the GraphQL query may depend on. */ public writeQueryOptimistically( data: any, @@ -492,9 +540,9 @@ export default class ApolloClient { } /** - * Writes some data to the store without that data being the result of a - * network request. This method will write to a GraphQL fragment from any - * arbitrary id that is currently cached. Unlike `writeQueryOptimistically` + * Writes some data in the shape of the provided GraphQL fragment directly to + * the store. This method will write to a GraphQL fragment from any + * arbitrary id that is currently cached, unlike `writeQueryOptimistically` * which will only write from the root query. * * You must pass in a GraphQL document with a single fragment or a document @@ -506,6 +554,22 @@ export default class ApolloClient { * the optimistic portion of the cache and so will not be persisted. This * optimistic write may also be rolled back with the `rollback` function that * was returned. + * + * @param data The data you will be writing to the store. + * + * @param id The root id to be used. This id should take the same form as the + * value returned by your `dataIdFromObject` function. + * + * @param fragment A GraphQL document with one or more fragments the shape of + * which will be used. If you provide more then one fragments then you must + * also specify the next argument, `fragmentName`, to select a single + * fragment to use when reading. + * + * @param fragmentName The name of the fragment in your GraphQL document to + * be used. Pass `undefined` if there is only one fragment and you want to + * use that. + * + * @param variables Any variables that your GraphQL fragments depend on. */ public writeFragmentOptimistically( data: any, From 116f9b0aa665e1a904f74ea40f9393d4ac4645a5 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 22 Feb 2017 10:49:37 -0500 Subject: [PATCH 15/29] =?UTF-8?q?swap=20=E2=80=9Cstate=E2=80=9D=20for=20?= =?UTF-8?q?=E2=80=9Cstore=E2=80=9D=20in=20test=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/ApolloClient.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index 40d182e0110..06c30b72de3 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -5,7 +5,7 @@ import ApolloClient from '../src/ApolloClient'; describe('ApolloClient', () => { describe('readQuery', () => { - it('will read some data from state', () => { + it('will read some data from the store', () => { const client = new ApolloClient({ initialState: { apollo: { @@ -25,7 +25,7 @@ describe('ApolloClient', () => { assert.deepEqual(client.readQuery(gql`{ a b c }`), { a: 1, b: 2, c: 3 }); }); - it('will read some deeply nested data from state', () => { + it('will read some deeply nested data from the store', () => { const client = new ApolloClient({ initialState: { apollo: { @@ -74,7 +74,7 @@ describe('ApolloClient', () => { ); }); - it('will read some data from state with variables', () => { + it('will read some data from the store with variables', () => { const client = new ApolloClient({ initialState: { apollo: { @@ -102,7 +102,7 @@ describe('ApolloClient', () => { }); describe('readFragment', () => { - it('will read some deeply nested data from state at any id', () => { + it('will read some deeply nested data from the store at any id', () => { const client = new ApolloClient({ initialState: { apollo: { @@ -196,7 +196,7 @@ describe('ApolloClient', () => { }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); }); - it('will read some data from state with variables', () => { + it('will read some data from the store with variables', () => { const client = new ApolloClient({ initialState: { apollo: { @@ -255,7 +255,7 @@ describe('ApolloClient', () => { }); describe('writeQuery', () => { - it('will write some data to the state', () => { + it('will write some data to the store', () => { const client = new ApolloClient(); client.writeQuery({ a: 1 }, gql`{ a }`); @@ -287,7 +287,7 @@ describe('ApolloClient', () => { }); }); - it('will write some deeply nested data to state', () => { + it('will write some deeply nested data to the store', () => { const client = new ApolloClient(); client.writeQuery( @@ -370,7 +370,7 @@ describe('ApolloClient', () => { }); }); - it('will write some data to state with variables', () => { + it('will write some data to the store with variables', () => { const client = new ApolloClient(); client.writeQuery( @@ -400,7 +400,7 @@ describe('ApolloClient', () => { }); describe('writeFragment', () => { - it('will write some deeply nested data into state at any id', () => { + it('will write some deeply nested data into the store at any id', () => { const client = new ApolloClient({ dataIdFromObject: (o: any) => o.id, }); @@ -570,7 +570,7 @@ describe('ApolloClient', () => { }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); }); - it('will write some data to state with variables', () => { + it('will write some data to the store with variables', () => { const client = new ApolloClient(); client.writeFragment( @@ -606,7 +606,7 @@ describe('ApolloClient', () => { return client.store.getState().apollo.optimistic.map((optimistic: any) => optimistic.data); } - it('will write some data to the state that can be rolled back', () => { + it('will write some data to the store that can be rolled back', () => { const client = new ApolloClient(); const optimistic1 = client.writeQueryOptimistically({ a: 1 }, gql`{ a }`); @@ -687,7 +687,7 @@ describe('ApolloClient', () => { assert.deepEqual(getOptimisticData(client), []); }); - it('will write some deeply nested data to state and roll it back', () => { + it('will write some deeply nested data to the store and roll it back', () => { const client = new ApolloClient(); const optimistic1 = client.writeQueryOptimistically( @@ -872,7 +872,7 @@ describe('ApolloClient', () => { return client.store.getState().apollo.optimistic.map((optimistic: any) => optimistic.data); } - it('will write some deeply nested data into state at any id and roll it back', () => { + it('will write some deeply nested data into the store at any id and roll it back', () => { const client = new ApolloClient({ dataIdFromObject: (o: any) => o.id, }); From 23eb614dae1a642fd6f51e9593375bb4df65c129 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 22 Feb 2017 10:51:25 -0500 Subject: [PATCH 16/29] move error tests to suite top --- test/ApolloClient.ts | 132 +++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index 06c30b72de3..3aa53c44520 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -102,6 +102,28 @@ describe('ApolloClient', () => { }); describe('readFragment', () => { + it('will throw an error when there is no fragment', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.readFragment('x', gql`query { a b c }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.readFragment('x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + it('will read some deeply nested data from the store at any id', () => { const client = new ApolloClient({ initialState: { @@ -174,28 +196,6 @@ describe('ApolloClient', () => { ); }); - it('will throw an error when there is no fragment', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.readFragment('x', gql`query { a b c }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - assert.throws(() => { - client.readFragment('x', gql`schema { query: Query }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - }); - - it('will throw an error when there is more than one fragment but no fragment name', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); - }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - assert.throws(() => { - client.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); - }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - }); - it('will read some data from the store with variables', () => { const client = new ApolloClient({ initialState: { @@ -255,6 +255,28 @@ describe('ApolloClient', () => { }); describe('writeQuery', () => { + it('will throw an error when there is no fragment', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.writeFragment({}, 'x', gql`query { a b c }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.writeFragment({}, 'x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + it('will write some data to the store', () => { const client = new ApolloClient(); @@ -548,28 +570,6 @@ describe('ApolloClient', () => { }); }); - it('will throw an error when there is no fragment', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.writeFragment({}, 'x', gql`query { a b c }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - assert.throws(() => { - client.writeFragment({}, 'x', gql`schema { query: Query }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - }); - - it('will throw an error when there is more than one fragment but no fragment name', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); - }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - assert.throws(() => { - client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); - }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - }); - it('will write some data to the store with variables', () => { const client = new ApolloClient(); @@ -872,6 +872,28 @@ describe('ApolloClient', () => { return client.store.getState().apollo.optimistic.map((optimistic: any) => optimistic.data); } + it('will throw an error when there is no fragment', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.writeFragmentOptimistically({}, 'x', gql`query { a b c }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.writeFragmentOptimistically({}, 'x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + assert.throws(() => { + client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }); + it('will write some deeply nested data into the store at any id and roll it back', () => { const client = new ApolloClient({ dataIdFromObject: (o: any) => o.id, @@ -1076,27 +1098,5 @@ describe('ApolloClient', () => { assert.deepEqual(client.store.getState().apollo.data, {}); assert.deepEqual(getOptimisticData(client), []); }); - - it('will throw an error when there is no fragment', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.writeFragmentOptimistically({}, 'x', gql`query { a b c }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - assert.throws(() => { - client.writeFragmentOptimistically({}, 'x', gql`schema { query: Query }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - }); - - it('will throw an error when there is more than one fragment but no fragment name', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); - }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - assert.throws(() => { - client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); - }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - }); }); }); From d3b399564d0cf897ea568e6f7318426c811b4486 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 22 Feb 2017 10:59:16 -0500 Subject: [PATCH 17/29] add an error for operations in getFragmentQuery --- src/queries/getFromAST.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/queries/getFromAST.ts b/src/queries/getFromAST.ts index 87d3d36eead..0d1cb3fff71 100644 --- a/src/queries/getFromAST.ts +++ b/src/queries/getFromAST.ts @@ -176,10 +176,22 @@ export function getFragmentQuery(document: DocumentNode, fragmentName?: string): // If the user did not give us a fragment name then let us try to get a // name from a single fragment in the definition. if (typeof actualFragmentName === 'undefined') { - const fragments = document.definitions.filter(({ kind }) => kind === 'FragmentDefinition') as Array; + const fragments = document.definitions.filter(definition => { + // Throw an error if we encounter an operation definition because we will + // define our own operation definition later on. + if (definition.kind === 'OperationDefinition') { + throw new Error( + `Found a ${definition.operation} operation${definition.name ? ` named '${definition.name.value}'` : ''}. ` + + 'No operations are allowed when using a fragment as a query. Only fragments are allowed.', + ); + } + return definition.kind === 'FragmentDefinition'; + }) as Array; + if (fragments.length !== 1) { throw new Error(`Found ${fragments.length} fragments. \`fragmentName\` must be provided when there are more then 1 fragments.`); } + actualFragmentName = fragments[0].name.value; } From dc915ec4edc1ba1417fe5e6eb54597c438d55ecd Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 22 Feb 2017 11:16:03 -0500 Subject: [PATCH 18/29] add tests for getFragmentQueryDocument --- src/ApolloClient.ts | 8 +-- src/queries/getFromAST.ts | 37 ++++++++------ test/ApolloClient.ts | 60 +++++++++++----------- test/getFromAST.ts | 103 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 49 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index ed7f934f8bd..f5e233c1692 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -65,7 +65,7 @@ import { } from './data/readFromStore'; import { - getFragmentQuery, + getFragmentQueryDocument, } from './queries/getFromAST'; import { @@ -408,7 +408,7 @@ export default class ApolloClient { ): FragmentType | null { this.initStore(); - const query = getFragmentQuery(fragment, fragmentName); + const query = getFragmentQueryDocument(fragment, fragmentName); const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; const store = reduxRootSelector(this.store.getState()).data; @@ -492,7 +492,7 @@ export default class ApolloClient { type: 'APOLLO_WRITE', rootId: id, result: data, - document: getFragmentQuery(fragment, fragmentName), + document: getFragmentQueryDocument(fragment, fragmentName), variables: variables || {}, }); } @@ -587,7 +587,7 @@ export default class ApolloClient { optimisticWriteId, rootId: id, result: data, - document: getFragmentQuery(fragment, fragmentName), + document: getFragmentQueryDocument(fragment, fragmentName), variables: variables || {}, }); return { diff --git a/src/queries/getFromAST.ts b/src/queries/getFromAST.ts index 0d1cb3fff71..672e1daef0f 100644 --- a/src/queries/getFromAST.ts +++ b/src/queries/getFromAST.ts @@ -170,28 +170,35 @@ export function createFragmentMap(fragments: FragmentDefinitionNode[] = []): Fra * fragment specified by the provided `fragmentName`. If there is more then one * fragment, but a `fragmentName` was not defined then an error will be thrown. */ -export function getFragmentQuery(document: DocumentNode, fragmentName?: string): DocumentNode { +export function getFragmentQueryDocument(document: DocumentNode, fragmentName?: string): DocumentNode { let actualFragmentName = fragmentName; + // Build an array of all our fragment definitions that will be used for + // validations. We also do some validations on the other definitions in the + // document while building this list. + const fragments: Array = []; + document.definitions.forEach(definition => { + // Throw an error if we encounter an operation definition because we will + // define our own operation definition later on. + if (definition.kind === 'OperationDefinition') { + throw new Error( + `Found a ${definition.operation} operation${definition.name ? ` named '${definition.name.value}'` : ''}. ` + + 'No operations are allowed when using a fragment as a query. Only fragments are allowed.', + ); + } + // Add our definition to the fragments array if it is a fragment + // definition. + if (definition.kind === 'FragmentDefinition') { + fragments.push(definition); + } + }); + // If the user did not give us a fragment name then let us try to get a // name from a single fragment in the definition. if (typeof actualFragmentName === 'undefined') { - const fragments = document.definitions.filter(definition => { - // Throw an error if we encounter an operation definition because we will - // define our own operation definition later on. - if (definition.kind === 'OperationDefinition') { - throw new Error( - `Found a ${definition.operation} operation${definition.name ? ` named '${definition.name.value}'` : ''}. ` + - 'No operations are allowed when using a fragment as a query. Only fragments are allowed.', - ); - } - return definition.kind === 'FragmentDefinition'; - }) as Array; - if (fragments.length !== 1) { - throw new Error(`Found ${fragments.length} fragments. \`fragmentName\` must be provided when there are more then 1 fragments.`); + throw new Error(`Found ${fragments.length} fragments. \`fragmentName\` must be provided when there is not exactly 1 fragment.`); } - actualFragmentName = fragments[0].name.value; } diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index 3aa53c44520..f59f2221b83 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -107,10 +107,10 @@ describe('ApolloClient', () => { assert.throws(() => { client.readFragment('x', gql`query { a b c }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); assert.throws(() => { client.readFragment('x', gql`schema { query: Query }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); it('will throw an error when there is more than one fragment but no fragment name', () => { @@ -118,10 +118,10 @@ describe('ApolloClient', () => { assert.throws(() => { client.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); - }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); assert.throws(() => { client.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); - }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); it('will read some deeply nested data from the store at any id', () => { @@ -255,28 +255,6 @@ describe('ApolloClient', () => { }); describe('writeQuery', () => { - it('will throw an error when there is no fragment', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.writeFragment({}, 'x', gql`query { a b c }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - assert.throws(() => { - client.writeFragment({}, 'x', gql`schema { query: Query }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - }); - - it('will throw an error when there is more than one fragment but no fragment name', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); - }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - assert.throws(() => { - client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); - }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); - }); - it('will write some data to the store', () => { const client = new ApolloClient(); @@ -422,6 +400,28 @@ describe('ApolloClient', () => { }); describe('writeFragment', () => { + it('will throw an error when there is no fragment', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.writeFragment({}, 'x', gql`query { a b c }`); + }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); + assert.throws(() => { + client.writeFragment({}, 'x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const client = new ApolloClient(); + + assert.throws(() => { + client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + assert.throws(() => { + client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + it('will write some deeply nested data into the store at any id', () => { const client = new ApolloClient({ dataIdFromObject: (o: any) => o.id, @@ -877,10 +877,10 @@ describe('ApolloClient', () => { assert.throws(() => { client.writeFragmentOptimistically({}, 'x', gql`query { a b c }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); assert.throws(() => { client.writeFragmentOptimistically({}, 'x', gql`schema { query: Query }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); it('will throw an error when there is more than one fragment but no fragment name', () => { @@ -888,10 +888,10 @@ describe('ApolloClient', () => { assert.throws(() => { client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); - }, 'Found 2 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); assert.throws(() => { client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); - }, 'Found 3 fragments. `fragmentName` must be provided when there are more then 1 fragments.'); + }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); it('will write some deeply nested data into the store at any id and roll it back', () => { diff --git a/test/getFromAST.ts b/test/getFromAST.ts index 5e75cb54756..70272044095 100644 --- a/test/getFromAST.ts +++ b/test/getFromAST.ts @@ -6,6 +6,7 @@ import { createFragmentMap, FragmentMap, getOperationName, + getFragmentQueryDocument, } from '../src/queries/getFromAST'; import { @@ -229,4 +230,106 @@ describe('AST utility functions', () => { getQueryDefinition(queryWithTypeDefination); }, 'Schema type definitions not allowed in queries. Found: "InputObjectTypeDefinition"'); }); + + describe('getFragmentQueryDocument', () => { + it('will throw an error if there is an operation', () => { + assert.throws( + () => getFragmentQueryDocument(gql`{ a b c }`), + 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.', + ); + assert.throws( + () => getFragmentQueryDocument(gql`query { a b c }`), + 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.', + ); + assert.throws( + () => getFragmentQueryDocument(gql`query Named { a b c }`), + 'Found a query operation named \'Named\'. No operations are allowed when using a fragment as a query. Only fragments are allowed.', + ); + assert.throws( + () => getFragmentQueryDocument(gql`mutation Named { a b c }`), + 'Found a mutation operation named \'Named\'. No operations are allowed when using a fragment as a query. ' + + 'Only fragments are allowed.', + ); + assert.throws( + () => getFragmentQueryDocument(gql`subscription Named { a b c }`), + 'Found a subscription operation named \'Named\'. No operations are allowed when using a fragment as a query. ' + + 'Only fragments are allowed.', + ); + }); + + it('will throw an error if there is not exactly one fragment but no `fragmentName`', () => { + assert.throws(() => { + getFragmentQueryDocument(gql` + fragment foo on Foo { a b c } + fragment bar on Bar { d e f } + `); + }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + assert.throws(() => { + getFragmentQueryDocument(gql` + fragment foo on Foo { a b c } + fragment bar on Bar { d e f } + fragment baz on Baz { g h i } + `); + }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + assert.throws(() => { + getFragmentQueryDocument(gql` + scalar Foo + `); + }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + + it('will create a query document where the single fragment is spread in the root query', () => { + assert.deepEqual( + print(getFragmentQueryDocument(gql` + fragment foo on Foo { a b c } + `)), + print(gql` + { ...foo } + fragment foo on Foo { a b c } + `), + ); + }); + + it('will create a query document where the named fragment is spread in the root query', () => { + assert.deepEqual( + print(getFragmentQueryDocument(gql` + fragment foo on Foo { a b c } + fragment bar on Bar { d e f ...foo } + fragment baz on Baz { g h i ...foo ...bar } + `, 'foo')), + print(gql` + { ...foo } + fragment foo on Foo { a b c } + fragment bar on Bar { d e f ...foo } + fragment baz on Baz { g h i ...foo ...bar } + `), + ); + assert.deepEqual( + print(getFragmentQueryDocument(gql` + fragment foo on Foo { a b c } + fragment bar on Bar { d e f ...foo } + fragment baz on Baz { g h i ...foo ...bar } + `, 'bar')), + print(gql` + { ...bar } + fragment foo on Foo { a b c } + fragment bar on Bar { d e f ...foo } + fragment baz on Baz { g h i ...foo ...bar } + `), + ); + assert.deepEqual( + print(getFragmentQueryDocument(gql` + fragment foo on Foo { a b c } + fragment bar on Bar { d e f ...foo } + fragment baz on Baz { g h i ...foo ...bar } + `, 'baz')), + print(gql` + { ...baz } + fragment foo on Foo { a b c } + fragment bar on Bar { d e f ...foo } + fragment baz on Baz { g h i ...foo ...bar } + `), + ); + }); + }); }); From f56d476251ba34591a127c68d57d06ec383021e9 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Fri, 24 Feb 2017 11:34:01 -0500 Subject: [PATCH 19/29] add a data proxy and transactions --- src/ApolloClient.ts | 135 +++++++++------- src/actions.ts | 13 +- src/data/proxy.ts | 302 +++++++++++++++++++++++++++++++++++ src/data/store.ts | 26 +-- src/optimistic-data/store.ts | 5 +- 5 files changed, 400 insertions(+), 81 deletions(-) create mode 100644 src/data/proxy.ts diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index f5e233c1692..adccd832279 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -60,14 +60,16 @@ import { storeKeyNameFromFieldNameAndArgs, } from './data/storeUtils'; -import { - readQueryFromStore, -} from './data/readFromStore'; - import { getFragmentQueryDocument, } from './queries/getFromAST'; +import { + DataProxy, + ReduxDataProxy, + TransactionDataProxy, +} from './data/proxy'; + import { version, } from './version'; @@ -111,6 +113,7 @@ export default class ApolloClient { private devToolsHookCb: Function; private optimisticWriteId: number; + private proxy: DataProxy | undefined; /** * Constructs an instance of {@link ApolloClient}. @@ -363,15 +366,7 @@ export default class ApolloClient { query: DocumentNode, variables?: Object, ): QueryType { - this.initStore(); - const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; - return readQueryFromStore({ - rootId: 'ROOT_QUERY', - store: reduxRootSelector(this.store.getState()).data, - query, - variables, - returnPartialData: false, - }); + return this.initProxy().readQuery(query, variables); } /** @@ -406,25 +401,7 @@ export default class ApolloClient { fragmentName?: string, variables?: Object, ): FragmentType | null { - this.initStore(); - - const query = getFragmentQueryDocument(fragment, fragmentName); - const reduxRootSelector = this.reduxRootSelector || defaultReduxRootSelector; - const store = reduxRootSelector(this.store.getState()).data; - - // If we could not find an item in the store with the provided id then we - // just return `null`. - if (typeof store[id] === 'undefined') { - return null; - } - - return readQueryFromStore({ - rootId: id, - store, - query, - variables, - returnPartialData: false, - }); + return this.initProxy().readFragment(id, fragment, fragmentName, variables); } /** @@ -442,15 +419,8 @@ export default class ApolloClient { data: any, query: DocumentNode, variables?: Object, - ) { - this.initStore(); - this.store.dispatch({ - type: 'APOLLO_WRITE', - rootId: 'ROOT_QUERY', - result: data, - document: query, - variables: variables || {}, - }); + ): void { + return this.initProxy().writeQuery(data, query, variables); } /** @@ -486,15 +456,8 @@ export default class ApolloClient { fragment: DocumentNode, fragmentName?: string, variables?: Object, - ) { - this.initStore(); - this.store.dispatch({ - type: 'APOLLO_WRITE', - rootId: id, - result: data, - document: getFragmentQueryDocument(fragment, fragmentName), - variables: variables || {}, - }); + ): void { + return this.initProxy().writeFragment(data, id, fragment, fragmentName, variables); } /** @@ -526,10 +489,12 @@ export default class ApolloClient { this.store.dispatch({ type: 'APOLLO_WRITE_OPTIMISTIC', optimisticWriteId, - rootId: 'ROOT_QUERY', - result: data, - document: query, - variables: variables || {}, + writes: [{ + rootId: 'ROOT_QUERY', + result: data, + document: query, + variables: variables || {}, + }], }); return { rollback: () => this.store.dispatch({ @@ -585,10 +550,48 @@ export default class ApolloClient { this.store.dispatch({ type: 'APOLLO_WRITE_OPTIMISTIC', optimisticWriteId, - rootId: id, - result: data, - document: getFragmentQueryDocument(fragment, fragmentName), - variables: variables || {}, + writes: [{ + rootId: id, + result: data, + document: getFragmentQueryDocument(fragment, fragmentName), + variables: variables || {}, + }], + }); + return { + rollback: () => this.store.dispatch({ + type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK', + optimisticWriteId, + }), + }; + } + + public writeTransaction( + transactionFn: (proxy: DataProxy) => void, + ): void { + this.initStore(); + const transactionProxy = new TransactionDataProxy(this.initProxy()); + transactionFn(transactionProxy); + const writes = transactionProxy.finish(); + this.store.dispatch({ + type: 'APOLLO_WRITE', + writes, + }); + } + + public writeTransactionOptimistically( + transactionFn: (proxy: DataProxy) => void, + ): { + rollback: () => void, + } { + const optimisticWriteId = (this.optimisticWriteId++).toString(); + this.initStore(); + const transactionProxy = new TransactionDataProxy(this.initProxy()); + transactionFn(transactionProxy); + const writes = transactionProxy.finish(); + this.store.dispatch({ + type: 'APOLLO_WRITE_OPTIMISTIC', + optimisticWriteId, + writes, }); return { rollback: () => this.store.dispatch({ @@ -719,4 +722,20 @@ export default class ApolloClient { queryDeduplication: this.queryDeduplication, }); }; + + /** + * Initializes a data proxy for this client instance if one does not already + * exist and returns either a previously initialized proxy instance or the + * newly initialized instance. + */ + private initProxy(): DataProxy { + if (!this.proxy) { + this.initStore(); + this.proxy = new ReduxDataProxy( + this.store, + this.reduxRootSelector || defaultReduxRootSelector, + ); + } + return this.proxy; + } } diff --git a/src/actions.ts b/src/actions.ts index a65cbf1ff6c..39ed85d8b55 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -152,14 +152,18 @@ export function isSubscriptionResultAction(action: ApolloAction): action is Subs return action.type === 'APOLLO_SUBSCRIPTION_RESULT'; } -export interface WriteAction { - type: 'APOLLO_WRITE'; +export interface DataWrite { rootId: string; result: any; document: DocumentNode; variables: Object; } +export interface WriteAction { + type: 'APOLLO_WRITE'; + writes: Array; +} + export function isWriteAction(action: ApolloAction): action is WriteAction { return action.type === 'APOLLO_WRITE'; } @@ -167,10 +171,7 @@ export function isWriteAction(action: ApolloAction): action is WriteAction { export interface WriteActionOptimistic { type: 'APOLLO_WRITE_OPTIMISTIC'; optimisticWriteId: string; - rootId: string; - result: any; - document: DocumentNode; - variables: Object; + writes: Array; } export function isWriteOptimisticAction(action: ApolloAction): action is WriteActionOptimistic { diff --git a/src/data/proxy.ts b/src/data/proxy.ts new file mode 100644 index 00000000000..39149c5ab48 --- /dev/null +++ b/src/data/proxy.ts @@ -0,0 +1,302 @@ +import { DocumentNode } from 'graphql'; +import { ApolloStore, Store } from '../store'; +import { DataWrite } from '../actions'; +import { getFragmentQueryDocument } from '../queries/getFromAST'; +import { readQueryFromStore } from './readFromStore'; + +/** + * A proxy to the normalized data living in our store. This interface allows a + * user to read and write denormalized data which feels natural to the user + * whilst in the background this data is being converted into the normalized + * store format. + */ +export interface DataProxy extends DataProxyRead, DataProxyWrite {} + +/** + * A subset of the methods on `DataProxy` which just involve the methods for + * reading some data. These methods will not change any data. + */ +export interface DataProxyRead { + /** + * Reads a GraphQL query from the root query id. + */ + readQuery( + query: DocumentNode, + variables?: Object, + ): QueryType; + + /** + * Reads a GraphQL fragment from any arbitrary id. If there are more then + * one fragments in the provided document then a `fragmentName` must be + * provided to select the correct fragment. + */ + readFragment( + id: string, + fragment: DocumentNode, + fragmentName?: string, + variables?: Object, + ): FragmentType | null; +} + +/** + * A subset of the methods on `DataProxy` which just involve the methods for + * writing some data. These methods *will* change the underlying data + * representation. How and when that change happens is up to the implementor, + * but the expectation is that calling these methods will eventually change + * some data. + */ +export interface DataProxyWrite { + /** + * Writes a GraphQL query to the root query id. + */ + writeQuery( + data: any, + query: DocumentNode, + variables?: Object, + ): void; + + /** + * Writes a GraphQL fragment to any arbitrary id. If there are more then + * one fragments in the provided document then a `fragmentName` must be + * provided to select the correct fragment. + */ + writeFragment( + data: any, + id: string, + fragment: DocumentNode, + fragmentName?: string, + variables?: Object, + ): void; +} + +/** + * A data proxy that is completely powered by our Redux store. Reads are read + * from the Redux state and writes are dispatched using actions where they will + * update the store. + * + * Needs a Redux store and a selector function to get the Apollo state from the + * root Redux state. + */ +export class ReduxDataProxy implements DataProxy { + /** + * The Redux store that we read and write to. + */ + private store: ApolloStore; + + /** + * A function that selects the Apollo state from Redux state. + */ + private reduxRootSelector: (state: any) => Store; + + constructor( + store: ApolloStore, + reduxRootSelector: (state: any) => Store, + ) { + this.store = store; + this.reduxRootSelector = reduxRootSelector; + } + + /** + * Reads a query from the Redux state. + */ + public readQuery( + query: DocumentNode, + variables?: Object, + ): QueryType { + return readQueryFromStore({ + rootId: 'ROOT_QUERY', + store: this.reduxRootSelector(this.store.getState()).data, + query, + variables, + returnPartialData: false, + }); + } + + /** + * Reads a fragment from the Redux state. + */ + public readFragment( + id: string, + fragment: DocumentNode, + fragmentName?: string, + variables?: Object, + ): FragmentType | null { + const query = getFragmentQueryDocument(fragment, fragmentName); + const { data } = this.reduxRootSelector(this.store.getState()); + + // If we could not find an item in the store with the provided id then we + // just return `null`. + if (typeof data[id] === 'undefined') { + return null; + } + + return readQueryFromStore({ + rootId: id, + store: data, + query, + variables, + returnPartialData: false, + }); + } + + /** + * Writes a query to the Redux state. + */ + public writeQuery( + data: any, + query: DocumentNode, + variables?: Object, + ): void { + this.store.dispatch({ + type: 'APOLLO_WRITE', + writes: [{ + rootId: 'ROOT_QUERY', + result: data, + document: query, + variables: variables || {}, + }], + }); + } + + /** + * Writes a fragment to the Redux state. + */ + public writeFragment( + data: any, + id: string, + fragment: DocumentNode, + fragmentName?: string, + variables?: Object, + ): void { + this.store.dispatch({ + type: 'APOLLO_WRITE', + writes: [{ + rootId: id, + result: data, + document: getFragmentQueryDocument(fragment, fragmentName), + variables: variables || {}, + }], + }); + } +} + +/** + * A data proxy to be used within a transaction. It uses another data proxy for + * reads and pushes all writes to an actions array which can be retrieved when + * the transaction finishes. As soon as a transaction is constructed it has + * started. Once a transaction has finished none of its methods are usable. + */ +export class TransactionDataProxy implements DataProxy { + /** + * The proxy to use for reading. The reason a transaction data proxy is not + * just a write proxy is that we want to throw errors if a transaction has + * finished and a user is trying to read. + */ + private proxy: DataProxyRead; + + /** + * An array of actions that we build up during the life of the transaction. + * Once a transaction finishes the actions array will be returned. + */ + private writes: Array; + + /** + * A boolean flag signaling if the transaction has finished or not. + */ + private isFinished: boolean; + + constructor(proxy: DataProxyRead) { + this.proxy = proxy; + this.writes = []; + this.isFinished = false; + } + + /** + * Finishes a transaction and returns the actions accumulated during this + * transaction. The actions are not ready for dispatch in Redux, however. The + * `type` must be added before that. + */ + public finish(): Array { + this.assertNotFinished(); + const writes = this.writes; + this.writes = []; + this.isFinished = true; + return writes; + } + + /** + * Reads some data from the store from the root query id. Cannot be called + * after the transaction finishes. + */ + public readQuery( + query: DocumentNode, + variables?: Object, + ): QueryType { + this.assertNotFinished(); + return this.proxy.readQuery(query, variables); + } + + /** + * Reads a fragment from the store at an arbitrary id. Cannot be called after + * the transaction finishes. + */ + public readFragment( + id: string, + fragment: DocumentNode, + fragmentName?: string, + variables?: Object, + ): FragmentType | null { + this.assertNotFinished(); + return this.proxy.readFragment(id, fragment, fragmentName, variables); + } + + /** + * Creates an action to be consumed after `finish` is called that writes + * some query data to the store at the root query id. Cannot be called after + * the transaction finishes. + */ + public writeQuery( + data: any, + query: DocumentNode, + variables?: Object, + ): void { + this.assertNotFinished(); + this.writes.push({ + rootId: 'ROOT_QUERY', + result: data, + document: query, + variables: variables || {}, + }); + } + + /** + * Creates an action to be consumed after `finish` is called that writes some + * fragment data to the store at an arbitrary id. Cannot be called after the + * transaction finishes. + */ + public writeFragment( + data: any, + id: string, + fragment: DocumentNode, + fragmentName?: string, + variables?: Object, + ): void { + this.assertNotFinished(); + this.writes.push({ + rootId: id, + result: data, + document: getFragmentQueryDocument(fragment, fragmentName), + variables: variables || {}, + }); + } + + /** + * Throws an error if the transaction has finished. All methods in the + * transaction data proxy should use this method. + */ + private assertNotFinished() { + if (this.isFinished) { + throw new Error('Cannot call transaction methods after the transaction has finished.'); + } + } +} diff --git a/src/data/store.ts b/src/data/store.ts index 5c0899a7ff3..14fbb0c6f41 100644 --- a/src/data/store.ts +++ b/src/data/store.ts @@ -200,19 +200,19 @@ export function data( // the store so we can just throw it all away. return {}; } else if (isWriteAction(action)) { - const clonedState = { ...previousState } as NormalizedCache; - - // Simply write our result to the store for this action. - const newState = writeResultToStore({ - result: action.result, - dataId: action.rootId, - document: action.document, - variables: action.variables, - store: clonedState, - dataIdFromObject: config.dataIdFromObject, - }); - - return newState; + // Simply write our result to the store for this action for all of the + // writes that were specified. + return action.writes.reduce( + (currentState, write) => writeResultToStore({ + result: write.result, + dataId: write.rootId, + document: write.document, + variables: write.variables, + store: currentState, + dataIdFromObject: config.dataIdFromObject, + }), + { ...previousState } as NormalizedCache, + ); } return previousState; diff --git a/src/optimistic-data/store.ts b/src/optimistic-data/store.ts index a3fe545f8d5..76d69d9846d 100644 --- a/src/optimistic-data/store.ts +++ b/src/optimistic-data/store.ts @@ -101,10 +101,7 @@ export function optimistic( } else if (isWriteOptimisticAction(action)) { const fakeWriteAction: WriteAction = { type: 'APOLLO_WRITE', - rootId: action.rootId, - result: action.result, - document: action.document, - variables: action.variables, + writes: action.writes, }; const optimisticData = getDataWithOptimisticResults({ From 72ad7c6c1f78dc89c230c60001bdcf7f78cfc57f Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Mon, 27 Feb 2017 11:27:15 -0500 Subject: [PATCH 20/29] remove write transaction from client --- src/ApolloClient.ts | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index adccd832279..da39d8795c9 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -565,42 +565,6 @@ export default class ApolloClient { }; } - public writeTransaction( - transactionFn: (proxy: DataProxy) => void, - ): void { - this.initStore(); - const transactionProxy = new TransactionDataProxy(this.initProxy()); - transactionFn(transactionProxy); - const writes = transactionProxy.finish(); - this.store.dispatch({ - type: 'APOLLO_WRITE', - writes, - }); - } - - public writeTransactionOptimistically( - transactionFn: (proxy: DataProxy) => void, - ): { - rollback: () => void, - } { - const optimisticWriteId = (this.optimisticWriteId++).toString(); - this.initStore(); - const transactionProxy = new TransactionDataProxy(this.initProxy()); - transactionFn(transactionProxy); - const writes = transactionProxy.finish(); - this.store.dispatch({ - type: 'APOLLO_WRITE_OPTIMISTIC', - optimisticWriteId, - writes, - }); - return { - rollback: () => this.store.dispatch({ - type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK', - optimisticWriteId, - }), - }; - } - /** * Returns a reducer function configured according to the `reducerConfig` instance variable. */ From b2b9ae7143ac730e5fa19f4855f97d4478834971 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Mon, 27 Feb 2017 11:29:09 -0500 Subject: [PATCH 21/29] add tests for proxies --- test/proxy.ts | 790 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/tests.ts | 1 + 2 files changed, 791 insertions(+) create mode 100644 test/proxy.ts diff --git a/test/proxy.ts b/test/proxy.ts new file mode 100644 index 00000000000..97c7b20bdb1 --- /dev/null +++ b/test/proxy.ts @@ -0,0 +1,790 @@ +import { assert } from 'chai'; +import { createStore } from 'redux'; +import gql from 'graphql-tag'; +import { print } from 'graphql-tag/printer'; +import { createApolloStore } from '../src/store'; +import { ReduxDataProxy, TransactionDataProxy } from '../src/data/proxy'; + +describe('ReduxDataProxy', () => { + function createDataProxy({ + initialState, + dataIdFromObject, + }: { + initialState?: any, + dataIdFromObject?: (object: any) => string | null, + } = {}) { + const store = createApolloStore({ + initialState, + config: { dataIdFromObject }, + }); + return new ReduxDataProxy(store, ({ apollo }) => apollo); + } + + describe('readQuery', () => { + it('will read some data from the store', () => { + const proxy = createDataProxy({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + }, + }, + }, + }, + }); + + assert.deepEqual(proxy.readQuery(gql`{ a }`), { a: 1 }); + assert.deepEqual(proxy.readQuery(gql`{ b c }`), { b: 2, c: 3 }); + assert.deepEqual(proxy.readQuery(gql`{ a b c }`), { a: 1, b: 2, c: 3 }); + }); + + it('will read some deeply nested data from the store', () => { + const proxy = createDataProxy({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: 'foo', + generated: false, + }, + }, + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }, + }, + }, + }); + + assert.deepEqual( + proxy.readQuery(gql`{ a d { e } }`), + { a: 1, d: { e: 4 } }, + ); + assert.deepEqual( + proxy.readQuery(gql`{ a d { e h { i } } }`), + { a: 1, d: { e: 4, h: { i: 7 } } }, + ); + assert.deepEqual( + proxy.readQuery(gql`{ a b c d { e f g h { i j k } } }`), + { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, + ); + }); + + it('will read some data from the store with variables', () => { + const proxy = createDataProxy({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }, + }, + }, + }); + + assert.deepEqual(proxy.readQuery( + gql`query ($literal: Boolean, $value: Int) { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + }`, + { + literal: false, + value: 42, + }, + ), { a: 1, b: 2 }); + }); + }); + + describe('readFragment', () => { + it('will throw an error when there is no fragment', () => { + const proxy = createDataProxy(); + + assert.throws(() => { + proxy.readFragment('x', gql`query { a b c }`); + }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); + assert.throws(() => { + proxy.readFragment('x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const proxy = createDataProxy(); + + assert.throws(() => { + proxy.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + assert.throws(() => { + proxy.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + + it('will read some deeply nested data from the store at any id', () => { + const proxy = createDataProxy({ + initialState: { + apollo: { + data: { + 'ROOT_QUERY': { + __typename: 'Type1', + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: 'foo', + generated: false, + }, + }, + 'foo': { + __typename: 'Type2', + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + __typename: 'Type3', + i: 7, + j: 8, + k: 9, + }, + }, + }, + }, + }); + + assert.deepEqual( + proxy.readFragment('foo', gql`fragment fragmentFoo on Foo { e h { i } }`), + { e: 4, h: { i: 7 } }, + ); + assert.deepEqual( + proxy.readFragment('foo', gql`fragment fragmentFoo on Foo { e f g h { i j k } }`), + { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, + ); + assert.deepEqual( + proxy.readFragment('bar', gql`fragment fragmentBar on Bar { i }`), + { i: 7 }, + ); + assert.deepEqual( + proxy.readFragment('bar', gql`fragment fragmentBar on Bar { i j k }`), + { i: 7, j: 8, k: 9 }, + ); + assert.deepEqual( + proxy.readFragment( + 'foo', + gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + 'fragmentFoo', + ), + { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, + ); + assert.deepEqual( + proxy.readFragment( + 'bar', + gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + 'fragmentBar', + ), + { i: 7, j: 8, k: 9 }, + ); + }); + + it('will read some data from the store with variables', () => { + const proxy = createDataProxy({ + initialState: { + apollo: { + data: { + 'foo': { + __typename: 'Type1', + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }, + }, + }, + }); + + assert.deepEqual(proxy.readFragment( + 'foo', + gql` + fragment foo on Foo { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + } + `, + undefined, + { + literal: false, + value: 42, + }, + ), { a: 1, b: 2 }); + }); + + it('will return null when an id that can’t be found is provided', () => { + const client1 = createDataProxy(); + const client2 = createDataProxy({ + initialState: { + apollo: { + data: { + 'bar': { __typename: 'Type1', a: 1, b: 2, c: 3 }, + }, + }, + }, + }); + const client3 = createDataProxy({ + initialState: { + apollo: { + data: { + 'foo': { __typename: 'Type1', a: 1, b: 2, c: 3 }, + }, + }, + }, + }); + + assert.equal(client1.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); + assert.equal(client2.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); + assert.deepEqual(client3.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), { a: 1, b: 2, c: 3 }); + }); + }); + + describe('writeQuery', () => { + it('will write some data to the store', () => { + const proxy = createDataProxy(); + + proxy.writeQuery({ a: 1 }, gql`{ a }`); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + }, + }); + + proxy.writeQuery({ b: 2, c: 3 }, gql`{ b c }`); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + }, + }); + + proxy.writeQuery({ a: 4, b: 5, c: 6 }, gql`{ a b c }`); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 4, + b: 5, + c: 6, + }, + }); + }); + + it('will write some deeply nested data to the store', () => { + const proxy = createDataProxy(); + + proxy.writeQuery( + { a: 1, d: { e: 4 } }, + gql`{ a d { e } }`, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + }, + }); + + proxy.writeQuery( + { a: 1, d: { h: { i: 7 } } }, + gql`{ a d { h { i } } }`, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + }, + }); + + proxy.writeQuery( + { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, + gql`{ a b c d { e f g h { i j k } } }`, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: '$ROOT_QUERY.d', + generated: true, + }, + }, + '$ROOT_QUERY.d': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: '$ROOT_QUERY.d.h', + generated: true, + }, + }, + '$ROOT_QUERY.d.h': { + i: 7, + j: 8, + k: 9, + }, + }); + }); + + it('will write some data to the store with variables', () => { + const proxy = createDataProxy(); + + proxy.writeQuery( + { + a: 1, + b: 2, + }, + gql` + query ($literal: Boolean, $value: Int) { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + } + `, + { + literal: false, + value: 42, + }, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'ROOT_QUERY': { + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }); + }); + }); + + describe('writeFragment', () => { + it('will throw an error when there is no fragment', () => { + const proxy = createDataProxy(); + + assert.throws(() => { + proxy.writeFragment({}, 'x', gql`query { a b c }`); + }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); + assert.throws(() => { + proxy.writeFragment({}, 'x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const proxy = createDataProxy(); + + assert.throws(() => { + proxy.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + assert.throws(() => { + proxy.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + + it('will write some deeply nested data into the store at any id', () => { + const proxy = createDataProxy({ + dataIdFromObject: (o: any) => o.id, + }); + + proxy.writeFragment( + { e: 4, h: { id: 'bar', i: 7 } }, + 'foo', + gql`fragment fragmentFoo on Foo { e h { i } }`, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'foo': { + e: 4, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + }, + }); + + proxy.writeFragment( + { f: 5, g: 6, h: { id: 'bar', j: 8, k: 9 } }, + 'foo', + gql`fragment fragmentFoo on Foo { f g h { j k } }`, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }); + + proxy.writeFragment( + { i: 10 }, + 'bar', + gql`fragment fragmentBar on Bar { i }`, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 10, + j: 8, + k: 9, + }, + }); + + proxy.writeFragment( + { j: 11, k: 12 }, + 'bar', + gql`fragment fragmentBar on Bar { j k }`, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 10, + j: 11, + k: 12, + }, + }); + + proxy.writeFragment( + { e: 4, f: 5, g: 6, h: { id: 'bar', i: 7, j: 8, k: 9 } }, + 'foo', + gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, + 'fooFragment', + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }); + + proxy.writeFragment( + { i: 10, j: 11, k: 12 }, + 'bar', + gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, + 'barFragment', + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 10, + j: 11, + k: 12, + }, + }); + }); + + it('will write some data to the store with variables', () => { + const proxy = createDataProxy(); + + proxy.writeFragment( + { + a: 1, + b: 2, + }, + 'foo', + gql` + fragment foo on Foo { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + } + `, + undefined, + { + literal: false, + value: 42, + }, + ); + + assert.deepEqual((proxy as any).store.getState().apollo.data, { + 'foo': { + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }); + }); + }); +}); + +describe('TransactionDataProxy', () => { + function createReadDataProxy(): DataProxyRead { + return { + readQuery() { + throw new Error('Should not have valled `readQuery`.'); + }, + readFragment() { + throw new Error('Should not have called `readFragment`.'); + }, + }; + } + + describe('readQuery', () => { + it('will throw an error if the transaction has finished', () => { + const proxy: any = new TransactionDataProxy(createReadDataProxy()); + proxy.finish(); + + assert.throws(() => { + proxy.readQuery(); + }, 'Cannot call transaction methods after the transaction has finished.'); + }); + + it('will forward a request to the provided proxy', () => { + const query: any = Symbol('query'); + const variables: any = Symbol('variables'); + const data: any = Symbol('data'); + + const readProxy = createReadDataProxy(); + + readProxy.readQuery = (...args: Array): any => { + assert.equal(args[0], query); + assert.equal(args[1], variables); + return data; + }; + + const proxy = new TransactionDataProxy(readProxy); + + assert.equal(proxy.readQuery(query, variables), data); + }); + }); + + describe('readFragment', () => { + it('will throw an error if the transaction has finished', () => { + const proxy: any = new TransactionDataProxy(createReadDataProxy()); + proxy.finish(); + + assert.throws(() => { + proxy.readFragment(); + }, 'Cannot call transaction methods after the transaction has finished.'); + }); + + it('will forward a request to the provided proxy', () => { + const id: any = Symbol('id'); + const fragment: any = Symbol('fragment'); + const fragmentName: any = Symbol('fragmentName'); + const variables: any = Symbol('variables'); + const data: any = Symbol('data'); + + const readProxy = createReadDataProxy(); + + readProxy.readFragment = (...args: Array): any => { + assert.equal(args[0], id); + assert.equal(args[1], fragment); + assert.equal(args[2], fragmentName); + assert.equal(args[3], variables); + return data; + }; + + const proxy = new TransactionDataProxy(readProxy); + + assert.equal(proxy.readFragment(id, fragment, fragmentName, variables), data); + }); + }); + + describe('writeQuery', () => { + it('will throw an error if the transaction has finished', () => { + const proxy: any = new TransactionDataProxy(createReadDataProxy()); + proxy.finish(); + + assert.throws(() => { + proxy.writeQuery(); + }, 'Cannot call transaction methods after the transaction has finished.'); + }); + + it('will create writes that get returned when finished', () => { + const proxy = new TransactionDataProxy(createReadDataProxy()); + + proxy.writeQuery( + { a: 1, b: 2, c: 3 }, + gql`{ a b c }`, + ); + + proxy.writeQuery( + { foo: { d: 4, e: 5, bar: { f: 6, g: 7 } } }, + gql`{ foo(id: $id) { d e bar { f g } } }`, + { id: 7 }, + ); + + const writes = proxy.finish(); + + assert.deepEqual(writes, [ + { + rootId: 'ROOT_QUERY', + result: { a: 1, b: 2, c: 3 }, + document: gql`{ a b c }`, + variables: {}, + }, + { + rootId: 'ROOT_QUERY', + result: { foo: { d: 4, e: 5, bar: { f: 6, g: 7 } } }, + document: gql`{ foo(id: $id) { d e bar { f g } } }`, + variables: { id: 7 }, + }, + ]); + }); + }); + + describe('writeFragment', () => { + it('will throw an error if the transaction has finished', () => { + const proxy: any = new TransactionDataProxy(createReadDataProxy()); + proxy.finish(); + + assert.throws(() => { + proxy.writeFragment(); + }, 'Cannot call transaction methods after the transaction has finished.'); + }); + + it('will create writes that get returned when finished', () => { + const proxy = new TransactionDataProxy(createReadDataProxy()); + + proxy.writeFragment( + { a: 1, b: 2, c: 3 }, + 'foo', + gql`fragment fragment1 on Foo { a b c }`, + ); + + proxy.writeFragment( + { foo: { d: 4, e: 5, bar: { f: 6, g: 7 } } }, + 'bar', + gql` + fragment fragment1 on Foo { a b c } + fragment fragment2 on Bar { foo(id: $id) { d e bar { f g } } } + `, + 'fragment2', + { id: 7 }, + ); + + const writes = proxy.finish(); + + assert.equal(writes.length, 2); + assert.deepEqual(Object.keys(writes[0]), ['rootId', 'result', 'document', 'variables']); + assert.equal(writes[0].rootId, 'foo'); + assert.deepEqual(writes[0].result, { a: 1, b: 2, c: 3 }); + assert.deepEqual(writes[0].variables, {}); + assert.equal(print(writes[0].document), print(gql` + { ...fragment1 } + fragment fragment1 on Foo { a b c } + `)); + assert.deepEqual(Object.keys(writes[1]), ['rootId', 'result', 'document', 'variables']); + assert.equal(writes[1].rootId, 'bar'); + assert.deepEqual(writes[1].result, { foo: { d: 4, e: 5, bar: { f: 6, g: 7 } } }); + assert.deepEqual(writes[1].variables, { id: 7 }); + assert.equal(print(writes[1].document), print(gql` + { ...fragment2 } + fragment fragment1 on Foo { a b c } + fragment fragment2 on Bar { foo(id: $id) { d e bar { f g } } } + `)); + }); + }); +}); diff --git a/test/tests.ts b/test/tests.ts index ad65fa99680..12d3e808e46 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -54,3 +54,4 @@ import './cloneDeep'; import './assign'; import './environment'; import './ApolloClient'; +import './proxy'; From 2e965a2acd1d508d20e0ccf3d490f095bd11cff7 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Mon, 27 Feb 2017 11:29:29 -0500 Subject: [PATCH 22/29] add mutation update function --- src/actions.ts | 6 +++ src/core/QueryManager.ts | 8 ++++ src/core/watchQueryOptions.ts | 5 +++ src/data/proxy.ts | 78 ++++++++++++++++++++--------------- src/data/store.ts | 21 ++++++++++ src/optimistic-data/store.ts | 1 + 6 files changed, 85 insertions(+), 34 deletions(-) diff --git a/src/actions.ts b/src/actions.ts index 39ed85d8b55..b0e24dedd60 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -7,6 +7,10 @@ import { MutationQueryReducer, } from './data/mutationResults'; +import { + DataProxy, +} from './data/proxy'; + import { ApolloReducer, } from './store'; @@ -86,6 +90,7 @@ export interface MutationInitAction { optimisticResponse: Object | undefined; extraReducers?: ApolloReducer[]; updateQueries?: { [queryId: string]: MutationQueryReducer }; + update?: (proxy: DataProxy, mutationResult: Object) => void; } export function isMutationInitAction(action: ApolloAction): action is MutationInitAction { @@ -102,6 +107,7 @@ export interface MutationResultAction { mutationId: string; extraReducers?: ApolloReducer[]; updateQueries?: { [queryId: string]: MutationQueryReducer }; + update?: (proxy: DataProxy, mutationResult: Object) => void; } export function isMutationResultAction(action: ApolloAction): action is MutationResultAction { diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 91c040f549f..85a6108232c 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -55,6 +55,10 @@ import { createStoreReducer, } from '../data/resultReducers'; +import { + DataProxy, +} from '../data/proxy'; + import { isProduction, } from '../util/environment'; @@ -226,12 +230,14 @@ export class QueryManager { optimisticResponse, updateQueries: updateQueriesByName, refetchQueries = [], + update: updateWithProxyFn, }: { mutation: DocumentNode, variables?: Object, optimisticResponse?: Object, updateQueries?: MutationQueryReducersMap, refetchQueries?: string[] | PureQueryOptions[], + update?: (proxy: DataProxy, mutationResult: Object) => void, }): Promise> { const mutationId = this.generateQueryId(); @@ -267,6 +273,7 @@ export class QueryManager { optimisticResponse, extraReducers: this.getExtraReducers(), updateQueries, + update: updateWithProxyFn, }); return new Promise((resolve, reject) => { @@ -288,6 +295,7 @@ export class QueryManager { variables: variables || {}, extraReducers: this.getExtraReducers(), updateQueries, + update: updateWithProxyFn, }); // If there was an error in our reducers, reject this promise! diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index b3ef0fdaf7d..46cef6074c0 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -8,6 +8,10 @@ import { MutationQueryReducersMap, } from '../data/mutationResults'; +import { + DataProxy, +} from '../data/proxy'; + /** * We can change these options to an ObservableQuery */ @@ -98,4 +102,5 @@ export interface MutationOptions { optimisticResponse?: Object; updateQueries?: MutationQueryReducersMap; refetchQueries?: string[]; + update?: (proxy: DataProxy, mutationResult: Object) => void; } diff --git a/src/data/proxy.ts b/src/data/proxy.ts index 39149c5ab48..31d36aa28df 100644 --- a/src/data/proxy.ts +++ b/src/data/proxy.ts @@ -1,7 +1,9 @@ import { DocumentNode } from 'graphql'; import { ApolloStore, Store } from '../store'; import { DataWrite } from '../actions'; +import { NormalizedCache } from '../data/storeUtils'; import { getFragmentQueryDocument } from '../queries/getFromAST'; +import { getDataWithOptimisticResults } from '../optimistic-data/store'; import { readQueryFromStore } from './readFromStore'; /** @@ -10,13 +12,7 @@ import { readQueryFromStore } from './readFromStore'; * whilst in the background this data is being converted into the normalized * store format. */ -export interface DataProxy extends DataProxyRead, DataProxyWrite {} - -/** - * A subset of the methods on `DataProxy` which just involve the methods for - * reading some data. These methods will not change any data. - */ -export interface DataProxyRead { +export interface DataProxy { /** * Reads a GraphQL query from the root query id. */ @@ -36,16 +32,7 @@ export interface DataProxyRead { fragmentName?: string, variables?: Object, ): FragmentType | null; -} -/** - * A subset of the methods on `DataProxy` which just involve the methods for - * writing some data. These methods *will* change the underlying data - * representation. How and when that change happens is up to the implementor, - * but the expectation is that calling these methods will eventually change - * some data. - */ -export interface DataProxyWrite { /** * Writes a GraphQL query to the root query id. */ @@ -105,7 +92,7 @@ export class ReduxDataProxy implements DataProxy { ): QueryType { return readQueryFromStore({ rootId: 'ROOT_QUERY', - store: this.reduxRootSelector(this.store.getState()).data, + store: getDataWithOptimisticResults(this.reduxRootSelector(this.store.getState())), query, variables, returnPartialData: false, @@ -122,7 +109,7 @@ export class ReduxDataProxy implements DataProxy { variables?: Object, ): FragmentType | null { const query = getFragmentQueryDocument(fragment, fragmentName); - const { data } = this.reduxRootSelector(this.store.getState()); + const data = getDataWithOptimisticResults(this.reduxRootSelector(this.store.getState())); // If we could not find an item in the store with the provided id then we // just return `null`. @@ -181,18 +168,18 @@ export class ReduxDataProxy implements DataProxy { } /** - * A data proxy to be used within a transaction. It uses another data proxy for - * reads and pushes all writes to an actions array which can be retrieved when - * the transaction finishes. As soon as a transaction is constructed it has - * started. Once a transaction has finished none of its methods are usable. + * A data proxy to be used within a transaction. It saves all writes to be + * returned when the transaction finishes. As soon as a transaction is + * constructed it has started. Once a transaction has finished none of its + * methods are usable. + * + * The transaction will read from a single normalized cache instance. */ export class TransactionDataProxy implements DataProxy { /** - * The proxy to use for reading. The reason a transaction data proxy is not - * just a write proxy is that we want to throw errors if a transaction has - * finished and a user is trying to read. + * The normalized cache that this transaction reads from. */ - private proxy: DataProxyRead; + private data: NormalizedCache; /** * An array of actions that we build up during the life of the transaction. @@ -205,8 +192,8 @@ export class TransactionDataProxy implements DataProxy { */ private isFinished: boolean; - constructor(proxy: DataProxyRead) { - this.proxy = proxy; + constructor(data: NormalizedCache) { + this.data = data; this.writes = []; this.isFinished = false; } @@ -225,20 +212,28 @@ export class TransactionDataProxy implements DataProxy { } /** - * Reads some data from the store from the root query id. Cannot be called - * after the transaction finishes. + * Reads a query from the normalized cache. + * + * Throws an error if the transaction has finished. */ public readQuery( query: DocumentNode, variables?: Object, ): QueryType { this.assertNotFinished(); - return this.proxy.readQuery(query, variables); + return readQueryFromStore({ + rootId: 'ROOT_QUERY', + store: this.data, + query, + variables, + returnPartialData: false, + }); } /** - * Reads a fragment from the store at an arbitrary id. Cannot be called after - * the transaction finishes. + * Reads a fragment from the normalized cache. + * + * Throws an error if the transaction has finished. */ public readFragment( id: string, @@ -247,7 +242,22 @@ export class TransactionDataProxy implements DataProxy { variables?: Object, ): FragmentType | null { this.assertNotFinished(); - return this.proxy.readFragment(id, fragment, fragmentName, variables); + const { data } = this; + const query = getFragmentQueryDocument(fragment, fragmentName); + + // If we could not find an item in the store with the provided id then we + // just return `null`. + if (typeof data[id] === 'undefined') { + return null; + } + + return readQueryFromStore({ + rootId: id, + store: data, + query, + variables, + returnPartialData: false, + }); } /** diff --git a/src/data/store.ts b/src/data/store.ts index 14fbb0c6f41..b7876010939 100644 --- a/src/data/store.ts +++ b/src/data/store.ts @@ -12,6 +12,10 @@ import { writeResultToStore, } from './writeToStore'; +import { + TransactionDataProxy, +} from '../data/proxy'; + import { QueryStore, } from '../queries/store'; @@ -183,6 +187,23 @@ export function data( }); } + // If the mutation has some writes associated with it then we need to + // apply those writes to the store by running this reducer again with a + // write action. + if (constAction.update) { + const update = constAction.update; + const proxy = new TransactionDataProxy(newState); + tryFunctionOrLogError(() => update(proxy, constAction.result)); + const writes = proxy.finish(); + newState = data( + newState, + { type: 'APOLLO_WRITE', writes }, + queries, + mutations, + config, + ); + } + // XXX each reducer gets the state from the previous reducer. // Maybe they should all get a clone instead and then compare at the end to make sure it's consistent. if (constAction.extraReducers) { diff --git a/src/optimistic-data/store.ts b/src/optimistic-data/store.ts index 76d69d9846d..92beb008fba 100644 --- a/src/optimistic-data/store.ts +++ b/src/optimistic-data/store.ts @@ -66,6 +66,7 @@ export function optimistic( mutationId: action.mutationId, extraReducers: action.extraReducers, updateQueries: action.updateQueries, + update: action.update, }; const optimisticData = getDataWithOptimisticResults({ From 3f57b275b62908680678b4b1798ff568e9a8c430 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Mon, 27 Feb 2017 11:29:34 -0500 Subject: [PATCH 23/29] add mutation update function tests --- test/mutationResults.ts | 247 ++++++++++++++ test/optimistic.ts | 737 ++++++++++++++++++++++++++++++++++------ test/store.ts | 5 +- 3 files changed, 883 insertions(+), 106 deletions(-) diff --git a/test/mutationResults.ts b/test/mutationResults.ts index 18fb872b5ea..a161268c30e 100644 --- a/test/mutationResults.ts +++ b/test/mutationResults.ts @@ -1196,4 +1196,251 @@ describe('mutation results', () => { done(); }).catch(done); }); + + describe('store transaction updater', () => { + const mutation = gql` + mutation createTodo { + # skipping arguments in the test since they don't matter + createTodo { + id + text + completed + __typename + } + __typename + } + `; + + const mutationResult = { + data: { + __typename: 'Mutation', + createTodo: { + id: '99', + __typename: 'Todo', + text: 'This one was created with a mutation.', + completed: true, + }, + }, + }; + + it('analogous of ARRAY_INSERT', () => { + let subscriptionHandle: Subscription; + return setup({ + request: { query: mutation }, + result: mutationResult, + }) + .then(() => { + // we have to actually subscribe to the query to be able to update it + return new Promise( (resolve, reject) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res) { resolve(res); }, + }); + }); + }) + .then(() => { + return client.mutate({ + mutation, + update: (proxy, mResult: any) => { + assert.equal(mResult.data.createTodo.id, '99'); + assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.'); + + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + }, + }); + }) + .then(() => { + return client.query({ query }); + }) + .then((newResult: any) => { + subscriptionHandle.unsubscribe(); + + // There should be one more todo item than before + assert.equal(newResult.data.todoList.todos.length, 4); + + // Since we used `prepend` it should be at the front + assert.equal(newResult.data.todoList.todos[0].text, 'This one was created with a mutation.'); + }); + }); + + it('does not fail if optional query variables are not supplied', () => { + let subscriptionHandle: Subscription; + const mutationWithVars = gql` + mutation createTodo($requiredVar: String!, $optionalVar: String) { + createTodo(requiredVar: $requiredVar, optionalVar:$optionalVar) { + id + text + completed + __typename + } + __typename + } + `; + + // the test will pass if optionalVar is uncommented + const variables = { + requiredVar: 'x', + // optionalVar: 'y', + }; + return setup({ + request: { + query: mutationWithVars, + variables, + }, + result: mutationResult, + }) + .then(() => { + // we have to actually subscribe to the query to be able to update it + return new Promise((resolve, reject) => { + const handle = client.watchQuery({ + query, + variables, + }); + subscriptionHandle = handle.subscribe({ + next(res) { + resolve(res); + }, + }); + }); + }) + .then(() => { + return client.mutate({ + mutation: mutationWithVars, + variables, + update: (proxy, mResult: any) => { + assert.equal(mResult.data.createTodo.id, '99'); + assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.'); + + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + }, + }); + }) + .then(() => { + return client.query({query}); + }) + .then((newResult: any) => { + subscriptionHandle.unsubscribe(); + + // There should be one more todo item than before + assert.equal(newResult.data.todoList.todos.length, 4); + + // Since we used `prepend` it should be at the front + assert.equal(newResult.data.todoList.todos[0].text, 'This one was created with a mutation.'); + }); + }); + + it('does not make next queries fail if a mutation fails', (done) => { + const obsHandle = setupObsHandle({ + request: { query: mutation }, + result: {errors: [new Error('mock error')]}, + }, { + request: { query }, + result, + }); + + obsHandle.subscribe({ + next(obj) { + client.mutate({ + mutation, + update: (proxy, mResult: any) => { + assert.equal(mResult.data.createTodo.id, '99'); + assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.'); + + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + }, + }) + .then( + () => done(new Error('Mutation should have failed')), + () => client.mutate({ + mutation, + update: (proxy, mResult: any) => { + assert.equal(mResult.data.createTodo.id, '99'); + assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.'); + + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + }, + }), + ) + .then( + () => done(new Error('Mutation should have failed')), + () => obsHandle.refetch(), + ) + .then(() => done(), done); + }, + }); + }); + + it('error handling in reducer functions', () => { + const oldError = console.error; + const errors: any[] = []; + console.error = (msg: string) => { + errors.push(msg); + }; + + let subscriptionHandle: Subscription; + return setup({ + request: { query: mutation }, + result: mutationResult, + }) + .then(() => { + // we have to actually subscribe to the query to be able to update it + return new Promise( (resolve, reject) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res) { resolve(res); }, + }); + }); + }) + .then(() => { + return client.mutate({ + mutation, + update: () => { + throw new Error(`Hello... It's me.`); + }, + }); + }) + .then(() => { + subscriptionHandle.unsubscribe(); + assert.lengthOf(errors, 1); + assert.equal(errors[0].message, `Hello... It's me.`); + console.error = oldError; + }); + }); + }); }); diff --git a/test/optimistic.ts b/test/optimistic.ts index c6f3cc42c13..04dd5f010dc 100644 --- a/test/optimistic.ts +++ b/test/optimistic.ts @@ -189,24 +189,422 @@ describe('optimistic mutation results', () => { }), }); - const updateQueries = { - todoList: (prev: any, options: any) => { - const state = cloneDeep(prev); - state.todoList.todos.unshift(options.mutationResult.data.createTodo); - return state; + describe('with `updateQueries`', () => { + const updateQueries = { + todoList: (prev: any, options: any) => { + const state = cloneDeep(prev); + state.todoList.todos.unshift(options.mutationResult.data.createTodo); + return state; + }, + }; + + it('handles a single error for a single mutation', () => { + return setup({ + request: { query: mutation }, + error: new Error('forbidden (test error)'), + }) + .then(() => { + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries, + }); + + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 4); + assert.equal((dataInStore['Todo99'] as any).text, 'Optimistically generated'); + + return promise; + }) + .catch((err) => { + assert.instanceOf(err, Error); + assert.equal(err.message, 'Network error: forbidden (test error)'); + + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 3); + assert.notProperty(dataInStore, 'Todo99'); + }); + }); + + it('handles errors produced by one mutation in a series', () => { + let subscriptionHandle: Subscription; + return setup({ + request: { query: mutation }, + error: new Error('forbidden (test error)'), + }, { + request: { query: mutation }, + result: mutationResult2, + }) + .then(() => { + // we have to actually subscribe to the query to be able to update it + return new Promise( (resolve, reject) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res) { resolve(res); }, + }); + }); + }) + .then(() => { + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries, + }).catch((err) => { + // it is ok to fail here + assert.instanceOf(err, Error); + assert.equal(err.message, 'Network error: forbidden (test error)'); + return null; + }); + + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + updateQueries, + }); + + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 5); + assert.equal((dataInStore['Todo99'] as any).text, 'Optimistically generated'); + assert.equal((dataInStore['Todo66'] as any).text, 'Optimistically generated 2'); + + return Promise.all([promise, promise2]); + }) + .then(() => { + subscriptionHandle.unsubscribe(); + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 4); + assert.notProperty(dataInStore, 'Todo99'); + assert.property(dataInStore, 'Todo66'); + assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo66')); + assert.notInclude((dataInStore['TodoList5'] as any).todos, realIdValue('Todo99')); + }); + }); + + it('can run 2 mutations concurrently and handles all intermediate states well', () => { + function checkBothMutationsAreApplied(expectedText1: any, expectedText2: any) { + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 5); + assert.property(dataInStore, 'Todo99'); + assert.property(dataInStore, 'Todo66'); + assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo66')); + assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo99')); + assert.equal((dataInStore['Todo99'] as any).text, expectedText1); + assert.equal((dataInStore['Todo66'] as any).text, expectedText2); + } + let subscriptionHandle: Subscription; + return setup({ + request: { query: mutation }, + result: mutationResult, + }, { + request: { query: mutation }, + result: mutationResult2, + // make sure it always happens later + delay: 100, + }) + .then(() => { + // we have to actually subscribe to the query to be able to update it + return new Promise( (resolve, reject) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res) { resolve(res); }, + }); + }); + }) + .then(() => { + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries, + }).then((res) => { + checkBothMutationsAreApplied('This one was created with a mutation.', 'Optimistically generated 2'); + const mutationsState = client.store.getState().apollo.mutations; + assert.equal(mutationsState['5'].loading, false); + assert.equal(mutationsState['6'].loading, true); + + return res; + }); + + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + updateQueries, + }).then((res) => { + checkBothMutationsAreApplied('This one was created with a mutation.', 'Second mutation.'); + const mutationsState = client.store.getState().apollo.mutations; + assert.equal(mutationsState[5].loading, false); + assert.equal(mutationsState[6].loading, false); + + return res; + }); + + const mutationsState = client.store.getState().apollo.mutations; + assert.equal(mutationsState[5].loading, true); + assert.equal(mutationsState[6].loading, true); + + checkBothMutationsAreApplied('Optimistically generated', 'Optimistically generated 2'); + + return Promise.all([promise, promise2]); + }) + .then(() => { + subscriptionHandle.unsubscribe(); + checkBothMutationsAreApplied('This one was created with a mutation.', 'Second mutation.'); + }); + }); + }); + + describe('with `update`', () => { + const update = (proxy: any, mResult: any) => { + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + }; + + it('handles a single error for a single mutation', () => { + return setup({ + request: { query: mutation }, + error: new Error('forbidden (test error)'), + }) + .then(() => { + const promise = client.mutate({ + mutation, + optimisticResponse, + update, + }); + + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 4); + assert.equal((dataInStore['Todo99'] as any).text, 'Optimistically generated'); + + return promise; + }) + .catch((err) => { + assert.instanceOf(err, Error); + assert.equal(err.message, 'Network error: forbidden (test error)'); + + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 3); + assert.notProperty(dataInStore, 'Todo99'); + }); + }); + + it('handles errors produced by one mutation in a series', () => { + let subscriptionHandle: Subscription; + return setup({ + request: { query: mutation }, + error: new Error('forbidden (test error)'), + }, { + request: { query: mutation }, + result: mutationResult2, + }) + .then(() => { + // we have to actually subscribe to the query to be able to update it + return new Promise( (resolve, reject) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res) { resolve(res); }, + }); + }); + }) + .then(() => { + const promise = client.mutate({ + mutation, + optimisticResponse, + update, + }).catch((err) => { + // it is ok to fail here + assert.instanceOf(err, Error); + assert.equal(err.message, 'Network error: forbidden (test error)'); + return null; + }); + + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + update, + }); + + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 5); + assert.equal((dataInStore['Todo99'] as any).text, 'Optimistically generated'); + assert.equal((dataInStore['Todo66'] as any).text, 'Optimistically generated 2'); + + return Promise.all([promise, promise2]); + }) + .then(() => { + subscriptionHandle.unsubscribe(); + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 4); + assert.notProperty(dataInStore, 'Todo99'); + assert.property(dataInStore, 'Todo66'); + assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo66')); + assert.notInclude((dataInStore['TodoList5'] as any).todos, realIdValue('Todo99')); + }); + }); + + it('can run 2 mutations concurrently and handles all intermediate states well', () => { + function checkBothMutationsAreApplied(expectedText1: any, expectedText2: any) { + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 5); + assert.property(dataInStore, 'Todo99'); + assert.property(dataInStore, 'Todo66'); + assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo66')); + assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo99')); + assert.equal((dataInStore['Todo99'] as any).text, expectedText1); + assert.equal((dataInStore['Todo66'] as any).text, expectedText2); + } + let subscriptionHandle: Subscription; + return setup({ + request: { query: mutation }, + result: mutationResult, + }, { + request: { query: mutation }, + result: mutationResult2, + // make sure it always happens later + delay: 100, + }) + .then(() => { + // we have to actually subscribe to the query to be able to update it + return new Promise( (resolve, reject) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res) { resolve(res); }, + }); + }); + }) + .then(() => { + const promise = client.mutate({ + mutation, + optimisticResponse, + update, + }).then((res) => { + checkBothMutationsAreApplied('This one was created with a mutation.', 'Optimistically generated 2'); + const mutationsState = client.store.getState().apollo.mutations; + assert.equal(mutationsState['5'].loading, false); + assert.equal(mutationsState['6'].loading, true); + + return res; + }); + + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + update, + }).then((res) => { + checkBothMutationsAreApplied('This one was created with a mutation.', 'Second mutation.'); + const mutationsState = client.store.getState().apollo.mutations; + assert.equal(mutationsState[5].loading, false); + assert.equal(mutationsState[6].loading, false); + + return res; + }); + + const mutationsState = client.store.getState().apollo.mutations; + assert.equal(mutationsState[5].loading, true); + assert.equal(mutationsState[6].loading, true); + + checkBothMutationsAreApplied('Optimistically generated', 'Optimistically generated 2'); + + return Promise.all([promise, promise2]); + }) + .then(() => { + subscriptionHandle.unsubscribe(); + checkBothMutationsAreApplied('This one was created with a mutation.', 'Second mutation.'); + }); + }); + }); + }); + + describe('optimistic updates using `updateQueries`', () => { + const mutation = gql` + mutation createTodo { + # skipping arguments in the test since they don't matter + createTodo { + id + text + completed + __typename + } + __typename + } + `; + + const mutationResult = { + data: { + __typename: 'Mutation', + createTodo: { + id: '99', + __typename: 'Todo', + text: 'This one was created with a mutation.', + completed: true, + }, }, }; - it('handles a single error for a single mutation', () => { + const optimisticResponse = { + __typename: 'Mutation', + createTodo: { + __typename: 'Todo', + id: '99', + text: 'Optimistically generated', + completed: true, + }, + }; + + const mutationResult2 = { + data: assign({}, mutationResult.data, { + createTodo: assign({}, mutationResult.data.createTodo, { + id: '66', + text: 'Second mutation.', + }), + }), + }; + + const optimisticResponse2 = { + __typename: 'Mutation', + createTodo: { + __typename: 'Todo', + id: '66', + text: 'Optimistically generated 2', + completed: true, + }, + }; + + it('will insert a single item to the beginning', () => { + let subscriptionHandle: Subscription; return setup({ request: { query: mutation }, - error: new Error('forbidden (test error)'), + result: mutationResult, + }) + .then(() => { + // we have to actually subscribe to the query to be able to update it + return new Promise( (resolve, reject) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res) { resolve(res); }, + }); + }); }) .then(() => { const promise = client.mutate({ mutation, optimisticResponse, - updateQueries, + updateQueries: { + todoList: (prev, options) => { + const mResult = options.mutationResult as any; + assert.equal(mResult.data.createTodo.id, '99'); + + const state = cloneDeep(prev) as any; + state.todoList.todos.unshift(mResult.data.createTodo); + return state; + }, + }, }); const dataInStore = client.queryManager.getDataWithOptimisticResults(); @@ -215,24 +613,28 @@ describe('optimistic mutation results', () => { return promise; }) - .catch((err) => { - assert.instanceOf(err, Error); - assert.equal(err.message, 'Network error: forbidden (test error)'); + .then(() => { + return client.query({ query }); + }) + .then((newResult: any) => { + subscriptionHandle.unsubscribe(); + // There should be one more todo item than before + assert.equal(newResult.data.todoList.todos.length, 4); - const dataInStore = client.queryManager.getDataWithOptimisticResults(); - assert.equal((dataInStore['TodoList5'] as any).todos.length, 3); - assert.notProperty(dataInStore, 'Todo99'); + // Since we used `prepend` it should be at the front + assert.equal(newResult.data.todoList.todos[0].text, 'This one was created with a mutation.'); }); }); - it('handles errors produced by one mutation in a series', () => { + it('two array insert like mutations', () => { let subscriptionHandle: Subscription; return setup({ request: { query: mutation }, - error: new Error('forbidden (test error)'), + result: mutationResult, }, { request: { query: mutation }, result: mutationResult2, + delay: 50, }) .then(() => { // we have to actually subscribe to the query to be able to update it @@ -244,15 +646,25 @@ describe('optimistic mutation results', () => { }); }) .then(() => { + const updateQueries = { + todoList: (prev, options) => { + const mResult = options.mutationResult as any; + + const state = cloneDeep(prev) as any; + state.todoList.todos.unshift(mResult.data.createTodo); + return state; + }, + } as MutationQueryReducersMap; const promise = client.mutate({ mutation, optimisticResponse, updateQueries, - }).catch((err) => { - // it is ok to fail here - assert.instanceOf(err, Error); - assert.equal(err.message, 'Network error: forbidden (test error)'); - return null; + }).then((res) => { + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 5); + assert.equal((dataInStore['Todo99'] as any).text, 'This one was created with a mutation.'); + assert.equal((dataInStore['Todo66'] as any).text, 'Optimistically generated 2'); + return res; }); const promise2 = client.mutate({ @@ -269,36 +681,34 @@ describe('optimistic mutation results', () => { return Promise.all([promise, promise2]); }) .then(() => { + return client.query({ query }); + }) + .then((newResult: any) => { subscriptionHandle.unsubscribe(); - const dataInStore = client.queryManager.getDataWithOptimisticResults(); - assert.equal((dataInStore['TodoList5'] as any).todos.length, 4); - assert.notProperty(dataInStore, 'Todo99'); - assert.property(dataInStore, 'Todo66'); - assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo66')); - assert.notInclude((dataInStore['TodoList5'] as any).todos, realIdValue('Todo99')); + // There should be one more todo item than before + assert.equal(newResult.data.todoList.todos.length, 5); + + // Since we used `prepend` it should be at the front + assert.equal(newResult.data.todoList.todos[0].text, 'Second mutation.'); + assert.equal(newResult.data.todoList.todos[1].text, 'This one was created with a mutation.'); }); }); - it('can run 2 mutations concurrently and handles all intermediate states well', () => { - function checkBothMutationsAreApplied(expectedText1: any, expectedText2: any) { - const dataInStore = client.queryManager.getDataWithOptimisticResults(); - assert.equal((dataInStore['TodoList5'] as any).todos.length, 5); - assert.property(dataInStore, 'Todo99'); - assert.property(dataInStore, 'Todo66'); - assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo66')); - assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo99')); - assert.equal((dataInStore['Todo99'] as any).text, expectedText1); - assert.equal((dataInStore['Todo66'] as any).text, expectedText2); - } + it('two mutations, one fails', () => { let subscriptionHandle: Subscription; return setup({ request: { query: mutation }, - result: mutationResult, + error: new Error('forbidden (test error)'), + delay: 20, }, { request: { query: mutation }, result: mutationResult2, - // make sure it always happens later - delay: 100, + // XXX this test will uncover a flaw in the design of optimistic responses combined with + // updateQueries or result reducers if you un-comment the line below. The issue is that + // optimistic updates are not commutative but are treated as such. When undoing an + // optimistic update, other optimistic updates should be rolled back and re-applied in the + // same order as before, otherwise the store can end up in an inconsistent state. + // delay: 50, }) .then(() => { // we have to actually subscribe to the query to be able to update it @@ -310,48 +720,154 @@ describe('optimistic mutation results', () => { }); }) .then(() => { + const updateQueries = { + todoList: (prev, options) => { + const mResult = options.mutationResult as any; + + const state = cloneDeep(prev) as any; + state.todoList.todos.unshift(mResult.data.createTodo); + return state; + }, + } as MutationQueryReducersMap; const promise = client.mutate({ mutation, optimisticResponse, updateQueries, - }).then((res) => { - checkBothMutationsAreApplied('This one was created with a mutation.', 'Optimistically generated 2'); - const mutationsState = client.store.getState().apollo.mutations; - assert.equal(mutationsState['5'].loading, false); - assert.equal(mutationsState['6'].loading, true); - - return res; + }).catch((err) => { + // it is ok to fail here + assert.instanceOf(err, Error); + assert.equal(err.message, 'Network error: forbidden (test error)'); + return null; }); const promise2 = client.mutate({ mutation, optimisticResponse: optimisticResponse2, updateQueries, - }).then((res) => { - checkBothMutationsAreApplied('This one was created with a mutation.', 'Second mutation.'); - const mutationsState = client.store.getState().apollo.mutations; - assert.equal(mutationsState[5].loading, false); - assert.equal(mutationsState[6].loading, false); - - return res; }); - const mutationsState = client.store.getState().apollo.mutations; - assert.equal(mutationsState[5].loading, true); - assert.equal(mutationsState[6].loading, true); - - checkBothMutationsAreApplied('Optimistically generated', 'Optimistically generated 2'); + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 5); + assert.equal((dataInStore['Todo99'] as any).text, 'Optimistically generated'); + assert.equal((dataInStore['Todo66'] as any).text, 'Optimistically generated 2'); return Promise.all([promise, promise2]); }) .then(() => { subscriptionHandle.unsubscribe(); - checkBothMutationsAreApplied('This one was created with a mutation.', 'Second mutation.'); + const dataInStore = client.queryManager.getDataWithOptimisticResults(); + assert.equal((dataInStore['TodoList5'] as any).todos.length, 4); + assert.notProperty(dataInStore, 'Todo99'); + assert.property(dataInStore, 'Todo66'); + assert.include((dataInStore['TodoList5'] as any).todos, realIdValue('Todo66')); + assert.notInclude((dataInStore['TodoList5'] as any).todos, realIdValue('Todo99')); }); }); + + it('will handle dependent updates', done => { + networkInterface = mockNetworkInterface({ + request: { query }, + result, + }, { + request: { query: mutation }, + result: mutationResult, + delay: 10, + }, { + request: { query: mutation }, + result: mutationResult2, + delay: 20, + }); + + const customOptimisticResponse1 = { + __typename: 'Mutation', + createTodo: { + __typename: 'Todo', + id: 'optimistic-99', + text: 'Optimistically generated', + completed: true, + }, + }; + + const customOptimisticResponse2 = { + __typename: 'Mutation', + createTodo: { + __typename: 'Todo', + id: 'optimistic-66', + text: 'Optimistically generated 2', + completed: true, + }, + }; + + const updateQueries = { + todoList: (prev, options) => { + const mResult = options.mutationResult as any; + + const state = cloneDeep(prev) as any; + state.todoList.todos.unshift(mResult.data.createTodo); + return state; + }, + } as MutationQueryReducersMap; + + client = new ApolloClient({ + networkInterface, + dataIdFromObject: (obj: any) => { + if (obj.id && obj.__typename) { + return obj.__typename + obj.id; + } + return null; + }, + }); + + const defaultTodos = result.data.todoList.todos; + let count = 0; + + client.watchQuery({ query }).subscribe({ + next: (value: any) => { + const todos = value.data.todoList.todos; + switch (count++) { + case 0: + assert.deepEqual(defaultTodos, todos); + twoMutations(); + break; + case 1: + assert.deepEqual([customOptimisticResponse1.createTodo, ...defaultTodos], todos); + break; + case 2: + assert.deepEqual([customOptimisticResponse2.createTodo, customOptimisticResponse1.createTodo, ...defaultTodos], todos); + break; + case 3: + assert.deepEqual([customOptimisticResponse2.createTodo, mutationResult.data.createTodo, ...defaultTodos], todos); + break; + case 4: + assert.deepEqual([mutationResult2.data.createTodo, mutationResult.data.createTodo, ...defaultTodos], todos); + done(); + break; + default: + done(new Error('Next should not have been called again.')); + } + }, + error: error => done(error), + }); + + function twoMutations () { + client.mutate({ + mutation, + optimisticResponse: customOptimisticResponse1, + updateQueries, + }) + .catch(error => done(error)); + + client.mutate({ + mutation, + optimisticResponse: customOptimisticResponse2, + updateQueries, + }) + .catch(error => done(error)); + } + }); }); - describe('optimistic updates using updateQueries', () => { + describe('optimistic updates using `update`', () => { const mutation = gql` mutation createTodo { # skipping arguments in the test since they don't matter @@ -425,15 +941,19 @@ describe('optimistic mutation results', () => { const promise = client.mutate({ mutation, optimisticResponse, - updateQueries: { - todoList: (prev, options) => { - const mResult = options.mutationResult as any; - assert.equal(mResult.data.createTodo.id, '99'); - - const state = cloneDeep(prev) as any; - state.todoList.todos.unshift(mResult.data.createTodo); - return state; - }, + update: (proxy, mResult: any) => { + assert.equal(mResult.data.createTodo.id, '99'); + + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); }, }); @@ -476,19 +996,22 @@ describe('optimistic mutation results', () => { }); }) .then(() => { - const updateQueries = { - todoList: (prev, options) => { - const mResult = options.mutationResult as any; - - const state = cloneDeep(prev) as any; - state.todoList.todos.unshift(mResult.data.createTodo); - return state; - }, - } as MutationQueryReducersMap; + const update = (proxy: any, mResult: any) => { + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + }; const promise = client.mutate({ mutation, optimisticResponse, - updateQueries, + update, }).then((res) => { const dataInStore = client.queryManager.getDataWithOptimisticResults(); assert.equal((dataInStore['TodoList5'] as any).todos.length, 5); @@ -500,7 +1023,7 @@ describe('optimistic mutation results', () => { const promise2 = client.mutate({ mutation, optimisticResponse: optimisticResponse2, - updateQueries, + update, }); const dataInStore = client.queryManager.getDataWithOptimisticResults(); @@ -550,19 +1073,22 @@ describe('optimistic mutation results', () => { }); }) .then(() => { - const updateQueries = { - todoList: (prev, options) => { - const mResult = options.mutationResult as any; - - const state = cloneDeep(prev) as any; - state.todoList.todos.unshift(mResult.data.createTodo); - return state; - }, - } as MutationQueryReducersMap; + const update = (proxy: any, mResult: any) => { + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + }; const promise = client.mutate({ mutation, optimisticResponse, - updateQueries, + update, }).catch((err) => { // it is ok to fail here assert.instanceOf(err, Error); @@ -573,7 +1099,7 @@ describe('optimistic mutation results', () => { const promise2 = client.mutate({ mutation, optimisticResponse: optimisticResponse2, - updateQueries, + update, }); const dataInStore = client.queryManager.getDataWithOptimisticResults(); @@ -628,15 +1154,18 @@ describe('optimistic mutation results', () => { }, }; - const updateQueries = { - todoList: (prev, options) => { - const mResult = options.mutationResult as any; - - const state = cloneDeep(prev) as any; - state.todoList.todos.unshift(mResult.data.createTodo); - return state; - }, - } as MutationQueryReducersMap; + const update = (proxy: any, mResult: any) => { + const data: any = proxy.readFragment( + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + + proxy.writeFragment( + { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + 'TodoList5', + gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + ); + }; client = new ApolloClient({ networkInterface, @@ -683,14 +1212,14 @@ describe('optimistic mutation results', () => { client.mutate({ mutation, optimisticResponse: customOptimisticResponse1, - updateQueries, + update, }) .catch(error => done(error)); client.mutate({ mutation, optimisticResponse: customOptimisticResponse2, - updateQueries, + update, }) .catch(error => done(error)); } diff --git a/test/store.ts b/test/store.ts index e0f88b255cd..8cdc7bab060 100644 --- a/test/store.ts +++ b/test/store.ts @@ -251,8 +251,9 @@ describe('createApolloStore', () => { operationName: 'Increment', variables: {}, mutationId: '1', - extraReducers: undefined as undefined, - updateQueries: undefined as undefined, + extraReducers: undefined, + updateQueries: undefined, + update: undefined, }, }, ], From 312f96af3539dad0fdb5dc9939354eddeb14d663 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Mon, 27 Feb 2017 11:39:46 -0500 Subject: [PATCH 24/29] fix proxy tests --- test/proxy.ts | 243 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 198 insertions(+), 45 deletions(-) diff --git a/test/proxy.ts b/test/proxy.ts index 97c7b20bdb1..9ae22308f16 100644 --- a/test/proxy.ts +++ b/test/proxy.ts @@ -619,20 +619,9 @@ describe('ReduxDataProxy', () => { }); describe('TransactionDataProxy', () => { - function createReadDataProxy(): DataProxyRead { - return { - readQuery() { - throw new Error('Should not have valled `readQuery`.'); - }, - readFragment() { - throw new Error('Should not have called `readFragment`.'); - }, - }; - } - describe('readQuery', () => { it('will throw an error if the transaction has finished', () => { - const proxy: any = new TransactionDataProxy(createReadDataProxy()); + const proxy: any = new TransactionDataProxy({}); proxy.finish(); assert.throws(() => { @@ -640,28 +629,87 @@ describe('TransactionDataProxy', () => { }, 'Cannot call transaction methods after the transaction has finished.'); }); - it('will forward a request to the provided proxy', () => { - const query: any = Symbol('query'); - const variables: any = Symbol('variables'); - const data: any = Symbol('data'); + it('will read some data from the store', () => { + const proxy = new TransactionDataProxy({ + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + }, + }); + + assert.deepEqual(proxy.readQuery(gql`{ a }`), { a: 1 }); + assert.deepEqual(proxy.readQuery(gql`{ b c }`), { b: 2, c: 3 }); + assert.deepEqual(proxy.readQuery(gql`{ a b c }`), { a: 1, b: 2, c: 3 }); + }); - const readProxy = createReadDataProxy(); + it('will read some deeply nested data from the store', () => { + const proxy = new TransactionDataProxy({ + 'ROOT_QUERY': { + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: 'foo', + generated: false, + }, + }, + 'foo': { + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + i: 7, + j: 8, + k: 9, + }, + }); - readProxy.readQuery = (...args: Array): any => { - assert.equal(args[0], query); - assert.equal(args[1], variables); - return data; - }; + assert.deepEqual( + proxy.readQuery(gql`{ a d { e } }`), + { a: 1, d: { e: 4 } }, + ); + assert.deepEqual( + proxy.readQuery(gql`{ a d { e h { i } } }`), + { a: 1, d: { e: 4, h: { i: 7 } } }, + ); + assert.deepEqual( + proxy.readQuery(gql`{ a b c d { e f g h { i j k } } }`), + { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, + ); + }); - const proxy = new TransactionDataProxy(readProxy); + it('will read some data from the store with variables', () => { + const proxy = new TransactionDataProxy({ + 'ROOT_QUERY': { + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }); - assert.equal(proxy.readQuery(query, variables), data); + assert.deepEqual(proxy.readQuery( + gql`query ($literal: Boolean, $value: Int) { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + }`, + { + literal: false, + value: 42, + }, + ), { a: 1, b: 2 }); }); }); describe('readFragment', () => { it('will throw an error if the transaction has finished', () => { - const proxy: any = new TransactionDataProxy(createReadDataProxy()); + const proxy: any = new TransactionDataProxy({}); proxy.finish(); assert.throws(() => { @@ -669,32 +717,137 @@ describe('TransactionDataProxy', () => { }, 'Cannot call transaction methods after the transaction has finished.'); }); - it('will forward a request to the provided proxy', () => { - const id: any = Symbol('id'); - const fragment: any = Symbol('fragment'); - const fragmentName: any = Symbol('fragmentName'); - const variables: any = Symbol('variables'); - const data: any = Symbol('data'); + it('will throw an error when there is no fragment', () => { + const proxy = new TransactionDataProxy({}); + + assert.throws(() => { + proxy.readFragment('x', gql`query { a b c }`); + }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); + assert.throws(() => { + proxy.readFragment('x', gql`schema { query: Query }`); + }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + + it('will throw an error when there is more than one fragment but no fragment name', () => { + const proxy = new TransactionDataProxy({}); + + assert.throws(() => { + proxy.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); + }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + assert.throws(() => { + proxy.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); + }); + + it('will read some deeply nested data from the store at any id', () => { + const proxy = new TransactionDataProxy({ + 'ROOT_QUERY': { + __typename: 'Type1', + a: 1, + b: 2, + c: 3, + d: { + type: 'id', + id: 'foo', + generated: false, + }, + }, + 'foo': { + __typename: 'Type2', + e: 4, + f: 5, + g: 6, + h: { + type: 'id', + id: 'bar', + generated: false, + }, + }, + 'bar': { + __typename: 'Type3', + i: 7, + j: 8, + k: 9, + }, + }); + + assert.deepEqual( + proxy.readFragment('foo', gql`fragment fragmentFoo on Foo { e h { i } }`), + { e: 4, h: { i: 7 } }, + ); + assert.deepEqual( + proxy.readFragment('foo', gql`fragment fragmentFoo on Foo { e f g h { i j k } }`), + { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, + ); + assert.deepEqual( + proxy.readFragment('bar', gql`fragment fragmentBar on Bar { i }`), + { i: 7 }, + ); + assert.deepEqual( + proxy.readFragment('bar', gql`fragment fragmentBar on Bar { i j k }`), + { i: 7, j: 8, k: 9 }, + ); + assert.deepEqual( + proxy.readFragment( + 'foo', + gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + 'fragmentFoo', + ), + { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, + ); + assert.deepEqual( + proxy.readFragment( + 'bar', + gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + 'fragmentBar', + ), + { i: 7, j: 8, k: 9 }, + ); + }); - const readProxy = createReadDataProxy(); + it('will read some data from the store with variables', () => { + const proxy = new TransactionDataProxy({ + 'foo': { + __typename: 'Type1', + 'field({"literal":true,"value":42})': 1, + 'field({"literal":false,"value":42})': 2, + }, + }); - readProxy.readFragment = (...args: Array): any => { - assert.equal(args[0], id); - assert.equal(args[1], fragment); - assert.equal(args[2], fragmentName); - assert.equal(args[3], variables); - return data; - }; + assert.deepEqual(proxy.readFragment( + 'foo', + gql` + fragment foo on Foo { + a: field(literal: true, value: 42) + b: field(literal: $literal, value: $value) + } + `, + undefined, + { + literal: false, + value: 42, + }, + ), { a: 1, b: 2 }); + }); - const proxy = new TransactionDataProxy(readProxy); + it('will return null when an id that can’t be found is provided', () => { + const client1 = new TransactionDataProxy({}); + const client2 = new TransactionDataProxy({ + 'bar': { __typename: 'Type1', a: 1, b: 2, c: 3 }, + }); + const client3 = new TransactionDataProxy({ + 'foo': { __typename: 'Type1', a: 1, b: 2, c: 3 }, + }); - assert.equal(proxy.readFragment(id, fragment, fragmentName, variables), data); + assert.equal(client1.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); + assert.equal(client2.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); + assert.deepEqual(client3.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), { a: 1, b: 2, c: 3 }); }); }); describe('writeQuery', () => { it('will throw an error if the transaction has finished', () => { - const proxy: any = new TransactionDataProxy(createReadDataProxy()); + const proxy: any = new TransactionDataProxy({}); proxy.finish(); assert.throws(() => { @@ -703,7 +856,7 @@ describe('TransactionDataProxy', () => { }); it('will create writes that get returned when finished', () => { - const proxy = new TransactionDataProxy(createReadDataProxy()); + const proxy = new TransactionDataProxy({}); proxy.writeQuery( { a: 1, b: 2, c: 3 }, @@ -737,7 +890,7 @@ describe('TransactionDataProxy', () => { describe('writeFragment', () => { it('will throw an error if the transaction has finished', () => { - const proxy: any = new TransactionDataProxy(createReadDataProxy()); + const proxy: any = new TransactionDataProxy({}); proxy.finish(); assert.throws(() => { @@ -746,7 +899,7 @@ describe('TransactionDataProxy', () => { }); it('will create writes that get returned when finished', () => { - const proxy = new TransactionDataProxy(createReadDataProxy()); + const proxy = new TransactionDataProxy({}); proxy.writeFragment( { a: 1, b: 2, c: 3 }, From e69c8581302777e52cd01bc0f6e001a1b5c16190 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Mon, 27 Feb 2017 13:05:55 -0500 Subject: [PATCH 25/29] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9466e4af65..cfe6734cf6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,10 @@ Expect active development and potentially significant breaking changes in the `0 ### vNEXT - Clear pollInterval in `ObservableQuery#stopPolling` so that resubscriptions don't start polling again [PR #1328](https://github.com/apollographql/apollo-client/pull/1328) +- Add direct cache manipulation read and write methods to provide the user the power to interact with Apollo’s GraphQL data representation outside of mutations. [PR #1310](https://github.com/apollographql/apollo-client/pull/1310) ### 0.9.0 - Prefer stale data over partial data in cases where a user would previously get an error. [PR #1306](https://github.com/apollographql/apollo-client/pull/1306) -- Add direct cache manipulation read and write methods to provide the user the power to interact with Apollo’s GraphQL data representation outside of mutations. [PR #1310](https://github.com/apollographql/apollo-client/pull/1310) - Update TypeScript `MutationOptions` definition with the new object type available in `refetchQueries`. [PR #1315](https://github.com/apollographql/apollo-client/pull/1315) - Add `fetchMore` network status to enable loading information for `fetchMore` queries. [PR #1305](https://github.com/apollographql/apollo-client/pull/1305) From c42b0c200c79ef4c4bc2f62f60bb5e9aa5713cf2 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Mon, 27 Feb 2017 13:21:30 -0500 Subject: [PATCH 26/29] remove write(Query|Fragment)Optimistically --- src/ApolloClient.ts | 108 -------- src/actions.ts | 23 +- src/optimistic-data/store.ts | 40 +-- test/ApolloClient.ts | 499 ----------------------------------- 4 files changed, 2 insertions(+), 668 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index da39d8795c9..5a4046eecf1 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -112,7 +112,6 @@ export default class ApolloClient { public queryDeduplication: boolean; private devToolsHookCb: Function; - private optimisticWriteId: number; private proxy: DataProxy | undefined; /** @@ -246,8 +245,6 @@ export default class ApolloClient { } this.version = version; - - this.optimisticWriteId = 1; } /** @@ -460,111 +457,6 @@ export default class ApolloClient { return this.initProxy().writeFragment(data, id, fragment, fragmentName, variables); } - /** - * Writes some data in the shape of the provided GraphQL query directly to - * the store. This method will start at the root query. To start at a - * specific id returned by `dataIdFromObject` then use - * `writeFragmentOptimistically`. - * - * Unlike `writeQuery`, the data written with this method will be stored in - * the optimistic portion of the cache and so will not be persisted. This - * optimistic write may also be rolled back with the `rollback` function that - * was returned. - * - * @param data The data you will be writing to the store. - * - * @param query The GraphQL query shape to be used. - * - * @param variables Any variables that the GraphQL query may depend on. - */ - public writeQueryOptimistically( - data: any, - query: DocumentNode, - variables?: Object, - ): { - rollback: () => void, - } { - const optimisticWriteId = (this.optimisticWriteId++).toString(); - this.initStore(); - this.store.dispatch({ - type: 'APOLLO_WRITE_OPTIMISTIC', - optimisticWriteId, - writes: [{ - rootId: 'ROOT_QUERY', - result: data, - document: query, - variables: variables || {}, - }], - }); - return { - rollback: () => this.store.dispatch({ - type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK', - optimisticWriteId, - }), - }; - } - - /** - * Writes some data in the shape of the provided GraphQL fragment directly to - * the store. This method will write to a GraphQL fragment from any - * arbitrary id that is currently cached, unlike `writeQueryOptimistically` - * which will only write from the root query. - * - * You must pass in a GraphQL document with a single fragment or a document - * with multiple fragments that represent what you are writing. If you pass - * in a document with multiple fragments then you must also specify a - * `fragmentName`. - * - * Unlike `writeFragment`, the data written with this method will be stored in - * the optimistic portion of the cache and so will not be persisted. This - * optimistic write may also be rolled back with the `rollback` function that - * was returned. - * - * @param data The data you will be writing to the store. - * - * @param id The root id to be used. This id should take the same form as the - * value returned by your `dataIdFromObject` function. - * - * @param fragment A GraphQL document with one or more fragments the shape of - * which will be used. If you provide more then one fragments then you must - * also specify the next argument, `fragmentName`, to select a single - * fragment to use when reading. - * - * @param fragmentName The name of the fragment in your GraphQL document to - * be used. Pass `undefined` if there is only one fragment and you want to - * use that. - * - * @param variables Any variables that your GraphQL fragments depend on. - */ - public writeFragmentOptimistically( - data: any, - id: string, - fragment: DocumentNode, - fragmentName?: string, - variables?: Object, - ): { - rollback: () => void, - } { - const optimisticWriteId = (this.optimisticWriteId++).toString(); - this.initStore(); - this.store.dispatch({ - type: 'APOLLO_WRITE_OPTIMISTIC', - optimisticWriteId, - writes: [{ - rootId: id, - result: data, - document: getFragmentQueryDocument(fragment, fragmentName), - variables: variables || {}, - }], - }); - return { - rollback: () => this.store.dispatch({ - type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK', - optimisticWriteId, - }), - }; - } - /** * Returns a reducer function configured according to the `reducerConfig` instance variable. */ diff --git a/src/actions.ts b/src/actions.ts index bb2aa936c1e..09e09ff3ad8 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -177,25 +177,6 @@ export function isWriteAction(action: ApolloAction): action is WriteAction { return action.type === 'APOLLO_WRITE'; } -export interface WriteActionOptimistic { - type: 'APOLLO_WRITE_OPTIMISTIC'; - optimisticWriteId: string; - writes: Array; -} - -export function isWriteOptimisticAction(action: ApolloAction): action is WriteActionOptimistic { - return action.type === 'APOLLO_WRITE_OPTIMISTIC'; -} - -export interface WriteActionOptimisticRollback { - type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK'; - optimisticWriteId: string; -} - -export function isWriteOptimisticRollbackAction(action: ApolloAction): action is WriteActionOptimisticRollback { - return action.type === 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK'; -} - export type ApolloAction = QueryResultAction | QueryErrorAction | @@ -208,6 +189,4 @@ export type ApolloAction = UpdateQueryResultAction | StoreResetAction | SubscriptionResultAction | - WriteAction | - WriteActionOptimistic | - WriteActionOptimisticRollback; + WriteAction; diff --git a/src/optimistic-data/store.ts b/src/optimistic-data/store.ts index 92beb008fba..e62d48b80ed 100644 --- a/src/optimistic-data/store.ts +++ b/src/optimistic-data/store.ts @@ -4,8 +4,6 @@ import { isMutationInitAction, isMutationResultAction, isMutationErrorAction, - isWriteOptimisticAction, - isWriteOptimisticRollbackAction, } from '../actions'; import { @@ -32,8 +30,7 @@ import { import { assign } from '../util/assign'; export type OptimisticStoreItem = { - mutationId?: string, - optimisticWriteId?: string, + mutationId: string, data: NormalizedCache, }; @@ -99,41 +96,6 @@ export function optimistic( store, config, ); - } else if (isWriteOptimisticAction(action)) { - const fakeWriteAction: WriteAction = { - type: 'APOLLO_WRITE', - writes: action.writes, - }; - - const optimisticData = getDataWithOptimisticResults({ - ...store, - optimistic: previousState, - }); - - const patch = getOptimisticDataPatch( - optimisticData, - fakeWriteAction, - store.queries, - store.mutations, - config, - ); - - const optimisticState = { - action: fakeWriteAction, - data: patch, - optimisticWriteId: action.optimisticWriteId, - }; - - const newState = [...previousState, optimisticState]; - - return newState; - } else if (isWriteOptimisticRollbackAction(action)) { - return rollbackOptimisticData( - change => change.optimisticWriteId === action.optimisticWriteId, - previousState, - store, - config, - ); } return previousState; diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index f59f2221b83..ba6d6e45a77 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -600,503 +600,4 @@ describe('ApolloClient', () => { }); }); }); - - describe('writeQueryOptimistically', () => { - function getOptimisticData (client: ApolloClient) { - return client.store.getState().apollo.optimistic.map((optimistic: any) => optimistic.data); - } - - it('will write some data to the store that can be rolled back', () => { - const client = new ApolloClient(); - - const optimistic1 = client.writeQueryOptimistically({ a: 1 }, gql`{ a }`); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - a: 1, - }, - }, - ]); - - const optimistic2 = client.writeQueryOptimistically({ b: 2, c: 3 }, gql`{ b c }`); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - a: 1, - }, - }, - { - 'ROOT_QUERY': { - a: 1, - b: 2, - c: 3, - }, - }, - ]); - - optimistic1.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - b: 2, - c: 3, - }, - }, - ]); - - const optimistic3 = client.writeQueryOptimistically({ a: 4, b: 5, c: 6 }, gql`{ a b c }`); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - b: 2, - c: 3, - }, - }, - { - 'ROOT_QUERY': { - a: 4, - b: 5, - c: 6, - }, - }, - ]); - - optimistic3.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - b: 2, - c: 3, - }, - }, - ]); - - optimistic2.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), []); - }); - - it('will write some deeply nested data to the store and roll it back', () => { - const client = new ApolloClient(); - - const optimistic1 = client.writeQueryOptimistically( - { a: 1, d: { e: 4 } }, - gql`{ a d { e } }`, - ); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - a: 1, - d: { - type: 'id', - id: '$ROOT_QUERY.d', - generated: true, - }, - }, - '$ROOT_QUERY.d': { - e: 4, - }, - }, - ]); - - const optimistic2 = client.writeQueryOptimistically( - { d: { h: { i: 7 } } }, - gql`{ d { h { i } } }`, - ); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - a: 1, - d: { - type: 'id', - id: '$ROOT_QUERY.d', - generated: true, - }, - }, - '$ROOT_QUERY.d': { - e: 4, - }, - }, - { - 'ROOT_QUERY': { - a: 1, - d: { - type: 'id', - id: '$ROOT_QUERY.d', - generated: true, - }, - }, - '$ROOT_QUERY.d': { - e: 4, - h: { - type: 'id', - id: '$ROOT_QUERY.d.h', - generated: true, - }, - }, - '$ROOT_QUERY.d.h': { - i: 7, - }, - }, - ]); - - optimistic1.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - d: { - type: 'id', - id: '$ROOT_QUERY.d', - generated: true, - }, - }, - '$ROOT_QUERY.d': { - h: { - type: 'id', - id: '$ROOT_QUERY.d.h', - generated: true, - }, - }, - '$ROOT_QUERY.d.h': { - i: 7, - }, - }, - ]); - - const optimistic3 = client.writeQueryOptimistically( - { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, - gql`{ a b c d { e f g h { i j k } } }`, - ); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - d: { - type: 'id', - id: '$ROOT_QUERY.d', - generated: true, - }, - }, - '$ROOT_QUERY.d': { - h: { - type: 'id', - id: '$ROOT_QUERY.d.h', - generated: true, - }, - }, - '$ROOT_QUERY.d.h': { - i: 7, - }, - }, - { - 'ROOT_QUERY': { - a: 1, - b: 2, - c: 3, - d: { - type: 'id', - id: '$ROOT_QUERY.d', - generated: true, - }, - }, - '$ROOT_QUERY.d': { - e: 4, - f: 5, - g: 6, - h: { - type: 'id', - id: '$ROOT_QUERY.d.h', - generated: true, - }, - }, - '$ROOT_QUERY.d.h': { - i: 7, - j: 8, - k: 9, - }, - }, - ]); - - optimistic3.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'ROOT_QUERY': { - d: { - type: 'id', - id: '$ROOT_QUERY.d', - generated: true, - }, - }, - '$ROOT_QUERY.d': { - h: { - type: 'id', - id: '$ROOT_QUERY.d.h', - generated: true, - }, - }, - '$ROOT_QUERY.d.h': { - i: 7, - }, - }, - ]); - - optimistic2.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), []); - }); - }); - - describe('writeFragmentOptimistically', () => { - function getOptimisticData (client: ApolloClient) { - return client.store.getState().apollo.optimistic.map((optimistic: any) => optimistic.data); - } - - it('will throw an error when there is no fragment', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.writeFragmentOptimistically({}, 'x', gql`query { a b c }`); - }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); - assert.throws(() => { - client.writeFragmentOptimistically({}, 'x', gql`schema { query: Query }`); - }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); - }); - - it('will throw an error when there is more than one fragment but no fragment name', () => { - const client = new ApolloClient(); - - assert.throws(() => { - client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); - }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); - assert.throws(() => { - client.writeFragmentOptimistically({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); - }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); - }); - - it('will write some deeply nested data into the store at any id and roll it back', () => { - const client = new ApolloClient({ - dataIdFromObject: (o: any) => o.id, - }); - - const optimistic1 = client.writeFragmentOptimistically( - { e: 4, h: { id: 'bar', i: 7 } }, - 'foo', - gql`fragment fragmentFoo on Foo { e h { i } }`, - ); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'foo': { - e: 4, - h: { - type: 'id', - id: 'bar', - generated: false, - }, - }, - 'bar': { - i: 7, - }, - }, - ]); - - const optimistic2 = client.writeFragmentOptimistically( - { f: 5, g: 6, h: { id: 'bar', j: 8, k: 9 } }, - 'foo', - gql`fragment fragmentFoo on Foo { f g h { j k } }`, - ); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'foo': { - e: 4, - h: { - type: 'id', - id: 'bar', - generated: false, - }, - }, - 'bar': { - i: 7, - }, - }, - { - 'foo': { - e: 4, - f: 5, - g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, - }, - 'bar': { - i: 7, - j: 8, - k: 9, - }, - }, - ]); - - optimistic1.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'foo': { - f: 5, - g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, - }, - 'bar': { - j: 8, - k: 9, - }, - }, - ]); - - const optimistic3 = client.writeFragmentOptimistically( - { i: 10 }, - 'bar', - gql`fragment fragmentBar on Bar { i }`, - ); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'foo': { - f: 5, - g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, - }, - 'bar': { - j: 8, - k: 9, - }, - }, - { - 'bar': { - i: 10, - j: 8, - k: 9, - }, - }, - ]); - - const optimistic4 = client.writeFragmentOptimistically( - { j: 11, k: 12 }, - 'bar', - gql`fragment fragmentBar on Bar { j k }`, - ); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'foo': { - f: 5, - g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, - }, - 'bar': { - j: 8, - k: 9, - }, - }, - { - 'bar': { - j: 8, - k: 9, - i: 10, - }, - }, - { - 'bar': { - i: 10, - j: 11, - k: 12, - }, - }, - ]); - - optimistic3.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'foo': { - f: 5, - g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, - }, - 'bar': { - j: 8, - k: 9, - }, - }, - { - 'bar': { - j: 11, - k: 12, - }, - }, - ]); - - optimistic2.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), [ - { - 'bar': { - j: 11, - k: 12, - }, - }, - ]); - - optimistic4.rollback(); - - assert.deepEqual(client.store.getState().apollo.data, {}); - assert.deepEqual(getOptimisticData(client), []); - }); - }); }); From 3c7f401a6fcde1996264067206864d03c840834a Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Tue, 28 Feb 2017 13:21:29 -0500 Subject: [PATCH 27/29] change internal documentation phrasing --- src/data/proxy.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/proxy.ts b/src/data/proxy.ts index 31d36aa28df..0a4e6d6cb21 100644 --- a/src/data/proxy.ts +++ b/src/data/proxy.ts @@ -261,8 +261,8 @@ export class TransactionDataProxy implements DataProxy { } /** - * Creates an action to be consumed after `finish` is called that writes - * some query data to the store at the root query id. Cannot be called after + * Creates a write to be consumed after `finish` is called that instructs + * a write to the store at the root query id. Cannot be called after * the transaction finishes. */ public writeQuery( @@ -280,9 +280,9 @@ export class TransactionDataProxy implements DataProxy { } /** - * Creates an action to be consumed after `finish` is called that writes some - * fragment data to the store at an arbitrary id. Cannot be called after the - * transaction finishes. + * Creates a write to be consumed after `finish` is called that instructs a + * write to the store form some fragment data at an arbitrary id. Cannot be + * called after the transaction finishes. */ public writeFragment( data: any, From f2a6fc7834dc0d27bb93028b058b8a6f6c00efc1 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Tue, 28 Feb 2017 13:51:14 -0500 Subject: [PATCH 28/29] make data proxy arguments named arguments --- src/ApolloClient.ts | 24 +-- src/data/proxy.ts | 84 ++++++++--- test/ApolloClient.ts | 198 ++++++++++++------------- test/mutationResults.ts | 70 +++++---- test/optimistic.ts | 97 ++++++------ test/proxy.ts | 321 ++++++++++++++++++++-------------------- 6 files changed, 410 insertions(+), 384 deletions(-) diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index 5a4046eecf1..f31924eb38b 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -359,11 +359,11 @@ export default class ApolloClient { * * @param variables Any variables that the GraphQL query may depend on. */ - public readQuery( + public readQuery(config: { query: DocumentNode, variables?: Object, - ): QueryType { - return this.initProxy().readQuery(query, variables); + }): QueryType { + return this.initProxy().readQuery(config); } /** @@ -392,13 +392,13 @@ export default class ApolloClient { * * @param variables Any variables that your GraphQL fragments depend on. */ - public readFragment( + public readFragment(config: { id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): FragmentType | null { - return this.initProxy().readFragment(id, fragment, fragmentName, variables); + }): FragmentType | null { + return this.initProxy().readFragment(config); } /** @@ -412,12 +412,12 @@ export default class ApolloClient { * * @param variables Any variables that the GraphQL query may depend on. */ - public writeQuery( + public writeQuery(config: { data: any, query: DocumentNode, variables?: Object, - ): void { - return this.initProxy().writeQuery(data, query, variables); + }): void { + return this.initProxy().writeQuery(config); } /** @@ -447,14 +447,14 @@ export default class ApolloClient { * * @param variables Any variables that your GraphQL fragments depend on. */ - public writeFragment( + public writeFragment(config: { data: any, id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): void { - return this.initProxy().writeFragment(data, id, fragment, fragmentName, variables); + }): void { + return this.initProxy().writeFragment(config); } /** diff --git a/src/data/proxy.ts b/src/data/proxy.ts index 0a4e6d6cb21..2ade091eb39 100644 --- a/src/data/proxy.ts +++ b/src/data/proxy.ts @@ -16,44 +16,44 @@ export interface DataProxy { /** * Reads a GraphQL query from the root query id. */ - readQuery( + readQuery(config: { query: DocumentNode, variables?: Object, - ): QueryType; + }): QueryType; /** * Reads a GraphQL fragment from any arbitrary id. If there are more then * one fragments in the provided document then a `fragmentName` must be * provided to select the correct fragment. */ - readFragment( + readFragment(config: { id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): FragmentType | null; + }): FragmentType | null; /** * Writes a GraphQL query to the root query id. */ - writeQuery( + writeQuery(config: { data: any, query: DocumentNode, variables?: Object, - ): void; + }): void; /** * Writes a GraphQL fragment to any arbitrary id. If there are more then * one fragments in the provided document then a `fragmentName` must be * provided to select the correct fragment. */ - writeFragment( + writeFragment(config: { data: any, id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): void; + }): void; } /** @@ -86,10 +86,13 @@ export class ReduxDataProxy implements DataProxy { /** * Reads a query from the Redux state. */ - public readQuery( + public readQuery({ + query, + variables, + }: { query: DocumentNode, variables?: Object, - ): QueryType { + }): QueryType { return readQueryFromStore({ rootId: 'ROOT_QUERY', store: getDataWithOptimisticResults(this.reduxRootSelector(this.store.getState())), @@ -102,12 +105,17 @@ export class ReduxDataProxy implements DataProxy { /** * Reads a fragment from the Redux state. */ - public readFragment( + public readFragment({ + id, + fragment, + fragmentName, + variables, + }: { id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): FragmentType | null { + }): FragmentType | null { const query = getFragmentQueryDocument(fragment, fragmentName); const data = getDataWithOptimisticResults(this.reduxRootSelector(this.store.getState())); @@ -129,11 +137,15 @@ export class ReduxDataProxy implements DataProxy { /** * Writes a query to the Redux state. */ - public writeQuery( + public writeQuery({ + data, + query, + variables, + }: { data: any, query: DocumentNode, variables?: Object, - ): void { + }): void { this.store.dispatch({ type: 'APOLLO_WRITE', writes: [{ @@ -148,13 +160,19 @@ export class ReduxDataProxy implements DataProxy { /** * Writes a fragment to the Redux state. */ - public writeFragment( + public writeFragment({ + data, + id, + fragment, + fragmentName, + variables, + }: { data: any, id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): void { + }): void { this.store.dispatch({ type: 'APOLLO_WRITE', writes: [{ @@ -216,10 +234,13 @@ export class TransactionDataProxy implements DataProxy { * * Throws an error if the transaction has finished. */ - public readQuery( + public readQuery({ + query, + variables, + }: { query: DocumentNode, variables?: Object, - ): QueryType { + }): QueryType { this.assertNotFinished(); return readQueryFromStore({ rootId: 'ROOT_QUERY', @@ -235,12 +256,17 @@ export class TransactionDataProxy implements DataProxy { * * Throws an error if the transaction has finished. */ - public readFragment( + public readFragment({ + id, + fragment, + fragmentName, + variables, + }: { id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): FragmentType | null { + }): FragmentType | null { this.assertNotFinished(); const { data } = this; const query = getFragmentQueryDocument(fragment, fragmentName); @@ -265,11 +291,15 @@ export class TransactionDataProxy implements DataProxy { * a write to the store at the root query id. Cannot be called after * the transaction finishes. */ - public writeQuery( + public writeQuery({ + data, + query, + variables, + }: { data: any, query: DocumentNode, variables?: Object, - ): void { + }): void { this.assertNotFinished(); this.writes.push({ rootId: 'ROOT_QUERY', @@ -284,13 +314,19 @@ export class TransactionDataProxy implements DataProxy { * write to the store form some fragment data at an arbitrary id. Cannot be * called after the transaction finishes. */ - public writeFragment( + public writeFragment({ + data, + id, + fragment, + fragmentName, + variables, + }: { data: any, id: string, fragment: DocumentNode, fragmentName?: string, variables?: Object, - ): void { + }): void { this.assertNotFinished(); this.writes.push({ rootId: id, diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index ba6d6e45a77..0a5f4cae4ee 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -20,9 +20,9 @@ describe('ApolloClient', () => { }, }); - assert.deepEqual(client.readQuery(gql`{ a }`), { a: 1 }); - assert.deepEqual(client.readQuery(gql`{ b c }`), { b: 2, c: 3 }); - assert.deepEqual(client.readQuery(gql`{ a b c }`), { a: 1, b: 2, c: 3 }); + assert.deepEqual(client.readQuery({ query: gql`{ a }` }), { a: 1 }); + assert.deepEqual(client.readQuery({ query: gql`{ b c }` }), { b: 2, c: 3 }); + assert.deepEqual(client.readQuery({ query: gql`{ a b c }` }), { a: 1, b: 2, c: 3 }); }); it('will read some deeply nested data from the store', () => { @@ -61,15 +61,15 @@ describe('ApolloClient', () => { }); assert.deepEqual( - client.readQuery(gql`{ a d { e } }`), + client.readQuery({ query: gql`{ a d { e } }` }), { a: 1, d: { e: 4 } }, ); assert.deepEqual( - client.readQuery(gql`{ a d { e h { i } } }`), + client.readQuery({ query: gql`{ a d { e h { i } } }` }), { a: 1, d: { e: 4, h: { i: 7 } } }, ); assert.deepEqual( - client.readQuery(gql`{ a b c d { e f g h { i j k } } }`), + client.readQuery({ query: gql`{ a b c d { e f g h { i j k } } }` }), { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, ); }); @@ -88,16 +88,16 @@ describe('ApolloClient', () => { }, }); - assert.deepEqual(client.readQuery( - gql`query ($literal: Boolean, $value: Int) { + assert.deepEqual(client.readQuery({ + query: gql`query ($literal: Boolean, $value: Int) { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) }`, - { + variables: { literal: false, value: 42, }, - ), { a: 1, b: 2 }); + }), { a: 1, b: 2 }); }); }); @@ -106,10 +106,10 @@ describe('ApolloClient', () => { const client = new ApolloClient(); assert.throws(() => { - client.readFragment('x', gql`query { a b c }`); + client.readFragment({ id: 'x', fragment: gql`query { a b c }` }); }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); assert.throws(() => { - client.readFragment('x', gql`schema { query: Query }`); + client.readFragment({ id: 'x', fragment: gql`schema { query: Query }` }); }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -117,10 +117,10 @@ describe('ApolloClient', () => { const client = new ApolloClient(); assert.throws(() => { - client.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); + client.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b }` }); }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); assert.throws(() => { - client.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + client.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }` }); }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -163,35 +163,35 @@ describe('ApolloClient', () => { }); assert.deepEqual( - client.readFragment('foo', gql`fragment fragmentFoo on Foo { e h { i } }`), + client.readFragment({ id: 'foo', fragment: gql`fragment fragmentFoo on Foo { e h { i } }` }), { e: 4, h: { i: 7 } }, ); assert.deepEqual( - client.readFragment('foo', gql`fragment fragmentFoo on Foo { e f g h { i j k } }`), + client.readFragment({ id: 'foo', fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } }` }), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - client.readFragment('bar', gql`fragment fragmentBar on Bar { i }`), + client.readFragment({ id: 'bar', fragment: gql`fragment fragmentBar on Bar { i }` }), { i: 7 }, ); assert.deepEqual( - client.readFragment('bar', gql`fragment fragmentBar on Bar { i j k }`), + client.readFragment({ id: 'bar', fragment: gql`fragment fragmentBar on Bar { i j k }` }), { i: 7, j: 8, k: 9 }, ); assert.deepEqual( - client.readFragment( - 'foo', - gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, - 'fragmentFoo', - ), + client.readFragment({ + id: 'foo', + fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + fragmentName: 'fragmentFoo', + }), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - client.readFragment( - 'bar', - gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, - 'fragmentBar', - ), + client.readFragment({ + id: 'bar', + fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + fragmentName: 'fragmentBar', + }), { i: 7, j: 8, k: 9 }, ); }); @@ -211,20 +211,19 @@ describe('ApolloClient', () => { }, }); - assert.deepEqual(client.readFragment( - 'foo', - gql` + assert.deepEqual(client.readFragment({ + id: 'foo', + fragment: gql` fragment foo on Foo { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) } `, - undefined, - { + variables: { literal: false, value: 42, }, - ), { a: 1, b: 2 }); + }), { a: 1, b: 2 }); }); it('will return null when an id that can’t be found is provided', () => { @@ -248,9 +247,9 @@ describe('ApolloClient', () => { }, }); - assert.equal(client1.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); - assert.equal(client2.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); - assert.deepEqual(client3.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), { a: 1, b: 2, c: 3 }); + assert.equal(client1.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), null); + assert.equal(client2.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), null); + assert.deepEqual(client3.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), { a: 1, b: 2, c: 3 }); }); }); @@ -258,7 +257,7 @@ describe('ApolloClient', () => { it('will write some data to the store', () => { const client = new ApolloClient(); - client.writeQuery({ a: 1 }, gql`{ a }`); + client.writeQuery({ data: { a: 1 }, query: gql`{ a }` }); assert.deepEqual(client.store.getState().apollo.data, { 'ROOT_QUERY': { @@ -266,7 +265,7 @@ describe('ApolloClient', () => { }, }); - client.writeQuery({ b: 2, c: 3 }, gql`{ b c }`); + client.writeQuery({ data: { b: 2, c: 3 }, query: gql`{ b c }` }); assert.deepEqual(client.store.getState().apollo.data, { 'ROOT_QUERY': { @@ -276,7 +275,7 @@ describe('ApolloClient', () => { }, }); - client.writeQuery({ a: 4, b: 5, c: 6 }, gql`{ a b c }`); + client.writeQuery({ data: { a: 4, b: 5, c: 6 }, query: gql`{ a b c }` }); assert.deepEqual(client.store.getState().apollo.data, { 'ROOT_QUERY': { @@ -290,10 +289,10 @@ describe('ApolloClient', () => { it('will write some deeply nested data to the store', () => { const client = new ApolloClient(); - client.writeQuery( - { a: 1, d: { e: 4 } }, - gql`{ a d { e } }`, - ); + client.writeQuery({ + data: { a: 1, d: { e: 4 } }, + query: gql`{ a d { e } }`, + }); assert.deepEqual(client.store.getState().apollo.data, { 'ROOT_QUERY': { @@ -309,10 +308,10 @@ describe('ApolloClient', () => { }, }); - client.writeQuery( - { a: 1, d: { h: { i: 7 } } }, - gql`{ a d { h { i } } }`, - ); + client.writeQuery({ + data: { a: 1, d: { h: { i: 7 } } }, + query: gql`{ a d { h { i } } }`, + }); assert.deepEqual(client.store.getState().apollo.data, { 'ROOT_QUERY': { @@ -336,10 +335,10 @@ describe('ApolloClient', () => { }, }); - client.writeQuery( - { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, - gql`{ a b c d { e f g h { i j k } } }`, - ); + client.writeQuery({ + data: { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, + query: gql`{ a b c d { e f g h { i j k } } }`, + }); assert.deepEqual(client.store.getState().apollo.data, { 'ROOT_QUERY': { @@ -373,22 +372,22 @@ describe('ApolloClient', () => { it('will write some data to the store with variables', () => { const client = new ApolloClient(); - client.writeQuery( - { + client.writeQuery({ + data: { a: 1, b: 2, }, - gql` + query: gql` query ($literal: Boolean, $value: Int) { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) } `, - { + variables: { literal: false, value: 42, }, - ); + }); assert.deepEqual(client.store.getState().apollo.data, { 'ROOT_QUERY': { @@ -404,10 +403,10 @@ describe('ApolloClient', () => { const client = new ApolloClient(); assert.throws(() => { - client.writeFragment({}, 'x', gql`query { a b c }`); + client.writeFragment({ data: {}, id: 'x', fragment: gql`query { a b c }` }); }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); assert.throws(() => { - client.writeFragment({}, 'x', gql`schema { query: Query }`); + client.writeFragment({ data: {}, id: 'x', fragment: gql`schema { query: Query }` }); }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -415,10 +414,10 @@ describe('ApolloClient', () => { const client = new ApolloClient(); assert.throws(() => { - client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); + client.writeFragment({ data: {}, id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b }` }); }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); assert.throws(() => { - client.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + client.writeFragment({ data: {}, id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }` }); }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -427,11 +426,11 @@ describe('ApolloClient', () => { dataIdFromObject: (o: any) => o.id, }); - client.writeFragment( - { e: 4, h: { id: 'bar', i: 7 } }, - 'foo', - gql`fragment fragmentFoo on Foo { e h { i } }`, - ); + client.writeFragment({ + data: { e: 4, h: { id: 'bar', i: 7 } }, + id: 'foo', + fragment: gql`fragment fragmentFoo on Foo { e h { i } }`, + }); assert.deepEqual(client.store.getState().apollo.data, { 'foo': { @@ -447,11 +446,11 @@ describe('ApolloClient', () => { }, }); - client.writeFragment( - { f: 5, g: 6, h: { id: 'bar', j: 8, k: 9 } }, - 'foo', - gql`fragment fragmentFoo on Foo { f g h { j k } }`, - ); + client.writeFragment({ + data: { f: 5, g: 6, h: { id: 'bar', j: 8, k: 9 } }, + id: 'foo', + fragment: gql`fragment fragmentFoo on Foo { f g h { j k } }`, + }); assert.deepEqual(client.store.getState().apollo.data, { 'foo': { @@ -471,11 +470,11 @@ describe('ApolloClient', () => { }, }); - client.writeFragment( - { i: 10 }, - 'bar', - gql`fragment fragmentBar on Bar { i }`, - ); + client.writeFragment({ + data: { i: 10 }, + id: 'bar', + fragment: gql`fragment fragmentBar on Bar { i }`, + }); assert.deepEqual(client.store.getState().apollo.data, { 'foo': { @@ -495,11 +494,11 @@ describe('ApolloClient', () => { }, }); - client.writeFragment( - { j: 11, k: 12 }, - 'bar', - gql`fragment fragmentBar on Bar { j k }`, - ); + client.writeFragment({ + data: { j: 11, k: 12 }, + id: 'bar', + fragment: gql`fragment fragmentBar on Bar { j k }`, + }); assert.deepEqual(client.store.getState().apollo.data, { 'foo': { @@ -519,12 +518,12 @@ describe('ApolloClient', () => { }, }); - client.writeFragment( - { e: 4, f: 5, g: 6, h: { id: 'bar', i: 7, j: 8, k: 9 } }, - 'foo', - gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, - 'fooFragment', - ); + client.writeFragment({ + data: { e: 4, f: 5, g: 6, h: { id: 'bar', i: 7, j: 8, k: 9 } }, + id: 'foo', + fragment: gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, + fragmentName: 'fooFragment', + }); assert.deepEqual(client.store.getState().apollo.data, { 'foo': { @@ -544,12 +543,12 @@ describe('ApolloClient', () => { }, }); - client.writeFragment( - { i: 10, j: 11, k: 12 }, - 'bar', - gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, - 'barFragment', - ); + client.writeFragment({ + data: { i: 10, j: 11, k: 12 }, + id: 'bar', + fragment: gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, + fragmentName: 'barFragment', + }); assert.deepEqual(client.store.getState().apollo.data, { 'foo': { @@ -573,24 +572,23 @@ describe('ApolloClient', () => { it('will write some data to the store with variables', () => { const client = new ApolloClient(); - client.writeFragment( - { + client.writeFragment({ + data: { a: 1, b: 2, }, - 'foo', - gql` + id: 'foo', + fragment: gql` fragment foo on Foo { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) } `, - undefined, - { + variables: { literal: false, value: 42, }, - ); + }); assert.deepEqual(client.store.getState().apollo.data, { 'foo': { diff --git a/test/mutationResults.ts b/test/mutationResults.ts index a161268c30e..f5541ca2c48 100644 --- a/test/mutationResults.ts +++ b/test/mutationResults.ts @@ -1245,16 +1245,15 @@ describe('mutation results', () => { assert.equal(mResult.data.createTodo.id, '99'); assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.'); - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); - - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const id = 'TodoList5'; + const fragment = gql`fragment todoList on TodoList { todos { id text completed __typename } }`; + + const data: any = proxy.readFragment({ id, fragment }); + + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id, fragment, + }); }, }); }) @@ -1320,16 +1319,15 @@ describe('mutation results', () => { assert.equal(mResult.data.createTodo.id, '99'); assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.'); - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const id = 'TodoList5'; + const fragment = gql`fragment todoList on TodoList { todos { id text completed __typename } }`; + + const data: any = proxy.readFragment({ id, fragment }); - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id, fragment, + }); }, }); }) @@ -1364,16 +1362,15 @@ describe('mutation results', () => { assert.equal(mResult.data.createTodo.id, '99'); assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.'); - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const id = 'TodoList5'; + const fragment = gql`fragment todoList on TodoList { todos { id text completed __typename } }`; - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const data: any = proxy.readFragment({ id, fragment }); + + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id, fragment, + }); }, }) .then( @@ -1384,16 +1381,15 @@ describe('mutation results', () => { assert.equal(mResult.data.createTodo.id, '99'); assert.equal(mResult.data.createTodo.text, 'This one was created with a mutation.'); - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const id = 'TodoList5'; + const fragment = gql`fragment todoList on TodoList { todos { id text completed __typename } }`; + + const data: any = proxy.readFragment({ id, fragment }); - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id, fragment, + }); }, }), ) diff --git a/test/optimistic.ts b/test/optimistic.ts index 04dd5f010dc..3efaf7c103b 100644 --- a/test/optimistic.ts +++ b/test/optimistic.ts @@ -354,16 +354,16 @@ describe('optimistic mutation results', () => { describe('with `update`', () => { const update = (proxy: any, mResult: any) => { - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); - - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const data: any = proxy.readFragment({ + id: 'TodoList5', + fragment: gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + }); + + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id: 'TodoList5', + fragment: gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + }); }; it('handles a single error for a single mutation', () => { @@ -944,16 +944,15 @@ describe('optimistic mutation results', () => { update: (proxy, mResult: any) => { assert.equal(mResult.data.createTodo.id, '99'); - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const id = 'TodoList5'; + const fragment = gql`fragment todoList on TodoList { todos { id text completed __typename } }`; + + const data: any = proxy.readFragment({ id, fragment }); - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id, fragment, + }); }, }); @@ -997,16 +996,16 @@ describe('optimistic mutation results', () => { }) .then(() => { const update = (proxy: any, mResult: any) => { - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); - - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const data: any = proxy.readFragment({ + id: 'TodoList5', + fragment: gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + }); + + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id: 'TodoList5', + fragment: gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + }); }; const promise = client.mutate({ mutation, @@ -1074,16 +1073,16 @@ describe('optimistic mutation results', () => { }) .then(() => { const update = (proxy: any, mResult: any) => { - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); - - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const data: any = proxy.readFragment({ + id: 'TodoList5', + fragment: gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + }); + + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id: 'TodoList5', + fragment: gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + }); }; const promise = client.mutate({ mutation, @@ -1155,16 +1154,16 @@ describe('optimistic mutation results', () => { }; const update = (proxy: any, mResult: any) => { - const data: any = proxy.readFragment( - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); - - proxy.writeFragment( - { ...data, todos: [mResult.data.createTodo, ...data.todos] }, - 'TodoList5', - gql`fragment todoList on TodoList { todos { id text completed __typename } }`, - ); + const data: any = proxy.readFragment({ + id: 'TodoList5', + fragment: gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + }); + + proxy.writeFragment({ + data: { ...data, todos: [mResult.data.createTodo, ...data.todos] }, + id: 'TodoList5', + fragment: gql`fragment todoList on TodoList { todos { id text completed __typename } }`, + }); }; client = new ApolloClient({ diff --git a/test/proxy.ts b/test/proxy.ts index 9ae22308f16..af3a980b4b6 100644 --- a/test/proxy.ts +++ b/test/proxy.ts @@ -36,9 +36,9 @@ describe('ReduxDataProxy', () => { }, }); - assert.deepEqual(proxy.readQuery(gql`{ a }`), { a: 1 }); - assert.deepEqual(proxy.readQuery(gql`{ b c }`), { b: 2, c: 3 }); - assert.deepEqual(proxy.readQuery(gql`{ a b c }`), { a: 1, b: 2, c: 3 }); + assert.deepEqual(proxy.readQuery({ query: gql`{ a }` }), { a: 1 }); + assert.deepEqual(proxy.readQuery({ query: gql`{ b c }` }), { b: 2, c: 3 }); + assert.deepEqual(proxy.readQuery({ query: gql`{ a b c }` }), { a: 1, b: 2, c: 3 }); }); it('will read some deeply nested data from the store', () => { @@ -77,15 +77,15 @@ describe('ReduxDataProxy', () => { }); assert.deepEqual( - proxy.readQuery(gql`{ a d { e } }`), + proxy.readQuery({ query: gql`{ a d { e } }` }), { a: 1, d: { e: 4 } }, ); assert.deepEqual( - proxy.readQuery(gql`{ a d { e h { i } } }`), + proxy.readQuery({ query: gql`{ a d { e h { i } } }` }), { a: 1, d: { e: 4, h: { i: 7 } } }, ); assert.deepEqual( - proxy.readQuery(gql`{ a b c d { e f g h { i j k } } }`), + proxy.readQuery({ query: gql`{ a b c d { e f g h { i j k } } }` }), { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, ); }); @@ -104,16 +104,16 @@ describe('ReduxDataProxy', () => { }, }); - assert.deepEqual(proxy.readQuery( - gql`query ($literal: Boolean, $value: Int) { + assert.deepEqual(proxy.readQuery({ + query: gql`query ($literal: Boolean, $value: Int) { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) }`, - { + variables: { literal: false, value: 42, }, - ), { a: 1, b: 2 }); + }), { a: 1, b: 2 }); }); }); @@ -122,10 +122,10 @@ describe('ReduxDataProxy', () => { const proxy = createDataProxy(); assert.throws(() => { - proxy.readFragment('x', gql`query { a b c }`); + proxy.readFragment({ id: 'x', fragment: gql`query { a b c }` }); }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); assert.throws(() => { - proxy.readFragment('x', gql`schema { query: Query }`); + proxy.readFragment({ id: 'x', fragment: gql`schema { query: Query }` }); }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -133,10 +133,10 @@ describe('ReduxDataProxy', () => { const proxy = createDataProxy(); assert.throws(() => { - proxy.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); + proxy.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b }` }); }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); assert.throws(() => { - proxy.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + proxy.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }` }); }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -179,35 +179,35 @@ describe('ReduxDataProxy', () => { }); assert.deepEqual( - proxy.readFragment('foo', gql`fragment fragmentFoo on Foo { e h { i } }`), + proxy.readFragment({ id: 'foo', fragment: gql`fragment fragmentFoo on Foo { e h { i } }` }), { e: 4, h: { i: 7 } }, ); assert.deepEqual( - proxy.readFragment('foo', gql`fragment fragmentFoo on Foo { e f g h { i j k } }`), + proxy.readFragment({ id: 'foo', fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } }` }), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - proxy.readFragment('bar', gql`fragment fragmentBar on Bar { i }`), + proxy.readFragment({ id: 'bar', fragment: gql`fragment fragmentBar on Bar { i }` }), { i: 7 }, ); assert.deepEqual( - proxy.readFragment('bar', gql`fragment fragmentBar on Bar { i j k }`), + proxy.readFragment({ id: 'bar', fragment: gql`fragment fragmentBar on Bar { i j k }` }), { i: 7, j: 8, k: 9 }, ); assert.deepEqual( - proxy.readFragment( - 'foo', - gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, - 'fragmentFoo', - ), + proxy.readFragment({ + id: 'foo', + fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + fragmentName: 'fragmentFoo', + }), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - proxy.readFragment( - 'bar', - gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, - 'fragmentBar', - ), + proxy.readFragment({ + id: 'bar', + fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + fragmentName: 'fragmentBar', + }), { i: 7, j: 8, k: 9 }, ); }); @@ -227,20 +227,19 @@ describe('ReduxDataProxy', () => { }, }); - assert.deepEqual(proxy.readFragment( - 'foo', - gql` + assert.deepEqual(proxy.readFragment({ + id: 'foo', + fragment: gql` fragment foo on Foo { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) } `, - undefined, - { + variables: { literal: false, value: 42, }, - ), { a: 1, b: 2 }); + }), { a: 1, b: 2 }); }); it('will return null when an id that can’t be found is provided', () => { @@ -264,9 +263,9 @@ describe('ReduxDataProxy', () => { }, }); - assert.equal(client1.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); - assert.equal(client2.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); - assert.deepEqual(client3.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), { a: 1, b: 2, c: 3 }); + assert.equal(client1.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), null); + assert.equal(client2.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), null); + assert.deepEqual(client3.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), { a: 1, b: 2, c: 3 }); }); }); @@ -274,7 +273,7 @@ describe('ReduxDataProxy', () => { it('will write some data to the store', () => { const proxy = createDataProxy(); - proxy.writeQuery({ a: 1 }, gql`{ a }`); + proxy.writeQuery({ data: { a: 1 }, query: gql`{ a }` }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'ROOT_QUERY': { @@ -282,7 +281,7 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeQuery({ b: 2, c: 3 }, gql`{ b c }`); + proxy.writeQuery({ data: { b: 2, c: 3 }, query: gql`{ b c }` }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'ROOT_QUERY': { @@ -292,7 +291,7 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeQuery({ a: 4, b: 5, c: 6 }, gql`{ a b c }`); + proxy.writeQuery({ data: { a: 4, b: 5, c: 6 }, query: gql`{ a b c }` }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'ROOT_QUERY': { @@ -306,10 +305,10 @@ describe('ReduxDataProxy', () => { it('will write some deeply nested data to the store', () => { const proxy = createDataProxy(); - proxy.writeQuery( - { a: 1, d: { e: 4 } }, - gql`{ a d { e } }`, - ); + proxy.writeQuery({ + data: { a: 1, d: { e: 4 } }, + query: gql`{ a d { e } }`, + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'ROOT_QUERY': { @@ -325,10 +324,10 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeQuery( - { a: 1, d: { h: { i: 7 } } }, - gql`{ a d { h { i } } }`, - ); + proxy.writeQuery({ + data: { a: 1, d: { h: { i: 7 } } }, + query: gql`{ a d { h { i } } }`, + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'ROOT_QUERY': { @@ -352,10 +351,10 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeQuery( - { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, - gql`{ a b c d { e f g h { i j k } } }`, - ); + proxy.writeQuery({ + data: { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, + query: gql`{ a b c d { e f g h { i j k } } }`, + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'ROOT_QUERY': { @@ -389,22 +388,22 @@ describe('ReduxDataProxy', () => { it('will write some data to the store with variables', () => { const proxy = createDataProxy(); - proxy.writeQuery( - { + proxy.writeQuery({ + data: { a: 1, b: 2, }, - gql` + query: gql` query ($literal: Boolean, $value: Int) { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) } `, - { + variables: { literal: false, value: 42, }, - ); + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'ROOT_QUERY': { @@ -420,10 +419,10 @@ describe('ReduxDataProxy', () => { const proxy = createDataProxy(); assert.throws(() => { - proxy.writeFragment({}, 'x', gql`query { a b c }`); + proxy.writeFragment({ data: {}, id: 'x', fragment: gql`query { a b c }` }); }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); assert.throws(() => { - proxy.writeFragment({}, 'x', gql`schema { query: Query }`); + proxy.writeFragment({ data: {}, id: 'x', fragment: gql`schema { query: Query }` }); }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -431,10 +430,10 @@ describe('ReduxDataProxy', () => { const proxy = createDataProxy(); assert.throws(() => { - proxy.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b }`); + proxy.writeFragment({ data: {}, id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b }` }); }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); assert.throws(() => { - proxy.writeFragment({}, 'x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + proxy.writeFragment({ data: {}, id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }` }); }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -443,11 +442,11 @@ describe('ReduxDataProxy', () => { dataIdFromObject: (o: any) => o.id, }); - proxy.writeFragment( - { e: 4, h: { id: 'bar', i: 7 } }, - 'foo', - gql`fragment fragmentFoo on Foo { e h { i } }`, - ); + proxy.writeFragment({ + data: { e: 4, h: { id: 'bar', i: 7 } }, + id: 'foo', + fragment: gql`fragment fragmentFoo on Foo { e h { i } }`, + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'foo': { @@ -463,11 +462,11 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeFragment( - { f: 5, g: 6, h: { id: 'bar', j: 8, k: 9 } }, - 'foo', - gql`fragment fragmentFoo on Foo { f g h { j k } }`, - ); + proxy.writeFragment({ + data: { f: 5, g: 6, h: { id: 'bar', j: 8, k: 9 } }, + id: 'foo', + fragment: gql`fragment fragmentFoo on Foo { f g h { j k } }`, + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'foo': { @@ -487,11 +486,11 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeFragment( - { i: 10 }, - 'bar', - gql`fragment fragmentBar on Bar { i }`, - ); + proxy.writeFragment({ + data: { i: 10 }, + id: 'bar', + fragment: gql`fragment fragmentBar on Bar { i }`, + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'foo': { @@ -511,11 +510,11 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeFragment( - { j: 11, k: 12 }, - 'bar', - gql`fragment fragmentBar on Bar { j k }`, - ); + proxy.writeFragment({ + data: { j: 11, k: 12 }, + id: 'bar', + fragment: gql`fragment fragmentBar on Bar { j k }`, + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'foo': { @@ -535,12 +534,12 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeFragment( - { e: 4, f: 5, g: 6, h: { id: 'bar', i: 7, j: 8, k: 9 } }, - 'foo', - gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, - 'fooFragment', - ); + proxy.writeFragment({ + data: { e: 4, f: 5, g: 6, h: { id: 'bar', i: 7, j: 8, k: 9 } }, + id: 'foo', + fragment: gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, + fragmentName: 'fooFragment', + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'foo': { @@ -560,12 +559,12 @@ describe('ReduxDataProxy', () => { }, }); - proxy.writeFragment( - { i: 10, j: 11, k: 12 }, - 'bar', - gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, - 'barFragment', - ); + proxy.writeFragment({ + data: { i: 10, j: 11, k: 12 }, + id: 'bar', + fragment: gql`fragment fooFragment on Foo { e f g h { i j k } } fragment barFragment on Bar { i j k }`, + fragmentName: 'barFragment', + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'foo': { @@ -589,24 +588,23 @@ describe('ReduxDataProxy', () => { it('will write some data to the store with variables', () => { const proxy = createDataProxy(); - proxy.writeFragment( - { + proxy.writeFragment({ + data: { a: 1, b: 2, }, - 'foo', - gql` + id: 'foo', + fragment: gql` fragment foo on Foo { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) } `, - undefined, - { + variables: { literal: false, value: 42, }, - ); + }); assert.deepEqual((proxy as any).store.getState().apollo.data, { 'foo': { @@ -625,7 +623,7 @@ describe('TransactionDataProxy', () => { proxy.finish(); assert.throws(() => { - proxy.readQuery(); + proxy.readQuery({}); }, 'Cannot call transaction methods after the transaction has finished.'); }); @@ -638,9 +636,9 @@ describe('TransactionDataProxy', () => { }, }); - assert.deepEqual(proxy.readQuery(gql`{ a }`), { a: 1 }); - assert.deepEqual(proxy.readQuery(gql`{ b c }`), { b: 2, c: 3 }); - assert.deepEqual(proxy.readQuery(gql`{ a b c }`), { a: 1, b: 2, c: 3 }); + assert.deepEqual(proxy.readQuery({ query: gql`{ a }` }), { a: 1 }); + assert.deepEqual(proxy.readQuery({ query: gql`{ b c }` }), { b: 2, c: 3 }); + assert.deepEqual(proxy.readQuery({ query: gql`{ a b c }` }), { a: 1, b: 2, c: 3 }); }); it('will read some deeply nested data from the store', () => { @@ -673,15 +671,15 @@ describe('TransactionDataProxy', () => { }); assert.deepEqual( - proxy.readQuery(gql`{ a d { e } }`), + proxy.readQuery({ query: gql`{ a d { e } }` }), { a: 1, d: { e: 4 } }, ); assert.deepEqual( - proxy.readQuery(gql`{ a d { e h { i } } }`), + proxy.readQuery({ query: gql`{ a d { e h { i } } }` }), { a: 1, d: { e: 4, h: { i: 7 } } }, ); assert.deepEqual( - proxy.readQuery(gql`{ a b c d { e f g h { i j k } } }`), + proxy.readQuery({ query: gql`{ a b c d { e f g h { i j k } } }` }), { a: 1, b: 2, c: 3, d: { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } } }, ); }); @@ -694,16 +692,16 @@ describe('TransactionDataProxy', () => { }, }); - assert.deepEqual(proxy.readQuery( - gql`query ($literal: Boolean, $value: Int) { + assert.deepEqual(proxy.readQuery({ + query: gql`query ($literal: Boolean, $value: Int) { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) }`, - { + variables: { literal: false, value: 42, }, - ), { a: 1, b: 2 }); + }), { a: 1, b: 2 }); }); }); @@ -713,7 +711,7 @@ describe('TransactionDataProxy', () => { proxy.finish(); assert.throws(() => { - proxy.readFragment(); + proxy.readFragment({}); }, 'Cannot call transaction methods after the transaction has finished.'); }); @@ -721,10 +719,10 @@ describe('TransactionDataProxy', () => { const proxy = new TransactionDataProxy({}); assert.throws(() => { - proxy.readFragment('x', gql`query { a b c }`); + proxy.readFragment({ id: 'x', fragment: gql`query { a b c }` }); }, 'Found a query operation. No operations are allowed when using a fragment as a query. Only fragments are allowed.'); assert.throws(() => { - proxy.readFragment('x', gql`schema { query: Query }`); + proxy.readFragment({ id: 'x', fragment: gql`schema { query: Query }` }); }, 'Found 0 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -732,10 +730,10 @@ describe('TransactionDataProxy', () => { const proxy = new TransactionDataProxy({}); assert.throws(() => { - proxy.readFragment('x', gql`fragment a on A { a } fragment b on B { b }`); + proxy.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b }` }); }, 'Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); assert.throws(() => { - proxy.readFragment('x', gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }`); + proxy.readFragment({ id: 'x', fragment: gql`fragment a on A { a } fragment b on B { b } fragment c on C { c }` }); }, 'Found 3 fragments. `fragmentName` must be provided when there is not exactly 1 fragment.'); }); @@ -772,35 +770,35 @@ describe('TransactionDataProxy', () => { }); assert.deepEqual( - proxy.readFragment('foo', gql`fragment fragmentFoo on Foo { e h { i } }`), + proxy.readFragment({ id: 'foo', fragment: gql`fragment fragmentFoo on Foo { e h { i } }` }), { e: 4, h: { i: 7 } }, ); assert.deepEqual( - proxy.readFragment('foo', gql`fragment fragmentFoo on Foo { e f g h { i j k } }`), + proxy.readFragment({ id: 'foo', fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } }` }), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - proxy.readFragment('bar', gql`fragment fragmentBar on Bar { i }`), + proxy.readFragment({ id: 'bar', fragment: gql`fragment fragmentBar on Bar { i }` }), { i: 7 }, ); assert.deepEqual( - proxy.readFragment('bar', gql`fragment fragmentBar on Bar { i j k }`), + proxy.readFragment({ id: 'bar', fragment: gql`fragment fragmentBar on Bar { i j k }` }), { i: 7, j: 8, k: 9 }, ); assert.deepEqual( - proxy.readFragment( - 'foo', - gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, - 'fragmentFoo', - ), + proxy.readFragment({ + id: 'foo', + fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + fragmentName: 'fragmentFoo', + }), { e: 4, f: 5, g: 6, h: { i: 7, j: 8, k: 9 } }, ); assert.deepEqual( - proxy.readFragment( - 'bar', - gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, - 'fragmentBar', - ), + proxy.readFragment({ + id: 'bar', + fragment: gql`fragment fragmentFoo on Foo { e f g h { i j k } } fragment fragmentBar on Bar { i j k }`, + fragmentName: 'fragmentBar', + }), { i: 7, j: 8, k: 9 }, ); }); @@ -814,20 +812,19 @@ describe('TransactionDataProxy', () => { }, }); - assert.deepEqual(proxy.readFragment( - 'foo', - gql` + assert.deepEqual(proxy.readFragment({ + id: 'foo', + fragment: gql` fragment foo on Foo { a: field(literal: true, value: 42) b: field(literal: $literal, value: $value) } `, - undefined, - { + variables: { literal: false, value: 42, }, - ), { a: 1, b: 2 }); + }), { a: 1, b: 2 }); }); it('will return null when an id that can’t be found is provided', () => { @@ -839,9 +836,9 @@ describe('TransactionDataProxy', () => { 'foo': { __typename: 'Type1', a: 1, b: 2, c: 3 }, }); - assert.equal(client1.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); - assert.equal(client2.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), null); - assert.deepEqual(client3.readFragment('foo', gql`fragment fooFragment on Foo { a b c }`), { a: 1, b: 2, c: 3 }); + assert.equal(client1.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), null); + assert.equal(client2.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), null); + assert.deepEqual(client3.readFragment({ id: 'foo', fragment: gql`fragment fooFragment on Foo { a b c }` }), { a: 1, b: 2, c: 3 }); }); }); @@ -851,23 +848,23 @@ describe('TransactionDataProxy', () => { proxy.finish(); assert.throws(() => { - proxy.writeQuery(); + proxy.writeQuery({}); }, 'Cannot call transaction methods after the transaction has finished.'); }); it('will create writes that get returned when finished', () => { const proxy = new TransactionDataProxy({}); - proxy.writeQuery( - { a: 1, b: 2, c: 3 }, - gql`{ a b c }`, - ); + proxy.writeQuery({ + data: { a: 1, b: 2, c: 3 }, + query: gql`{ a b c }`, + }); - proxy.writeQuery( - { foo: { d: 4, e: 5, bar: { f: 6, g: 7 } } }, - gql`{ foo(id: $id) { d e bar { f g } } }`, - { id: 7 }, - ); + proxy.writeQuery({ + data: { foo: { d: 4, e: 5, bar: { f: 6, g: 7 } } }, + query: gql`{ foo(id: $id) { d e bar { f g } } }`, + variables: { id: 7 }, + }); const writes = proxy.finish(); @@ -894,29 +891,29 @@ describe('TransactionDataProxy', () => { proxy.finish(); assert.throws(() => { - proxy.writeFragment(); + proxy.writeFragment({}); }, 'Cannot call transaction methods after the transaction has finished.'); }); it('will create writes that get returned when finished', () => { const proxy = new TransactionDataProxy({}); - proxy.writeFragment( - { a: 1, b: 2, c: 3 }, - 'foo', - gql`fragment fragment1 on Foo { a b c }`, - ); + proxy.writeFragment({ + data: { a: 1, b: 2, c: 3 }, + id: 'foo', + fragment: gql`fragment fragment1 on Foo { a b c }`, + }); - proxy.writeFragment( - { foo: { d: 4, e: 5, bar: { f: 6, g: 7 } } }, - 'bar', - gql` + proxy.writeFragment({ + data: { foo: { d: 4, e: 5, bar: { f: 6, g: 7 } } }, + id: 'bar', + fragment: gql` fragment fragment1 on Foo { a b c } fragment fragment2 on Bar { foo(id: $id) { d e bar { f g } } } `, - 'fragment2', - { id: 7 }, - ); + fragmentName: 'fragment2', + variables: { id: 7 }, + }); const writes = proxy.finish(); From 97686e3df45dd5d6462bfad12307ce5c6d78d810 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Tue, 28 Feb 2017 14:20:50 -0500 Subject: [PATCH 29/29] apply writes locally in transactions --- src/data/proxy.ts | 39 ++++++++++-- src/data/store.ts | 5 +- test/ApolloClient.ts | 131 ++++++++++++++++++++++++++++++++++++++ test/proxy.ts | 147 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 315 insertions(+), 7 deletions(-) diff --git a/src/data/proxy.ts b/src/data/proxy.ts index 2ade091eb39..2e6fe750d5a 100644 --- a/src/data/proxy.ts +++ b/src/data/proxy.ts @@ -1,10 +1,12 @@ import { DocumentNode } from 'graphql'; import { ApolloStore, Store } from '../store'; import { DataWrite } from '../actions'; +import { IdGetter } from '../core/types'; import { NormalizedCache } from '../data/storeUtils'; import { getFragmentQueryDocument } from '../queries/getFromAST'; import { getDataWithOptimisticResults } from '../optimistic-data/store'; import { readQueryFromStore } from './readFromStore'; +import { writeResultToStore } from './writeToStore'; /** * A proxy to the normalized data living in our store. This interface allows a @@ -191,14 +193,22 @@ export class ReduxDataProxy implements DataProxy { * constructed it has started. Once a transaction has finished none of its * methods are usable. * - * The transaction will read from a single normalized cache instance. + * The transaction will read from a single local normalized cache instance and + * it will write to that cache instance as well. */ export class TransactionDataProxy implements DataProxy { /** - * The normalized cache that this transaction reads from. + * The normalized cache that this transaction reads from. This object will be + * a shallow clone of the `data` object passed into the constructor. */ private data: NormalizedCache; + /** + * Gets a data id from an object. This is used when writing to our local + * cache clone. + */ + private dataIdFromObject: IdGetter; + /** * An array of actions that we build up during the life of the transaction. * Once a transaction finishes the actions array will be returned. @@ -210,8 +220,9 @@ export class TransactionDataProxy implements DataProxy { */ private isFinished: boolean; - constructor(data: NormalizedCache) { - this.data = data; + constructor(data: NormalizedCache, dataIdFromObject: IdGetter = () => null) { + this.data = { ...data }; + this.dataIdFromObject = dataIdFromObject; this.writes = []; this.isFinished = false; } @@ -301,7 +312,7 @@ export class TransactionDataProxy implements DataProxy { variables?: Object, }): void { this.assertNotFinished(); - this.writes.push({ + this.applyWrite({ rootId: 'ROOT_QUERY', result: data, document: query, @@ -328,7 +339,7 @@ export class TransactionDataProxy implements DataProxy { variables?: Object, }): void { this.assertNotFinished(); - this.writes.push({ + this.applyWrite({ rootId: id, result: data, document: getFragmentQueryDocument(fragment, fragmentName), @@ -345,4 +356,20 @@ export class TransactionDataProxy implements DataProxy { throw new Error('Cannot call transaction methods after the transaction has finished.'); } } + + /** + * Takes a write and applies it to our local cache, and adds it to a writes + * array which will be returned later on when the transaction finishes. + */ + private applyWrite(write: DataWrite) { + writeResultToStore({ + result: write.result, + dataId: write.rootId, + document: write.document, + variables: write.variables, + store: this.data, + dataIdFromObject: this.dataIdFromObject, + }); + this.writes.push(write); + } } diff --git a/src/data/store.ts b/src/data/store.ts index b7876010939..230fab5a305 100644 --- a/src/data/store.ts +++ b/src/data/store.ts @@ -192,7 +192,10 @@ export function data( // write action. if (constAction.update) { const update = constAction.update; - const proxy = new TransactionDataProxy(newState); + const proxy = new TransactionDataProxy( + newState, + config.dataIdFromObject, + ); tryFunctionOrLogError(() => update(proxy, constAction.result)); const writes = proxy.finish(); newState = data( diff --git a/test/ApolloClient.ts b/test/ApolloClient.ts index 0a5f4cae4ee..d2959f42fe1 100644 --- a/test/ApolloClient.ts +++ b/test/ApolloClient.ts @@ -598,4 +598,135 @@ describe('ApolloClient', () => { }); }); }); + + describe('write then read', () => { + it('will write data locally which will then be read back', () => { + const client = new ApolloClient({ + initialState: { + apollo: { + data: { + 'foo': { + __typename: 'Type1', + a: 1, + b: 2, + c: 3, + bar: { + type: 'id', + id: '$foo.bar', + generated: true, + }, + }, + '$foo.bar': { + __typename: 'Type2', + d: 4, + e: 5, + f: 6, + }, + }, + }, + }, + }); + + assert.deepEqual( + client.readFragment({ id: 'foo', fragment: gql`fragment x on Foo { a b c bar { d e f } }` }), + { a: 1, b: 2, c: 3, bar: { d: 4, e: 5, f: 6 } }, + ); + + client.writeFragment({ + id: 'foo', + fragment: gql`fragment x on Foo { a }`, + data: { a: 7 }, + }); + + assert.deepEqual( + client.readFragment({ id: 'foo', fragment: gql`fragment x on Foo { a b c bar { d e f } }` }), + { a: 7, b: 2, c: 3, bar: { d: 4, e: 5, f: 6 } }, + ); + + client.writeFragment({ + id: 'foo', + fragment: gql`fragment x on Foo { bar { d } }`, + data: { bar: { d: 8 } }, + }); + + assert.deepEqual( + client.readFragment({ id: 'foo', fragment: gql`fragment x on Foo { a b c bar { d e f } }` }), + { a: 7, b: 2, c: 3, bar: { d: 8, e: 5, f: 6 } }, + ); + + client.writeFragment({ + id: '$foo.bar', + fragment: gql`fragment y on Bar { e }`, + data: { __typename: 'Type2', e: 9 }, + }); + + assert.deepEqual( + client.readFragment({ id: 'foo', fragment: gql`fragment x on Foo { a b c bar { d e f } }` }), + { a: 7, b: 2, c: 3, bar: { d: 8, e: 9, f: 6 } }, + ); + + assert.deepEqual(client.store.getState().apollo.data, { + 'foo': { + __typename: 'Type1', + a: 7, + b: 2, + c: 3, + bar: { + type: 'id', + id: '$foo.bar', + generated: true, + }, + }, + '$foo.bar': { + __typename: 'Type2', + d: 8, + e: 9, + f: 6, + }, + }); + }); + + it('will write data to a specific id', () => { + const client = new ApolloClient({ + initialState: { apollo: { data: {} } }, + dataIdFromObject: (o: any) => o.id, + }); + + client.writeQuery({ + query: gql`{ a b foo { c d bar { id e f } } }`, + data: { a: 1, b: 2, foo: { c: 3, d: 4, bar: { id: 'foobar', e: 5, f: 6 } } }, + }); + + assert.deepEqual( + client.readQuery({ query: gql`{ a b foo { c d bar { id e f } } }` }), + { a: 1, b: 2, foo: { c: 3, d: 4, bar: { id: 'foobar', e: 5, f: 6 } } }, + ); + + assert.deepEqual(client.store.getState().apollo.data, { + 'ROOT_QUERY': { + a: 1, + b: 2, + foo: { + type: 'id', + id: '$ROOT_QUERY.foo', + generated: true, + }, + }, + '$ROOT_QUERY.foo': { + c: 3, + d: 4, + bar: { + type: 'id', + id: 'foobar', + generated: false, + }, + }, + 'foobar': { + id: 'foobar', + e: 5, + f: 6, + }, + }); + }); + }); }); diff --git a/test/proxy.ts b/test/proxy.ts index af3a980b4b6..a887c786662 100644 --- a/test/proxy.ts +++ b/test/proxy.ts @@ -937,4 +937,151 @@ describe('TransactionDataProxy', () => { `)); }); }); + + describe('write then read', () => { + it('will write data locally which will then be read back', () => { + const data: any = { + 'foo': { + __typename: 'Type1', + a: 1, + b: 2, + c: 3, + bar: { + type: 'id', + id: '$foo.bar', + generated: true, + }, + }, + '$foo.bar': { + __typename: 'Type2', + d: 4, + e: 5, + f: 6, + }, + }; + + const proxy = new TransactionDataProxy(data); + + assert.deepEqual( + proxy.readFragment({ id: 'foo', fragment: gql`fragment x on Foo { a b c bar { d e f } }` }), + { a: 1, b: 2, c: 3, bar: { d: 4, e: 5, f: 6 } }, + ); + + proxy.writeFragment({ + id: 'foo', + fragment: gql`fragment x on Foo { a }`, + data: { a: 7 }, + }); + + assert.deepEqual( + proxy.readFragment({ id: 'foo', fragment: gql`fragment x on Foo { a b c bar { d e f } }` }), + { a: 7, b: 2, c: 3, bar: { d: 4, e: 5, f: 6 } }, + ); + + proxy.writeFragment({ + id: 'foo', + fragment: gql`fragment x on Foo { bar { d } }`, + data: { bar: { d: 8 } }, + }); + + assert.deepEqual( + proxy.readFragment({ id: 'foo', fragment: gql`fragment x on Foo { a b c bar { d e f } }` }), + { a: 7, b: 2, c: 3, bar: { d: 8, e: 5, f: 6 } }, + ); + + proxy.writeFragment({ + id: '$foo.bar', + fragment: gql`fragment y on Bar { e }`, + data: { __typename: 'Type2', e: 9 }, + }); + + assert.deepEqual( + proxy.readFragment({ id: 'foo', fragment: gql`fragment x on Foo { a b c bar { d e f } }` }), + { a: 7, b: 2, c: 3, bar: { d: 8, e: 9, f: 6 } }, + ); + + assert.deepEqual((proxy as any).data, { + 'foo': { + __typename: 'Type1', + a: 7, + b: 2, + c: 3, + bar: { + type: 'id', + id: '$foo.bar', + generated: true, + }, + }, + '$foo.bar': { + __typename: 'Type2', + d: 8, + e: 9, + f: 6, + }, + }); + + assert.deepEqual(data, { + 'foo': { + __typename: 'Type1', + a: 1, + b: 2, + c: 3, + bar: { + type: 'id', + id: '$foo.bar', + generated: true, + }, + }, + '$foo.bar': { + __typename: 'Type2', + d: 4, + e: 5, + f: 6, + }, + }); + }); + + it('will write data to a specific id', () => { + const data = {}; + const proxy = new TransactionDataProxy(data, (o: any) => o.id); + + proxy.writeQuery({ + query: gql`{ a b foo { c d bar { id e f } } }`, + data: { a: 1, b: 2, foo: { c: 3, d: 4, bar: { id: 'foobar', e: 5, f: 6 } } }, + }); + + assert.deepEqual( + proxy.readQuery({ query: gql`{ a b foo { c d bar { id e f } } }` }), + { a: 1, b: 2, foo: { c: 3, d: 4, bar: { id: 'foobar', e: 5, f: 6 } } }, + ); + + assert.deepEqual((proxy as any).data, { + 'ROOT_QUERY': { + a: 1, + b: 2, + foo: { + type: 'id', + id: '$ROOT_QUERY.foo', + generated: true, + }, + }, + '$ROOT_QUERY.foo': { + c: 3, + d: 4, + bar: { + type: 'id', + id: 'foobar', + generated: false, + }, + }, + 'foobar': { + id: 'foobar', + e: 5, + f: 6, + }, + }); + + assert.deepEqual(data, {}); + }); + }); });