diff --git a/packages/relay-compiler/ARCHITECTURE.md b/packages/relay-compiler/ARCHITECTURE.md index 325013a5cdebd..8d16dbea18fd2 100644 --- a/packages/relay-compiler/ARCHITECTURE.md +++ b/packages/relay-compiler/ARCHITECTURE.md @@ -91,4 +91,4 @@ foo { } ``` -- `GenerateRequisiteFieldTransform`: This optional, Relay-specific transform inserts `id` fields for globally identifiable objects and `__typename` fields wherever the type cannot be statically determined (e.g. for unions). +- `GenerateRequisiteFieldTransform`: This optional, Relay-specific transform inserts object identification fields (which is either an `ID!` field on the `Node` interface or defaults to `id`) and `__typename` fields wherever the type cannot be statically determined (e.g. for unions). diff --git a/packages/relay-compiler/codegen/RelayFileWriter.js b/packages/relay-compiler/codegen/RelayFileWriter.js index 4e8b7f8364caa..6081401bff890 100644 --- a/packages/relay-compiler/codegen/RelayFileWriter.js +++ b/packages/relay-compiler/codegen/RelayFileWriter.js @@ -27,7 +27,7 @@ const writeLegacyFlowFile = require('./writeLegacyFlowFile'); const writeRelayGeneratedFile = require('./writeRelayGeneratedFile'); const {isOperationDefinitionAST} = require('GraphQLSchemaUtils'); -const {generate} = require('RelayCodeGenerator'); +const RelayCodeGenerator = require('RelayCodeGenerator'); const {Map: ImmutableMap} = require('immutable'); import type {RelayGeneratedNode} from 'RelayCodeGenerator'; @@ -144,11 +144,12 @@ class RelayFileWriter implements FileWriterInterface { ); const compilerContext = new RelayCompilerContext(extendedSchema); + const codeGenerator = new RelayCodeGenerator(compilerContext.getNodeIDFieldName()); const compiler = new RelayCompiler( this._baseSchema, compilerContext, this._config.compilerTransforms, - generate, + codeGenerator.generate.bind(codeGenerator), ); const getGeneratedDirectory = definitionName => { diff --git a/packages/relay-compiler/core/RelayCodeGenerator.js b/packages/relay-compiler/core/RelayCodeGenerator.js index 14232cc7cbcb5..cdc1700180e3d 100644 --- a/packages/relay-compiler/core/RelayCodeGenerator.js +++ b/packages/relay-compiler/core/RelayCodeGenerator.js @@ -31,6 +31,7 @@ import type { } from 'RelayConcreteNode'; import type {GeneratedNode} from 'RelayConcreteNode'; import type {Fragment, Root} from 'RelayIR'; +import type {NodeVisitor} from 'RelayIRVisitor'; const {getRawType, isAbstractType, getNullableType} = GraphQLSchemaUtils; /* eslint-disable no-redeclare */ @@ -40,185 +41,192 @@ declare function generate(node: Fragment): ConcreteFragment; export type CompiledDocumentMap = Map; export type RelayGeneratedNode = ConcreteRoot | ConcreteFragment; -/** - * @public - * - * Converts a Relay IR node into a plain JS object representation that can be - * used at runtime. - */ -function generate(node: Root | Fragment): ConcreteRoot | ConcreteFragment { - invariant( - ['Root', 'Fragment'].indexOf(node.kind) >= 0, - 'RelayCodeGenerator: Unknown AST kind `%s`. Source: %s.', - node.kind, - getErrorMessage(node), - ); - return RelayIRVisitor.visit(node, RelayCodeGenVisitor); -} -/* eslint-enable no-redeclare */ +class RelayCodeGenerator { + _codeGeneratorVisitor: NodeVisitor; -const RelayCodeGenVisitor = { - leave: { - Root(node): ConcreteRoot { - return { - argumentDefinitions: node.argumentDefinitions, - kind: 'Root', - name: node.name, - operation: node.operation, - selections: flattenArray(node.selections), - }; - }, + constructor(idField: string) { + this._codeGeneratorVisitor = { + leave: { + Root(node): ConcreteRoot { + return { + argumentDefinitions: node.argumentDefinitions, + kind: 'Root', + name: node.name, + operation: node.operation, + selections: flattenArray(node.selections), + }; + }, - Fragment(node): ConcreteFragment { - return { - argumentDefinitions: node.argumentDefinitions, - kind: 'Fragment', - metadata: node.metadata || null, - name: node.name, - selections: flattenArray(node.selections), - type: node.type.toString(), - }; - }, + Fragment(node): ConcreteFragment { + return { + argumentDefinitions: node.argumentDefinitions, + kind: 'Fragment', + metadata: node.metadata || null, + name: node.name, + selections: flattenArray(node.selections), + type: node.type.toString(), + }; + }, - LocalArgumentDefinition(node): ConcreteArgumentDefinition { - return { - kind: 'LocalArgument', - name: node.name, - type: node.type.toString(), - defaultValue: node.defaultValue, - }; - }, + LocalArgumentDefinition(node): ConcreteArgumentDefinition { + return { + kind: 'LocalArgument', + name: node.name, + type: node.type.toString(), + defaultValue: node.defaultValue, + }; + }, - RootArgumentDefinition(node): ConcreteArgumentDefinition { - return { - kind: 'RootArgument', - name: node.name, - type: node.type ? node.type.toString() : null, - }; - }, + RootArgumentDefinition(node): ConcreteArgumentDefinition { + return { + kind: 'RootArgument', + name: node.name, + type: node.type ? node.type.toString() : null, + }; + }, - Condition(node, key, parent, ancestors): ConcreteSelection { - invariant( - node.condition.kind === 'Variable', - 'RelayCodeGenerator: Expected static `Condition` node to be ' + - 'pruned or inlined. Source: %s.', - getErrorMessage(ancestors[0]), - ); - return { - kind: 'Condition', - passingValue: node.passingValue, - condition: node.condition.variableName, - selections: flattenArray(node.selections), - }; - }, + Condition(node, key, parent, ancestors): ConcreteSelection { + invariant( + node.condition.kind === 'Variable', + 'RelayCodeGenerator: Expected static `Condition` node to be ' + + 'pruned or inlined. Source: %s.', + getErrorMessage(ancestors[0]), + ); + return { + kind: 'Condition', + passingValue: node.passingValue, + condition: node.condition.variableName, + selections: flattenArray(node.selections), + }; + }, - FragmentSpread(node): ConcreteSelection { - return { - kind: 'FragmentSpread', - name: node.name, - args: valuesOrNull(sortByName(node.args)), - }; - }, + FragmentSpread(node): ConcreteSelection { + return { + kind: 'FragmentSpread', + name: node.name, + args: valuesOrNull(sortByName(node.args)), + }; + }, - InlineFragment(node): ConcreteSelection { - return { - kind: 'InlineFragment', - type: node.typeCondition.toString(), - selections: flattenArray(node.selections), - }; - }, + InlineFragment(node): ConcreteSelection { + return { + kind: 'InlineFragment', + type: node.typeCondition.toString(), + selections: flattenArray(node.selections), + }; + }, - LinkedField(node): Array { - const handles = - (node.handles && - node.handles.map(handle => { - return { - kind: 'LinkedHandle', + LinkedField(node): Array { + const handles = + (node.handles && + node.handles.map(handle => { + return { + kind: 'LinkedHandle', + alias: node.alias, + args: valuesOrNull(sortByName(node.args)), + handle: handle.name, + name: node.name, + key: handle.key, + filters: handle.filters, + }; + })) || + []; + const type = getRawType(node.type); + return [ + { + kind: 'LinkedField', alias: node.alias, args: valuesOrNull(sortByName(node.args)), - handle: handle.name, + concreteType: !isAbstractType(type) ? type.toString() : null, name: node.name, - key: handle.key, - filters: handle.filters, - }; - })) || - []; - const type = getRawType(node.type); - return [ - { - kind: 'LinkedField', - alias: node.alias, - args: valuesOrNull(sortByName(node.args)), - concreteType: !isAbstractType(type) ? type.toString() : null, - name: node.name, - plural: isPlural(node.type), - selections: flattenArray(node.selections), - storageKey: getStorageKey(node.name, node.args), + plural: isPlural(node.type), + selections: flattenArray(node.selections), + storageKey: getStorageKey(node.name, node.args), + idField, + }, + ...handles, + ]; }, - ...handles, - ]; - }, - ScalarField(node): Array { - const handles = - (node.handles && - node.handles.map(handle => { - return { - kind: 'ScalarHandle', + ScalarField(node): Array { + const handles = + (node.handles && + node.handles.map(handle => { + return { + kind: 'ScalarHandle', + alias: node.alias, + args: valuesOrNull(sortByName(node.args)), + handle: handle.name, + name: node.name, + key: handle.key, + filters: handle.filters, + }; + })) || + []; + return [ + { + kind: 'ScalarField', alias: node.alias, args: valuesOrNull(sortByName(node.args)), - handle: handle.name, name: node.name, - key: handle.key, - filters: handle.filters, - }; - })) || - []; - return [ - { - kind: 'ScalarField', - alias: node.alias, - args: valuesOrNull(sortByName(node.args)), - name: node.name, - selections: valuesOrUndefined(flattenArray(node.selections)), - storageKey: getStorageKey(node.name, node.args), + selections: valuesOrUndefined(flattenArray(node.selections)), + storageKey: getStorageKey(node.name, node.args), + }, + ...handles, + ]; }, - ...handles, - ]; - }, - Variable(node, key, parent): ConcreteArgument { - return { - kind: 'Variable', - name: parent.name, - variableName: node.variableName, - type: parent.type ? parent.type.toString() : null, - }; - }, + Variable(node, key, parent): ConcreteArgument { + return { + kind: 'Variable', + name: parent.name, + variableName: node.variableName, + type: parent.type ? parent.type.toString() : null, + }; + }, + + Literal(node, key, parent): ConcreteArgument { + return { + kind: 'Literal', + name: parent.name, + value: node.value, + type: parent.type ? parent.type.toString() : null, + }; + }, - Literal(node, key, parent): ConcreteArgument { - return { - kind: 'Literal', - name: parent.name, - value: node.value, - type: parent.type ? parent.type.toString() : null, - }; - }, + Argument(node, key, parent, ancestors): ?ConcreteArgument { + invariant( + ['Variable', 'Literal'].indexOf(node.value.kind) >= 0, + 'RelayCodeGenerator: Complex argument values (Lists or ' + + 'InputObjects with nested variables) are not supported, argument ' + + '`%s` had value `%s`. Source: %s.', + node.name, + prettyStringify(node.value), + getErrorMessage(ancestors[0]), + ); + return node.value.value !== null ? node.value : null; + }, + }, + }; + } - Argument(node, key, parent, ancestors): ?ConcreteArgument { - invariant( - ['Variable', 'Literal'].indexOf(node.value.kind) >= 0, - 'RelayCodeGenerator: Complex argument values (Lists or ' + - 'InputObjects with nested variables) are not supported, argument ' + - '`%s` had value `%s`. Source: %s.', - node.name, - prettyStringify(node.value), - getErrorMessage(ancestors[0]), - ); - return node.value.value !== null ? node.value : null; - }, - }, -}; + /** + * @public + * + * Converts a Relay IR node into a plain JS object representation that can be + * used at runtime. + */ + generate(node: Root | Fragment): ConcreteRoot | ConcreteFragment { + invariant( + ['Root', 'Fragment'].indexOf(node.kind) >= 0, + 'RelayCodeGenerator: Unknown AST kind `%s`. Source: %s.', + node.kind, + getErrorMessage(node), + ); + return RelayIRVisitor.visit(node, this._codeGeneratorVisitor); + } +} +/* eslint-enable no-redeclare */ function isPlural(type: any): boolean { return getNullableType(type) instanceof GraphQLList; @@ -274,4 +282,4 @@ function getStorageKey( return isLiteral ? formatStorageKey(fieldName, preparedArgs) : null; } -module.exports = {generate}; +module.exports = RelayCodeGenerator; diff --git a/packages/relay-compiler/core/__tests__/RelayCodeGenerator-test.js b/packages/relay-compiler/core/__tests__/RelayCodeGenerator-test.js index d9a9b30939545..935c53bc479fb 100644 --- a/packages/relay-compiler/core/__tests__/RelayCodeGenerator-test.js +++ b/packages/relay-compiler/core/__tests__/RelayCodeGenerator-test.js @@ -31,9 +31,10 @@ describe('RelayCodeGenerator', () => { const context = new RelayCompilerContext(RelayTestSchema).addAll( definitions, ); + const codeGenerator = new RelayCodeGenerator(context.getNodeIDFieldName()); return context .documents() - .map(doc => prettyStringify(RelayCodeGenerator.generate(doc))) + .map(doc => prettyStringify(codeGenerator.generate(doc))) .join('\n\n'); }); }); diff --git a/packages/relay-compiler/core/__tests__/RelayCompiler-test.js b/packages/relay-compiler/core/__tests__/RelayCompiler-test.js index 1f1e0b4d87843..4715e85c6be7e 100644 --- a/packages/relay-compiler/core/__tests__/RelayCompiler-test.js +++ b/packages/relay-compiler/core/__tests__/RelayCompiler-test.js @@ -15,7 +15,7 @@ require('configureForRelayOSS'); const {transformASTSchema} = require('ASTConvert'); -const {generate} = require('RelayCodeGenerator'); +const RelayCodeGenerator = require('RelayCodeGenerator'); const RelayCompiler = require('RelayCompiler'); const RelayCompilerContext = require('RelayCompilerContext'); const RelayIRTransforms = require('RelayIRTransforms'); @@ -36,11 +36,13 @@ describe('RelayCompiler', () => { RelayTestSchema, RelayIRTransforms.schemaExtensions, ); + const context = new RelayCompilerContext(relaySchema); + const codeGenerator = new RelayCodeGenerator(context.getNodeIDFieldName()); const compiler = new RelayCompiler( RelayTestSchema, - new RelayCompilerContext(relaySchema), + context, RelayIRTransforms, - generate, + codeGenerator.generate.bind(codeGenerator), ); compiler.addDefinitions(parseGraphQLText(relaySchema, text).definitions); return Array.from(compiler.compile().values()) diff --git a/packages/relay-compiler/core/__tests__/fixtures/code-generator/linked-handle-field.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/code-generator/linked-handle-field.golden.txt index 39502c3686576..42b5137f49d06 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/code-generator/linked-handle-field.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/code-generator/linked-handle-field.golden.txt @@ -27,7 +27,8 @@ "storageKey": null } ], - "storageKey": "friends{\"first\":10}" + "storageKey": "friends{\"first\":10}", + "idField": "id" }, { "kind": "LinkedHandle", diff --git a/packages/relay-compiler/core/__tests__/fixtures/code-generator/query-with-fragment-variables.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/code-generator/query-with-fragment-variables.golden.txt index bd227e48701db..a71bc3dc453a8 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/code-generator/query-with-fragment-variables.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/code-generator/query-with-fragment-variables.golden.txt @@ -72,7 +72,8 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } @@ -133,13 +134,16 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": "friends{\"first\":10}" + "storageKey": "friends{\"first\":10}", + "idField": "id" } ], "type": "User" @@ -181,7 +185,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], "type": "User" diff --git a/packages/relay-compiler/core/__tests__/fixtures/compiler/client-fields.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/compiler/client-fields.golden.txt index 0972abf36265a..bee37ac54ddc9 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/compiler/client-fields.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/compiler/client-fields.golden.txt @@ -48,10 +48,12 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedField", @@ -69,10 +71,12 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "FragmentSpread", @@ -165,7 +169,8 @@ "args": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], "type": "Query" @@ -217,7 +222,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } diff --git a/packages/relay-compiler/core/__tests__/fixtures/compiler/connection.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/compiler/connection.golden.txt index 1c965ab70a040..cb2d586d4b02d 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/compiler/connection.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/compiler/connection.golden.txt @@ -116,7 +116,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "ScalarField", @@ -126,7 +127,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedField", @@ -151,13 +153,16 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "ScalarField", @@ -167,7 +172,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "ScalarField", @@ -177,7 +183,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedField", @@ -202,15 +209,18 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], "type": "Query" @@ -389,7 +399,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "ScalarField", @@ -399,7 +410,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedField", @@ -424,10 +436,12 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": "friends{\"first\":10}" + "storageKey": "friends{\"first\":10}", + "idField": "id" }, { "kind": "LinkedHandle", @@ -453,7 +467,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "ScalarField", @@ -470,7 +485,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "ScalarField", @@ -480,7 +496,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedField", @@ -505,10 +522,12 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": "comments{\"first\":10}" + "storageKey": "comments{\"first\":10}", + "idField": "id" }, { "kind": "LinkedHandle", @@ -529,7 +548,8 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } diff --git a/packages/relay-compiler/core/__tests__/fixtures/compiler/kitchen-sink.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/compiler/kitchen-sink.golden.txt index 4fca89106f140..73ac75671eaa8 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/compiler/kitchen-sink.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/compiler/kitchen-sink.golden.txt @@ -77,7 +77,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedField", @@ -116,7 +117,8 @@ "storageKey": null } ], - "storageKey": "profilePicture{\"size\":32}" + "storageKey": "profilePicture{\"size\":32}", + "idField": "id" }, { "kind": "LinkedField", @@ -155,7 +157,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedField", @@ -180,7 +183,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "Condition", @@ -281,7 +285,8 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], "type": "Query" @@ -383,7 +388,8 @@ "storageKey": null } ], - "storageKey": "friends{\"first\":5}" + "storageKey": "friends{\"first\":5}", + "idField": "id" }, { "kind": "LinkedField", @@ -422,7 +428,8 @@ "storageKey": null } ], - "storageKey": "profilePicture{\"size\":32}" + "storageKey": "profilePicture{\"size\":32}", + "idField": "id" }, { "kind": "LinkedField", @@ -461,7 +468,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedField", @@ -486,14 +494,16 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } diff --git a/packages/relay-compiler/core/__tests__/fixtures/compiler/linked-handle-field.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/compiler/linked-handle-field.golden.txt index 47505db2717d3..82173721d0b44 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/compiler/linked-handle-field.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/compiler/linked-handle-field.golden.txt @@ -47,12 +47,14 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], "type": "Query" @@ -130,7 +132,8 @@ "storageKey": null } ], - "storageKey": "friends{\"first\":10}" + "storageKey": "friends{\"first\":10}", + "idField": "id" }, { "kind": "LinkedHandle", @@ -151,7 +154,8 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } diff --git a/packages/relay-compiler/core/__tests__/fixtures/compiler/scalar-handle-field.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/compiler/scalar-handle-field.golden.txt index ddabf83f60c31..fd4ea17bf2d36 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/compiler/scalar-handle-field.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/compiler/scalar-handle-field.golden.txt @@ -41,7 +41,8 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], "type": "Query" @@ -115,7 +116,8 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } diff --git a/packages/relay-compiler/core/__tests__/fixtures/compiler/unions.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/compiler/unions.golden.txt index 193c8df588e5d..27550ef8f4d9a 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/compiler/unions.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/compiler/unions.golden.txt @@ -40,7 +40,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] }, @@ -58,7 +59,8 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], "type": "Query" @@ -115,7 +117,8 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] }, @@ -133,7 +136,8 @@ ] } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ] } diff --git a/packages/relay-compiler/core/__tests__/fixtures/compiler/viewer-query.golden.txt b/packages/relay-compiler/core/__tests__/fixtures/compiler/viewer-query.golden.txt index c36830ff247c2..aa3544b722497 100644 --- a/packages/relay-compiler/core/__tests__/fixtures/compiler/viewer-query.golden.txt +++ b/packages/relay-compiler/core/__tests__/fixtures/compiler/viewer-query.golden.txt @@ -29,10 +29,12 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], "type": "Query" @@ -78,10 +80,12 @@ "storageKey": null } ], - "storageKey": null + "storageKey": null, + "idField": "id" } ], - "storageKey": null + "storageKey": null, + "idField": "id" }, { "kind": "LinkedHandle", diff --git a/packages/relay-compiler/graphql-compiler/core/GraphQLSchemaUtils.js b/packages/relay-compiler/graphql-compiler/core/GraphQLSchemaUtils.js index 88002e789f5dd..1f19d7fd699fc 100644 --- a/packages/relay-compiler/graphql-compiler/core/GraphQLSchemaUtils.js +++ b/packages/relay-compiler/graphql-compiler/core/GraphQLSchemaUtils.js @@ -89,7 +89,7 @@ function canHaveSelections(type: GraphQLType): boolean { * * https://github.com/graphql/graphql-future/blob/master/01%20-%20__id.md */ -function hasID(schema: GraphQLSchema, type: GraphQLCompositeType): boolean { +function hasID(schema: GraphQLSchema, type: GraphQLCompositeType, fieldName: string): boolean { const unmodifiedType = getRawType(type); invariant( unmodifiedType instanceof GraphQLObjectType || @@ -99,7 +99,7 @@ function hasID(schema: GraphQLSchema, type: GraphQLCompositeType): boolean { type, ); const idType = schema.getType(ID_TYPE); - const idField = unmodifiedType.getFields()[ID]; + const idField = unmodifiedType.getFields()[fieldName]; return idField && getRawType(idField.type) === idType; } diff --git a/packages/relay-compiler/graphql-compiler/core/RelayCompilerContext.js b/packages/relay-compiler/graphql-compiler/core/RelayCompilerContext.js index d207330746839..bf74e463a06b4 100644 --- a/packages/relay-compiler/graphql-compiler/core/RelayCompilerContext.js +++ b/packages/relay-compiler/graphql-compiler/core/RelayCompilerContext.js @@ -21,6 +21,8 @@ const {createUserError} = require('./RelayCompilerUserError'); import type {Fragment, Root} from './RelayIR'; import type {GraphQLSchema} from 'graphql'; +import { GraphQLNonNull } from 'graphql'; + const { List: ImmutableList, OrderedMap: ImmutableOrderedMap, @@ -151,6 +153,30 @@ class RelayCompilerContext { context._documents = documents; return context; } + + // TODO + // * cache value + // * see if it has to be boxed as non-null or if it can be a null type too + // * figure out where the NODE_TYPE and ID_TYPE defs should really live + getNodeIDFieldName(): string { + const ID_TYPE = 'ID'; + const NODE_TYPE = 'Node'; + + const nodeInterface = this.schema.getType(NODE_TYPE); + if (nodeInterface) { + const fields = nodeInterface.getFields() + return Object.keys(fields).find(fieldName => { + let fieldType = fields[fieldName].type; + if (fieldType instanceof GraphQLNonNull) { + fieldType = fieldType.ofType; + const idType = this.schema.getType(ID_TYPE); + return fieldType === idType; + } + return null; + }); + } + return null; + } } module.exports = RelayCompilerContext; diff --git a/packages/relay-compiler/transforms/RelayGenerateRequisiteFieldsTransform.js b/packages/relay-compiler/transforms/RelayGenerateRequisiteFieldsTransform.js index 1fa0dda7bb09f..e126d79bafdd9 100644 --- a/packages/relay-compiler/transforms/RelayGenerateRequisiteFieldsTransform.js +++ b/packages/relay-compiler/transforms/RelayGenerateRequisiteFieldsTransform.js @@ -19,11 +19,11 @@ const RelayCompilerContext = require('RelayCompilerContext'); const { assertAbstractType, assertCompositeType, - assertLeafType, + assertLeafType } = require('graphql'); import type {InlineFragment, LinkedField, Node, Selection} from 'RelayIR'; -import type {GraphQLCompositeType, GraphQLLeafType, GraphQLType} from 'graphql'; +import type {GraphQLCompositeType, GraphQLLeafType, GraphQLType } from 'graphql'; const { canHaveSelections, getRawType, @@ -115,78 +115,110 @@ function transformField( * fragment if *any* concrete type implements Node. Then generate a * `... on PossibleType { id }` for every concrete type that does *not* * implement `Node` + * - If the field type implements the Node interface, return a selection of the + * one field in the Node interface that is of type `ID`. */ function generateIDSelections( context: RelayCompilerContext, field: LinkedField, type: GraphQLType, ): ?Array { - if (hasUnaliasedSelection(field, ID)) { + const generatedNodeIdSelections = generateSpecificIDSelections(context, field, type, context.getNodeIDFieldName()); + if (generatedNodeIdSelections === null) { + // The field already has an unaliased selection for the Node ID field. return null; + } else if (generatedNodeIdSelections) { + return [generatedNodeIdSelections]; } - const unmodifiedType = assertCompositeType(getRawType(type)); + + if (context.getNodeIDFieldName() !== ID) { + const generatedFallbackIdSelections = generateSpecificIDSelections(context, field, type, ID); + if (generatedFallbackIdSelections === null) { + // The field already has an unaliased selection for the fallback ID field. + return null; + } else if (generatedFallbackIdSelections) { + return [generatedFallbackIdSelections]; + } + } + const generatedSelections = []; - // Object or Interface type that has `id` field - if ( - canHaveSelections(unmodifiedType) && - hasID(context.schema, unmodifiedType) - ) { - const idType = assertLeafType(context.schema.getType(ID_TYPE)); - generatedSelections.push({ - kind: 'ScalarField', - alias: (null: ?string), - args: [], - directives: [], - handles: null, - metadata: null, - name: ID, - type: idType, - }); - } else if (isAbstractType(unmodifiedType)) { + const unmodifiedType = assertCompositeType(getRawType(type)); + if (isAbstractType(unmodifiedType)) { // Union or interface: concrete types may implement `Node` or have an `id` // field const idType = assertLeafType(context.schema.getType(ID_TYPE)); if (mayImplement(context.schema, unmodifiedType, NODE_TYPE)) { const nodeType = assertCompositeType(context.schema.getType(NODE_TYPE)); - generatedSelections.push(buildIdFragment(nodeType, idType)); + generatedSelections.push(buildIdFragment(nodeType, idType, context.getNodeIDFieldName())); } const abstractType = assertAbstractType(unmodifiedType); context.schema.getPossibleTypes(abstractType).forEach(possibleType => { - if ( - !implementsInterface(possibleType, NODE_TYPE) && - hasID(context.schema, possibleType) - ) { - generatedSelections.push(buildIdFragment(possibleType, idType)); + if (!implementsInterface(possibleType, NODE_TYPE)) { + if (hasID(context.schema, possibleType, context.getNodeIDFieldName())) { + generatedSelections.push(buildIdFragment(possibleType, idType, context.getNodeIDFieldName())); + } else if (hasID(context.schema, possibleType, ID)) { + generatedSelections.push(buildIdFragment(possibleType, idType, ID)); + } } }); } return generatedSelections; } +function generateSpecificIDSelections( + context: RelayCompilerContext, + field: LinkedField, + type: GraphQLType, + idFieldName: string, +): ?Selection { + if (hasUnaliasedSelection(field, idFieldName)) { + return null; + } + const unmodifiedType = assertCompositeType(getRawType(type)); + // Object or Interface type that has `id` field + if ( + canHaveSelections(unmodifiedType) && + hasID(context.schema, unmodifiedType, idFieldName) + ) { + const idType = assertLeafType(context.schema.getType(ID_TYPE)); + return buildIdSelection(idType, idFieldName); + } + return undefined; +} + +/** + * @internal + */ +function buildIdSelection( + idType: GraphQLLeafType, + idFieldName: string, +): Selection { + return { + kind: 'ScalarField', + alias: (null: ?string), + args: [], + directives: [], + handles: null, + metadata: null, + name: idFieldName, + type: idType, + }; +} + /** * @internal */ function buildIdFragment( fragmentType: GraphQLCompositeType, idType: GraphQLLeafType, + fieldName: string ): InlineFragment { return { kind: 'InlineFragment', directives: [], metadata: null, typeCondition: fragmentType, - selections: [ - { - kind: 'ScalarField', - alias: (null: ?string), - args: [], - directives: [], - handles: null, - metadata: null, - name: ID, - type: idType, - }, - ], + selections: [buildIdSelection(idType, fieldName)], }; } diff --git a/packages/relay-compiler/transforms/__tests__/RelayGenerateRequisiteFieldsTransform-test.js b/packages/relay-compiler/transforms/__tests__/RelayGenerateRequisiteFieldsTransform-test.js index 40822306d5d73..43f9f5ae6c8df 100644 --- a/packages/relay-compiler/transforms/__tests__/RelayGenerateRequisiteFieldsTransform-test.js +++ b/packages/relay-compiler/transforms/__tests__/RelayGenerateRequisiteFieldsTransform-test.js @@ -19,6 +19,7 @@ const RelayPrinter = require('RelayPrinter'); const RelayTestSchema = require('RelayTestSchema'); const getGoldenMatchers = require('getGoldenMatchers'); +const { buildSchema } = require('graphql'); describe('RelayGenerateRequisiteFieldsTransform', () => { beforeEach(() => { @@ -44,4 +45,162 @@ describe('RelayGenerateRequisiteFieldsTransform', () => { return documents.join('\n'); }); }); + + describe('concerning a custom Node ID field', () => { + const schema = buildSchema(` + schema { + query: Query + } + + type Query { + node(__id: ID!): Node + artist(slug: String!): Artist + artwork(slug: String!): Artwork + } + + interface Node { + __id: ID! + } + + type Painting { + __id: ID! + width: Float! + height: Float! + } + + type Statue { + __id: ID! + width: Float! + height: Float! + depth: Float! + } + + union Artwork = Painting | Statue + + type Artist implements Node { + __id: ID! + name: String! + artworks: [Artwork] + } + `); + + it('inflects the ID field name from the schema and tests if an unaliased selection for it exists', () => { + const ast = RelayParser.parse(schema, ` + query ArtistQuery { + artist(slug: "banksy") { + __id + } + } + `); + const context = ast.reduce( + (ctx, node) => ctx.add(node), + new RelayCompilerContext(schema) + ); + const nextContext = RelayGenerateRequisiteFieldsTransform.transform(context); + const documents = []; + nextContext.documents().map(doc => { + documents.push(RelayPrinter.print(doc)); + }); + expect(documents.join('\n').trim()).toEqual(` + query ArtistQuery { + artist(slug: "banksy") { + __id + } + } + `.replace(/^\s{8}/gm, '').trim()); + }); + + it('inflects the ID field name from the schema for concrete types', () => { + const ast = RelayParser.parse(schema, ` + query ArtistQuery { + artist(slug: "banksy") { + name + } + } + `); + const context = ast.reduce( + (ctx, node) => ctx.add(node), + new RelayCompilerContext(schema) + ); + const nextContext = RelayGenerateRequisiteFieldsTransform.transform(context); + const documents = []; + nextContext.documents().map(doc => { + documents.push(RelayPrinter.print(doc)); + }); + expect(documents.join('\n').trim()).toEqual(` + query ArtistQuery { + artist(slug: "banksy") { + name + __id + } + } + `.replace(/^\s{8}/gm, '').trim()); + }); + + it('inflects the ID field name from the schema for the `node` field', () => { + const ast = RelayParser.parse(schema, ` + query NodeFieldQuery { + node(__id: "Artist:banksy") { + __typename + } + } + `); + const context = ast.reduce( + (ctx, node) => ctx.add(node), + new RelayCompilerContext(schema) + ); + const nextContext = RelayGenerateRequisiteFieldsTransform.transform(context); + const documents = []; + nextContext.documents().map(doc => { + documents.push(RelayPrinter.print(doc)); + }); + expect(documents.join('\n').trim()).toEqual(` + query NodeFieldQuery { + node(__id: "Artist:banksy") { + __typename + __id + } + } + `.replace(/^\s{8}/gm, '').trim()); + }); + + it('inflects the ID field name from the schema for union types', () => { + const ast = RelayParser.parse(schema, ` + query ArtworkQuery { + artwork(slug: "mona-lisa") { + ... on Painting { + width + height + } + } + } + `); + const context = ast.reduce( + (ctx, node) => ctx.add(node), + new RelayCompilerContext(schema) + ); + const nextContext = RelayGenerateRequisiteFieldsTransform.transform(context); + const documents = []; + nextContext.documents().map(doc => { + documents.push(RelayPrinter.print(doc)); + }); + expect(documents.join('\n').trim()).toEqual(` + query ArtworkQuery { + artwork(slug: "mona-lisa") { + __typename + ... on Painting { + width + height + } + ... on Painting { + __id + } + ... on Statue { + __id + } + } + } + `.replace(/^\s{8}/gm, '').trim()); + }); + }); }); diff --git a/packages/relay-runtime/ARCHITECTURE.md b/packages/relay-runtime/ARCHITECTURE.md index be07b127d7e74..900ff59522386 100644 --- a/packages/relay-runtime/ARCHITECTURE.md +++ b/packages/relay-runtime/ARCHITECTURE.md @@ -14,7 +14,7 @@ The Relay runtime is a full-featured GraphQL client that is designed for high pe ## Comparison to Classic Relay -For users of classic Relay, note that the runtime makes as few assumptions as possible about GraphQL. Compared to earlier versions of Relay there is no concept of routes, there are no limitations on mutation input arguments or side-effects, arbitrary root fields just work, etc. At present, the main restriction from classic Relay that remains is the use of the `Node` interface and `id` field for object identification. However there is no fundamental reason that this restriction can't be relaxed (there is a single place in the codebase where object identity is determined), and we welcome feedback from the community about ways to support customizable object identity without negatively impacting performance. +For users of classic Relay, note that the runtime makes as few assumptions as possible about GraphQL. Compared to earlier versions of Relay there is no concept of routes, there are no limitations on mutation input arguments or side-effects, arbitrary root fields just work, etc. Like classic Relay, modern Relay still uses the `Node` interface for object identification, however modern Relay will inflect the object identification field from the interface by searching for an `ID!` field. ## Data Types diff --git a/packages/relay-runtime/__mocks__/RelayModernTestUtils.js b/packages/relay-runtime/__mocks__/RelayModernTestUtils.js index d417ead256a46..c42c960419d22 100644 --- a/packages/relay-runtime/__mocks__/RelayModernTestUtils.js +++ b/packages/relay-runtime/__mocks__/RelayModernTestUtils.js @@ -146,21 +146,25 @@ const RelayModernTestUtils = { transforms?: ?Array<{ transform: (context: RelayCompilerContext) => RelayCompilerContext, }>, + schema?: ?GraphQLSchema, ): {[key: string]: ConcreteRoot | ConcreteFragment} { const RelayCodeGenerator = require('RelayCodeGenerator'); // eslint-disable-next-line no-shadow const RelayCompilerContext = require('RelayCompilerContext'); const RelayParser = require('RelayParser'); - const RelayTestSchema = require('RelayTestSchema'); + if (!schema) { + schema = require('RelayTestSchema'); + } - const ast = RelayParser.parse(RelayTestSchema, text); - let context = new RelayCompilerContext(RelayTestSchema); + const ast = RelayParser.parse(schema, text); + let context = new RelayCompilerContext(schema); context = ast.reduce((ctx, node) => ctx.add(node), context); context = (transforms || []) .reduce((ctx, {transform}) => transform(ctx), context); + const codeGenerator = new RelayCodeGenerator(context.getNodeIDFieldName()) const documentMap = {}; context.documents().forEach(node => { - documentMap[node.name] = RelayCodeGenerator.generate(node); + documentMap[node.name] = codeGenerator.generate(node); }); return documentMap; }, @@ -175,7 +179,7 @@ const RelayModernTestUtils = { schema?: ?GraphQLSchema, ): {[key: string]: ConcreteBatch | ConcreteFragment} { const {transformASTSchema} = require('ASTConvert'); - const {generate} = require('RelayCodeGenerator'); + const RelayCodeGenerator = require('RelayCodeGenerator'); const RelayCompiler = require('RelayCompiler'); const RelayCompilerContext = require('RelayCompilerContext'); const RelayIRTransforms = require('RelayIRTransforms'); @@ -187,11 +191,13 @@ const RelayModernTestUtils = { schema, RelayIRTransforms.schemaExtensions, ); + const context = new RelayCompilerContext(relaySchema); + const codeGenerator = new RelayCodeGenerator(context.getNodeIDFieldName()) const compiler = new RelayCompiler( schema, - new RelayCompilerContext(relaySchema), + context, RelayIRTransforms, - generate, + codeGenerator.generate.bind(codeGenerator), ); compiler.addDefinitions(parseGraphQLText(relaySchema, text).definitions); diff --git a/packages/relay-runtime/store/RelayResponseNormalizer.js b/packages/relay-runtime/store/RelayResponseNormalizer.js index 4862dcaf8e29a..9f13b5f45a107 100644 --- a/packages/relay-runtime/store/RelayResponseNormalizer.js +++ b/packages/relay-runtime/store/RelayResponseNormalizer.js @@ -79,6 +79,13 @@ function normalize( return normalizer.normalizeResponse(node, dataID, response); } +/** + * Retrieves either the ID field recorded by the compiler or defaults to `id`. + */ +function nodeIDField(field: ConcreteLinkedField, fieldValue: mixed): ?string { + return (field.idField && fieldValue[field.idField]) || fieldValue.id; +} + /** * @private * @@ -241,7 +248,7 @@ class RelayResponseNormalizer { storageKey, ); const nextID = - fieldValue.id || + nodeIDField(field, fieldValue) || // Reuse previously generated client IDs RelayModernRecord.getLinkedRecordID(record, storageKey) || generateRelayClientID(RelayModernRecord.getDataID(record), storageKey); @@ -290,7 +297,7 @@ class RelayResponseNormalizer { ); const nextID = - item.id || + nodeIDField(field, item) || (prevIDs && prevIDs[nextIndex]) || // Reuse previously generated client IDs generateRelayClientID( RelayModernRecord.getDataID(record), diff --git a/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js b/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js index c3b77d6de259c..6502dc2844340 100644 --- a/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js +++ b/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js @@ -338,6 +338,91 @@ describe('RelayResponseNormalizer', () => { }); }); + it('normalizes queries with custom Node "DataID" fields', () => { + const { buildSchema } = require('graphql'); + const schema = buildSchema(` + schema { + query: Query + } + + type Query { + node(customID: ID): Node + } + + interface Node { + customID: ID! + } + + type Artwork implements Node { + customID: ID! + title: String! + artists: [Artist] + } + + type Artist implements Node { + customID: ID! + name: String! + } + `); + + const {ArtworkQuery} = generateWithTransforms( + ` + query ArtworkQuery($id: ID) { + node(customID: $id) { + ... on Artwork { + title + artists { + name + } + } + } + } + `, + null, + schema, + ); + + const payload = { + node: { + __typename: 'Artwork', + customID: 'Artwork:good-song', + title: 'Good Song', + artists: [{ + customID: 'Artist:banksy', + name: 'Banksy', + }], + }, + }; + + const recordSource = new RelayInMemoryRecordSource(); + recordSource.set(ROOT_ID, RelayModernRecord.create(ROOT_ID, ROOT_TYPE)); + const handleFieldPayloads = normalize( + recordSource, + { + dataID: ROOT_ID, + node: ArtworkQuery, + variables: {id: 'Artwork:good-song'}, + }, + payload, + ); + expect(recordSource.toJSON()).toMatchSnapshot(); + // expect(handleFieldPayloads.length).toBe(2); + // expect(handleFieldPayloads[0]).toEqual({ + // args: {}, + // dataID: 'pet', + // fieldKey: 'name', + // handle: 'friendsName', + // handleKey: '__name_friendsName', + // }); + // expect(handleFieldPayloads[1]).toEqual({ + // args: {first: 1}, + // dataID: '4', + // fieldKey: 'friends{"first":1}', + // handle: 'bestFriends', + // handleKey: '__friends_bestFriends', + // }); + }); + it('warns in __DEV__ if payload data is missing an expected field', () => { jest.mock('warning'); diff --git a/packages/relay-runtime/store/__tests__/__snapshots__/RelayResponseNormalizer-test.js.snap b/packages/relay-runtime/store/__tests__/__snapshots__/RelayResponseNormalizer-test.js.snap index e178be6db0828..e8684982ca57c 100644 --- a/packages/relay-runtime/store/__tests__/__snapshots__/RelayResponseNormalizer-test.js.snap +++ b/packages/relay-runtime/store/__tests__/__snapshots__/RelayResponseNormalizer-test.js.snap @@ -151,3 +151,30 @@ Object { }, } `; + +exports[`RelayResponseNormalizer normalizes queries with custom Node "DataID" fields 1`] = ` +Object { + "Artist:banksy": Object { + "__id": "Artist:banksy", + "__typename": "Artist", + "name": "Banksy", + }, + "Artwork:good-song": Object { + "__id": "Artwork:good-song", + "__typename": "Artwork", + "artists": Object { + "__refs": Array [ + "Artist:banksy", + ], + }, + "title": "Good Song", + }, + "client:root": Object { + "__id": "client:root", + "__typename": "__Root", + "node{\\"customID\\":\\"Artwork:good-song\\"}": Object { + "__ref": "Artwork:good-song", + }, + }, +} +`; diff --git a/packages/relay-runtime/util/RelayConcreteNode.js b/packages/relay-runtime/util/RelayConcreteNode.js index 37608f3c9d4a0..2eef0c9057eb0 100644 --- a/packages/relay-runtime/util/RelayConcreteNode.js +++ b/packages/relay-runtime/util/RelayConcreteNode.js @@ -78,6 +78,7 @@ export type ConcreteLinkedField = { plural: boolean, selections: Array, storageKey: ?string, + idField: ?string, }; export type ConcreteLinkedHandle = { alias: ?string,