Skip to content

Commit

Permalink
Add utility to insert GraphMode into a MutableRecordSource
Browse files Browse the repository at this point in the history
Reviewed By: josephsavona

Differential Revision: D32601674

fbshipit-source-id: 001f9c8584c57948fd89cc08184f0a5a3228d6d7
  • Loading branch information
captbaritone authored and facebook-github-bot committed Dec 7, 2021
1 parent 65bf22f commit 03647ab
Show file tree
Hide file tree
Showing 6 changed files with 785 additions and 0 deletions.
124 changes: 124 additions & 0 deletions packages/relay-runtime/store/RelayExperimentalGraphResponseHandler.js
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;
}
}
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",
},
},
}
`);
});
Loading

0 comments on commit 03647ab

Please sign in to comment.