diff --git a/package.json b/package.json index c876f440db9..e4672f48379 100644 --- a/package.json +++ b/package.json @@ -29,17 +29,17 @@ { "name": "apollo-cache-inmemory", "path": "./packages/apollo-cache-inmemory/lib/bundle.cjs.min.js", - "maxSize": "5.05 kB" + "maxSize": "4.3 kB" }, { "name": "apollo-client", "path": "./packages/apollo-client/lib/bundle.cjs.min.js", - "maxSize": "9.95 kB" + "maxSize": "9.9 kB" }, { "name": "apollo-utilities", "path": "./packages/apollo-utilities/lib/bundle.cjs.min.js", - "maxSize": "4.2 kB" + "maxSize": "4.25 kB" } ], "lint-staged": { diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap index d972da111c5..7e33ebcb999 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap +++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/cache.ts.snap @@ -8,10 +8,7 @@ Object { "foo": Object { "e": 4, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -29,10 +26,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -50,10 +44,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -71,10 +62,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -92,10 +80,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -113,10 +98,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -130,10 +112,7 @@ Object { "foo": Object { "e": 4, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -151,10 +130,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -172,10 +148,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -193,10 +166,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -214,10 +184,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -235,10 +202,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -252,10 +216,7 @@ Object { "foo": Object { "e": 4, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -273,10 +234,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -294,10 +252,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -315,10 +270,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -336,10 +288,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -357,10 +306,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap index 4682281f7f0..35dc1353edb 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap +++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap @@ -8,10 +8,7 @@ Object { "foo": Object { "e": 4, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -29,10 +26,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -50,10 +44,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -71,10 +62,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -92,10 +80,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -113,10 +98,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -130,10 +112,7 @@ Object { "foo": Object { "e": 4, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -151,10 +130,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -172,10 +148,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -193,10 +166,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -214,10 +184,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -235,10 +202,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -252,10 +216,7 @@ Object { "foo": Object { "e": 4, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -273,10 +234,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -294,10 +252,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -315,10 +270,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": undefined, + "__ref": "bar", }, }, } @@ -336,10 +288,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -357,10 +306,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -369,6 +315,5 @@ Object { exports[`MapCache writing to the store throws when trying to write an object without id that was previously queried with id 1`] = ` "Error writing result to store for query: {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}} -Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is: -{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}" +Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object." `; diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap index b42d4237c6d..404860212e7 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap +++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap @@ -1,10 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[ - `writing to the store throws when trying to write an object without id that was previously queried with id 1` -] = ` +exports[`writing to the store throws when trying to write an object without id that was previously queried with id 1`] = ` "Error writing result to store for query: {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}} -Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is: -{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}" +Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object." `; diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap index b42d4237c6d..404860212e7 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap +++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap @@ -1,10 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[ - `writing to the store throws when trying to write an object without id that was previously queried with id 1` -] = ` +exports[`writing to the store throws when trying to write an object without id that was previously queried with id 1`] = ` "Error writing result to store for query: {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}} -Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is: -{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}" +Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object." `; diff --git a/packages/apollo-cache-inmemory/src/__tests__/cache.ts b/packages/apollo-cache-inmemory/src/__tests__/cache.ts index d525b22805e..d12b17e4a0e 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/cache.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/cache.ts @@ -1,8 +1,8 @@ import gql, { disableFragmentWarnings } from 'graphql-tag'; -import { stripSymbols } from 'apollo-utilities'; -import { cloneDeep } from 'lodash'; +import { stripSymbols, cloneDeep } from 'apollo-utilities'; import { InMemoryCache, InMemoryCacheConfig } from '..'; +import { makeReference } from '../helpers'; disableFragmentWarnings(); @@ -127,21 +127,13 @@ describe('Cache', () => { a: 1, b: 2, c: 3, - d: { - type: 'id', - id: 'foo', - generated: false, - }, + d: makeReference('foo'), }, foo: { e: 4, f: 5, g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, + h: makeReference('bar'), }, bar: { i: 7, @@ -391,22 +383,14 @@ describe('Cache', () => { a: 1, b: 2, c: 3, - d: { - type: 'id', - id: 'foo', - generated: false, - }, + d: makeReference('foo'), }, foo: { __typename: 'Foo', e: 4, f: 5, g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, + h: makeReference('bar'), }, bar: { __typename: 'Bar', @@ -700,14 +684,9 @@ describe('Cache', () => { ROOT_QUERY: { a: 1, d: { - type: 'id', - id: '$ROOT_QUERY.d', - generated: true, + e: 4, }, }, - '$ROOT_QUERY.d': { - e: 4, - }, }); proxy.writeQuery({ @@ -728,22 +707,12 @@ describe('Cache', () => { 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, + e: 4, + h: { + i: 7, + }, }, }, - '$ROOT_QUERY.d.h': { - i: 7, - }, }); proxy.writeQuery({ @@ -778,26 +747,16 @@ describe('Cache', () => { 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, + e: 4, + f: 5, + g: 6, + h: { + i: 7, + j: 8, + k: 9, + }, }, }, - '$ROOT_QUERY.d.h': { - i: 7, - j: 8, - k: 9, - }, }); }, ); diff --git a/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts b/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts index 8dd3b3028dd..ecf9ee38e39 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts @@ -1,11 +1,11 @@ import gql, { disableFragmentWarnings } from 'graphql-tag'; -import { toIdValue } from 'apollo-utilities'; import { defaultNormalizedCacheFactory } from '../objectCache'; import { StoreReader } from '../readFromStore'; import { StoreWriter } from '../writeToStore'; import { defaultDataIdFromObject } from '../inMemoryCache'; import { NormalizedCache } from '../types'; +import { makeReference } from '../helpers'; disableFragmentWarnings(); @@ -952,18 +952,15 @@ describe('diffing queries against the store', () => { const cacheRedirects = { Query: { - person: (_: any, args: any) => - toIdValue({ id: args['id'], typename: 'Person' }), + person: (_: any, args: any) => makeReference(args['id']) }, }; - const config = { dataIdFromObject, cacheRedirects }; - const { result } = reader.diffQueryAgainstStore({ store, query: itemQuery, previousResult, - config, + config: { dataIdFromObject, cacheRedirects }, }); expect(result).toEqual(previousResult); diff --git a/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts b/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts index a5e15db8943..af07026ca58 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts @@ -1,5 +1,4 @@ import { assign, omit } from 'lodash'; -import { IdValue, JsonValue } from 'apollo-utilities'; import gql from 'graphql-tag'; import { stripSymbols } from 'apollo-utilities'; @@ -7,6 +6,7 @@ import { StoreObject } from '../'; import { StoreReader } from '../readFromStore'; import { defaultNormalizedCacheFactory } from '../objectCache'; import { withError } from './diffAgainstStore'; +import { makeReference } from '../helpers'; describe('reading from the store', () => { const reader = new StoreReader(); @@ -16,18 +16,17 @@ describe('reading from the store', () => { const store = defaultNormalizedCacheFactory({ ROOT_QUERY: { __typename: 'Query', - nestedObj: { type: 'id', id: 'abcde', generated: false }, + nestedObj: makeReference('abcde'), } as StoreObject, abcde: { id: 'abcde', innerArray: [ - { type: 'id', generated: true, id: 'abcde.innerArray.0' } as any, + { + id: 'abcdef', + someField: 3, + }, ], } as StoreObject, - 'abcde.innerArray.0': { - id: 'abcdef', - someField: 3, - } as StoreObject, }); const queryResult = reader.readQueryFromStore({ @@ -255,11 +254,7 @@ describe('reading from the store', () => { const store = defaultNormalizedCacheFactory({ ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), { - nestedObj: { - type: 'id', - id: 'abcde', - generated: false, - }, + nestedObj: makeReference('abcde'), } as StoreObject), abcde: result.nestedObj, }); @@ -316,19 +311,11 @@ describe('reading from the store', () => { assign({}, omit(result, 'nestedObj', 'deepNestedObj')), { __typename: 'Query', - nestedObj: { - type: 'id', - id: 'abcde', - generated: false, - }, + nestedObj: makeReference('abcde'), } as StoreObject, ), abcde: assign({}, result.nestedObj, { - deepNestedObj: { - type: 'id', - id: 'abcdef', - generated: false, - }, + deepNestedObj: makeReference('abcdef'), }) as StoreObject, abcdef: result.deepNestedObj as StoreObject, }); @@ -407,14 +394,7 @@ describe('reading from the store', () => { }; const store = defaultNormalizedCacheFactory({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), { - nestedArray: [ - { type: 'id', generated: true, id: 'abcd.nestedArray.0' } as IdValue, - { type: 'id', generated: true, id: 'abcd.nestedArray.1' } as IdValue, - ], - }) as StoreObject, - 'abcd.nestedArray.0': result.nestedArray[0], - 'abcd.nestedArray.1': result.nestedArray[1], + ROOT_QUERY: result, }); const queryResult = reader.readQueryFromStore({ @@ -465,13 +445,7 @@ describe('reading from the store', () => { }; const store = defaultNormalizedCacheFactory({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), { - nestedArray: [ - null, - { type: 'id', generated: true, id: 'abcd.nestedArray.1' } as IdValue, - ], - }) as StoreObject, - 'abcd.nestedArray.1': result.nestedArray[1], + ROOT_QUERY: result, }); const queryResult = reader.readQueryFromStore({ @@ -521,7 +495,7 @@ describe('reading from the store', () => { const store = defaultNormalizedCacheFactory({ ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), { - nestedArray: [null, { type: 'id', generated: false, id: 'abcde' }], + nestedArray: [null, makeReference('abcde')], }) as StoreObject, abcde: result.nestedArray[1], }); @@ -626,12 +600,7 @@ describe('reading from the store', () => { }; const store = defaultNormalizedCacheFactory({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), { - simpleArray: { - type: 'json', - json: result.simpleArray, - } as JsonValue, - }) as StoreObject, + ROOT_QUERY: result, }); const queryResult = reader.readQueryFromStore({ @@ -663,12 +632,7 @@ describe('reading from the store', () => { }; const store = defaultNormalizedCacheFactory({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), { - simpleArray: { - type: 'json', - json: result.simpleArray, - } as JsonValue, - }) as StoreObject, + ROOT_QUERY: result, }); const queryResult = reader.readQueryFromStore({ @@ -717,19 +681,11 @@ describe('reading from the store', () => { assign({}, omit(data, 'nestedObj', 'deepNestedObj')), { __typename: 'Query', - nestedObj: { - type: 'id', - id: 'abcde', - generated: false, - } as IdValue, + nestedObj: makeReference('abcde'), }, ) as StoreObject, abcde: assign({}, data.nestedObj, { - deepNestedObj: { - type: 'id', - id: 'abcdef', - generated: false, - }, + deepNestedObj: makeReference('abcdef'), }) as StoreObject, abcdef: data.deepNestedObj as StoreObject, }); @@ -786,15 +742,10 @@ describe('reading from the store', () => { ROOT_QUERY: { abc: [ { - generated: true, - id: 'ROOT_QUERY.abc.0', - type: 'id', + name: 'efgh', }, ], }, - 'ROOT_QUERY.abc.0': { - name: 'efgh', - }, }); const queryResult = reader.readQueryFromStore({ @@ -816,4 +767,98 @@ describe('reading from the store', () => { ], }); }); + + it('refuses to return raw Reference objects', () => { + const store = defaultNormalizedCacheFactory({ + ROOT_QUERY: { + author: { + __typename: 'Author', + name: 'Toni Morrison', + books: [ + { + title: 'The Bluest Eye', + publisher: makeReference('Publisher1'), + }, + { + title: 'Song of Solomon', + publisher: makeReference('Publisher2'), + }, + { + title: 'Beloved', + publisher: makeReference('Publisher2'), + }, + ], + }, + }, + Publisher1: { + __typename: 'Publisher', + id: 1, + name: 'Holt, Rinehart and Winston', + }, + Publisher2: { + __typename: 'Publisher', + id: 2, + name: 'Alfred A. Knopf, Inc.', + }, + }); + + expect(() => { + reader.readQueryFromStore({ + store, + query: gql` + { + author { + name + books + } + } + `, + }); + }).toThrow( + /Missing selection set for object of type Publisher returned for query field books/, + ); + + expect( + reader.readQueryFromStore({ + store, + query: gql` + { + author { + name + books { + title + publisher { + name + } + } + } + } + `, + }), + ).toEqual({ + author: { + name: 'Toni Morrison', + books: [ + { + title: 'The Bluest Eye', + publisher: { + name: 'Holt, Rinehart and Winston', + }, + }, + { + title: 'Song of Solomon', + publisher: { + name: 'Alfred A. Knopf, Inc.', + }, + }, + { + title: 'Beloved', + publisher: { + name: 'Alfred A. Knopf, Inc.', + }, + }, + ], + }, + }); + }); }); diff --git a/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts b/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts index 19c816c2226..0c7bc4eb9b4 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts @@ -1,4 +1,4 @@ -import { cloneDeep, assign, omit } from 'lodash'; +import { assign, omit } from 'lodash'; import { SelectionNode, @@ -11,15 +11,15 @@ import { import gql from 'graphql-tag'; import { storeKeyNameFromField, - IdValue, addTypenameToDocument, + cloneDeep, } from 'apollo-utilities'; import { StoreWriter } from '../writeToStore'; import { defaultNormalizedCacheFactory } from '../objectCache'; -import { StoreObject } from '../'; +import { makeReference } from '../helpers'; export function withWarning(func: Function, regex?: RegExp) { let message: string = null as never; @@ -289,11 +289,7 @@ describe('writing to the store', () => { .toObject(), ).toEqual({ ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), { - nestedObj: { - type: 'id', - id: result.nestedObj.id, - generated: false, - }, + nestedObj: makeReference(result.nestedObj.id), }), [result.nestedObj.id]: result.nestedObj, }); @@ -334,14 +330,7 @@ describe('writing to the store', () => { }) .toObject(), ).toEqual({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), { - nestedObj: { - type: 'id', - id: `$ROOT_QUERY.nestedObj`, - generated: true, - }, - }), - [`$ROOT_QUERY.nestedObj`]: result.nestedObj, + ROOT_QUERY: result, }); }); @@ -380,14 +369,9 @@ describe('writing to the store', () => { }) .toObject(), ).toEqual({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), { - 'nestedObj({"arg":"val"})': { - type: 'id', - id: `$ROOT_QUERY.nestedObj({"arg":"val"})`, - generated: true, - }, + ROOT_QUERY: assign(omit(result, 'nestedObj'), { + 'nestedObj({"arg":"val"})': result.nestedObj, }), - [`$ROOT_QUERY.nestedObj({"arg":"val"})`]: result.nestedObj, }); }); @@ -438,11 +422,9 @@ describe('writing to the store', () => { .toObject(), ).toEqual({ ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), { - nestedArray: result.nestedArray.map((obj: any) => ({ - type: 'id', - id: obj.id, - generated: false, - })), + nestedArray: result.nestedArray.map( + (obj: any) => makeReference(obj.id), + ), }), [result.nestedArray[0].id]: result.nestedArray[0], [result.nestedArray[1].id]: result.nestedArray[1], @@ -491,10 +473,7 @@ describe('writing to the store', () => { .toObject(), ).toEqual({ ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), { - nestedArray: [ - { type: 'id', id: result.nestedArray[0].id, generated: false }, - null, - ], + nestedArray: [makeReference(result.nestedArray[0].id), null], }), [result.nestedArray[0].id]: result.nestedArray[0], }); @@ -540,14 +519,7 @@ describe('writing to the store', () => { }); expect(normalized.toObject()).toEqual({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), { - nestedArray: [ - { type: 'id', generated: true, id: `ROOT_QUERY.nestedArray.0` }, - { type: 'id', generated: true, id: `ROOT_QUERY.nestedArray.1` }, - ], - }), - [`ROOT_QUERY.nestedArray.0`]: result.nestedArray[0], - [`ROOT_QUERY.nestedArray.1`]: result.nestedArray[1], + ROOT_QUERY: result, }); }); @@ -587,13 +559,7 @@ describe('writing to the store', () => { }); expect(normalized.toObject()).toEqual({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), { - nestedArray: [ - null, - { type: 'id', generated: true, id: `ROOT_QUERY.nestedArray.1` }, - ], - }), - [`ROOT_QUERY.nestedArray.1`]: result.nestedArray[1], + ROOT_QUERY: result, }); }); @@ -623,16 +589,7 @@ describe('writing to the store', () => { }); expect(normalized.toObject()).toEqual({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), { - simpleArray: { - type: 'json', - json: [ - result.simpleArray[0], - result.simpleArray[1], - result.simpleArray[2], - ], - }, - }), + ROOT_QUERY: result, }); }); @@ -661,16 +618,7 @@ describe('writing to the store', () => { }); expect(normalized.toObject()).toEqual({ - ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), { - simpleArray: { - type: 'json', - json: [ - result.simpleArray[0], - result.simpleArray[1], - result.simpleArray[2], - ], - }, - }), + ROOT_QUERY: result, }); }); @@ -710,16 +658,8 @@ describe('writing to the store', () => { expect(normalized.toObject()).toEqual({ ROOT_QUERY: { id: 'a', - object1: { - type: 'id', - id: 'aa', - generated: false, - }, - object2: { - type: 'id', - id: 'aa', - generated: false, - }, + object1: makeReference('aa'), + object2: makeReference('aa'), }, aa: { id: 'aa', @@ -785,38 +725,18 @@ describe('writing to the store', () => { expect(normalized.toObject()).toEqual({ ROOT_QUERY: { id: 'a', - array1: [ - { - type: 'id', - id: 'aa', - generated: false, - }, - ], - array2: [ - { - type: 'id', - id: 'ab', - generated: false, - }, - ], + array1: [makeReference('aa')], + array2: [makeReference('ab')], }, aa: { id: 'aa', stringField: 'string', - obj: { - type: 'id', - id: 'aaa', - generated: false, - }, + obj: makeReference('aaa'), }, ab: { id: 'ab', stringField: 'string2', - obj: { - type: 'id', - id: 'aaa', - generated: false, - }, + obj: makeReference('aaa'), }, aaa: { id: 'aaa', @@ -875,36 +795,17 @@ describe('writing to the store', () => { expect(normalized.toObject()).toEqual({ ROOT_QUERY: { id: 'a', - array1: [ - { - type: 'id', - id: 'aa', - generated: false, - }, - { - type: 'id', - id: 'ab', - generated: false, - }, - ], + array1: [makeReference('aa'), makeReference('ab')], }, aa: { id: 'aa', stringField: 'string', - obj: { - type: 'id', - id: 'aaa', - generated: false, - }, + obj: makeReference('aaa'), }, ab: { id: 'ab', stringField: 'string2', - obj: { - type: 'id', - id: 'aaa', - generated: false, - }, + obj: makeReference('aaa'), }, aaa: { id: 'aaa', @@ -1026,15 +927,10 @@ describe('writing to the store', () => { ).toEqual({ ROOT_QUERY: { 'people_one({"id":"5"})': { - type: 'id', - id: '$ROOT_QUERY.people_one({"id":"5"})', - generated: true, + id: 'abcd', + stringField: 'This is a string!', }, }, - '$ROOT_QUERY.people_one({"id":"5"})': { - id: 'abcd', - stringField: 'This is a string!', - }, }); }); @@ -1193,6 +1089,7 @@ describe('writing to the store', () => { result: cloneDeep(result), context: { store: defaultNormalizedCacheFactory(), + processedData: {}, variables, dataIdFromObject: () => '5', }, @@ -1201,16 +1098,8 @@ describe('writing to the store', () => { ).toEqual({ '5': { id: 'id', - 'some_mutation({"input":{"arr":[1,{"a":"b"}],"bo":true,"id":"5","nil":null,"num":5.5,"obj":{"a":"b"}}})': { - generated: false, - id: '5', - type: 'id', - }, - 'some_mutation_with_variables({"input":{"arr":[1,{"a":"b"}],"bo":true,"id":"5","nil":null,"num":5.5,"obj":{"a":"b"}}})': { - generated: false, - id: '5', - type: 'id', - }, + 'some_mutation({"input":{"arr":[1,{"a":"b"}],"bo":true,"id":"5","nil":null,"num":5.5,"obj":{"a":"b"}}})': makeReference('5'), + 'some_mutation_with_variables({"input":{"arr":[1,{"a":"b"}],"bo":true,"id":"5","nil":null,"num":5.5,"obj":{"a":"b"}}})': makeReference('5'), }, }); } else { @@ -1219,50 +1108,6 @@ describe('writing to the store', () => { }); }); - it('should write to store if `dataIdFromObject` returns an ID of 0', () => { - const query = gql` - query { - author { - firstName - id - __typename - } - } - `; - const data = { - author: { - id: 0, - __typename: 'Author', - firstName: 'John', - }, - }; - const expStore = defaultNormalizedCacheFactory({ - ROOT_QUERY: { - author: { - id: 0 as any, - typename: 'Author', - type: 'id', - generated: false, - }, - }, - 0: { - id: data.author.id, - __typename: data.author.__typename, - firstName: data.author.firstName, - }, - }); - - expect( - writer - .writeQueryToStore({ - result: data, - query, - dataIdFromObject: () => (0 as any) as string, - }) - .toObject(), - ).toEqual(expStore.toObject()); - }); - describe('type escaping', () => { const dataIdFromObject = (object: any) => { if (object.__typename && object.id) { @@ -1287,14 +1132,7 @@ describe('writing to the store', () => { }, }; const expStore = defaultNormalizedCacheFactory({ - ROOT_QUERY: { - author: { - type: 'id' as any, - id: '$ROOT_QUERY.author', - generated: true, - }, - }, - '$ROOT_QUERY.author': data.author, + ROOT_QUERY: data, }); expect( writer @@ -1325,12 +1163,7 @@ describe('writing to the store', () => { }; const expStore = defaultNormalizedCacheFactory({ ROOT_QUERY: { - author: { - type: 'id', - id: dataIdFromObject(data.author)!, - generated: false, - typename: 'Author', - }, + author: makeReference(dataIdFromObject(data.author)), }, [dataIdFromObject(data.author)!]: { firstName: data.author.firstName, @@ -1349,7 +1182,7 @@ describe('writing to the store', () => { ).toEqual(expStore.toObject()); }); - it('should correctly escape json blobs', () => { + it('should not need to escape json blobs', () => { const query = gql` query { author { @@ -1370,20 +1203,12 @@ describe('writing to the store', () => { }; const expStore = defaultNormalizedCacheFactory({ ROOT_QUERY: { - author: { - type: 'id', - id: dataIdFromObject(data.author)!, - generated: false, - typename: 'Author', - }, + author: makeReference(dataIdFromObject(data.author)), }, [dataIdFromObject(data.author)!]: { __typename: data.author.__typename, id: data.author.id, - info: { - type: 'json', - json: data.author.info, - }, + info: data.author.info, }, }); expect( @@ -1439,17 +1264,11 @@ describe('writing to the store', () => { } `; const expStoreWithoutId = defaultNormalizedCacheFactory({ - '$ROOT_QUERY.author': { - firstName: 'John', - lastName: 'Smith', - __typename: 'Author', - }, ROOT_QUERY: { author: { - type: 'id', - id: '$ROOT_QUERY.author', - generated: true, - typename: 'Author', + firstName: 'John', + lastName: 'Smith', + __typename: 'Author', }, }, }); @@ -1461,12 +1280,7 @@ describe('writing to the store', () => { __typename: 'Author', }, ROOT_QUERY: { - author: { - type: 'id', - id: 'Author__129', - generated: false, - typename: 'Author', - }, + author: makeReference('Author__129'), }, }); const storeWithoutId = writer.writeQueryToStore({ @@ -1522,16 +1336,10 @@ describe('writing to the store', () => { } `; const expStoreWithPlaceholder = defaultNormalizedCacheFactory({ - '$ROOT_QUERY.author': { - hello: 'Foo', - __typename: 'Placeholder', - }, ROOT_QUERY: { author: { - type: 'id', - id: '$ROOT_QUERY.author', - generated: true, - typename: 'Placeholder', + hello: 'Foo', + __typename: 'Placeholder', }, }, }); @@ -1543,12 +1351,7 @@ describe('writing to the store', () => { __typename: 'Author', }, ROOT_QUERY: { - author: { - type: 'id', - id: 'Author__129', - generated: false, - typename: 'Author', - }, + author: makeReference('Author__129'), }, }); @@ -1656,10 +1459,9 @@ describe('writing to the store', () => { ], }; - const newStore = writer.writeResultToStore({ - dataId: 'ROOT_QUERY', + const newStore = writer.writeQueryToStore({ + query, result, - document: query, dataIdFromObject: getIdField, }); @@ -1681,10 +1483,9 @@ describe('writing to the store', () => { }); return withWarning(() => { - const newStore = writer.writeResultToStore({ - dataId: 'ROOT_QUERY', + const newStore = writer.writeQueryToStore({ + query, result, - document: query, dataIdFromObject: getIdField, }); @@ -1737,10 +1538,9 @@ describe('writing to the store', () => { }); return withWarning(() => { - const newStore = writer.writeResultToStore({ - dataId: 'ROOT_QUERY', + const newStore = writer.writeQueryToStore({ + query: queryWithInterface, result, - document: queryWithInterface, dataIdFromObject: getIdField, }); @@ -1764,10 +1564,9 @@ describe('writing to the store', () => { }); return withWarning(() => { - const newStore = writer.writeResultToStore({ - dataId: 'ROOT_QUERY', + const newStore = writer.writeQueryToStore({ + query: addTypenameToDocument(query), result, - document: addTypenameToDocument(query), dataIdFromObject: getIdField, }); @@ -1780,10 +1579,9 @@ describe('writing to the store', () => { todos: null, }; - const newStore = writer.writeResultToStore({ - dataId: 'ROOT_QUERY', + const newStore = writer.writeQueryToStore({ + query, result, - document: query, dataIdFromObject: getIdField, }); @@ -1802,10 +1600,9 @@ describe('writing to the store', () => { id: 1, }; - const newStore = writer.writeResultToStore({ - dataId: 'ROOT_QUERY', + const newStore = writer.writeQueryToStore({ + query: defered, result, - document: defered, dataIdFromObject: getIdField, }); @@ -1817,25 +1614,15 @@ describe('writing to the store', () => { it('throws when trying to write an object without id that was previously queried with id', () => { const store = defaultNormalizedCacheFactory({ - ROOT_QUERY: assign( - {}, - { - __typename: 'Query', - item: { - type: 'id', - id: 'abcd', - generated: false, - } as IdValue, - }, - ) as StoreObject, - abcd: assign( - {}, - { - id: 'abcd', - __typename: 'Item', - stringField: 'This is a string!', - }, - ) as StoreObject, + ROOT_QUERY: { + __typename: 'Query', + item: makeReference('abcd'), + }, + abcd: { + id: 'abcd', + __typename: 'Item', + stringField: 'This is a string!', + }, }); expect(() => { @@ -1859,22 +1646,21 @@ describe('writing to the store', () => { }).toThrowErrorMatchingSnapshot(); expect(() => { - writer.writeResultToStore({ + writer.writeQueryToStore({ store, - result: { - item: { - __typename: 'Item', - stringField: 'This is still a string!', - }, - }, - dataId: 'ROOT_QUERY', - document: gql` + query: gql` query { item { stringField } } `, + result: { + item: { + __typename: 'Item', + stringField: 'This is still a string!', + }, + }, dataIdFromObject: getIdField, }); }).toThrowError(/stringField(.|\n)*abcd/g); @@ -1923,15 +1709,10 @@ describe('writing to the store', () => { ROOT_QUERY: { abc: [ { - generated: true, - id: 'ROOT_QUERY.abc.0', - type: 'id', + name: 'efgh', }, ], }, - 'ROOT_QUERY.abc.0': { - name: 'efgh', - }, }); }); @@ -1965,25 +1746,13 @@ describe('writing to the store', () => { }); expect(store.toObject()).toEqual({ - '$ROOT_QUERY.animals.0.species': { name: 'cat' }, ROOT_QUERY: { animals: [ { - generated: true, - id: 'ROOT_QUERY.animals.0', - type: 'id', - typename: 'Animal', + species: { name: 'cat' }, }, ], }, - 'ROOT_QUERY.animals.0': { - species: { - generated: true, - id: '$ROOT_QUERY.animals.0.species', - type: 'id', - typename: 'Cat', - }, - }, }); writer.writeQueryToStore({ @@ -2003,25 +1772,13 @@ describe('writing to the store', () => { }); expect(store.toObject()).toEqual({ - '$ROOT_QUERY.animals.0.species': { name: 'dog' }, ROOT_QUERY: { animals: [ { - generated: true, - id: 'ROOT_QUERY.animals.0', - type: 'id', - typename: 'Animal', + species: { name: 'dog' }, }, ], }, - 'ROOT_QUERY.animals.0': { - species: { - generated: true, - id: '$ROOT_QUERY.animals.0.species', - type: 'id', - typename: 'Dog', - }, - }, }); }); @@ -2064,25 +1821,13 @@ describe('writing to the store', () => { }); expect(store.toObject()).toEqual({ - '$ROOT_QUERY.animals.0.species': { name: 'cat' }, ROOT_QUERY: { animals: [ { - generated: true, - id: 'ROOT_QUERY.animals.0', - type: 'id', - typename: 'Animal', + species: { name: 'cat' }, }, ], }, - 'ROOT_QUERY.animals.0': { - species: { - generated: true, - id: '$ROOT_QUERY.animals.0.species', - type: 'id', - typename: 'Cat', - }, - }, }); writer.writeQueryToStore({ @@ -2104,7 +1849,6 @@ describe('writing to the store', () => { }); expect(store.toObject()).toEqual({ - '$ROOT_QUERY.animals.0.species': undefined, 'Dog__dog-species': { id: 'dog-species', name: 'dog', @@ -2112,21 +1856,10 @@ describe('writing to the store', () => { ROOT_QUERY: { animals: [ { - generated: true, - id: 'ROOT_QUERY.animals.0', - type: 'id', - typename: 'Animal', + species: makeReference('Dog__dog-species'), }, ], }, - 'ROOT_QUERY.animals.0': { - species: { - generated: false, - id: 'Dog__dog-species', - type: 'id', - typename: 'Dog', - }, - }, }); }); }); diff --git a/packages/apollo-cache-inmemory/src/helpers.ts b/packages/apollo-cache-inmemory/src/helpers.ts new file mode 100644 index 00000000000..01fdb8c1ed6 --- /dev/null +++ b/packages/apollo-cache-inmemory/src/helpers.ts @@ -0,0 +1,22 @@ +import { NormalizedCache, StoreObject } from './types'; + +export interface Reference { + readonly __ref: string; +} + +export function makeReference(id: string): Reference { + return { __ref: String(id) }; +} + +export function isReference(obj: any): obj is Reference { + return obj && typeof obj === 'object' && typeof obj.__ref === 'string'; +} + +export function getTypenameFromStoreObject( + store: NormalizedCache, + storeObject: StoreObject | Reference, +): string | undefined { + return isReference(storeObject) + ? getTypenameFromStoreObject(store, store.get(storeObject.__ref)) + : storeObject && storeObject.__typename; +} diff --git a/packages/apollo-cache-inmemory/src/inMemoryCache.ts b/packages/apollo-cache-inmemory/src/inMemoryCache.ts index ecdd5b23240..321944c9f69 100644 --- a/packages/apollo-cache-inmemory/src/inMemoryCache.ts +++ b/packages/apollo-cache-inmemory/src/inMemoryCache.ts @@ -206,12 +206,12 @@ export class InMemoryCache extends ApolloCache { } public write(write: Cache.WriteOptions): void { - this.storeWriter.writeResultToStore({ - dataId: write.dataId, + this.storeWriter.writeQueryToStore({ + store: this.data, + query: this.transformDocument(write.query), result: write.result, + dataId: write.dataId, variables: write.variables, - document: this.transformDocument(write.query), - store: this.data, dataIdFromObject: this.config.dataIdFromObject, }); diff --git a/packages/apollo-cache-inmemory/src/index.ts b/packages/apollo-cache-inmemory/src/index.ts index 29b3035b80c..69e1e0bced5 100644 --- a/packages/apollo-cache-inmemory/src/index.ts +++ b/packages/apollo-cache-inmemory/src/index.ts @@ -8,3 +8,8 @@ export * from './readFromStore'; export * from './writeToStore'; export * from './objectCache'; export * from './types'; +export { + Reference, + makeReference, + isReference, +} from './helpers'; diff --git a/packages/apollo-cache-inmemory/src/readFromStore.ts b/packages/apollo-cache-inmemory/src/readFromStore.ts index 60e02f13a6b..bf3f065bc01 100644 --- a/packages/apollo-cache-inmemory/src/readFromStore.ts +++ b/packages/apollo-cache-inmemory/src/readFromStore.ts @@ -11,18 +11,14 @@ import { getMainDefinition, getQueryDefinition, getStoreKeyName, - IdValue, isEqual, isField, - isIdValue, isInlineFragment, - isJsonValue, maybeDeepFreeze, mergeDeepArray, resultKeyNameFromField, shouldInclude, StoreValue, - toIdValue, } from 'apollo-utilities'; import { Cache } from 'apollo-cache'; @@ -32,6 +28,7 @@ import { DiffQueryAgainstStoreOptions, ReadQueryOptions, StoreObject, + NormalizedCache, } from './types'; import { @@ -44,8 +41,14 @@ import { import { wrap, KeyTrie } from 'optimism'; import { DepTrackingCache } from './depTrackingCache'; -import { invariant, InvariantError } from 'ts-invariant'; +import { InvariantError } from 'ts-invariant'; import { fragmentMatches } from './fragments'; +import { + isReference, + makeReference, + Reference, + getTypenameFromStoreObject, +} from './helpers'; export type VariableMap = { [name: string]: any }; @@ -75,14 +78,14 @@ export type ExecResult = { type ExecStoreQueryOptions = { query: DocumentNode; - rootValue: IdValue; + objectOrReference: StoreObject | Reference; contextValue: ReadStoreContext; variableValues: VariableMap; }; type ExecSelectionSetOptions = { selectionSet: SelectionSetNode; - rootValue: any; + objectOrReference: StoreObject | Reference; execContext: ExecContext; }; @@ -122,7 +125,7 @@ export class StoreReader { }, { makeCacheKey({ query, - rootValue, + objectOrReference, contextValue, variableValues, }: ExecStoreQueryOptions) { @@ -134,7 +137,7 @@ export class StoreReader { contextValue.store, query, JSON.stringify(variableValues), - rootValue.id, + isReference(objectOrReference) ? objectOrReference.__ref : objectOrReference, ); } } @@ -145,7 +148,7 @@ export class StoreReader { }, { makeCacheKey({ selectionSet, - rootValue, + objectOrReference, execContext, }: ExecSelectionSetOptions) { if (execContext.contextValue.store instanceof DepTrackingCache) { @@ -153,7 +156,7 @@ export class StoreReader { execContext.contextValue.store, selectionSet, JSON.stringify(execContext.variableValues), - rootValue.id, + isReference(objectOrReference) ? objectOrReference.__ref : objectOrReference, ); } } @@ -230,12 +233,9 @@ export class StoreReader { const execResult = this.executeStoreQuery({ query, - rootValue: { - type: 'id', - id: rootId, - generated: true, - typename: 'Query', - }, + objectOrReference: rootId === 'ROOT_QUERY' + ? makeReference('ROOT_QUERY') + : store.get(rootId) || makeReference(rootId), contextValue: context, variableValues: variables, }); @@ -288,7 +288,7 @@ export class StoreReader { */ private executeStoreQuery({ query, - rootValue, + objectOrReference, contextValue, variableValues, }: ExecStoreQueryOptions): ExecResult { @@ -304,27 +304,31 @@ export class StoreReader { return this.executeSelectionSet({ selectionSet: mainDefinition.selectionSet, - rootValue, + objectOrReference, execContext, }); } private executeSelectionSet({ selectionSet, - rootValue, + objectOrReference, execContext, }: ExecSelectionSetOptions): ExecResult { - const { fragmentMap, contextValue, variableValues: variables } = execContext; + const { fragmentMap, variableValues: variables } = execContext; const finalResult: ExecResult = { result: null }; - const objectsToMerge: { [key: string]: any }[] = []; - const object: StoreObject = contextValue.store.get(rootValue.id); - - const typename = - (object && object.__typename) || - (rootValue.id === 'ROOT_QUERY' && 'Query') || - void 0; + let object: StoreObject; + let typename: string; + if (isReference(objectOrReference)) { + object = execContext.contextValue.store.get(objectOrReference.__ref); + typename = + (object && object.__typename) || + (objectOrReference.__ref === 'ROOT_QUERY' && 'Query'); + } else { + object = objectOrReference; + typename = object && object.__typename; + } function handleMissing(result: ExecResult): T { if (result.missing) { @@ -374,7 +378,7 @@ export class StoreReader { if (match && (object || typename === 'Query')) { let fragmentExecResult = this.executeSelectionSet({ selectionSet: fragment.selectionSet, - rootValue, + objectOrReference: object, execContext, }); @@ -440,9 +444,11 @@ export class StoreReader { // Handle all scalar types here if (!field.selectionSet) { - assertSelectionSetForIdValue(field, readStoreResult.result); - if (this.freezeResults && process.env.NODE_ENV !== 'production') { - maybeDeepFreeze(readStoreResult); + if (process.env.NODE_ENV !== 'production') { + assertSelectionSetForIdValue(contextValue.store, field, readStoreResult.result); + if (this.freezeResults) { + maybeDeepFreeze(readStoreResult); + } } return readStoreResult; } @@ -459,7 +465,7 @@ export class StoreReader { readStoreResult, this.executeSelectionSet({ selectionSet: field.selectionSet, - rootValue: readStoreResult.result, + objectOrReference: readStoreResult.result as StoreObject | Reference, execContext, }), ); @@ -516,12 +522,14 @@ export class StoreReader { if (field.selectionSet) { return handleMissing(this.executeSelectionSet({ selectionSet: field.selectionSet, - rootValue: item, + objectOrReference: item, execContext, })); } - assertSelectionSetForIdValue(field, item); + if (process.env.NODE_ENV !== 'production') { + assertSelectionSetForIdValue(execContext.contextValue.store, field, item); + } return item; }); @@ -535,32 +543,33 @@ export class StoreReader { } function assertSelectionSetForIdValue( + store: NormalizedCache, field: FieldNode, - value: any, + fieldValue: any, ) { - if (!field.selectionSet && isIdValue(value)) { - throw new InvariantError( - `Missing selection set for object of type ${ - value.typename - } returned for query field ${field.name.value}` - ); + if (!field.selectionSet) { + // This ensures not only that value contains no Reference objects, but also + // that the result contains no cycles. + JSON.stringify(fieldValue, (_key, nestedValue) => { + if (isReference(nestedValue)) { + throw new InvariantError( + `Missing selection set for object of type ${ + getTypenameFromStoreObject(store, nestedValue) + } returned for query field ${field.name.value}`, + ) + } + return nestedValue; + }); } } -export function assertIdValue(idValue: IdValue) { - invariant(isIdValue(idValue), `\ -Encountered a sub-selection on the query, but the store doesn't have \ -an object reference. This should never happen during normal use unless you have custom code \ -that is directly manipulating the store; please file an issue.`); -} - function readStoreResolver( object: StoreObject, typename: string | void, fieldName: string, args: any, context: ReadStoreContext, - { resultKey, directives }: ExecInfo, + { directives }: ExecInfo, ): ExecResult { let storeKeyName = fieldName; if (args || directives) { @@ -590,10 +599,7 @@ function readStoreResolver( fieldValue = resolver(object, args, { getCacheKey(storeObj: StoreObject) { const id = context.dataIdFromObject!(storeObj); - return id && toIdValue({ - id, - typename: storeObj.__typename, - }); + return id && makeReference(id); }, }); } @@ -612,10 +618,6 @@ function readStoreResolver( }; } - if (isJsonValue(fieldValue)) { - fieldValue = fieldValue.json; - } - return { result: fieldValue, }; diff --git a/packages/apollo-cache-inmemory/src/writeToStore.ts b/packages/apollo-cache-inmemory/src/writeToStore.ts index ec2071a2f73..4a4a64765c2 100644 --- a/packages/apollo-cache-inmemory/src/writeToStore.ts +++ b/packages/apollo-cache-inmemory/src/writeToStore.ts @@ -1,10 +1,4 @@ -import { - SelectionSetNode, - FieldNode, - DocumentNode, - InlineFragmentNode, - FragmentDefinitionNode, -} from 'graphql'; +import { SelectionSetNode, FieldNode, DocumentNode } from 'graphql'; import { assign, @@ -12,29 +6,29 @@ import { FragmentMap, getDefaultValues, getFragmentDefinitions, + getFragmentFromSelection, getOperationDefinition, - IdValue, isField, - isIdValue, - isInlineFragment, resultKeyNameFromField, shouldInclude, storeKeyNameFromField, StoreValue, - toIdValue, - isEqual, + DeepMerger, + getTypenameFromResult, } from 'apollo-utilities'; -import { invariant } from 'ts-invariant'; +import { invariant, InvariantError } from 'ts-invariant'; import { defaultNormalizedCacheFactory } from './depTrackingCache'; -import { - IdGetter, - NormalizedCache, - StoreObject, -} from './types'; +import { IdGetter, NormalizedCache, StoreObject } from './types'; import { fragmentMatches } from './fragments'; +import { + makeReference, + isReference, + getTypenameFromStoreObject, +} from './helpers'; +import { defaultDataIdFromObject } from './inMemoryCache'; export class WriteError extends Error { public type = 'WriteError'; @@ -52,7 +46,7 @@ export function enhanceErrorWithDocument(error: Error, document: DocumentNode) { export type WriteContext = { readonly store: NormalizedCache; - readonly processedData?: { [x: string]: FieldNode[] }; + readonly processedData: { [x: string]: Set }; readonly variables?: any; readonly dataIdFromObject?: IdGetter; readonly fragmentMap?: FragmentMap; @@ -86,44 +80,19 @@ export class StoreWriter { public writeQueryToStore({ query, result, + dataId = 'ROOT_QUERY', store = defaultNormalizedCacheFactory(), variables, - dataIdFromObject, + dataIdFromObject = defaultDataIdFromObject, }: { query: DocumentNode; result: Object; + dataId?: string; store?: NormalizedCache; variables?: Object; dataIdFromObject?: IdGetter; }): NormalizedCache { - return this.writeResultToStore({ - dataId: 'ROOT_QUERY', - result, - document: query, - store, - variables, - dataIdFromObject, - }); - } - - public writeResultToStore({ - dataId, - result, - document, - store = defaultNormalizedCacheFactory(), - variables, - dataIdFromObject, - }: { - dataId: string; - result: any; - document: DocumentNode; - store?: NormalizedCache; - variables?: Object; - dataIdFromObject?: IdGetter; - }): NormalizedCache { - // XXX TODO REFACTOR: this is a temporary workaround until query normalization is made to work with documents. - const operationDefinition = getOperationDefinition(document)!; - + const operationDefinition = getOperationDefinition(query)!; try { return this.writeSelectionSetToStore({ result, @@ -138,11 +107,11 @@ export class StoreWriter { variables, ), dataIdFromObject, - fragmentMap: createFragmentMap(getFragmentDefinitions(document)), + fragmentMap: createFragmentMap(getFragmentDefinitions(query)), }, }); } catch (e) { - throw enhanceErrorWithDocument(e, document); + throw enhanceErrorWithDocument(e, query); } } @@ -157,24 +126,50 @@ export class StoreWriter { selectionSet: SelectionSetNode; context: WriteContext; }): NormalizedCache { - const { variables, store, fragmentMap } = context; + const { store } = context; + const newFields = this.processSelectionSet({ + result, + selectionSet, + context, + }); + store.set(dataId, mergeStoreObjects(store, store.get(dataId), newFields)); + return store; + } + + private processSelectionSet({ + result, + selectionSet, + typename, + context, + }: { + result: any; + selectionSet: SelectionSetNode; + typename?: string; + context: WriteContext; + }): StoreObject { + const newFields: { + [storeFieldName: string]: StoreValue; + } = Object.create(null); selectionSet.selections.forEach(selection => { - if (!shouldInclude(selection, variables)) { + if (!shouldInclude(selection, context.variables)) { return; } if (isField(selection)) { - const resultFieldKey: string = resultKeyNameFromField(selection); - const value: any = result[resultFieldKey]; + const resultFieldKey = resultKeyNameFromField(selection); + const value = result[resultFieldKey]; if (typeof value !== 'undefined') { - this.writeFieldToStore({ - dataId, + const storeFieldName = storeKeyNameFromField( + selection, + context.variables, + ); + newFields[storeFieldName] = this.processFieldValue( value, - field: selection, + selection, context, - }); + ); } else if ( this.config.possibleTypes && !( @@ -197,21 +192,17 @@ export class StoreWriter { ); } } else { - // This is not a field, so it must be a fragment, either inline or named - let fragment: InlineFragmentNode | FragmentDefinitionNode; + // If the typename of the object we're processing was not provided, + // compute it lazily. + typename = + typename || + getTypenameFromResult(result, selectionSet, context.fragmentMap); - if (isInlineFragment(selection)) { - fragment = selection; - } else { - // Named fragment - fragment = (fragmentMap || {})[selection.name.value]; - invariant(fragment, `No fragment named ${selection.name.value}.`); - } - - const typename = - (result && result.__typename) || - (dataId === 'ROOT_QUERY' && 'Query') || - void 0; + // This is not a field, so it must be a fragment, either inline or named + const fragment = getFragmentFromSelection( + selection, + context.fragmentMap, + ); const match = fragmentMatches( fragment, @@ -220,271 +211,132 @@ export class StoreWriter { ); if (match && (result || typename === 'Query')) { - this.writeSelectionSetToStore({ - result, - selectionSet: fragment.selectionSet, - dataId, - context, - }); + Object.assign( + newFields, + this.processSelectionSet({ + result, + selectionSet: fragment.selectionSet, + typename, + context, + }), + ); } } }); - return store; + return newFields; } - private writeFieldToStore({ - field, - value, - dataId, - context, - }: { - field: FieldNode; - value: any; - dataId: string; - context: WriteContext; - }) { - const { variables, dataIdFromObject, store } = context; - - let storeValue: StoreValue; - let storeObject: StoreObject; - - const storeFieldName: string = storeKeyNameFromField(field, variables); - - // If this is a scalar value... + private processFieldValue( + value: any, + field: FieldNode, + context: WriteContext, + ): StoreValue { if (!field.selectionSet || value === null) { - storeValue = - value != null && typeof value === 'object' - ? // If the scalar value is a JSON blob, we have to "escape" it so it can’t pretend to be - // an id. - { type: 'json', json: value } - : // Otherwise, just store the scalar directly in the store. - value; - } else if (Array.isArray(value)) { - const generatedId = `${dataId}.${storeFieldName}`; - - storeValue = this.processArrayValue( - value, - generatedId, - field.selectionSet, - context, - ); - } else { - // It's an object - let valueDataId = `${dataId}.${storeFieldName}`; - let generated = true; - - // We only prepend the '$' if the valueDataId isn't already a generated - // id. - if (!isGeneratedId(valueDataId)) { - valueDataId = '$' + valueDataId; - } - - if (dataIdFromObject) { - const semanticId = dataIdFromObject(value); - - // We throw an error if the first character of the id is '$. This is - // because we use that character to designate an Apollo-generated id - // and we use the distinction between user-desiginated and application-provided - // ids when managing overwrites. - invariant( - !semanticId || !isGeneratedId(semanticId), - 'IDs returned by dataIdFromObject cannot begin with the "$" character.', - ); - - if ( - semanticId || - (typeof semanticId === 'number' && semanticId === 0) - ) { - valueDataId = semanticId; - generated = false; - } - } - - if (!isDataProcessed(valueDataId, field, context.processedData)) { - this.writeSelectionSetToStore({ - dataId: valueDataId, - result: value, - selectionSet: field.selectionSet, - context, - }); - } - - // We take the id and escape it (i.e. wrap it with an enclosing object). - // This allows us to distinguish IDs from normal scalars. - const typename = value.__typename; - storeValue = toIdValue({ id: valueDataId, typename }, generated); - - // check if there was a generated id at the location where we're - // about to place this new id. If there was, we have to merge the - // data from that id with the data we're about to write in the store. - storeObject = store.get(dataId); - const escapedId = - storeObject && (storeObject[storeFieldName] as IdValue | undefined); - if (escapedId !== storeValue && isIdValue(escapedId)) { - const hadTypename = escapedId.typename !== undefined; - const hasTypename = typename !== undefined; - const typenameChanged = - hadTypename && hasTypename && escapedId.typename !== typename; - - // If there is already a real id in the store and the current id we - // are dealing with is generated, we throw an error. - // One exception we allow is when the typename has changed, which occurs - // when schema defines a union, both with and without an ID in the same place. - // checks if we "lost" the read id - invariant( - !generated || escapedId.generated || typenameChanged, - `Store error: the application attempted to write an object with no provided id but the store already contains an id of ${ - escapedId.id - } for this object. The selectionSet that was trying to be written is:\n${ - JSON.stringify(field) - }`, - ); - - // checks if we "lost" the typename - invariant( - !hadTypename || hasTypename, - `Store error: the application attempted to write an object with no provided typename but the store already contains an object with typename of ${ - escapedId.typename - } for the object of id ${escapedId.id}. The selectionSet that was trying to be written is:\n${ - JSON.stringify(field) - }`, - ); - - if (escapedId.generated) { - // We should only merge if it's an object of the same type, - // otherwise we should delete the generated object - if (typenameChanged) { - // Only delete the generated object when the old object was - // inlined, and the new object is not. This is indicated by - // the old id being generated, and the new id being real. - if (!generated) { - store.delete(escapedId.id); - } - } else { - mergeWithGenerated(escapedId.id, (storeValue as IdValue).id, store); - } - } - } + return value; } - storeObject = store.get(dataId); - if (!storeObject || !isEqual(storeValue, storeObject[storeFieldName])) { - store.set(dataId, { - ...storeObject, - [storeFieldName]: storeValue, - }); + if (Array.isArray(value)) { + return value.map(item => this.processFieldValue(item, field, context)); } - } - - private processArrayValue( - value: any[], - generatedId: string, - selectionSet: SelectionSetNode, - context: WriteContext, - ): any[] { - return value.map((item: any, index: any) => { - if (item === null) { - return null; - } - - let itemDataId = `${generatedId}.${index}`; - - if (Array.isArray(item)) { - return this.processArrayValue(item, itemDataId, selectionSet, context); - } - - let generated = true; - if (context.dataIdFromObject) { - const semanticId = context.dataIdFromObject(item); - - if (semanticId) { - itemDataId = semanticId; - generated = false; + if (value && context.dataIdFromObject) { + const dataId = context.dataIdFromObject(value); + if (typeof dataId === 'string') { + if (!isDataProcessed(dataId, field, context.processedData)) { + this.writeSelectionSetToStore({ + dataId, + result: value, + selectionSet: field.selectionSet, + context, + }); } + return makeReference(dataId); } + } - if (!isDataProcessed(itemDataId, selectionSet, context.processedData)) { - this.writeSelectionSetToStore({ - dataId: itemDataId, - result: item, - selectionSet, - context, - }); - } - - return toIdValue( - { id: itemDataId, typename: item.__typename }, - generated, - ); + return this.processSelectionSet({ + result: value, + selectionSet: field.selectionSet, + context, }); } } -// Checks if the id given is an id that was generated by Apollo -// rather than by dataIdFromObject. -function isGeneratedId(id: string): boolean { - return id[0] === '$'; -} - -function mergeWithGenerated( - generatedKey: string, - realKey: string, - cache: NormalizedCache, -): boolean { - if (generatedKey === realKey) { - return false; - } - - const generated = cache.get(generatedKey); - const real = cache.get(realKey); - let madeChanges = false; - - Object.keys(generated).forEach(key => { - const value = generated[key]; - const realValue = real[key]; +function mergeStoreObjects( + store: NormalizedCache, + existing: StoreObject, + incoming: StoreObject, +): StoreObject { + return new DeepMerger(function(existingObject, incomingObject, property) { + // In the future, reconciliation logic may depend on the type of the parent + // StoreObject, not just the values of the given property. + const existing = existingObject[property]; + const incoming = incomingObject[property]; if ( - isIdValue(value) && - isGeneratedId(value.id) && - isIdValue(realValue) && - !isEqual(value, realValue) && - mergeWithGenerated(value.id, realValue.id, cache) + existing !== incoming && + // The DeepMerger class has various helpful utilities that we might as + // well reuse here. + this.isObject(existing) && + this.isObject(incoming) ) { - madeChanges = true; - } - }); + const eType = getTypenameFromStoreObject(store, existing); + const iType = getTypenameFromStoreObject(store, incoming); + // If both objects have a typename and the typename is different, let the + // incoming object win. The typename can change when a different subtype + // of a union or interface is written to the cache. + if ( + typeof eType === 'string' && + typeof iType === 'string' && + eType !== iType + ) { + return incoming; + } - cache.delete(generatedKey); - const newRealValue = { ...generated, ...real }; + if (isReference(incoming)) { + // Incoming references always overwrite existing references. + if (isReference(existing)) return incoming; + // Incoming references can be merged with existing non-reference data + // if the existing data appears to be of a compatible type. + store.set( + incoming.__ref, + this.merge(existing, store.get(incoming.__ref)), + ); + return incoming; + } else if (isReference(existing)) { + throw new InvariantError( + `Store error: the application attempted to write an object with no provided id but the store already contains an id of ${existing.__ref} for this object.`, + ); + } - if (isEqual(newRealValue, real)) { - return madeChanges; - } + if (Array.isArray(incoming)) { + if (!Array.isArray(existing)) return incoming; + if (existing.length > incoming.length) { + // Allow the incoming array to truncate the existing array, if the + // incoming array is shorter. + return this.merge(existing.slice(0, incoming.length), incoming); + } + } + + return this.merge(existing, incoming); + } - cache.set(realKey, newRealValue); - return true; + return incoming; + }).merge(existing, incoming); } function isDataProcessed( dataId: string, - field: FieldNode | SelectionSetNode, - processedData?: { [x: string]: (FieldNode | SelectionSetNode)[] }, + field: FieldNode, + processedData: { [x: string]: Set }, ): boolean { - if (!processedData) { - return false; - } - - if (processedData[dataId]) { - if (processedData[dataId].indexOf(field) >= 0) { - return true; - } else { - processedData[dataId].push(field); - } + const fieldSet = processedData[dataId]; + if (fieldSet) { + if (fieldSet.has(field)) return true; + fieldSet.add(field); } else { - processedData[dataId] = [field]; + processedData[dataId] = new Set([field]); } - return false; } diff --git a/packages/apollo-client/src/__tests__/ApolloClient.ts b/packages/apollo-client/src/__tests__/ApolloClient.ts index 3c90c53e9b5..025744e6304 100644 --- a/packages/apollo-client/src/__tests__/ApolloClient.ts +++ b/packages/apollo-client/src/__tests__/ApolloClient.ts @@ -1,6 +1,6 @@ import gql from 'graphql-tag'; import { ApolloLink, Observable } from 'apollo-link'; -import { InMemoryCache } from 'apollo-cache-inmemory'; +import { InMemoryCache, makeReference } from 'apollo-cache-inmemory'; import { stripSymbols } from 'apollo-utilities'; import { withWarning } from '../util/wrap'; import ApolloClient from '../'; @@ -82,22 +82,14 @@ describe('ApolloClient', () => { a: 1, b: 2, c: 3, - d: { - type: 'id', - id: 'foo', - generated: false, - }, + d: makeReference('foo'), }, foo: { __typename: 'Foo', e: 4, f: 5, g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, + h: makeReference('bar'), }, bar: { __typename: 'Bar', @@ -339,22 +331,14 @@ describe('ApolloClient', () => { a: 1, b: 2, c: 3, - d: { - type: 'id', - id: 'foo', - generated: false, - }, + d: makeReference('foo'), }, foo: { __typename: 'Foo', e: 4, f: 5, g: 6, - h: { - type: 'id', - id: 'bar', - generated: false, - }, + h: makeReference('bar'), }, bar: { __typename: 'Bar', @@ -1355,6 +1339,7 @@ describe('ApolloClient', () => { fragment bestFriends on Person { friends { id + type } } `, @@ -1527,6 +1512,7 @@ describe('ApolloClient', () => { query, data: { obj: { + __typename: 'Obj', field: { field2: 1, __typename: 'Field', @@ -1537,7 +1523,7 @@ describe('ApolloClient', () => { }); client.writeData({ - id: '$ROOT_QUERY.obj', + id: 'Obj:uniqueId', data: { field: { field2: 2, @@ -1550,7 +1536,7 @@ describe('ApolloClient', () => { .query({ query }) .then(({ data }: any) => { console.warn = originalWarn; - expect(data.obj.__typename).toEqual('__ClientData'); + expect(data.obj.__typename).toEqual('Obj'); expect(data.obj.field.__typename).toEqual('Field'); }) .catch(e => console.log(e)); @@ -1561,19 +1547,21 @@ describe('ApolloClient', () => { it('will write data locally which will then be read back', () => { const client = new ApolloClient({ link: ApolloLink.empty(), - cache: new InMemoryCache().restore({ + cache: new InMemoryCache({ + dataIdFromObject(object) { + if (typeof object.__typename === 'string') { + return object.__typename.toLowerCase(); + } + }, + }).restore({ foo: { __typename: 'Foo', a: 1, b: 2, c: 3, - bar: { - type: 'id', - id: '$foo.bar', - generated: true, - }, + bar: makeReference('bar'), }, - '$foo.bar': { + bar: { __typename: 'Bar', d: 4, e: 5, @@ -1683,7 +1671,7 @@ describe('ApolloClient', () => { }); client.writeFragment({ - id: '$foo.bar', + id: 'bar', fragment: gql` fragment y on Bar { e @@ -1852,44 +1840,24 @@ describe('ApolloClient', () => { g: 8, h: 9, bar: { - type: 'id', - id: '$ROOT_QUERY.bar', - generated: true, - }, - foo: { - type: 'id', - id: '$ROOT_QUERY.foo', - generated: true, - }, - }, - '$ROOT_QUERY.foo': { - c: 3, - d: 4, - bar: { - type: 'id', - id: '$ROOT_QUERY.foo.bar', - generated: true, + i: 10, + j: 11, + foo: { + _id: 'barfoo', + k: 12, + l: 13, + }, }, - }, - '$ROOT_QUERY.bar': { - i: 10, - j: 11, foo: { - type: 'id', - id: '$ROOT_QUERY.bar.foo', - generated: true, + c: 3, + d: 4, + bar: { + id: 'foobar', + e: 5, + f: 6, + }, }, }, - '$ROOT_QUERY.foo.bar': { - id: 'foobar', - e: 5, - f: 6, - }, - '$ROOT_QUERY.bar.foo': { - _id: 'barfoo', - k: 12, - l: 13, - }, }); }); @@ -2066,25 +2034,15 @@ describe('ApolloClient', () => { a: 1, b: 2, foo: { - type: 'id', - id: '$ROOT_QUERY.foo', - generated: true, - }, - }, - '$ROOT_QUERY.foo': { - c: 3, - d: 4, - bar: { - type: 'id', - id: '$ROOT_QUERY.foo.bar', - generated: true, + c: 3, + d: 4, + bar: { + id: 'foobar', + e: 5, + f: 6, + }, }, }, - '$ROOT_QUERY.foo.bar': { - id: 'foobar', - e: 5, - f: 6, - }, }); }); @@ -2124,25 +2082,15 @@ describe('ApolloClient', () => { a: 1, b: 2, foo: { - type: 'id', - id: '$ROOT_QUERY.foo', - generated: true, - }, - }, - '$ROOT_QUERY.foo': { - c: 3, - d: 4, - bar: { - type: 'id', - id: '$ROOT_QUERY.foo.bar', - generated: true, + c: 3, + d: 4, + bar: { + _id: 'foobar', + e: 5, + f: 6, + }, }, }, - '$ROOT_QUERY.foo.bar': { - _id: 'foobar', - e: 5, - f: 6, - }, }); }); diff --git a/packages/apollo-client/src/__tests__/__snapshots__/ApolloClient.ts.snap b/packages/apollo-client/src/__tests__/__snapshots__/ApolloClient.ts.snap index e8d1b19fb68..7e1f778fd06 100644 --- a/packages/apollo-client/src/__tests__/__snapshots__/ApolloClient.ts.snap +++ b/packages/apollo-client/src/__tests__/__snapshots__/ApolloClient.ts.snap @@ -14,45 +14,24 @@ For more information, please visit: https://www.apollographql.com/docs/tutorial/ exports[`ApolloClient write then read will not use a default id getter if either _id or id is present when __typename is not also present 1`] = ` Object { - "$ROOT_QUERY.bar": Object { - "foo": Object { - "generated": true, - "id": "$ROOT_QUERY.bar.foo", - "type": "id", - "typename": undefined, - }, - "i": 10, - "j": 11, - }, - "$ROOT_QUERY.bar.foo": Object { - "_id": "barfoo", - "k": 12, - "l": 13, - }, - "$ROOT_QUERY.foo": Object { - "bar": Object { - "generated": false, - "id": "bar:foobar", - "type": "id", - "typename": "bar", - }, - "c": 3, - "d": 4, - }, "ROOT_QUERY": Object { "a": 1, "b": 2, "bar": Object { - "generated": true, - "id": "$ROOT_QUERY.bar", - "type": "id", - "typename": undefined, + "foo": Object { + "_id": "barfoo", + "k": 12, + "l": 13, + }, + "i": 10, + "j": 11, }, "foo": Object { - "generated": true, - "id": "$ROOT_QUERY.foo", - "type": "id", - "typename": undefined, + "bar": Object { + "__ref": "bar:foobar", + }, + "c": 3, + "d": 4, }, "g": 8, "h": 9, @@ -67,52 +46,28 @@ Object { exports[`ApolloClient write then read will not use a default id getter if id and _id are not present 1`] = ` Object { - "$ROOT_QUERY.bar": Object { - "__typename": "bar", - "foo": Object { - "generated": true, - "id": "$ROOT_QUERY.bar.foo", - "type": "id", - "typename": "foo", - }, - "i": 10, - "j": 11, - }, - "$ROOT_QUERY.bar.foo": Object { - "__typename": "foo", - "k": 12, - "l": 13, - }, - "$ROOT_QUERY.foo": Object { - "__typename": "foo", - "bar": Object { - "generated": true, - "id": "$ROOT_QUERY.foo.bar", - "type": "id", - "typename": "bar", - }, - "c": 3, - "d": 4, - }, - "$ROOT_QUERY.foo.bar": Object { - "__typename": "bar", - "e": 5, - "f": 6, - }, "ROOT_QUERY": Object { "a": 1, "b": 2, "bar": Object { - "generated": true, - "id": "$ROOT_QUERY.bar", - "type": "id", - "typename": "bar", + "__typename": "bar", + "foo": Object { + "__typename": "foo", + "k": 12, + "l": 13, + }, + "i": 10, + "j": 11, }, "foo": Object { - "generated": true, - "id": "$ROOT_QUERY.foo", - "type": "id", - "typename": "foo", + "__typename": "foo", + "bar": Object { + "__typename": "bar", + "e": 5, + "f": 6, + }, + "c": 3, + "d": 4, }, "g": 8, "h": 9, @@ -122,25 +77,16 @@ Object { exports[`ApolloClient write then read will use a default id getter if __typename and _id are present 1`] = ` Object { - "$ROOT_QUERY.foo": Object { - "__typename": "foo", - "bar": Object { - "generated": false, - "id": "bar:foobar", - "type": "id", - "typename": "bar", - }, - "c": 3, - "d": 4, - }, "ROOT_QUERY": Object { "a": 1, "b": 2, "foo": Object { - "generated": true, - "id": "$ROOT_QUERY.foo", - "type": "id", - "typename": "foo", + "__typename": "foo", + "bar": Object { + "__ref": "bar:foobar", + }, + "c": 3, + "d": 4, }, }, "bar:foobar": Object { @@ -154,25 +100,16 @@ Object { exports[`ApolloClient write then read will use a default id getter if __typename and id are present 1`] = ` Object { - "$ROOT_QUERY.foo": Object { - "__typename": "foo", - "bar": Object { - "generated": false, - "id": "bar:foobar", - "type": "id", - "typename": "bar", - }, - "c": 3, - "d": 4, - }, "ROOT_QUERY": Object { "a": 1, "b": 2, "foo": Object { - "generated": true, - "id": "$ROOT_QUERY.foo", - "type": "id", - "typename": "foo", + "__typename": "foo", + "bar": Object { + "__ref": "bar:foobar", + }, + "c": 3, + "d": 4, }, }, "bar:foobar": Object { @@ -186,42 +123,24 @@ Object { exports[`ApolloClient write then read will use a default id getter if one is not specified and __typename is present along with either _id or id 1`] = ` Object { - "$ROOT_QUERY.bar": Object { - "__typename": "bar", - "foo": Object { - "generated": false, - "id": "foo:barfoo", - "type": "id", - "typename": "foo", - }, - "i": 10, - "j": 11, - }, - "$ROOT_QUERY.foo": Object { - "__typename": "foo", - "bar": Object { - "generated": false, - "id": "bar:foobar", - "type": "id", - "typename": "bar", - }, - "c": 3, - "d": 4, - }, "ROOT_QUERY": Object { "a": 1, "b": 2, "bar": Object { - "generated": true, - "id": "$ROOT_QUERY.bar", - "type": "id", - "typename": "bar", + "__typename": "bar", + "foo": Object { + "__ref": "foo:barfoo", + }, + "i": 10, + "j": 11, }, "foo": Object { - "generated": true, - "id": "$ROOT_QUERY.foo", - "type": "id", - "typename": "foo", + "__typename": "foo", + "bar": Object { + "__ref": "bar:foobar", + }, + "c": 3, + "d": 4, }, "g": 8, "h": 9, @@ -243,7 +162,7 @@ Object { exports[`ApolloClient write then read will write data locally which will then be read back 1`] = ` Object { - "$foo.bar": Object { + "bar": Object { "__typename": "Bar", "d": 8, "e": 9, @@ -254,10 +173,7 @@ Object { "a": 7, "b": 2, "bar": Object { - "generated": true, - "id": "$foo.bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, "c": 3, }, @@ -266,25 +182,16 @@ Object { exports[`ApolloClient write then read will write data to a specific id 1`] = ` Object { - "$ROOT_QUERY.foo": Object { - "__typename": "foo", - "bar": Object { - "generated": false, - "id": "foobar", - "type": "id", - "typename": "bar", - }, - "c": 3, - "d": 4, - }, "ROOT_QUERY": Object { "a": 1, "b": 2, "foo": Object { - "generated": true, - "id": "$ROOT_QUERY.foo", - "type": "id", - "typename": "foo", + "__typename": "foo", + "bar": Object { + "__ref": "foobar", + }, + "c": 3, + "d": 4, }, }, "foobar": Object { @@ -306,10 +213,7 @@ Object { "__typename": "Foo", "e": 4, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -329,10 +233,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -352,10 +253,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -375,10 +273,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -398,10 +293,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -421,10 +313,7 @@ Object { "f": 5, "g": 6, "h": Object { - "generated": false, - "id": "bar", - "type": "id", - "typename": "Bar", + "__ref": "bar", }, }, } @@ -432,17 +321,11 @@ Object { exports[`ApolloClient writeQuery will write some deeply nested data to the store 1`] = ` Object { - "$ROOT_QUERY.d": Object { - "__typename": "D", - "e": 4, - }, "ROOT_QUERY": Object { "a": 1, "d": Object { - "generated": true, - "id": "$ROOT_QUERY.d", - "type": "id", - "typename": "D", + "__typename": "D", + "e": 4, }, }, } @@ -450,27 +333,15 @@ Object { exports[`ApolloClient writeQuery will write some deeply nested data to the store 2`] = ` Object { - "$ROOT_QUERY.d": Object { - "__typename": "D", - "e": 4, - "h": Object { - "generated": true, - "id": "$ROOT_QUERY.d.h", - "type": "id", - "typename": "H", - }, - }, - "$ROOT_QUERY.d.h": Object { - "__typename": "H", - "i": 7, - }, "ROOT_QUERY": Object { "a": 1, "d": Object { - "generated": true, - "id": "$ROOT_QUERY.d", - "type": "id", - "typename": "D", + "__typename": "D", + "e": 4, + "h": Object { + "__typename": "H", + "i": 7, + }, }, }, } @@ -478,33 +349,21 @@ Object { exports[`ApolloClient writeQuery will write some deeply nested data to the store 3`] = ` Object { - "$ROOT_QUERY.d": Object { - "__typename": "D", - "e": 4, - "f": 5, - "g": 6, - "h": Object { - "generated": true, - "id": "$ROOT_QUERY.d.h", - "type": "id", - "typename": "H", - }, - }, - "$ROOT_QUERY.d.h": Object { - "__typename": "H", - "i": 7, - "j": 8, - "k": 9, - }, "ROOT_QUERY": Object { "a": 1, "b": 2, "c": 3, "d": Object { - "generated": true, - "id": "$ROOT_QUERY.d", - "type": "id", - "typename": "D", + "__typename": "D", + "e": 4, + "f": 5, + "g": 6, + "h": Object { + "__typename": "H", + "i": 7, + "j": 8, + "k": 9, + }, }, }, } diff --git a/packages/apollo-client/src/__tests__/__snapshots__/client.ts.snap b/packages/apollo-client/src/__tests__/__snapshots__/client.ts.snap index 5f7d0104491..cb19bac5cc9 100644 --- a/packages/apollo-client/src/__tests__/__snapshots__/client.ts.snap +++ b/packages/apollo-client/src/__tests__/__snapshots__/client.ts.snap @@ -1,43 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[ - `@connect should run a query with the connection directive and filter arguments and write the result to the correct store key 1` -] = ` +exports[`@connect should run a query with the connection directive and filter arguments and write the result to the correct store key 1`] = ` Object { "ROOT_QUERY": Object { "abc({\\"order\\":\\"popularity\\"})": Array [ Object { - "generated": true, - "id": "ROOT_QUERY.abc({\\"order\\":\\"popularity\\"}).0", - "type": "id", - "typename": "Book", + "__typename": "Book", + "name": "abcd", }, ], }, - "ROOT_QUERY.abc({\\"order\\":\\"popularity\\"}).0": Object { - "__typename": "Book", - "name": "abcd", - }, } `; -exports[ - `@connect should run a query with the connection directive and write the result to the store key defined in the directive 1` -] = ` +exports[`@connect should run a query with the connection directive and write the result to the store key defined in the directive 1`] = ` Object { "ROOT_QUERY": Object { "abc": Array [ Object { - "generated": true, - "id": "ROOT_QUERY.abc.0", - "type": "id", - "typename": "Book", + "__typename": "Book", + "name": "abcd", }, ], }, - "ROOT_QUERY.abc.0": Object { - "__typename": "Book", - "name": "abcd", - }, } `; diff --git a/packages/apollo-client/src/__tests__/client.ts b/packages/apollo-client/src/__tests__/client.ts index 47fe6d2148d..7b9ce390633 100644 --- a/packages/apollo-client/src/__tests__/client.ts +++ b/packages/apollo-client/src/__tests__/client.ts @@ -391,23 +391,13 @@ describe('client', () => { const initialState: any = { data: { - 'ROOT_QUERY.allPeople({"first":"1"}).people.0': { - name: 'Luke Skywalker', - }, - 'ROOT_QUERY.allPeople({"first":1})': { - people: [ - { - type: 'id', - generated: true, - id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0', - }, - ], - }, ROOT_QUERY: { 'allPeople({"first":1})': { - type: 'id', - id: 'ROOT_QUERY.allPeople({"first":1})', - generated: true, + people: [ + { + name: 'Luke Skywalker', + }, + ], }, }, optimistic: [], @@ -459,23 +449,13 @@ describe('client', () => { const initialState: any = { data: { - 'ROOT_QUERY.allPeople({"first":"1"}).people.0': { - name: 'Luke Skywalker', - }, - 'ROOT_QUERY.allPeople({"first":1})': { - people: [ - { - type: 'id', - generated: true, - id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0', - }, - ], - }, ROOT_QUERY: { 'allPeople({"first":1})': { - type: 'id', - id: 'ROOT_QUERY.allPeople({"first":1})', - generated: true, + people: [ + { + name: 'Luke Skywalker', + }, + ], }, }, optimistic: [], diff --git a/packages/apollo-client/src/__tests__/optimistic.ts b/packages/apollo-client/src/__tests__/optimistic.ts index 433d3edf281..6a23f68344a 100644 --- a/packages/apollo-client/src/__tests__/optimistic.ts +++ b/packages/apollo-client/src/__tests__/optimistic.ts @@ -2,7 +2,7 @@ import { from } from 'rxjs'; import { take, toArray, map } from 'rxjs/operators'; import { assign, cloneDeep } from 'lodash'; import { addTypenameToDocument } from 'apollo-utilities'; -import { InMemoryCache } from 'apollo-cache-inmemory'; +import { InMemoryCache, makeReference } from 'apollo-cache-inmemory'; import { mockSingleLink } from '../__mocks__/mockLinks'; @@ -279,10 +279,10 @@ describe('optimistic mutation results', () => { expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99'); expect(dataInStore).toHaveProperty('Todo66'); expect((dataInStore['TodoList5'] as any).todos).toContainEqual( - realIdValue('Todo66', 'Todo'), + makeReference('Todo66'), ); expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual( - realIdValue('Todo99', 'Todo'), + makeReference('Todo99'), ); }); @@ -298,10 +298,10 @@ describe('optimistic mutation results', () => { expect(dataInStore).toHaveProperty('Todo66'); // can be removed once @types/chai adds deepInclude expect((dataInStore['TodoList5'] as any).todos).toContainEqual( - realIdValue('Todo66', 'Todo'), + makeReference('Todo66'), ); expect((dataInStore['TodoList5'] as any).todos).toContainEqual( - realIdValue('Todo99', 'Todo'), + makeReference('Todo99'), ); expect((dataInStore['Todo99'] as any).text).toBe(expectedText1); expect((dataInStore['Todo66'] as any).text).toBe(expectedText2); @@ -507,10 +507,10 @@ describe('optimistic mutation results', () => { expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99'); expect(dataInStore).toHaveProperty('Todo66'); expect((dataInStore['TodoList5'] as any).todos).toContainEqual( - realIdValue('Todo66', 'Todo'), + makeReference('Todo66'), ); expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual( - realIdValue('Todo99', 'Todo'), + makeReference('Todo99'), ); }); @@ -525,10 +525,10 @@ describe('optimistic mutation results', () => { expect(dataInStore).toHaveProperty('Todo99'); expect(dataInStore).toHaveProperty('Todo66'); expect((dataInStore['TodoList5'] as any).todos).toContainEqual( - realIdValue('Todo66', 'Todo'), + makeReference('Todo66'), ); expect((dataInStore['TodoList5'] as any).todos).toContainEqual( - realIdValue('Todo99', 'Todo'), + makeReference('Todo99'), ); expect((dataInStore['Todo99'] as any).text).toBe(expectedText1); expect((dataInStore['Todo66'] as any).text).toBe(expectedText2); @@ -1189,10 +1189,10 @@ describe('optimistic mutation results', () => { expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99'); expect(dataInStore).toHaveProperty('Todo66'); expect((dataInStore['TodoList5'] as any).todos).toContainEqual( - realIdValue('Todo66', 'Todo'), + makeReference('Todo66'), ); expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual( - realIdValue('Todo99', 'Todo'), + makeReference('Todo99'), ); }); @@ -1638,10 +1638,10 @@ describe('optimistic mutation results', () => { expect(stripSymbols(dataInStore)).not.toHaveProperty('Todo99'); expect(dataInStore).toHaveProperty('Todo66'); expect((dataInStore['TodoList5'] as any).todos).toContainEqual( - realIdValue('Todo66', 'Todo'), + makeReference('Todo66'), ); expect((dataInStore['TodoList5'] as any).todos).not.toContainEqual( - realIdValue('Todo99', 'Todo'), + makeReference('Todo99'), ); }); @@ -1964,12 +1964,3 @@ describe('optimistic mutation - githunt comments', () => { expect(newResult.data.entry.comments.length).toBe(2); }); }); - -function realIdValue(id: string, typename: string) { - return { - type: 'id', - generated: false, - id, - typename, - }; -} diff --git a/packages/apollo-client/src/__tests__/subscribeToMore.ts b/packages/apollo-client/src/__tests__/subscribeToMore.ts index 75558cd386d..c00076059fd 100644 --- a/packages/apollo-client/src/__tests__/subscribeToMore.ts +++ b/packages/apollo-client/src/__tests__/subscribeToMore.ts @@ -239,23 +239,13 @@ describe('subscribeToMore', () => { const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }).restore({ - 'ROOT_QUERY.entry.0': { - value: 1, - }, - 'ROOT_QUERY.entry.1': { - value: 2, - }, ROOT_QUERY: { entry: [ { - type: 'id', - id: 'ROOT_QUERY.entry.0', - generated: true, + value: 1, }, { - type: 'id', - id: 'ROOT_QUERY.entry.1', - generated: true, + value: 2, }, ], }, diff --git a/packages/apollo-client/src/core/__tests__/QueryManager/index.ts b/packages/apollo-client/src/core/__tests__/QueryManager/index.ts index 153c17815eb..96d89f06229 100644 --- a/packages/apollo-client/src/core/__tests__/QueryManager/index.ts +++ b/packages/apollo-client/src/core/__tests__/QueryManager/index.ts @@ -1820,44 +1820,6 @@ describe('QueryManager', () => { .catch(done.fail); }); - it('should error when we attempt to give an id beginning with $', done => { - const query = gql` - query { - author { - firstName - lastName - id - __typename - } - } - `; - const data = { - author: { - firstName: 'John', - lastName: 'Smith', - id: '129', - __typename: 'Author', - }, - }; - const reducerConfig = { - dataIdFromObject: (x: any) => '$' + dataIdFromObject(x), - }; - createQueryManager({ - link: mockSingleLink({ - request: { query }, - result: { data }, - }), - config: reducerConfig, - }) - .query({ query }) - .then(() => { - done.fail(new Error('Returned a result when it should not have.')); - }) - .catch(() => { - done(); - }); - }); - it('should reject a query promise given a GraphQL error', () => { const query = gql` query { @@ -1926,10 +1888,8 @@ describe('QueryManager', () => { .catch(() => { // make that the error thrown doesn't empty the state expect( - (queryManager.dataStore.getCache() as InMemoryCache).extract()[ - '$ROOT_QUERY.author' - ] as Object, - ).toEqual(data['author']); + (queryManager.dataStore.getCache() as InMemoryCache).extract().ROOT_QUERY.author, + ).toEqual(data.author); done(); }); }) @@ -2009,18 +1969,16 @@ describe('QueryManager', () => { errorCallbacks: [ () => { expect( - (queryManager.dataStore.getCache() as InMemoryCache).extract()[ - '$ROOT_QUERY.author' - ] as Object, + (queryManager.dataStore.getCache() as InMemoryCache).extract().ROOT_QUERY.author, ).toEqual(data.author); }, ], }, result => { expect(stripSymbols(result.data)).toEqual(data); - expect((queryManager.dataStore.getCache() as InMemoryCache).extract()[ - '$ROOT_QUERY.author' - ] as Object).toEqual(data.author); + expect( + (queryManager.dataStore.getCache() as InMemoryCache).extract().ROOT_QUERY.author + ).toEqual(data.author); }, ); }); diff --git a/packages/apollo-client/src/core/__tests__/QueryManager/links.ts b/packages/apollo-client/src/core/__tests__/QueryManager/links.ts index 3db21969f81..45e8da2ff7e 100644 --- a/packages/apollo-client/src/core/__tests__/QueryManager/links.ts +++ b/packages/apollo-client/src/core/__tests__/QueryManager/links.ts @@ -347,15 +347,15 @@ describe('Link interactions', () => { link, store: new DataStore( new InMemoryCache({ - cacheResolvers: { + cacheRedirects: { Query: { - book: (_, { id }, context) => { + book(_, { id }, context) { expect(context.getCacheKey).toBeDefined(); const cacheKey = context.getCacheKey({ id, __typename: 'Book', }); - expect(cacheKey.id).toEqual(`Book:${id}`); + expect(cacheKey.__ref).toEqual(`Book:${id}`); return cacheKey; }, }, diff --git a/packages/apollo-utilities/src/getFromAST.ts b/packages/apollo-utilities/src/getFromAST.ts index febe52604d9..39e0d24292f 100644 --- a/packages/apollo-utilities/src/getFromAST.ts +++ b/packages/apollo-utilities/src/getFromAST.ts @@ -3,6 +3,8 @@ import { OperationDefinitionNode, FragmentDefinitionNode, ValueNode, + InlineFragmentNode, + SelectionNode, } from 'graphql'; import { invariant, InvariantError } from 'ts-invariant'; @@ -183,10 +185,26 @@ export function createFragmentMap( fragments.forEach(fragment => { symTable[fragment.name.value] = fragment; }); - return symTable; } +export function getFragmentFromSelection( + selection: SelectionNode, + fragmentMap: FragmentMap, +): InlineFragmentNode | FragmentDefinitionNode | null { + switch (selection.kind) { + case 'InlineFragment': + return selection; + case 'FragmentSpread': { + const fragment = fragmentMap && fragmentMap[selection.name.value]; + invariant(fragment, `No fragment named ${selection.name.value}.`); + return fragment; + } + default: + return null; + } +} + export function getDefaultValues( definition: OperationDefinitionNode | undefined, ): { [key: string]: JsonValue } { diff --git a/packages/apollo-utilities/src/index.ts b/packages/apollo-utilities/src/index.ts index 18b0a815040..946c60395b0 100644 --- a/packages/apollo-utilities/src/index.ts +++ b/packages/apollo-utilities/src/index.ts @@ -13,4 +13,3 @@ export * from './util/maybeDeepFreeze'; export * from './util/mergeDeep'; export * from './util/warnOnce'; export * from './util/stripSymbols'; -export * from './util/mergeDeep'; diff --git a/packages/apollo-utilities/src/storeUtils.ts b/packages/apollo-utilities/src/storeUtils.ts index 2bd1e53a60f..df07fcf6c90 100644 --- a/packages/apollo-utilities/src/storeUtils.ts +++ b/packages/apollo-utilities/src/storeUtils.ts @@ -14,10 +14,12 @@ import { ValueNode, SelectionNode, NameNode, + SelectionSetNode, } from 'graphql'; import stringify from 'fast-json-stable-stringify'; import { InvariantError } from 'ts-invariant'; +import { FragmentMap, getFragmentFromSelection } from './getFromAST'; export interface IdValue { type: 'id'; @@ -261,6 +263,29 @@ export function resultKeyNameFromField(field: FieldNode): string { return field.alias ? field.alias.value : field.name.value; } +export function getTypenameFromResult( + result: Record, + selectionSet: SelectionSetNode, + fragmentMap: FragmentMap, +): string | undefined { + for (const selection of selectionSet.selections) { + if (isField(selection)) { + if (selection.name.value === '__typename') { + return result[resultKeyNameFromField(selection)]; + } + } else { + const typename = getTypenameFromResult( + result, + getFragmentFromSelection(selection, fragmentMap).selectionSet, + fragmentMap, + ); + if (typeof typename === 'string') { + return typename; + } + } + } +} + export function isField(selection: SelectionNode): selection is FieldNode { return selection.kind === 'Field'; } diff --git a/packages/apollo-utilities/src/util/__tests__/mergeDeep.ts b/packages/apollo-utilities/src/util/__tests__/mergeDeep.ts index 13778069032..f16a29c7e03 100644 --- a/packages/apollo-utilities/src/util/__tests__/mergeDeep.ts +++ b/packages/apollo-utilities/src/util/__tests__/mergeDeep.ts @@ -1,4 +1,4 @@ -import { mergeDeep, mergeDeepArray } from '../mergeDeep'; +import { mergeDeep, mergeDeepArray, DeepMerger } from '../mergeDeep'; describe('mergeDeep', function() { it('should return an object if first argument falsy', function() { @@ -136,4 +136,32 @@ describe('mergeDeep', function() { // mergeDeep, it does infer that F should be the return type here: expect(mergeDeepArray(fs).check()).toBe("ok"); }); + + it('supports custom reconciler functions', function () { + const merger = new DeepMerger((target, source, key) => { + const targetValue = target[key]; + const sourceValue = source[key]; + if (Array.isArray(sourceValue)) { + if (!Array.isArray(targetValue)) { + return sourceValue; + } + return [...targetValue, ...sourceValue]; + } + return this.merge(targetValue, sourceValue); + }); + + expect(merger.merge( + { + a: [1, 2, 3], + b: "replace me", + }, + { + a: [4, 5], + b: ["I", "win"], + }, + )).toEqual({ + a: [1, 2, 3, 4, 5], + b: ["I", "win"], + }); + }); }); diff --git a/packages/apollo-utilities/src/util/mergeDeep.ts b/packages/apollo-utilities/src/util/mergeDeep.ts index d83a33fe1df..28d5d112acc 100644 --- a/packages/apollo-utilities/src/util/mergeDeep.ts +++ b/packages/apollo-utilities/src/util/mergeDeep.ts @@ -35,13 +35,13 @@ export function mergeDeep( // element type, which works perfectly when the sources array has a // consistent element type. export function mergeDeepArray(sources: T[]): T { - let target = sources[0] || {} as T; + let target = sources[0] || ({} as T); const count = sources.length; if (count > 1) { - const pastCopies: any[] = []; - target = shallowCopyForMerge(target, pastCopies); + const merger = new DeepMerger(); + target = merger.shallowCopyForMerge(target); for (let i = 1; i < count; ++i) { - target = mergeHelper(target, sources[i], pastCopies); + target = merger.merge(target, sources[i]); } } return target; @@ -51,65 +51,62 @@ function isObject(obj: any): obj is Record { return obj !== null && typeof obj === 'object'; } -function mergeHelper( - target: any, - source: any, - pastCopies: any[], +function defaultReconciler( + this: DeepMerger, + target: Record, + source: Record, + property: string | number, ) { - if (isObject(source) && isObject(target)) { - // In case the target has been frozen, make an extensible copy so that - // we can merge properties into the copy. - if (Object.isExtensible && !Object.isExtensible(target)) { - target = shallowCopyForMerge(target, pastCopies); - } + return this.merge(target[property], source[property]); +} + +export class DeepMerger { + private pastCopies: any[] = []; - Object.keys(source).forEach(sourceKey => { - const sourceValue = source[sourceKey]; - if (hasOwnProperty.call(target, sourceKey)) { - const targetValue = target[sourceKey]; - if (sourceValue !== targetValue) { - // When there is a key collision, we need to make a shallow copy of - // target[sourceKey] so the merge does not modify any source objects. - // To avoid making unnecessary copies, we use a simple array to track - // past copies, since it's safe to modify copies created earlier in - // the merge. We use an array for pastCopies instead of a Map or Set, - // since the number of copies should be relatively small, and some - // Map/Set polyfills modify their keys. - target[sourceKey] = mergeHelper( - shallowCopyForMerge(targetValue, pastCopies), - sourceValue, - pastCopies, - ); + constructor(private reconciler = defaultReconciler) {} + + public merge(target: any, source: any): any { + if (isObject(source) && isObject(target)) { + // Make a shallow copy of target so that we can merge properties into it. + target = this.shallowCopyForMerge(target); + + Object.keys(source).forEach(sourceKey => { + if (hasOwnProperty.call(target, sourceKey)) { + if (source[sourceKey] !== target[sourceKey]) { + target[sourceKey] = this.reconciler(target, source, sourceKey); + } + } else { + // If there is no collision, the target can safely share memory with + // the source, and the recursion can terminate here. + target[sourceKey] = source[sourceKey]; } - } else { - // If there is no collision, the target can safely share memory with - // the source, and the recursion can terminate here. - target[sourceKey] = sourceValue; - } - }); + }); + + return target; + } - return target; + // If source (or target) is not an object, let source replace target. + return source; } - // If source (or target) is not an object, let source replace target. - return source; -} + public isObject = isObject; -function shallowCopyForMerge(value: T, pastCopies: any[]): T { - if ( - value !== null && - typeof value === 'object' && - pastCopies.indexOf(value) < 0 - ) { - if (Array.isArray(value)) { - value = (value as any).slice(0); - } else { - value = { - __proto__: Object.getPrototypeOf(value), - ...value, - }; + public shallowCopyForMerge(value: T): T { + if ( + value !== null && + typeof value === 'object' && + this.pastCopies.indexOf(value) < 0 + ) { + if (Array.isArray(value)) { + value = (value as any).slice(0); + } else { + value = { + __proto__: Object.getPrototypeOf(value), + ...value, + }; + } + this.pastCopies.push(value); } - pastCopies.push(value); + return value; } - return value; }