beforeEach(() => { const route = RelayMetaRoute.get('$fetchRelayQuery'); const queryA = RelayQuery.Root.create( Relay.QL`query{node(id:"123"){id}}`, route, {} ); const queryB = RelayQuery.Root.create( Relay.QL`query{node(id:"456"){id}}`, route, {} ); requestA = new RelayQueryRequest(queryA); requestB = new RelayQueryRequest(queryB); });
/** * Given a query fragment and a data ID, returns a root query that applies * the fragment to the object specified by the data ID. */ buildFragmentQueryForDataID( fragment: RelayQuery.Fragment, dataID: DataID ): RelayQuery.Root { if (RelayRecord.isClientID(dataID)) { const path = this._queuedStore.getPathToRecord( this._rangeData.getCanonicalClientID(dataID), ); invariant( path, 'RelayStoreData.buildFragmentQueryForDataID(): Cannot refetch ' + 'record `%s` without a path.', dataID ); return path.getQuery(fragment); } // Fragment fields cannot be spread directly into the root because they // may not exist on the `Node` type. return RelayQuery.Root.build( fragment.getDebugName() || 'UnknownQuery', RelayNodeInterface.NODE, dataID, [fragment], {identifyingArgName: RelayNodeInterface.ID}, NODE_TYPE ); }
function buildRoot( rootID: DataID, nodes: Array<RelayQuery.Node>, name: string, type: string ): RelayQuery.Root { const children = [idField, typeField]; const fields = []; nodes.forEach(node => { if (node instanceof RelayQuery.Field) { fields.push(node); } else { children.push(node); } }); children.push(RelayQuery.Fragment.build( 'diffRelayQuery', type, fields )); return RelayQuery.Root.build( name, NODE, rootID, children, {identifyingArgName: RelayNodeInterface.ID}, NODE_TYPE ); }
it('prints a generated query with one root argument', () => { const query = RelayQuery.Root.build( 'FooQuery', 'node', '123', [ RelayQuery.Field.build({ fieldName: 'id', type: 'String', }), ], { identifyingArgName: RelayNodeInterface.ID, identifyingArgType: RelayNodeInterface.ID_TYPE, isAbstract: true, isDeferred: false, isPlural: false, }, 'Node' ); const {text, variables} = printRelayOSSQuery(query); expect(text).toEqualPrintedQuery(` query FooQuery($id_0: ID!) { node(id: $id_0) { id } } `); expect(variables).toEqual({ id_0: '123', }); });
it('returns an array including the identifying argument', () => { const root = RelayQuery.Root.build( 'RelayQueryTest', 'foo', '123', null, {identifyingArgName: 'id'}, ); expect(root.getCallsWithValues()).toEqual([{name: 'id', value: '123'}]); });
/** * Wraps `descriptor` in a new top-level ref query. */ function createRefQuery( descriptor: RelayRefQueryDescriptor, context: RelayQuery.Root ): RelayQuery.Root { const node = descriptor.node; invariant( node instanceof RelayQuery.Field || node instanceof RelayQuery.Fragment, 'splitDeferredRelayQueries(): Ref query requires a field or fragment.' ); // Build up JSONPath. const jsonPath = ['$', '*']; let parent; for (let ii = 0; ii < descriptor.nodePath.length; ii++) { parent = descriptor.nodePath[ii]; if (parent instanceof RelayQuery.Field) { jsonPath.push(parent.getSerializationKey()); if (parent.isPlural()) { jsonPath.push('*'); } } } invariant( jsonPath.length > 2, 'splitDeferredRelayQueries(): Ref query requires a complete path.' ); const field: RelayQuery.Field = (parent: any); // Flow const primaryKey = field.getInferredPrimaryKey(); invariant( primaryKey, 'splitDeferredRelayQueries(): Ref query requires a primary key.' ); jsonPath.push(primaryKey); // Create the wrapper root query. const root = RelayQuery.Root.build( context.getName(), RelayNodeInterface.NODES, QueryBuilder.createBatchCallVariable(context.getID(), jsonPath.join('.')), [node], { identifyingArgName: RelayNodeInterface.ID, identifyingArgType: RelayNodeInterface.ID_TYPE, isAbstract: true, isDeferred: true, isPlural: false, }, RelayNodeInterface.NODE_TYPE ); const result: RelayQuery.Root = (root: any); // Flow return result; }
it('warns if the identifyingArgValue is missing', () => { const field = buildIdField(); RelayQuery.Root.build('RelayQueryTest', 'node', null, [field], { isDeferred: true, }); expect([ 'QueryBuilder.createQuery(): An argument value may be required for ' + 'query `%s(%s: ???)`.', 'node', 'id', ]).toBeWarnedNTimes(1); });
it('creates roots', () => { var field = buildIdField(); var root = RelayQuery.Root.build( 'RelayQueryTest', 'node', '4', [field] ); expect(root instanceof RelayQuery.Root).toBe(true); expect(root.getChildren().length).toBe(1); expect(root.getChildren()[0]).toBe(field); });
it('creates deferred roots', () => { const field = buildIdField(); const root = RelayQuery.Root.build( 'RelayQueryTest', 'node', '4', [field], {isDeferred: true}, ); expect(root instanceof RelayQuery.Root).toBe(true); expect(root.getChildren().length).toBe(1); expect(root.getChildren()[0]).toBe(field); });
it('returns the sole identifying argument', () => { const root = RelayQuery.Root.build( 'RelayQueryTest', 'foo', '123', null, {identifyingArgName: 'id'}, ); expect(root.getIdentifyingArg()).toEqual({ name: 'id', value: '123', }); });
it('returns the identifying argument with type', () => { const root = RelayQuery.Root.build( 'RelayQueryTest', 'foo', '123', null, {identifyingArgName: 'id', identifyingArgType: 'scalar'} ); expect(root.getIdentifyingArg()).toEqual({ name: 'id', type: 'scalar', value: '123', }); });
it('creates roots with batch calls', () => { const root = RelayQuery.Root.build( 'RelayQueryTest', 'node', QueryBuilder.createBatchCallVariable('q0', '$.*.id'), [], ); expect(root instanceof RelayQuery.Root).toBe(true); expect(root.getBatchCall()).toEqual({ refParamName: 'ref_q0', sourceQueryID: 'q0', sourceQueryPath: '$.*.id', }); });
function createRelayQuery( node: Object, variables: {[key: string]: mixed} ): RelayQuery.Root { invariant( typeof variables === 'object' && variables != null && !Array.isArray(variables), 'Relay.Query: Expected `variables` to be an object.' ); return RelayQuery.Root.create( node, RelayMetaRoute.get('$createRelayQuery'), variables ); }
it('creates roots', () => { const field = buildIdField(); const root = RelayQuery.Root.build( 'RelayQueryTest', 'node', '4', [field], {}, 'Node', 'FooRoute', ); expect(root instanceof RelayQuery.Root).toBe(true); expect(root.getChildren().length).toBe(1); expect(root.getChildren()[0]).toBe(field); expect(root.getRoute().name).toBe('FooRoute'); });
function createRootQueryFromNodePath(nodePath: NodePath): RelayQuery.Root { return RelayQuery.Root.build( nodePath.name, NODE, nodePath.dataID, [idField, typeField], { identifyingArgName: ID, identifyingArgType: ID_TYPE, isAbstract: true, isDeferred: false, isPlural: false, }, NODE_TYPE, nodePath.routeName, ); }
/** * Traverse the parent chain of `node` wrapping it at each level until it is * either: * * - wrapped in a RelayQuery.Root node * - wrapped in a non-root node that can be split off in a "ref query" (ie. a * root call with a ref param that references another query) * * Additionally ensures that any requisite sibling fields are embedded in each * layer of the wrapper. */ function wrapNode( node: RelayQuery.Node, nodePath: NodePath ): (RelayQuery.Node | RelayRefQueryDescriptor) { for (let ii = nodePath.length - 1; ii >= 0; ii--) { const parent = nodePath[ii]; if ( parent instanceof RelayQuery.Field && parent.getInferredRootCallName() ) { // We can make a "ref query" at this point, so stop wrapping. return new RelayRefQueryDescriptor(node, nodePath.slice(0, ii + 1)); } const siblings = getRequisiteSiblings(node, parent); const children = [node].concat(siblings); // Cast here because we know that `clone` will never return `null` (because // we always give it at least one child). node = (parent.clone(children): any); } invariant( node instanceof RelayQuery.Root, 'splitDeferredRelayQueries(): Cannot build query without a root node.' ); const identifyingArg = node.getIdentifyingArg(); const identifyingArgName = (identifyingArg && identifyingArg.name) || null; const identifyingArgValue = (identifyingArg && identifyingArg.value) || null; const metadata = { identifyingArgName, identifyingArgType: RelayNodeInterface.ID_TYPE, isAbstract: true, isDeferred: true, isPlural: false, }; return RelayQuery.Root.build( node.getName(), node.getFieldName(), identifyingArgValue, node.getChildren(), metadata, node.getType() ); }
Object.keys(route.queries).forEach(queryName => { if (!Component.hasFragment(queryName)) { warning( false, 'Relay.QL: query `%s.queries.%s` is invalid, expected fragment ' + '`%s.fragments.%s` to be defined.', route.name, queryName, Component.displayName, queryName ); return; } var queryBuilder = route.queries[queryName]; if (queryBuilder) { var concreteQuery = buildRQL.Query( queryBuilder, Component, queryName, route.params ); invariant( concreteQuery !== undefined, 'Relay.QL: query `%s.queries.%s` is invalid, a typical query is ' + 'defined using: () => Relay.QL`query { ... }`.', route.name, queryName ); if (concreteQuery) { var rootQuery = RelayQuery.Root.create( concreteQuery, RelayMetaRoute.get(route.name), route.params ); const identifyingArg = rootQuery.getIdentifyingArg(); if (!identifyingArg || identifyingArg.value !== undefined) { querySet[queryName] = rootQuery; return; } } } querySet[queryName] = null; });
it('throws for ref queries', () => { const query = RelayQuery.Root.build( 'RefQueryName', RelayNodeInterface.NODE, QueryBuilder.createBatchCallVariable('q0', '$.*.actor.id'), [ RelayQuery.Field.build({fieldName: 'id', type: 'String'}), RelayQuery.Field.build({fieldName: 'name', type: 'String'}), ], { isDeferred: true, identifyingArgName: RelayNodeInterface.ID, type: RelayNodeInterface.NODE_TYPE, } ); expect(() => printRelayOSSQuery(query)).toFailInvariant( 'printRelayOSSQuery(): Deferred queries are not supported.' ); });
forEachRootCallArg(root, identifyingArgValue => { var nodeRoot; if (isPluralCall) { invariant( identifyingArgValue != null, 'diffRelayQuery(): Unexpected null or undefined value in root call ' + 'argument array for query, `%s(...).', fieldName ); nodeRoot = RelayQuery.Root.build( root.getName(), fieldName, [identifyingArgValue], root.getChildren(), metadata, root.getType() ); } else { // Reuse `root` if it only maps to one result. nodeRoot = root; } // The whole query must be fetched if the root dataID is unknown. var dataID = store.getDataID(storageKey, identifyingArgValue); if (dataID == null) { queries.push(nodeRoot); return; } // Diff the current dataID var scope = makeScope(dataID); var diffOutput = visitor.visit(nodeRoot, path, scope); var diffNode = diffOutput ? diffOutput.diffNode : null; if (diffNode) { invariant( diffNode instanceof RelayQuery.Root, 'diffRelayQuery(): Expected result to be a root query.' ); queries.push(diffNode); } });
it('clones roots with different route', () => { const field = buildIdField(); const root = RelayQuery.Root.build( 'RelayQueryTest', 'node', '4', [field], {}, 'Node', 'FooRoute' ); const newRoute = RelayMetaRoute.get('BarRoute'); const clone = root.cloneWithRoute([field], newRoute); expect(clone instanceof RelayQuery.Root).toBe(true); expect(clone.getChildren().length).toBe(1); expect(clone.getChildren()[0]).toBe(field); expect(clone.getRoute().name).toBe('BarRoute'); expect(root.getRoute().name).toBe('FooRoute'); expect(root.cloneWithRoute([field], RelayMetaRoute.get('FooRoute'))) .toBe(root); });
/** * Given a query fragment and a data ID, returns a root query that applies * the fragment to the object specified by the data ID. */ buildFragmentQueryForDataID( fragment: RelayQuery.Fragment, dataID: DataID ): RelayQuery.Root { if (RelayRecord.isClientID(dataID)) { const path = this._queuedStore.getPathToRecord( this._rangeData.getCanonicalClientID(dataID), ); invariant( path, 'RelayStoreData.buildFragmentQueryForDataID(): Cannot refetch ' + 'record `%s` without a path.', dataID ); return RelayQueryPath.getQuery( this._cachedStore, path, fragment ); } // Fragment fields cannot be spread directly into the root because they // may not exist on the `Node` type. return RelayQuery.Root.build( fragment.getDebugName() || 'UnknownQuery', NODE, dataID, [idField, typeField, fragment], { identifyingArgName: ID, identifyingArgType: ID_TYPE, isAbstract: true, isDeferred: false, isPlural: false, }, NODE_TYPE ); }
function buildRoot( rootID: DataID, nodes: Array<RelayQuery.Node>, name: string, type: string ): RelayQuery.Root { const children = [idField, typeField]; const fields = []; nodes.forEach(node => { if (node instanceof RelayQuery.Field) { fields.push(node); } else { children.push(node); } }); children.push(RelayQuery.Fragment.build( 'diffRelayQuery', type, fields )); return RelayQuery.Root.build( name, NODE, rootID, children, { identifyingArgName: ID, identifyingArgType: ID_TYPE, isAbstract: true, isDeferred: false, isPlural: false, }, NODE_TYPE ); }
it('does not omit "empty" required ref query dependencies', () => { // It isn't possible to produce an "empty" ref query dependency with // `Relay.QL`, but in order to be future-proof against this possible edge // case, we create such a query by hand. const fragment = Relay.QL`fragment on Node{name}`; const id = RelayQuery.Field.build({ fieldName: 'id', metadata: {isRequisite: true}, type: 'String', }); const typename = RelayQuery.Field.build({ fieldName: '__typename', metadata: {isRequisite: true}, type: 'String', }); let queryNode = RelayQuery.Root.build( 'splitDeferredRelayQueries', 'node', '4', [ id, typename, RelayQuery.Field.build({ fieldName: 'hometown', children: [id, getNode(defer(fragment))], metadata: { canHaveSubselections: true, isGenerated: true, inferredPrimaryKey: 'id', inferredRootCallName: 'node', }, type: 'Page', }), ], { identifyingArgName: 'id', } ); queryNode = queryNode.clone( queryNode.getChildren().map((outerChild, ii) => { if (ii === 1) { return outerChild.clone( outerChild.getChildren().map((innerChild, jj) => { if (jj === 0) { return innerChild.cloneAsRefQueryDependency(); } else { return innerChild; } }) ); } else { return outerChild; } }) ); const {required, deferred} = splitDeferredRelayQueries(queryNode); // required part expect(deferred[0].required.getName()).toBe(queryNode.getName()); expect(required).toEqualQueryRoot(getNode(Relay.QL` query { node(id:"4"){hometown{id},id} } `)); expect(required.getID()).toBe('q1'); // deferred part expect(deferred.length).toBe(1); expect(deferred[0].required.getName()).toBe(queryNode.getName()); expect(deferred[0].required).toEqualQueryRoot( filterGeneratedRootFields(getRefNode( Relay.QL` query { nodes(ids:$ref_q1) { ${fragment} } } `, {path: '$.*.hometown.id'} )) ); expect(deferred[0].required.getID()).toBe('q2'); expect(deferred[0].required.isDeferred()).toBe(true); // no nested deferreds expect(deferred[0].deferred).toEqual([]); });
it('updates the range when edge data changes', () => { // NOTE: Hack to preserve `source{id}` in all environments for now. const query = RelayQuery.Root.create(Relay.QL` query { node(id:"123") { friends(find:"node1") { edges { node { id } source { id } } } } } `, RelayMetaRoute.get('$RelayTest'), {}); const payload = { node: { id: '123', friends: { edges: [{ node: { id: 'node1', }, source: { // new edge field id: '456', }, cursor: 'cursor1', }], [PAGE_INFO]: { [HAS_NEXT_PAGE]: true, [HAS_PREV_PAGE]: true, }, }, __typename: 'User', }, }; const results = writePayload(store, writer, query, payload); expect(results).toEqual({ created: { '456': true, // `source` added }, updated: { 'client:1': true, // range updated because an edge had a change 'client:client:1:node1': true, // `source` added to edge }, }); expect(store.getRangeMetadata('client:1', [ {name: 'first', value: 1}, ])).toEqual({ diffCalls: [], filterCalls: [], pageInfo: { [END_CURSOR]: 'cursor1', [HAS_NEXT_PAGE]: true, [HAS_PREV_PAGE]: false, [START_CURSOR]: 'cursor1', }, requestedEdgeIDs: ['client:client:1:node1'], filteredEdges: [ {edgeID: 'client:client:1:node1', nodeID: 'node1'}, ], }); const sourceID = store.getLinkedRecordID('client:client:1:node1', 'source'); expect(sourceID).toBe('456'); expect(store.getField(sourceID, 'id')).toBe('456'); });
/** * Merges the results of a single top-level field into the store. */ function mergeField( writer: RelayQueryWriter, fieldName: string, payload: PayloadObject | PayloadArray, operation: RelayQuery.Operation ): void { // don't write mutation/subscription metadata fields if (fieldName in IGNORED_KEYS) { return; } if (Array.isArray(payload)) { payload.forEach(item => { if (typeof item === 'object' && item != null && !Array.isArray(item)) { if (getString(item, ID)) { mergeField(writer, fieldName, item, operation); } } }); return; } // reassign to preserve type information in below closure const payloadData = payload; const store = writer.getRecordStore(); let recordID = getString(payloadData, ID); let path; if (recordID != null) { path = RelayQueryPath.createForID(recordID, 'writeRelayUpdatePayload'); } else { recordID = store.getDataID(fieldName); // Root fields that do not accept arguments path = RelayQueryPath.create(RelayQuery.Root.build( 'writeRelayUpdatePayload', fieldName, null, null, { identifyingArgName: null, identifyingArgType: null, isAbstract: true, isDeferred: false, isPlural: false, }, ANY_TYPE )); } invariant( recordID, 'writeRelayUpdatePayload(): Expected a record ID in the response payload ' + 'supplied to update the store.' ); // write the results for only the current field, for every instance of that // field in any subfield/fragment in the query. const handleNode = node => { node.getChildren().forEach(child => { if (child instanceof RelayQuery.Fragment) { handleNode(child); } else if ( child instanceof RelayQuery.Field && child.getSerializationKey() === fieldName ) { // for flow: types are lost in closures if (path && recordID) { // ensure the record exists and then update it writer.createRecordIfMissing( child, recordID, path, payloadData ); writer.writePayload( child, recordID, payloadData, path ); } } }); }; handleNode(operation); }
it('returns the name of the root field', () => { const root = RelayQuery.Root.build('RelayQueryTest', 'viewer'); expect(root.getFieldName()).toBe('viewer'); });
invariant( match, 'getRefNode(): Expected call variable of the form `<ref_q\\d+>`.', ); // e.g. `q0` const id = match[1]; // e.g. `{ref_q0: '<ref_q0>'}` const variables = {[name]: '<' + callValue.callVariableName + '>'}; return RelayQuery.Root.create( { ...node, calls: [ QueryBuilder.createCall( 'id', QueryBuilder.createBatchCallVariable(id, refParam.path), ), ], isDeferred: true, }, RelayMetaRoute.get('$RelayTestUtils'), variables, ); }, getVerbatimNode(node, variables) { return RelayTestUtils.filterGeneratedFields( RelayTestUtils.getNode(node, variables), ); }, filterGeneratedFields(query) {
it('returns an empty array when there are no arguments', () => { const root = RelayQuery.Root.build('RelayQueryTest', 'viewer'); expect(root.getCallsWithValues()).toEqual([]); });
it('returns nothing when there is no identifying argument', () => { const root = RelayQuery.Root.build('RelayQueryTest', 'viewer'); expect(root.getIdentifyingArg()).toBeUndefined(); });