-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add utility to insert GraphMode into a MutableRecordSource
Reviewed By: josephsavona Differential Revision: D32601674 fbshipit-source-id: 001f9c8584c57948fd89cc08184f0a5a3228d6d7
- Loading branch information
1 parent
65bf22f
commit 03647ab
Showing
6 changed files
with
785 additions
and
0 deletions.
There are no files selected for viewing
124 changes: 124 additions & 0 deletions
124
packages/relay-runtime/store/RelayExperimentalGraphResponseHandler.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @emails oncall+relay | ||
* @flow | ||
* @format | ||
*/ | ||
|
||
import type { | ||
DataChunk, | ||
GraphModeResponse, | ||
RecordChunk, | ||
} from './RelayExperimentalGraphResponseTransform'; | ||
import type { | ||
MutableRecordSource, | ||
Record, | ||
} from 'relay-runtime/store/RelayStoreTypes'; | ||
|
||
const invariant = require('invariant'); | ||
const RelayModernRecord = require('relay-runtime/store/RelayModernRecord'); | ||
|
||
/** | ||
* Given a stream of GraphMode chunks, populate a MutableRecordSource. | ||
*/ | ||
export function handleGraphModeResponse( | ||
recordSource: MutableRecordSource, | ||
response: GraphModeResponse, | ||
): MutableRecordSource { | ||
const handler = new GraphModeHandler(recordSource); | ||
return handler.populateRecordSource(response); | ||
} | ||
|
||
class GraphModeHandler { | ||
_recordSource: MutableRecordSource; | ||
_streamIdToCacheKey: Map<number, string>; | ||
constructor(recordSource: MutableRecordSource) { | ||
this._recordSource = recordSource; | ||
this._streamIdToCacheKey = new Map(); | ||
} | ||
populateRecordSource(response: GraphModeResponse): MutableRecordSource { | ||
for (const chunk of response) { | ||
switch (chunk.$kind) { | ||
case 'Record': | ||
this._handleRecordChunk(chunk); | ||
break; | ||
case 'Extend': { | ||
const cacheKey = this._lookupCacheKey(chunk.$streamID); | ||
const record = this._recordSource.get(cacheKey); | ||
invariant( | ||
record != null, | ||
`Expected to have a record for cache key ${cacheKey}`, | ||
); | ||
this._populateRecord(record, chunk); | ||
break; | ||
} | ||
case 'Complete': | ||
this._streamIdToCacheKey.clear(); | ||
break; | ||
default: | ||
(chunk.$kind: empty); | ||
} | ||
} | ||
return this._recordSource; | ||
} | ||
|
||
_handleRecordChunk(chunk: RecordChunk) { | ||
const cacheKey = chunk.__id; | ||
let record = this._recordSource.get(cacheKey); | ||
if (record == null) { | ||
record = RelayModernRecord.create(cacheKey, chunk.__typename); | ||
this._recordSource.set(cacheKey, record); | ||
} | ||
|
||
this._streamIdToCacheKey.set(chunk.$streamID, cacheKey); | ||
this._populateRecord(record, chunk); | ||
} | ||
|
||
_populateRecord(parentRecord: Record, chunk: DataChunk) { | ||
for (const [key, value] of Object.entries(chunk)) { | ||
switch (key) { | ||
case '$streamID': | ||
case '$kind': | ||
case '__typename': | ||
break; | ||
default: | ||
if ( | ||
typeof value !== 'object' || | ||
value == null || | ||
Array.isArray(value) | ||
) { | ||
RelayModernRecord.setValue(parentRecord, key, value); | ||
} else { | ||
if (value.hasOwnProperty('__id')) { | ||
// Singular | ||
const streamID = ((value.__id: any): number); | ||
const id = this._lookupCacheKey(streamID); | ||
RelayModernRecord.setLinkedRecordID(parentRecord, key, id); | ||
} else if (value.hasOwnProperty('__ids')) { | ||
// Plural | ||
const streamIDs = ((value.__ids: any): Array<number | null>); | ||
const ids = streamIDs.map(sID => { | ||
return sID == null ? null : this._lookupCacheKey(sID); | ||
}); | ||
RelayModernRecord.setLinkedRecordIDs(parentRecord, key, ids); | ||
} else { | ||
invariant(false, 'Expected object to have either __id or __ids.'); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
_lookupCacheKey(streamID: number): string { | ||
const cacheKey = this._streamIdToCacheKey.get(streamID); | ||
invariant( | ||
cacheKey != null, | ||
`Expected to have a cacheKey for $streamID ${streamID}`, | ||
); | ||
return cacheKey; | ||
} | ||
} |
205 changes: 205 additions & 0 deletions
205
packages/relay-runtime/store/__tests__/RelayExperimentalGraphResponseHandler-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @emails oncall+relay | ||
* @flow strict-local | ||
* @format | ||
*/ | ||
|
||
'use strict'; | ||
|
||
import type {MutableRecordSource} from '../RelayStoreTypes'; | ||
import type {GraphQLTaggedNode, PayloadData, Variables} from 'relay-runtime'; | ||
|
||
const { | ||
handleGraphModeResponse, | ||
} = require('../RelayExperimentalGraphResponseHandler'); | ||
const { | ||
normalizeResponse, | ||
} = require('../RelayExperimentalGraphResponseTransform'); | ||
const {createNormalizationSelector} = require('../RelayModernSelector'); | ||
const RelayRecordSource = require('../RelayRecordSource'); | ||
const {ROOT_ID} = require('../RelayStoreUtils'); | ||
const {graphql} = require('relay-runtime'); | ||
const {getRequest} = require('relay-runtime/query/GraphQLTag'); | ||
|
||
function applyTransform( | ||
query: GraphQLTaggedNode, | ||
response: PayloadData, | ||
variables: Variables, | ||
): MutableRecordSource { | ||
const selector = createNormalizationSelector( | ||
getRequest(query).operation, | ||
ROOT_ID, | ||
variables, | ||
); | ||
const graphModeResponse = normalizeResponse(response, selector); | ||
|
||
const recordSource = new RelayRecordSource(); | ||
|
||
return handleGraphModeResponse(recordSource, graphModeResponse); | ||
} | ||
|
||
test('Basic', () => { | ||
const query = graphql` | ||
query RelayExperimentalGraphResponseHandlerTestQuery { | ||
me { | ||
name | ||
} | ||
} | ||
`; | ||
const response = { | ||
me: { | ||
__typename: 'User', | ||
name: 'Alice', | ||
id: '100', | ||
}, | ||
}; | ||
|
||
const actual = applyTransform(query, response, {}); | ||
expect(actual).toMatchInlineSnapshot(` | ||
Object { | ||
"100": Object { | ||
"__id": "100", | ||
"__typename": "User", | ||
"id": "100", | ||
"name": "Alice", | ||
}, | ||
"client:root": Object { | ||
"__id": "client:root", | ||
"__typename": "__Root", | ||
"me": Object { | ||
"__ref": "100", | ||
}, | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
test('Null Linked Field', () => { | ||
const query = graphql` | ||
query RelayExperimentalGraphResponseHandlerTestNullLinkedQuery { | ||
fetch__User(id: "100") { | ||
name | ||
} | ||
} | ||
`; | ||
const response = { | ||
fetch__User: null, | ||
}; | ||
|
||
const actual = applyTransform(query, response, {}); | ||
|
||
expect(actual).toMatchInlineSnapshot(` | ||
Object { | ||
"client:root": Object { | ||
"__id": "client:root", | ||
"__typename": "__Root", | ||
"fetch__User(id:\\"100\\")": null, | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
test('Plural Linked Fields', () => { | ||
const query = graphql` | ||
query RelayExperimentalGraphResponseHandlerTestPluralLinkedQuery { | ||
me { | ||
allPhones { | ||
isVerified | ||
} | ||
} | ||
} | ||
`; | ||
const response = { | ||
me: { | ||
id: '100', | ||
__typename: 'User', | ||
allPhones: [ | ||
{ | ||
__typename: 'Phone', | ||
isVerified: true, | ||
}, | ||
{ | ||
__typename: 'Phone', | ||
isVerified: false, | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
const actual = applyTransform(query, response, {}); | ||
|
||
expect(actual).toMatchInlineSnapshot(` | ||
Object { | ||
"100": Object { | ||
"__id": "100", | ||
"__typename": "User", | ||
"allPhones": Object { | ||
"__refs": Array [ | ||
"client:100:allPhones:0", | ||
"client:100:allPhones:1", | ||
], | ||
}, | ||
"id": "100", | ||
}, | ||
"client:100:allPhones:0": Object { | ||
"__id": "client:100:allPhones:0", | ||
"__typename": "Phone", | ||
"isVerified": true, | ||
}, | ||
"client:100:allPhones:1": Object { | ||
"__id": "client:100:allPhones:1", | ||
"__typename": "Phone", | ||
"isVerified": false, | ||
}, | ||
"client:root": Object { | ||
"__id": "client:root", | ||
"__typename": "__Root", | ||
"me": Object { | ||
"__ref": "100", | ||
}, | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
test('Plural Scalar Fields', () => { | ||
const query = graphql` | ||
query RelayExperimentalGraphResponseHandlerTestPluralScalarQuery { | ||
me { | ||
emailAddresses | ||
} | ||
} | ||
`; | ||
const response = { | ||
me: { | ||
id: '100', | ||
__typename: 'User', | ||
emailAddreses: ['me@example.com', 'me+spam@example.com'], | ||
}, | ||
}; | ||
|
||
const actual = applyTransform(query, response, {}); | ||
|
||
expect(actual).toMatchInlineSnapshot(` | ||
Object { | ||
"100": Object { | ||
"__id": "100", | ||
"__typename": "User", | ||
"emailAddresses": undefined, | ||
"id": "100", | ||
}, | ||
"client:root": Object { | ||
"__id": "client:root", | ||
"__typename": "__Root", | ||
"me": Object { | ||
"__ref": "100", | ||
}, | ||
}, | ||
} | ||
`); | ||
}); |
Oops, something went wrong.