diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 4dea28d5c4..b7804cfc15 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -94,18 +94,28 @@ export type RootSelectionSetExecutor = ( * import { buildSchema } from 'graphql/utilities'; * import { execute } from 'graphql/execution'; * - * const schema = buildSchema(` - * type Query { - * greeting(name: String!): String - * } - * `); + * const schema = buildSchema( + * ` + * type Query { + * greeting(name: String!): String + * } + * `, + * { + * supplementalConfig: { + * objectTypes: { + * Query: { + * fields: { + * greeting: (_source, { name }) => `Hello, ${name}!`, + * }, + * }, + * }, + * }, + * }, + * ); * * const result = await execute({ * schema, * document: parse('query ($name: String!) { greeting(name: $name) }'), - * rootValue: { - * greeting: ({ name }) => `Hello, ${name}!`, - * }, * variableValues: { name: 'Ada' }, * }); * @@ -185,16 +195,28 @@ function executeImpl(args: ExecutionArgs): PromiseOrValue { * import { buildSchema } from 'graphql/utilities'; * import { experimentalExecuteIncrementally } from 'graphql/execution'; * - * const schema = buildSchema(` - * type Query { - * greeting: String - * } - * `); + * const schema = buildSchema( + * ` + * type Query { + * greeting: String + * } + * `, + * { + * supplementalConfig: { + * objectTypes: { + * Query: { + * fields: { + * greeting: () => 'Hello', + * }, + * }, + * }, + * }, + * }, + * ); * * const result = await experimentalExecuteIncrementally({ * schema, * document: parse('{ greeting }'), - * rootValue: { greeting: 'Hello' }, * }); * * result; // => { data: { greeting: 'Hello' } } @@ -455,7 +477,6 @@ export function executeSubscriptionEvent( * @returns A response stream for a valid subscription, or an execution result containing errors. * @example * ```ts - * // Use a same-named rootValue function to provide the source event stream. * import assert from 'node:assert'; * import { parse } from 'graphql/language'; * import { buildSchema } from 'graphql/utilities'; @@ -466,20 +487,34 @@ export function executeSubscriptionEvent( * yield { greeting: 'Bonjour' }; * } * - * const schema = buildSchema(` - * type Query { - * noop: String - * } - * - * type Subscription { - * greeting: String - * } - * `); + * const schema = buildSchema( + * ` + * type Query { + * noop: String + * } + * + * type Subscription { + * greeting: String + * } + * `, + * { + * supplementalConfig: { + * objectTypes: { + * Subscription: { + * fields: { + * greeting: { + * subscribe: () => greetings(), + * }, + * }, + * }, + * }, + * }, + * }, + * ); * * const result = await subscribe({ * schema, * document: parse('subscription { greeting }'), - * rootValue: { greeting: () => greetings() }, * }); * * assert('next' in result); @@ -627,19 +662,33 @@ function subscribeImpl( * yield { greeting: 'Hello' }; * } * - * const schema = buildSchema(` - * type Query { - * noop: String - * } - * - * type Subscription { - * greeting: String - * } - * `); + * const schema = buildSchema( + * ` + * type Query { + * noop: String + * } + * + * type Subscription { + * greeting: String + * } + * `, + * { + * supplementalConfig: { + * objectTypes: { + * Subscription: { + * fields: { + * greeting: { + * subscribe: () => greetings(), + * }, + * }, + * }, + * }, + * }, + * }, + * ); * const validatedArgs = validateSubscriptionArgs({ * schema, * document: parse('subscription { greeting }'), - * rootValue: { greeting: () => greetings() }, * }); * * assert('schema' in validatedArgs); diff --git a/src/graphql.ts b/src/graphql.ts index 75cc914e8b..94bb4daefc 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -66,18 +66,28 @@ export interface GraphQLArgs * // Execute a complete asynchronous request with variables. * import { graphql, buildSchema } from 'graphql'; * - * const schema = buildSchema(` - * type Query { - * greeting(name: String!): String - * } - * `); + * const schema = buildSchema( + * ` + * type Query { + * greeting(name: String!): String + * } + * `, + * { + * supplementalConfig: { + * objectTypes: { + * Query: { + * fields: { + * greeting: (_source, { name }) => `Hello, ${name}!`, + * }, + * }, + * }, + * }, + * }, + * ); * * const result = await graphql({ * schema, * source: 'query SayHello($name: String!) { greeting(name: $name) }', - * rootValue: { - * greeting: ({ name }) => `Hello, ${name}!`, - * }, * variableValues: { name: 'Ada' }, * operationName: 'SayHello', * }); @@ -124,11 +134,24 @@ export interface GraphQLArgs * // This variant customizes the request pipeline with a harness. * import { buildSchema, defaultHarness, graphql } from 'graphql'; * - * const schema = buildSchema(` - * type Query { - * greeting: String - * } - * `); + * const schema = buildSchema( + * ` + * type Query { + * greeting: String + * } + * `, + * { + * supplementalConfig: { + * objectTypes: { + * Query: { + * fields: { + * greeting: () => 'Hello', + * }, + * }, + * }, + * }, + * }, + * ); * const stages = []; * const abortController = new AbortController(); * const harness = { @@ -153,7 +176,6 @@ export interface GraphQLArgs * const result = await graphql({ * schema, * source: '{ greeting }', - * rootValue: { greeting: 'Hello' }, * rules: [], * maxErrors: 25, * hideSuggestions: true, @@ -186,18 +208,28 @@ export function graphql(args: GraphQLArgs): Promise { * // Execute a complete synchronous request with variables. * import { graphqlSync, buildSchema } from 'graphql'; * - * const schema = buildSchema(` - * type Query { - * greeting(name: String!): String - * } - * `); + * const schema = buildSchema( + * ` + * type Query { + * greeting(name: String!): String + * } + * `, + * { + * supplementalConfig: { + * objectTypes: { + * Query: { + * fields: { + * greeting: (_source, { name }) => `Hello, ${name}!`, + * }, + * }, + * }, + * }, + * }, + * ); * * const result = graphqlSync({ * schema, * source: 'query SayHello($name: String!) { greeting(name: $name) }', - * rootValue: { - * greeting: ({ name }) => `Hello, ${name}!`, - * }, * variableValues: { name: 'Ada' }, * operationName: 'SayHello', * }); diff --git a/src/index.ts b/src/index.ts index 219478ad42..3028b165c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -598,6 +598,20 @@ export type { IntrospectionEnumValue, IntrospectionDirective, BuildSchemaOptions, + ExtendSchemaOptions, + GraphQLSchemaSupplementalConfig, + GraphQLScalarTypeSupplementalConfig, + GraphQLObjectTypeSupplementalConfig, + GraphQLFieldSupplementalConfig, + GraphQLFieldSupplementalConfigMap, + GraphQLArgumentSupplementalConfig, + GraphQLInterfaceTypeSupplementalConfig, + GraphQLUnionTypeSupplementalConfig, + GraphQLEnumTypeSupplementalConfig, + GraphQLEnumValueSupplementalConfig, + GraphQLInputObjectTypeSupplementalConfig, + GraphQLInputFieldSupplementalConfig, + GraphQLDirectiveSupplementalConfig, BreakingChange, SafeChange, DangerousChange, diff --git a/src/type/scalars.ts b/src/type/scalars.ts index a5a05c6a81..25db3a2b69 100644 --- a/src/type/scalars.ts +++ b/src/type/scalars.ts @@ -335,14 +335,20 @@ export const specifiedScalarTypes: ReadonlyArray = * @example * ```ts * import { - * GraphQLScalarType, + * assertScalarType, * GraphQLString, * isSpecifiedScalarType, * } from 'graphql/type'; + * import { buildSchema } from 'graphql/utilities'; * - * const DateTime = new GraphQLScalarType({ - * name: 'DateTime', - * }); + * const schema = buildSchema(` + * scalar DateTime + * + * type Query { + * now: DateTime + * } + * `); + * const DateTime = assertScalarType(schema.getType('DateTime')); * * isSpecifiedScalarType(GraphQLString); // => true * isSpecifiedScalarType(DateTime); // => false diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 15ff4c098d..5b872dfb57 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -1089,6 +1089,33 @@ describe('Schema Builder', () => { buildSchema(sdl, { assumeValidSDL: true }); }); + it('Rejects invalid supplemental config by default', () => { + const supplementalConfig = { + objectTypes: { + Missing: {}, + }, + }; + + expect(() => buildSchema('type Query', { supplementalConfig })).to.throw( + 'Type supplemental config "Missing" does not match a type declared by the document.', + ); + }); + + it('Allows to disable supplemental config validation', () => { + const supplementalConfig = { + objectTypes: { + Missing: {}, + }, + }; + + expect(() => + buildSchema('type Query', { + assumeValidSupplementalConfig: true, + supplementalConfig, + }), + ).to.not.throw(); + }); + it('buildSchema parses directives on directive definitions', () => { const schema = buildSchema( dedent` diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index 1c85e96ecc..11243298ed 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -7,6 +7,7 @@ import { dedent } from '../../__testUtils__/dedent.ts'; import type { Maybe } from '../../jsutils/Maybe.ts'; import type { ASTNode } from '../../language/ast.ts'; +import { Kind } from '../../language/kinds.ts'; import { parse } from '../../language/parser.ts'; import { print } from '../../language/printer.ts'; @@ -43,7 +44,7 @@ function expectExtensionASTNodes(obj: { } function expectASTNode(obj: Maybe<{ readonly astNode: Maybe }>) { - assert(obj?.astNode != null); + assert(obj?.astNode !== undefined && obj.astNode !== null); return expect(print(obj.astNode)); } @@ -67,6 +68,37 @@ describe('extendSchema', () => { expect(extendedSchema).to.equal(schema); }); + it('supplements schema extensions when there are no type definitions', () => { + const schema = buildSchema('type Query'); + const extendedSchema = extendSchema(schema, parse('{ field }'), { + supplementalConfig: { + extensions: { added: 'supplement' }, + }, + }); + + expect(extendedSchema).to.not.equal(schema); + expect(extendedSchema.extensions).to.deep.equal({ + added: 'supplement', + }); + }); + + it('does not supplement schema extensions when the schema already has extensions', () => { + const schema = new GraphQLSchema({ + ...buildSchema('type Query').toConfig(), + extensions: { existing: 'schema' }, + }); + + expect(() => + extendSchema(schema, parse('{ field }'), { + supplementalConfig: { + extensions: { added: 'supplement' }, + }, + }), + ).to.throw( + 'Schema supplemental config cannot add extensions to a schema that already has extensions.', + ); + }); + it('can be used for limited execution', () => { const schema = buildSchema('type Query'); const extendAST = parse(` @@ -86,6 +118,1307 @@ describe('extendSchema', () => { }); }); + it('supplements SDL definitions and extensions with supported config', () => { + const serialize = (value: unknown) => `legacy:${String(value)}`; + const parseValue = (value: unknown) => `parse:${String(value)}`; + const parseLiteral = () => 'literal'; + const coerceOutputValue = (value: unknown) => `output:${String(value)}`; + const coerceInputValue = (value: unknown) => `input:${String(value)}`; + const coerceInputLiteral = () => 'inputLiteral'; + const valueToLiteral = (value: unknown) => + ({ + kind: Kind.STRING, + value: String(value), + }) as const; + + const schema = new GraphQLSchema({}); + const extendAST = parse(` + schema { + query: Query + } + + directive @tag(label: String) on FIELD_DEFINITION + directive @marker on FIELD_DEFINITION + + interface Node { + id: ID + } + + type Result implements Node { + id: ID + } + + scalar Odd @specifiedBy(url: "https://odd.example") + + enum Episode { + NEW_HOPE + } + + enum Side { + LEFT + } + + input Filter { + text: String + } + + union SearchResult = Result + + type Query { + result: Result + } + + interface Named { + name: String + } + + type Product implements Named { + name: String + } + + type ObjectOnly { + id: ID + } + + input ProductFilter { + id: ID = "1" + } + + input InputOnly { + id: ID + } + + enum EnumOnly { + VALUE + } + + scalar Even + scalar MethodOnly + union ProductSearch = Product + + extend interface Node { + label: String + } + + extend type Result { + label: String + } + + extend enum Episode { + EMPIRE + } + + extend enum Side { + RIGHT + } + + extend input Filter { + limit: Int + } + + extend union SearchResult = Product + + extend type Query { + product(name: String): Product + salutation(name: String): String + greeting: String + } + `); + + const extendedSchema = extendSchema(schema, extendAST, { + supplementalConfig: { + extensions: { added: 'schema' }, + directives: { + tag: { + args: { + label: { extensions: { argument: true } }, + }, + extensions: { directive: true }, + }, + marker: { + extensions: { marker: true }, + }, + }, + objectTypes: { + Query: { + fields: { + greeting: () => 'hi', + product: { + resolve: () => ({ name: 'Ada' }), + subscribe: () => undefined, + args: { + name: { extensions: { queryArgument: true } }, + }, + extensions: { queryField: true }, + }, + salutation: { + resolve: (_source, args) => `Hello ${String(args.name)}`, + }, + }, + }, + Result: { + fields: { + label: { extensions: { objectExtension: true } }, + }, + }, + Product: { + isTypeOf: () => true, + fields: { + name: { extensions: { objectDefinition: true } }, + }, + extensions: { objectType: true }, + }, + ObjectOnly: { + isTypeOf: () => true, + extensions: { objectOnly: true }, + }, + }, + scalarTypes: { + Even: { + serialize, + parseValue, + parseLiteral, + coerceOutputValue, + coerceInputValue, + coerceInputLiteral, + valueToLiteral, + extensions: { scalar: true }, + }, + MethodOnly: { + serialize, + }, + }, + interfaceTypes: { + Named: { + resolveType: () => 'Product', + fields: { + name: { extensions: { interfaceDefinition: true } }, + }, + extensions: { interfaceType: true }, + }, + Node: { + fields: { + label: { extensions: { interfaceExtension: true } }, + }, + }, + }, + unionTypes: { + ProductSearch: { + resolveType: () => 'Product', + extensions: { unionType: true }, + }, + }, + enumTypes: { + EnumOnly: { + values: { + VALUE: { value: 'value', extensions: { enumValue: true } }, + }, + extensions: { enumOnly: true }, + }, + Episode: { + values: { + EMPIRE: { value: 5, extensions: { enumExtension: true } }, + }, + }, + Side: {}, + }, + inputObjectTypes: { + ProductFilter: { + fields: { + id: { extensions: { inputDefinition: true } }, + }, + extensions: { inputType: true }, + }, + InputOnly: { + extensions: { inputOnly: true }, + }, + Filter: { + fields: { + limit: { extensions: { inputExtension: true } }, + }, + }, + }, + }, + }); + + expect(extendedSchema.extensions).to.deep.equal({ added: 'schema' }); + + const tag = assertDirective(extendedSchema.getDirective('tag')); + expect(tag.extensions).to.deep.equal({ directive: true }); + expect(tag.args[0].extensions).to.deep.equal({ argument: true }); + const marker = assertDirective(extendedSchema.getDirective('marker')); + expect(marker.extensions).to.deep.equal({ marker: true }); + + const query = assertObjectType(extendedSchema.getType('Query')); + const greetingField = query.getFields().greeting; + expect(greetingField.resolve).to.be.a('function'); + const productField = query.getFields().product; + expect(productField.resolve).to.be.a('function'); + expect(productField.subscribe).to.be.a('function'); + expect(productField.extensions).to.deep.equal({ queryField: true }); + expect(productField.args[0].extensions).to.deep.equal({ + queryArgument: true, + }); + expect( + graphqlSync({ + schema: extendedSchema, + source: + '{ greeting salutation(name: "Ada") product(name: "Ada") { name } }', + }), + ).to.deep.equal({ + data: { + greeting: 'hi', + salutation: 'Hello Ada', + product: { name: 'Ada' }, + }, + }); + + const named = assertInterfaceType(extendedSchema.getType('Named')); + expect(named.resolveType).to.be.a('function'); + expect(named.extensions).to.deep.equal({ interfaceType: true }); + expect(named.getFields().name.extensions).to.deep.equal({ + interfaceDefinition: true, + }); + + const product = assertObjectType(extendedSchema.getType('Product')); + expect(product.isTypeOf).to.be.a('function'); + expect(product.extensions).to.deep.equal({ objectType: true }); + expect(product.getFields().name.extensions).to.deep.equal({ + objectDefinition: true, + }); + + const result = assertObjectType(extendedSchema.getType('Result')); + expect(result.getFields().label.extensions).to.deep.equal({ + objectExtension: true, + }); + + const objectOnly = assertObjectType(extendedSchema.getType('ObjectOnly')); + expect(objectOnly.isTypeOf).to.be.a('function'); + expect(objectOnly.extensions).to.deep.equal({ objectOnly: true }); + + const productFilter = assertInputObjectType( + extendedSchema.getType('ProductFilter'), + ); + expect(productFilter.extensions).to.deep.equal({ inputType: true }); + expect(productFilter.getFields().id.extensions).to.deep.equal({ + inputDefinition: true, + }); + + const inputOnly = assertInputObjectType( + extendedSchema.getType('InputOnly'), + ); + expect(inputOnly.extensions).to.deep.equal({ inputOnly: true }); + + const productSearch = assertUnionType( + extendedSchema.getType('ProductSearch'), + ); + expect(productSearch.resolveType).to.be.a('function'); + expect(productSearch.extensions).to.deep.equal({ unionType: true }); + + const enumOnly = assertEnumType(extendedSchema.getType('EnumOnly')); + expect(enumOnly.extensions).to.deep.equal({ enumOnly: true }); + expect(enumOnly.getValue('VALUE')?.value).to.equal('value'); + expect(enumOnly.getValue('VALUE')?.extensions).to.deep.equal({ + enumValue: true, + }); + + const node = assertInterfaceType(extendedSchema.getType('Node')); + expect(node.getFields().label.extensions).to.deep.equal({ + interfaceExtension: true, + }); + + const episode = assertEnumType(extendedSchema.getType('Episode')); + expect(episode.getValue('EMPIRE')?.value).to.equal(5); + expect(episode.getValue('EMPIRE')?.extensions).to.deep.equal({ + enumExtension: true, + }); + + const filter = assertInputObjectType(extendedSchema.getType('Filter')); + expect(filter.getFields().limit.extensions).to.deep.equal({ + inputExtension: true, + }); + + const odd = assertScalarType(extendedSchema.getType('Odd')); + expect(odd.specifiedByURL).to.equal('https://odd.example'); + const even = assertScalarType(extendedSchema.getType('Even')); + expect(even.serialize).to.equal(serialize); + expect(even.parseValue).to.equal(parseValue); + expect(even.parseLiteral).to.equal(parseLiteral); + expect(even.coerceOutputValue).to.equal(coerceOutputValue); + expect(even.coerceInputValue).to.equal(coerceInputValue); + expect(even.coerceInputLiteral).to.equal(coerceInputLiteral); + expect(even.valueToLiteral).to.equal(valueToLiteral); + expect(even.extensions).to.deep.equal({ scalar: true }); + const methodOnly = assertScalarType(extendedSchema.getType('MethodOnly')); + expect(methodOnly.serialize).to.equal(serialize); + + const side = assertEnumType(extendedSchema.getType('Side')); + expect(side.getValue('RIGHT')).to.not.equal(undefined); + + const searchResult = assertUnionType( + extendedSchema.getType('SearchResult'), + ); + expect(searchResult.getTypes().map((type) => type.name)).to.deep.equal([ + 'Result', + 'Product', + ]); + }); + + it('supplements a new non-root object definition and extension', () => { + const defined = () => 'defined'; + const extended = () => 'extended'; + const schema = buildSchema('type Query'); + const extendAST = parse(` + type ObjectType { + defined: String + } + + extend type ObjectType { + extended: String + } + + extend type Query { + object: ObjectType + } + `); + const extendedSchema = extendSchema(schema, extendAST, { + supplementalConfig: { + objectTypes: { + ObjectType: { + fields: { + defined, + extended, + }, + }, + }, + }, + }); + + expect( + graphqlSync({ + schema: extendedSchema, + source: '{ object { defined extended } }', + rootValue: { object: {} }, + }), + ).to.deep.equal({ + data: { + object: { + defined: 'defined', + extended: 'extended', + }, + }, + }); + }); + + it('preserves SDL descriptions and defaults without supplemental config', () => { + const schema = buildSchema('type Query'); + const extendedSchema = extendSchema( + schema, + parse(` + """Directive description""" + directive @other on SCALAR + + """Object description""" + type DescribedObject { + describedField( + """Argument description""" + arg: String = "default" + ): String + } + + """Interface description""" + interface DescribedInterface { + id: ID + } + + """Enum description""" + enum DescribedEnum { + VALUE + } + + """Union description""" + union DescribedUnion = DescribedObject + + """Scalar description""" + scalar DescribedScalar + + extend scalar DescribedScalar @specifiedBy(url: "https://specified.example/v2") + + scalar FallbackScalar @specifiedBy(url: "https://specified.example/v1") + + extend scalar FallbackScalar @other + + """Input description""" + input DescribedInput { + value: String + } + `), + ); + + const object = assertObjectType(extendedSchema.getType('DescribedObject')); + expect(object.description).to.equal('Object description'); + const arg = object.getFields().describedField.args[0]; + expect(arg.description).to.equal('Argument description'); + expect(arg.default?.literal).to.include({ + kind: Kind.STRING, + value: 'default', + block: false, + }); + + const iface = assertInterfaceType( + extendedSchema.getType('DescribedInterface'), + ); + expect(iface.description).to.equal('Interface description'); + + const enumType = assertEnumType(extendedSchema.getType('DescribedEnum')); + expect(enumType.description).to.equal('Enum description'); + + const union = assertUnionType(extendedSchema.getType('DescribedUnion')); + expect(union.description).to.equal('Union description'); + + const scalar = assertScalarType(extendedSchema.getType('DescribedScalar')); + expect(scalar.description).to.equal('Scalar description'); + expect(scalar.specifiedByURL).to.equal('https://specified.example/v2'); + const fallbackScalar = assertScalarType( + extendedSchema.getType('FallbackScalar'), + ); + expect(fallbackScalar.specifiedByURL).to.equal( + 'https://specified.example/v1', + ); + + const input = assertInputObjectType( + extendedSchema.getType('DescribedInput'), + ); + expect(input.description).to.equal('Input description'); + }); + + it('extends existing elements while supplementing only new definitions', () => { + const schema = buildSchema(` + interface Node { + id: ID + } + + type Result implements Node { + id: ID + } + + enum Episode { + NEW_HOPE + } + + input Filter { + text: String + } + + type Query { + result: Result + } + `); + + const extendedSchema = extendSchema( + schema, + parse(` + extend interface Node { + label: String + } + + extend type Query { + filter(input: Filter): Episode + } + + extend enum Episode { + EMPIRE + } + + extend input Filter { + limit: Int + } + + type AddedObject { + id: ID + } + + interface AddedInterface { + id: ID + } + + enum AddedEnum { + VALUE + } + + input AddedInput { + value: String + } + `), + { + supplementalConfig: { + objectTypes: { + Query: {}, + AddedObject: { + extensions: { added: true }, + }, + }, + interfaceTypes: { + Node: {}, + AddedInterface: { + extensions: { added: true }, + }, + }, + enumTypes: { + Episode: {}, + AddedEnum: { + extensions: { added: true }, + }, + }, + inputObjectTypes: { + Filter: {}, + AddedInput: { + extensions: { added: true }, + }, + }, + }, + }, + ); + + const query = assertObjectType(extendedSchema.getType('Query')); + expect(query.getFields().filter).to.not.equal(undefined); + + const node = assertInterfaceType(extendedSchema.getType('Node')); + expect(node.getFields().label).to.not.equal(undefined); + + const episode = assertEnumType(extendedSchema.getType('Episode')); + expect(episode.getValue('EMPIRE')).to.not.equal(undefined); + + const filter = assertInputObjectType(extendedSchema.getType('Filter')); + expect(filter.getFields().limit).to.not.equal(undefined); + }); + + it('rejects supplemental config for a missing directive', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema( + schema, + parse('directive @tag(label: String) on FIELD_DEFINITION'), + { + supplementalConfig: { + directives: { + missing: {}, + }, + }, + }, + ), + ).to.throw( + 'Directive supplemental config "@missing" does not match a directive declared by the document.', + ); + }); + + it('rejects supplemental config for a missing directive argument', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema( + schema, + parse('directive @tag(label: String) on FIELD_DEFINITION'), + { + supplementalConfig: { + directives: { + tag: { + args: { + missing: { extensions: { argument: true } }, + }, + }, + }, + }, + }, + ), + ).to.throw( + 'Argument supplemental config "@tag(missing:)" does not match an argument declared by the document.', + ); + }); + + it('rejects supplemental config for a missing field coordinate', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema( + schema, + parse('extend type Query { fieldKnown(argKnown: String): String }'), + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + fieldUnknown: () => 'unknown', + }, + }, + }, + }, + }, + ), + ).to.throw( + 'Field supplemental config "Query.fieldUnknown" does not match a field declared by the document.', + ); + }); + + it('rejects supplemental config for a missing field argument coordinate', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema( + schema, + parse('extend type Query { fieldKnown(argKnown: String): String }'), + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + fieldKnown: { + args: { + argUnknown: { extensions: { argument: true } }, + }, + }, + }, + }, + }, + }, + }, + ), + ).to.throw( + 'Argument supplemental config "Query.fieldKnown(argUnknown:)" does not match an argument declared by the document.', + ); + }); + + it('rejects supplemental config for an existing object field', () => { + const schema = buildSchema('type Query { oldField: String }'); + + expect(() => + extendSchema(schema, parse('extend type Query { oldField: String }'), { + assumeValidSDL: true, + supplementalConfig: { + objectTypes: { + Query: { + fields: { + oldField: () => 'old', + }, + }, + }, + }, + }), + ).to.throw( + 'Field supplemental config "Query.oldField" cannot modify an existing field.', + ); + }); + + it('rejects supplemental config for an existing interface field', () => { + const schema = buildSchema(` + interface Node { id: ID } + type Item implements Node { id: ID } + type Query { item: Item } + `); + + expect(() => + extendSchema(schema, parse('extend interface Node { id: ID }'), { + assumeValidSDL: true, + supplementalConfig: { + interfaceTypes: { + Node: { + fields: { + id: { extensions: { field: true } }, + }, + }, + }, + }, + }), + ).to.throw( + 'Field supplemental config "Node.id" cannot modify an existing field.', + ); + }); + + it('rejects supplemental config for an input field not added by the extension', () => { + const schema = buildSchema('input Filter { text: String } type Query'); + + expect(() => + extendSchema(schema, parse('extend input Filter { missing: String }'), { + supplementalConfig: { + inputObjectTypes: { + Filter: { + fields: { + text: { extensions: { input: true } }, + }, + }, + }, + }, + }), + ).to.throw( + 'Input field supplemental config "Filter.text" does not match an input field declared by the document.', + ); + }); + + it('rejects supplemental config for a missing input field coordinate', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema(schema, parse('input EmptyInput'), { + supplementalConfig: { + inputObjectTypes: { + EmptyInput: { + fields: { + missing: { extensions: { input: true } }, + }, + }, + }, + }, + }), + ).to.throw( + 'Input field supplemental config "EmptyInput.missing" does not match an input field declared by the document.', + ); + }); + + it('rejects supplemental config for an existing input field', () => { + const schema = buildSchema('input Filter { text: String } type Query'); + + expect(() => + extendSchema(schema, parse('extend input Filter { text: String }'), { + assumeValidSDL: true, + supplementalConfig: { + inputObjectTypes: { + Filter: { + fields: { + text: { extensions: { input: true } }, + }, + }, + }, + }, + }), + ).to.throw( + 'Input field supplemental config "Filter.text" cannot modify an existing input field.', + ); + }); + + it('rejects supplemental config for an enum value not added by the extension', () => { + const schema = buildSchema('enum Episode { NEW_HOPE } type Query'); + + expect(() => + extendSchema(schema, parse('extend enum Episode { EMPIRE }'), { + supplementalConfig: { + enumTypes: { + Episode: { + values: { + JEDI: { value: 6 }, + }, + }, + }, + }, + }), + ).to.throw( + 'Enum value supplemental config "Episode.JEDI" does not match an enum value declared by the document.', + ); + }); + + it('rejects supplemental config for a missing enum value coordinate', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema(schema, parse('enum EmptyEnum'), { + supplementalConfig: { + enumTypes: { + EmptyEnum: { + values: { + MISSING: { value: 1 }, + }, + }, + }, + }, + }), + ).to.throw( + 'Enum value supplemental config "EmptyEnum.MISSING" does not match an enum value declared by the document.', + ); + }); + + it('rejects supplemental config for an existing enum value', () => { + const schema = buildSchema('enum Episode { NEW_HOPE } type Query'); + + expect(() => + extendSchema(schema, parse('extend enum Episode { NEW_HOPE }'), { + assumeValidSDL: true, + supplementalConfig: { + enumTypes: { + Episode: { + values: { + NEW_HOPE: { value: 4 }, + }, + }, + }, + }, + }), + ).to.throw( + 'Enum value supplemental config "Episode.NEW_HOPE" cannot modify an existing enum value.', + ); + }); + + it('rejects supplemental config for a missing type coordinate', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema(schema, parse('extend type Query { newField: String }'), { + supplementalConfig: { + objectTypes: { + Missing: {}, + }, + }, + }), + ).to.throw( + 'Type supplemental config "Missing" does not match a type declared by the document.', + ); + }); + + it('suggests the matching supplementalConfig property for a type in the wrong bucket', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema(schema, parse('scalar Odd'), { + supplementalConfig: { + objectTypes: { + Odd: { extensions: { objectType: true } }, + }, + }, + }), + ).to.throw( + 'Type supplemental config property "objectTypes.Odd" does not match the type declared or extended by the document. Did you mean "scalarTypes.Odd"?', + ); + }); + + it('suggests the matching supplementalConfig property for a type extension in the wrong bucket', () => { + const schema = buildSchema('scalar Odd type Query'); + + expect(() => + extendSchema( + schema, + parse('extend scalar Odd @specifiedBy(url: "https://odd.example")'), + { + supplementalConfig: { + objectTypes: { + Odd: {}, + }, + }, + }, + ), + ).to.throw( + 'Type supplemental config property "objectTypes.Odd" does not match the type declared or extended by the document. Did you mean "scalarTypes.Odd"?', + ); + }); + + it('rejects supplemental extensions for an existing type extension', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema(schema, parse('extend type Query { newField: String }'), { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + newField: () => 'new', + }, + extensions: { query: true }, + }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "Query.extensions" cannot modify an existing type.', + ); + }); + + it('rejects extra supplemental config for an existing type extension', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema(schema, parse('extend type Query { newField: String }'), { + supplementalConfig: { + objectTypes: { + Query: { + unusedOption: 'unused', + }, + }, + } as any, + }), + ).to.throw( + 'Type supplemental config "Query.unusedOption" cannot modify an existing type.', + ); + }); + + it('rejects field supplemental config when an object extension adds no fields', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema( + schema, + parse('directive @objectTag on OBJECT extend type Query @objectTag'), + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + missing: () => 'missing', + }, + }, + }, + }, + }, + ), + ).to.throw( + 'Field supplemental config "Query.missing" does not match a field declared by the document.', + ); + }); + + it('rejects field supplemental config for a missing interface field coordinate', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema(schema, parse('interface Empty'), { + supplementalConfig: { + interfaceTypes: { + Empty: { + fields: { + missing: { extensions: { field: true } }, + }, + }, + }, + }, + }), + ).to.throw( + 'Field supplemental config "Empty.missing" does not match a field declared by the document.', + ); + }); + + it('rejects field supplemental config when an interface extension adds no fields', () => { + const schema = buildSchema('interface Node { id: ID } type Query'); + + expect(() => + extendSchema( + schema, + parse( + 'directive @interfaceTag on INTERFACE extend interface Node @interfaceTag', + ), + { + supplementalConfig: { + interfaceTypes: { + Node: { + fields: { + missing: { extensions: { field: true } }, + }, + }, + }, + }, + }, + ), + ).to.throw( + 'Field supplemental config "Node.missing" does not match a field declared by the document.', + ); + }); + + it('rejects input field supplemental config when an input extension adds no fields', () => { + const schema = buildSchema('input Filter { text: String } type Query'); + + expect(() => + extendSchema( + schema, + parse( + 'directive @inputTag on INPUT_OBJECT extend input Filter @inputTag', + ), + { + supplementalConfig: { + inputObjectTypes: { + Filter: { + fields: { + missing: { extensions: { input: true } }, + }, + }, + }, + }, + }, + ), + ).to.throw( + 'Input field supplemental config "Filter.missing" does not match an input field declared by the document.', + ); + }); + + it('rejects enum value supplemental config when an enum extension adds no values', () => { + const schema = buildSchema('enum Episode { NEW_HOPE } type Query'); + + expect(() => + extendSchema( + schema, + parse('directive @enumTag on ENUM extend enum Episode @enumTag'), + { + supplementalConfig: { + enumTypes: { + Episode: { + values: { + MISSING: { value: 1 }, + }, + }, + }, + }, + }, + ), + ).to.throw( + 'Enum value supplemental config "Episode.MISSING" does not match an enum value declared by the document.', + ); + }); + + it('rejects supplemental config for an existing type without a matching extension', () => { + const schema = buildSchema('type Query { oldField: String }'); + + expect(() => + extendSchema(schema, parse('type Added'), { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + oldField: () => 'old', + }, + }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "Query" cannot modify an existing type.', + ); + }); + + it('rejects scalar supplemental config for an existing type without a matching extension', () => { + const schema = buildSchema('scalar Odd type Query'); + + expect(() => + extendSchema(schema, parse('type Added'), { + supplementalConfig: { + scalarTypes: { + Odd: { extensions: { scalar: true } }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "Odd" cannot modify an existing type.', + ); + }); + + it('rejects interface supplemental config for an existing type without a matching extension', () => { + const schema = buildSchema('interface Node { id: ID } type Query'); + + expect(() => + extendSchema(schema, parse('type Added'), { + supplementalConfig: { + interfaceTypes: { + Node: { + fields: { + label: { extensions: { field: true } }, + }, + }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "Node" cannot modify an existing type.', + ); + }); + + it('rejects union supplemental config for an existing type without a matching extension', () => { + const schema = buildSchema( + 'type Result union SearchResult = Result type Query', + ); + + expect(() => + extendSchema(schema, parse('type Added'), { + supplementalConfig: { + unionTypes: { + SearchResult: { resolveType: () => 'Result' }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "SearchResult" cannot modify an existing type.', + ); + }); + + it('rejects enum supplemental config for an existing type without a matching extension', () => { + const schema = buildSchema('enum Episode { NEW_HOPE } type Query'); + + expect(() => + extendSchema(schema, parse('type Added'), { + supplementalConfig: { + enumTypes: { + Episode: { + values: { + EMPIRE: { value: 5 }, + }, + }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "Episode" cannot modify an existing type.', + ); + }); + + it('rejects input object supplemental config for an existing type without a matching extension', () => { + const schema = buildSchema('input Filter { text: String } type Query'); + + expect(() => + extendSchema(schema, parse('type Added'), { + supplementalConfig: { + inputObjectTypes: { + Filter: { + fields: { + limit: { extensions: { input: true } }, + }, + }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "Filter" cannot modify an existing type.', + ); + }); + + it('rejects object type supplemental config for an existing type extension', () => { + const schema = buildSchema('type Query'); + + expect(() => + extendSchema(schema, parse('extend type Query { newField: String }'), { + supplementalConfig: { + objectTypes: { + Query: { + isTypeOf: () => true, + }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "Query.isTypeOf" cannot modify an existing type.', + ); + }); + + it('rejects scalar supplemental config for an existing type extension', () => { + const schema = buildSchema('scalar Odd type Query'); + + expect(() => + extendSchema( + schema, + parse('extend scalar Odd @specifiedBy(url: "https://odd.example")'), + { + supplementalConfig: { + scalarTypes: { + Odd: { + extensions: { scalar: true }, + }, + }, + }, + }, + ), + ).to.throw( + 'Type supplemental config "Odd.extensions" cannot modify an existing type.', + ); + }); + + it('rejects union supplemental config for an existing type extension', () => { + const schema = buildSchema(` + type Result + type Product + union SearchResult = Result + type Query + `); + + expect(() => + extendSchema(schema, parse('extend union SearchResult = Product'), { + supplementalConfig: { + unionTypes: { + SearchResult: { + resolveType: () => 'Product', + }, + }, + }, + }), + ).to.throw( + 'Type supplemental config "SearchResult.resolveType" cannot modify an existing type.', + ); + }); + + it('does not supplement fields that already existed on the schema', () => { + const schema = buildSchema('type Query { oldField: String }'); + const extendAST = parse('extend type Query { newField: String }'); + + expect(() => + extendSchema(schema, extendAST, { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + oldField: () => 'old', + }, + }, + }, + }, + }), + ).to.throw( + 'Field supplemental config "Query.oldField" does not match a field declared by the document.', + ); + }); + + it('ignores extra top-level supplementalConfig fields', () => { + const schema = buildSchema('type Query'); + const extendAST = parse('type Added'); + + const extendedSchema = extendSchema(schema, extendAST, { + supplementalConfig: { + unusedOption: 'ignored', + } as any, + }); + + expect(extendedSchema.getType('Added')).to.not.equal(undefined); + }); + + it('ignores extra type supplementalConfig fields from the regular config API', () => { + const schema = buildSchema('type Query'); + const extendAST = parse('type Added'); + + const extendedSchema = extendSchema(schema, extendAST, { + supplementalConfig: { + objectTypes: { + Added: { + description: 'Use SDL for descriptions', + }, + }, + } as any, + }); + + const added = assertObjectType(extendedSchema.getType('Added')); + expect(added.description).to.equal(undefined); + }); + + it('ignores extra field supplementalConfig fields from the regular config API', () => { + const schema = buildSchema('type Query'); + const extendAST = parse('type Added { newField: String }'); + + const extendedSchema = extendSchema(schema, extendAST, { + supplementalConfig: { + objectTypes: { + Added: { + fields: { + newField: { + description: 'Use SDL for descriptions', + }, + }, + }, + }, + } as any, + }); + + const added = assertObjectType(extendedSchema.getType('Added')); + expect(added.getFields().newField.description).to.equal(undefined); + }); + it('Do not modify built-in types and directives', () => { const schema = buildSchema(` type Query { @@ -1252,6 +2585,7 @@ describe('extendSchema', () => { expect(schema.getQueryType()).to.equal(undefined); const extensionSDL = dedent` + """Root schema.""" schema @foo { query: Foo } diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts index f20ccb8848..d94b946b28 100644 --- a/src/utilities/astFromValue.ts +++ b/src/utilities/astFromValue.ts @@ -47,21 +47,23 @@ import { GraphQLID } from '../type/scalars.ts'; * ```ts * import { print } from 'graphql/language'; * import { - * GraphQLInputObjectType, - * GraphQLInt, - * GraphQLList, + * assertInputObjectType, * GraphQLNonNull, * GraphQLString, * } from 'graphql/type'; - * import { astFromValue } from 'graphql/utilities'; + * import { astFromValue, buildSchema } from 'graphql/utilities'; * - * const ReviewInput = new GraphQLInputObjectType({ - * name: 'ReviewInput', - * fields: { - * stars: { type: new GraphQLNonNull(GraphQLInt) }, - * tags: { type: new GraphQLList(GraphQLString) }, - * }, - * }); + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * tags: [String] + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); * * const valueNode = astFromValue( * { stars: 5, tags: ['featured', 'verified'] }, diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index efe39ed337..3b12ad66a4 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -11,6 +11,7 @@ import { GraphQLSchema } from '../type/schema.ts'; import { assertValidSDL } from '../validation/validate.ts'; +import type { GraphQLSchemaSupplementalConfig } from './extendSchema.ts'; import { extendSchemaImpl } from './extendSchema.ts'; /** Options used when building a schema from SDL or a parsed SDL document. */ @@ -21,6 +22,14 @@ export interface BuildSchemaOptions extends GraphQLSchemaValidationOptions { * Default: false */ assumeValidSDL?: boolean | undefined; + /** + * Set to true to assume the supplemental config is valid. + * + * Default: false + */ + assumeValidSupplementalConfig?: boolean | undefined; + /** Supplemental schema constructor config not expressible in SDL. */ + supplementalConfig?: Readonly | undefined; } /** @@ -29,8 +38,8 @@ export interface BuildSchemaOptions extends GraphQLSchemaValidationOptions { * If no schema definition is provided, then it will look for types named Query, * Mutation and Subscription. * - * The resulting schema has no resolver functions, so execution will use the - * default field resolver. + * The resulting schema uses the default field resolver unless resolver + * functions are supplied through `supplementalConfig`. * @param documentAST - The parsed GraphQL document AST. * @param options - Optional configuration for this operation. * @returns The schema built from the provided SDL document. @@ -78,7 +87,7 @@ export function buildASTSchema( }; const config = extendSchemaImpl(emptySchemaConfig, documentAST, options); - if (config.astNode == null) { + if (config.astNode === undefined || config.astNode === null) { for (const type of config.types) { switch (type.name) { // Note: While this could make early assertions to get the correctly @@ -129,6 +138,38 @@ export function buildASTSchema( * ``` * @example * ```ts + * // Supply resolver functions and other constructor-only config alongside SDL. + * import { graphqlSync } from 'graphql'; + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema( + * ` + * type Query { + * greeting(name: String = "world"): String + * } + * `, + * { + * supplementalConfig: { + * objectTypes: { + * Query: { + * fields: { + * greeting: (_source, { name }) => `Hello, ${name}!`, + * }, + * }, + * }, + * }, + * }, + * ); + * + * const result = graphqlSync({ + * schema, + * source: '{ greeting(name: "GraphQL") }', + * }); + * + * result.data; // => { greeting: 'Hello, GraphQL!' } + * ``` + * @example + * ```ts * // This variant enables parser options and omits source locations. * import { buildSchema } from 'graphql/utilities'; * @@ -159,5 +200,7 @@ export function buildSchema( return buildASTSchema(document, { assumeValidSDL: options?.assumeValidSDL, assumeValid: options?.assumeValid, + assumeValidSupplementalConfig: options?.assumeValidSupplementalConfig, + supplementalConfig: options?.supplementalConfig, }); } diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 2d6c23f4a7..2539e6495c 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -39,22 +39,20 @@ import { replaceVariables } from './replaceVariables.ts'; * @example * ```ts * // Coerce runtime input values, returning undefined when coercion fails. - * import { - * GraphQLInputObjectType, - * GraphQLInt, - * GraphQLList, - * GraphQLNonNull, - * GraphQLString, - * } from 'graphql/type'; - * import { coerceInputValue } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * import { buildSchema, coerceInputValue } from 'graphql/utilities'; * - * const ReviewInput = new GraphQLInputObjectType({ - * name: 'ReviewInput', - * fields: { - * stars: { type: new GraphQLNonNull(GraphQLInt) }, - * tags: { type: new GraphQLList(GraphQLString) }, - * }, - * }); + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * tags: [String] + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); * * coerceInputValue({ stars: '5', tags: ['featured'] }, ReviewInput); // => { stars: 5, tags: ['featured'] } * coerceInputValue({ stars: 'bad' }, ReviewInput); // => undefined @@ -170,21 +168,20 @@ export function coerceInputValue( * ```ts * // Coerce literal input values without variables. * import { parseValue } from 'graphql/language'; - * import { - * GraphQLInputObjectType, - * GraphQLInt, - * GraphQLNonNull, - * GraphQLString, - * } from 'graphql/type'; - * import { coerceInputLiteral } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * import { buildSchema, coerceInputLiteral } from 'graphql/utilities'; * - * const ReviewInput = new GraphQLInputObjectType({ - * name: 'ReviewInput', - * fields: { - * stars: { type: new GraphQLNonNull(GraphQLInt) }, - * comment: { type: GraphQLString }, - * }, - * }); + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * comment: String + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); * * coerceInputLiteral( * parseValue('{ stars: 5, comment: "Loved it" }'), diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 381407e9cf..2e83994f3e 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -3,8 +3,10 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap.ts'; import { invariant } from '../jsutils/invariant.ts'; import type { Maybe } from '../jsutils/Maybe.ts'; +import type { ObjMap } from '../jsutils/ObjMap.ts'; import type { + ConstValueNode, DirectiveDefinitionNode, DirectiveExtensionNode, DocumentNode, @@ -31,15 +33,36 @@ import type { } from '../language/ast.ts'; import { Kind } from '../language/kinds.ts'; +/* eslint-disable import/no-deprecated */ import type { + GraphQLArgumentExtensions, + GraphQLEnumTypeExtensions, + GraphQLEnumValueExtensions, GraphQLEnumValueNormalizedConfigMap, GraphQLFieldConfigArgumentMap, + GraphQLFieldExtensions, GraphQLFieldNormalizedConfigMap, + GraphQLFieldResolver, + GraphQLInputFieldExtensions, GraphQLInputFieldNormalizedConfigMap, + GraphQLInputObjectTypeExtensions, + GraphQLInterfaceTypeExtensions, + GraphQLIsTypeOfFn, GraphQLNamedType, GraphQLNullableType, + GraphQLObjectTypeExtensions, + GraphQLScalarInputLiteralCoercer, + GraphQLScalarInputValueCoercer, + GraphQLScalarLiteralParser, + GraphQLScalarOutputValueCoercer, + GraphQLScalarSerializer, + GraphQLScalarTypeExtensions, + GraphQLScalarValueParser, GraphQLType, + GraphQLTypeResolver, + GraphQLUnionTypeExtensions, } from '../type/definition.ts'; +/* eslint-enable import/no-deprecated */ import { GraphQLEnumType, GraphQLInputObjectType, @@ -49,7 +72,14 @@ import { GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, + isEnumType, + isInputObjectType, + isInterfaceType, + isObjectType, + isScalarType, + isUnionType, } from '../type/definition.ts'; +import type { GraphQLDirectiveExtensions } from '../type/directives.ts'; import { GraphQLDeprecatedDirective, GraphQLDirective, @@ -59,6 +89,7 @@ import { import { introspectionTypes } from '../type/introspection.ts'; import { specifiedScalarTypes } from '../type/scalars.ts'; import type { + GraphQLSchemaExtensions, GraphQLSchemaNormalizedConfig, GraphQLSchemaValidationOptions, } from '../type/schema.ts'; @@ -70,7 +101,190 @@ import { getDirectiveValues } from '../execution/values.ts'; import { mapSchemaConfig, SchemaElementKind } from './mapSchemaConfig.ts'; -interface Options extends GraphQLSchemaValidationOptions { +/** + * Supplemental schema constructor config for SDL-built elements when SDL cannot + * express the config directly. + */ +export interface GraphQLSchemaSupplementalConfig { + /** Supplemental config keyed by scalar type name. */ + scalarTypes?: + | Readonly> + | undefined; + /** Supplemental config keyed by object type name. */ + objectTypes?: + | Readonly> + | undefined; + /** Supplemental config keyed by interface type name. */ + interfaceTypes?: + | Readonly> + | undefined; + /** Supplemental config keyed by union type name. */ + unionTypes?: Readonly> | undefined; + /** Supplemental config keyed by enum type name. */ + enumTypes?: Readonly> | undefined; + /** Supplemental config keyed by input object type name. */ + inputObjectTypes?: + | Readonly> + | undefined; + /** Supplemental config keyed by directive name. */ + directives?: Readonly> | undefined; + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +type SupplementalTypeConfigKey = Extract< + keyof GraphQLSchemaSupplementalConfig, + `${string}Types` +>; + +const typeConfigKeyByTypeDefinitionKind: { + readonly [K in TypeDefinitionNode['kind']]: SupplementalTypeConfigKey; +} = { + [Kind.SCALAR_TYPE_DEFINITION]: 'scalarTypes', + [Kind.OBJECT_TYPE_DEFINITION]: 'objectTypes', + [Kind.INTERFACE_TYPE_DEFINITION]: 'interfaceTypes', + [Kind.UNION_TYPE_DEFINITION]: 'unionTypes', + [Kind.ENUM_TYPE_DEFINITION]: 'enumTypes', + [Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'inputObjectTypes', +}; + +/* eslint-disable import/no-deprecated */ +/** Supplemental config for a scalar type. */ +export interface GraphQLScalarTypeSupplementalConfig { + /** + * Deprecated legacy serializer used to convert internal values for response + * output. Use `coerceOutputValue()` instead. + * @deprecated use `coerceOutputValue()` instead, `serialize()` will be removed in v18 + */ + serialize?: GraphQLScalarSerializer | undefined; + /** + * Deprecated legacy parser used to convert externally provided input values. + * Use `coerceInputValue()` instead. + * @deprecated use `coerceInputValue()` instead, `parseValue()` will be removed in v18 + */ + parseValue?: GraphQLScalarValueParser | undefined; + /** + * Deprecated legacy parser used to convert externally provided input + * literals. Use `replaceVariables()` and `coerceInputLiteral()` instead. + * @deprecated use `replaceVariables()` and `coerceInputLiteral()` instead, `parseLiteral()` will be removed in v18 + */ + parseLiteral?: GraphQLScalarLiteralParser | undefined; + /** Coerces an internal value to include in a response. */ + coerceOutputValue?: GraphQLScalarOutputValueCoercer | undefined; + /** Coerces an externally provided value to use as an input. */ + coerceInputValue?: GraphQLScalarInputValueCoercer | undefined; + /** Coerces an externally provided const literal value to use as an input. */ + coerceInputLiteral?: GraphQLScalarInputLiteralCoercer | undefined; + /** Translates an externally provided value to a literal (AST). */ + valueToLiteral?: + | ((inputValue: unknown) => ConstValueNode | undefined) + | undefined; + /** Custom extensions. */ + extensions?: Readonly | undefined; +} +/* eslint-enable import/no-deprecated */ + +/** Supplemental config for an object type. */ +export interface GraphQLObjectTypeSupplementalConfig { + /** Supplemental config for fields declared by the document. */ + fields?: Readonly> | undefined; + /** Predicate used to determine whether a runtime value belongs to this object type. */ + isTypeOf?: GraphQLIsTypeOfFn | undefined; + /** Custom extensions. */ + extensions?: + | Readonly> + | undefined; +} + +/** Supplemental config for an interface type. */ +export interface GraphQLInterfaceTypeSupplementalConfig { + /** Supplemental config for fields declared by the document. */ + fields?: Readonly> | undefined; + /** + * Optionally provide a custom type resolver function. If one is not provided, + * the default implementation will call `isTypeOf` on each implementing + * Object type. + */ + resolveType?: GraphQLTypeResolver | undefined; + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +/** Supplemental config for a union type. */ +export interface GraphQLUnionTypeSupplementalConfig { + /** + * Optionally provide a custom type resolver function. If one is not provided, + * the default implementation will call `isTypeOf` on each implementing + * Object type. + */ + resolveType?: GraphQLTypeResolver | undefined; + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +/** Supplemental config for an enum type. */ +export interface GraphQLEnumTypeSupplementalConfig { + /** Supplemental config for values declared by the document. */ + values?: Readonly> | undefined; + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +/** Supplemental config for an enum value. */ +export interface GraphQLEnumValueSupplementalConfig { + /** Internal runtime value represented by this enum value. */ + value?: unknown; + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +/** Supplemental config for an input object type. */ +export interface GraphQLInputObjectTypeSupplementalConfig { + /** Supplemental config for fields declared by the document. */ + fields?: Readonly> | undefined; + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +/** Supplemental config for a field declared by an object or interface. */ +export type GraphQLFieldSupplementalConfig = + | GraphQLFieldResolver + | GraphQLFieldSupplementalConfigMap; + +/** Supplemental config map for a field declared by an object or interface. */ +export interface GraphQLFieldSupplementalConfigMap { + /** Supplemental config for arguments declared by the document. */ + args?: Readonly> | undefined; + /** Resolver function used to produce this field value. */ + resolve?: GraphQLFieldResolver | undefined; + /** Resolver function used to create a subscription event stream for this field. */ + subscribe?: GraphQLFieldResolver | undefined; + /** Custom extensions. */ + extensions?: Readonly> | undefined; +} + +/** Supplemental config for an argument declared by a field or directive. */ +export interface GraphQLArgumentSupplementalConfig { + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +/** Supplemental config for an input object field. */ +export interface GraphQLInputFieldSupplementalConfig { + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +/** Supplemental config for a directive declared by SDL. */ +export interface GraphQLDirectiveSupplementalConfig { + /** Supplemental config for arguments declared by the document. */ + args?: Readonly> | undefined; + /** Custom extensions. */ + extensions?: Readonly | undefined; +} + +/** Options used when extending a schema from a parsed SDL document. */ +export interface ExtendSchemaOptions extends GraphQLSchemaValidationOptions { /** * Set to true to assume the SDL is valid. * @@ -79,6 +293,19 @@ interface Options extends GraphQLSchemaValidationOptions { * @internal */ assumeValidSDL?: boolean | undefined; + /** + * Set to true to assume the supplemental config is valid. + * + * Default: false + */ + assumeValidSupplementalConfig?: boolean | undefined; + /** + * Supplemental schema constructor config not expressible in SDL. + * + * It applies only to elements newly added by the document, including fields + * added to an existing type by a type extension. + */ + supplementalConfig?: Readonly | undefined; } /** @@ -125,6 +352,40 @@ interface Options extends GraphQLSchemaValidationOptions { * ``` * @example * ```ts + * // Attach resolvers to fields newly added by an SDL extension. + * import { graphqlSync } from 'graphql'; + * import { parse } from 'graphql/language'; + * import { buildSchema, extendSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const extensionAST = parse(` + * extend type Query { + * farewell: String + * } + * `); + * + * const extendedSchema = extendSchema(schema, extensionAST, { + * supplementalConfig: { + * objectTypes: { + * Query: { + * fields: { + * farewell: () => 'Goodbye!', + * }, + * }, + * }, + * }, + * }); + * + * const result = graphqlSync({ schema: extendedSchema, source: '{ farewell }' }); + * + * result.data; // => { farewell: 'Goodbye!' } + * ``` + * @example + * ```ts * // This variant bypasses validation for an otherwise invalid extension. * import { parse } from 'graphql/language'; * import { buildSchema, extendSchema } from 'graphql/utilities'; @@ -150,7 +411,7 @@ interface Options extends GraphQLSchemaValidationOptions { export function extendSchema( schema: GraphQLSchema, documentAST: DocumentNode, - options?: Options, + options?: ExtendSchemaOptions, ): GraphQLSchema { assertSchema(schema); @@ -169,7 +430,7 @@ export function extendSchema( export function extendSchemaImpl( schemaConfig: GraphQLSchemaNormalizedConfig, documentAST: DocumentNode, - options?: Options, + options?: ExtendSchemaOptions, ): GraphQLSchemaNormalizedConfig { // Collect the type definitions and extensions found in the document. const typeDefs: Array = []; @@ -201,7 +462,7 @@ export function extendSchemaImpl( // have the same name. For example, a type named "skip". const directiveDefs: Array = []; - let schemaDef: Maybe; + let schemaDef: SchemaDefinitionNode | undefined; // Schema extensions are collected which may add additional operation types. const schemaExtensions: Array = []; @@ -256,13 +517,39 @@ export function extendSchemaImpl( isSchemaChanged = true; } + const supplementalConfig = options?.supplementalConfig; + const scalarTypeSupplementalConfigMap = supplementalConfig?.scalarTypes; + const objectTypeSupplementalConfigMap = supplementalConfig?.objectTypes; + const interfaceTypeSupplementalConfigMap = supplementalConfig?.interfaceTypes; + const unionTypeSupplementalConfigMap = supplementalConfig?.unionTypes; + const enumTypeSupplementalConfigMap = supplementalConfig?.enumTypes; + const inputObjectTypeSupplementalConfigMap = + supplementalConfig?.inputObjectTypes; + const directiveSupplementalConfigMap = supplementalConfig?.directives; + const supplementalExtensions = supplementalConfig?.extensions; + + if ( + supplementalConfig !== undefined && + options?.assumeValidSupplementalConfig !== true + ) { + validateSchemaSupplementalConfig(supplementalConfig); + } + // If this document contains no new types, extensions, or directives then // return the same unmodified GraphQLSchema instance. if (!isSchemaChanged) { - return schemaConfig; + if (supplementalExtensions === undefined) { + return schemaConfig; + } + + return { + ...schemaConfig, + extensions: supplementalExtensions, + assumeValid: options?.assumeValid ?? false, + }; } - return mapSchemaConfig(schemaConfig, (context) => { + const extendedConfig = mapSchemaConfig(schemaConfig, (context) => { const { getNamedType, setNamedType, getNamedTypes } = context; return { [SchemaElementKind.SCHEMA]: (config) => { @@ -297,7 +584,7 @@ export function extendSchemaImpl( ...config.directives.map(extendDirective), ...directiveDefs.map(buildDirective), ], - extensions: config.extensions, + extensions: supplementalExtensions ?? config.extensions, astNode: schemaDef ?? config.astNode, extensionASTNodes: config.extensionASTNodes.concat(schemaExtensions), assumeValid: options?.assumeValid ?? false, @@ -309,7 +596,10 @@ export function extendSchemaImpl( ...config, fields: () => ({ ...config.fields(), - ...buildInputFieldMap(extensions), + ...buildInputFieldMap( + extensions, + inputObjectTypeSupplementalConfigMap?.[config.name]?.fields, + ), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), }; @@ -320,7 +610,10 @@ export function extendSchemaImpl( ...config, values: () => ({ ...config.values(), - ...buildEnumValueMap(extensions), + ...buildEnumValueMap( + extensions, + enumTypeSupplementalConfigMap?.[config.name]?.values, + ), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), }; @@ -347,7 +640,10 @@ export function extendSchemaImpl( ], fields: () => ({ ...config.fields(), - ...buildFieldMap(extensions), + ...buildFieldMap( + extensions, + objectTypeSupplementalConfigMap?.[config.name]?.fields, + ), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), }; @@ -362,7 +658,10 @@ export function extendSchemaImpl( ], fields: () => ({ ...config.fields(), - ...buildFieldMap(extensions), + ...buildFieldMap( + extensions, + interfaceTypeSupplementalConfigMap?.[config.name]?.fields, + ), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), }; @@ -423,11 +722,13 @@ export function extendSchemaImpl( function buildDirective(node: DirectiveDefinitionNode): GraphQLDirective { const extensionASTNodes = directiveExtensions.get(node.name.value) ?? []; + const directiveSupplement = + directiveSupplementalConfigMap?.[node.name.value]; const deprecationReason = getDeprecationReason(node) ?? extensionASTNodes .map((extensionNode) => getDeprecationReason(extensionNode)) - .find((reason) => reason != null); + .find((reason) => reason !== undefined); return new GraphQLDirective({ name: node.name.value, @@ -435,8 +736,9 @@ export function extendSchemaImpl( // @ts-expect-error locations: node.locations.map(({ value }) => value), isRepeatable: node.repeatable, - args: buildArgumentMap(node.arguments), + args: buildArgumentMap(node.arguments, directiveSupplement?.args), deprecationReason, + extensions: directiveSupplement?.extensions, astNode: node, extensionASTNodes, }); @@ -451,7 +753,7 @@ export function extendSchemaImpl( directive.deprecationReason ?? extensionASTNodes .map((extensionNode) => getDeprecationReason(extensionNode)) - .find((reason) => reason != null); + .find((reason) => reason !== undefined); return new GraphQLDirective({ ...directive.toConfig(), @@ -468,20 +770,36 @@ export function extendSchemaImpl( | ObjectTypeDefinitionNode | ObjectTypeExtensionNode >, + fieldSupplements: + | Readonly> + | undefined, ): GraphQLFieldNormalizedConfigMap { const fieldConfigMap = Object.create(null); for (const node of nodes) { const nodeFields = node.fields ?? []; for (const field of nodeFields) { + const fieldSupplement = fieldSupplements?.[field.name.value]; + let fieldSupplementMap: GraphQLFieldSupplementalConfigMap | undefined; + let resolve: GraphQLFieldResolver | undefined; + if (typeof fieldSupplement === 'function') { + resolve = fieldSupplement; + } else { + fieldSupplementMap = fieldSupplement; + resolve = fieldSupplementMap?.resolve; + } + fieldConfigMap[field.name.value] = { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. type: typeFromAST(field.type), description: field.description?.value, - args: buildArgumentMap(field.arguments), + args: buildArgumentMap(field.arguments, fieldSupplementMap?.args), + resolve, + subscribe: fieldSupplementMap?.subscribe, deprecationReason: getDeprecationReason(field), + extensions: fieldSupplementMap?.extensions, astNode: field, }; } @@ -491,6 +809,7 @@ export function extendSchemaImpl( function buildArgumentMap( args: Maybe>, + argSupplements?: Readonly>, ): GraphQLFieldConfigArgumentMap { const argsNodes = args ?? []; @@ -506,6 +825,7 @@ export function extendSchemaImpl( description: arg.description?.value, default: arg.defaultValue && { literal: arg.defaultValue }, deprecationReason: getDeprecationReason(arg), + extensions: argSupplements?.[arg.name.value]?.extensions, astNode: arg, }; } @@ -516,12 +836,17 @@ export function extendSchemaImpl( nodes: ReadonlyArray< InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode >, + fieldSupplements: + | Readonly> + | undefined, ): GraphQLInputFieldNormalizedConfigMap { const inputFieldMap = Object.create(null); for (const node of nodes) { const fieldsNodes = node.fields ?? []; for (const field of fieldsNodes) { + const fieldSupplement = fieldSupplements?.[field.name.value]; + // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. @@ -532,6 +857,7 @@ export function extendSchemaImpl( description: field.description?.value, default: field.defaultValue && { literal: field.defaultValue }, deprecationReason: getDeprecationReason(field), + extensions: fieldSupplement?.extensions, astNode: field, }; } @@ -541,15 +867,22 @@ export function extendSchemaImpl( function buildEnumValueMap( nodes: ReadonlyArray, + valueSupplements: + | Readonly> + | undefined, ): GraphQLEnumValueNormalizedConfigMap { const enumValueMap = Object.create(null); for (const node of nodes) { const valuesNodes = node.values ?? []; for (const value of valuesNodes) { + const valueSupplement = valueSupplements?.[value.name.value]; + enumValueMap[value.name.value] = { description: value.description?.value, + value: valueSupplement?.value, deprecationReason: getDeprecationReason(value), + extensions: valueSupplement?.extensions, astNode: value, }; } @@ -591,12 +924,15 @@ export function extendSchemaImpl( case Kind.OBJECT_TYPE_DEFINITION: { const extensionASTNodes = objectExtensions.get(name) ?? []; const allNodes = [astNode, ...extensionASTNodes]; + const objectTypeSupplement = objectTypeSupplementalConfigMap?.[name]; return new GraphQLObjectType({ name, description: astNode.description?.value, interfaces: () => buildInterfaces(allNodes), - fields: () => buildFieldMap(allNodes), + fields: () => buildFieldMap(allNodes, objectTypeSupplement?.fields), + isTypeOf: objectTypeSupplement?.isTypeOf, + extensions: objectTypeSupplement?.extensions, astNode, extensionASTNodes, }); @@ -604,12 +940,17 @@ export function extendSchemaImpl( case Kind.INTERFACE_TYPE_DEFINITION: { const extensionASTNodes = interfaceExtensions.get(name) ?? []; const allNodes = [astNode, ...extensionASTNodes]; + const interfaceTypeSupplement = + interfaceTypeSupplementalConfigMap?.[name]; return new GraphQLInterfaceType({ name, description: astNode.description?.value, interfaces: () => buildInterfaces(allNodes), - fields: () => buildFieldMap(allNodes), + fields: () => + buildFieldMap(allNodes, interfaceTypeSupplement?.fields), + resolveType: interfaceTypeSupplement?.resolveType, + extensions: interfaceTypeSupplement?.extensions, astNode, extensionASTNodes, }); @@ -617,11 +958,14 @@ export function extendSchemaImpl( case Kind.ENUM_TYPE_DEFINITION: { const extensionASTNodes = enumExtensions.get(name) ?? []; const allNodes = [astNode, ...extensionASTNodes]; + const enumTypeSupplement = enumTypeSupplementalConfigMap?.[name]; return new GraphQLEnumType({ name, description: astNode.description?.value, - values: () => buildEnumValueMap(allNodes), + values: () => + buildEnumValueMap(allNodes, enumTypeSupplement?.values), + extensions: enumTypeSupplement?.extensions, astNode, extensionASTNodes, }); @@ -629,11 +973,14 @@ export function extendSchemaImpl( case Kind.UNION_TYPE_DEFINITION: { const extensionASTNodes = unionExtensions.get(name) ?? []; const allNodes = [astNode, ...extensionASTNodes]; + const unionTypeSupplement = unionTypeSupplementalConfigMap?.[name]; return new GraphQLUnionType({ name, description: astNode.description?.value, types: () => buildUnionTypes(allNodes), + resolveType: unionTypeSupplement?.resolveType, + extensions: unionTypeSupplement?.extensions, astNode, extensionASTNodes, }); @@ -644,10 +991,19 @@ export function extendSchemaImpl( for (const extensionNode of extensionASTNodes) { specifiedByURL = getSpecifiedByURL(extensionNode) ?? specifiedByURL; } + const scalarTypeSupplement = scalarTypeSupplementalConfigMap?.[name]; return new GraphQLScalarType({ name, description: astNode.description?.value, specifiedByURL, + serialize: scalarTypeSupplement?.serialize, + parseValue: scalarTypeSupplement?.parseValue, + parseLiteral: scalarTypeSupplement?.parseLiteral, + coerceOutputValue: scalarTypeSupplement?.coerceOutputValue, + coerceInputValue: scalarTypeSupplement?.coerceInputValue, + coerceInputLiteral: scalarTypeSupplement?.coerceInputLiteral, + valueToLiteral: scalarTypeSupplement?.valueToLiteral, + extensions: scalarTypeSupplement?.extensions, astNode, extensionASTNodes, }); @@ -655,19 +1011,505 @@ export function extendSchemaImpl( case Kind.INPUT_OBJECT_TYPE_DEFINITION: { const extensionASTNodes = inputObjectExtensions.get(name) ?? []; const allNodes = [astNode, ...extensionASTNodes]; + const inputObjectTypeSupplement = + inputObjectTypeSupplementalConfigMap?.[name]; return new GraphQLInputObjectType({ name, description: astNode.description?.value, - fields: () => buildInputFieldMap(allNodes), + fields: () => + buildInputFieldMap(allNodes, inputObjectTypeSupplement?.fields), + extensions: inputObjectTypeSupplement?.extensions, astNode, extensionASTNodes, - isOneOf: isOneOf(astNode), + isOneOf: Boolean( + getDirectiveValues(GraphQLOneOfDirective, astNode), + ), }); } } } }); + + function validateSchemaSupplementalConfig( + config: Readonly, + ): void { + if ( + config.extensions !== undefined && + Reflect.ownKeys(schemaConfig.extensions).length > 0 + ) { + throw new Error( + 'Schema supplemental config cannot add extensions to a schema that already has extensions.', + ); + } + + function getExistingType(typeName: string): GraphQLNamedType | undefined { + return schemaConfig.types.find((type) => type.name === typeName); + } + + function getTypeDef(typeName: string): TypeDefinitionNode | undefined { + return typeDefs.find((typeDef) => typeDef.name.value === typeName); + } + + function throwTypeSupplementalConfigCannotModifyExistingType( + typeName: string, + expectedConfigKey: SupplementalTypeConfigKey, + ): never { + const actualConfigKey = getDocumentTypeConfigKey(typeName); + if ( + actualConfigKey !== undefined && + actualConfigKey !== expectedConfigKey + ) { + throwTypeSupplementalConfigMismatch(typeName, expectedConfigKey); + } + + throw new Error( + `Type supplemental config "${typeName}" cannot modify an existing type.`, + ); + } + + function throwTypeSupplementalConfigMismatch( + typeName: string, + expectedConfigKey: SupplementalTypeConfigKey, + ): never { + const actualConfigKey = getDocumentTypeConfigKey(typeName); + throw new Error( + actualConfigKey !== undefined && actualConfigKey !== expectedConfigKey + ? `Type supplemental config property "${expectedConfigKey}.${typeName}" does not match the type declared or extended by the document. Did you mean "${actualConfigKey}.${typeName}"?` + : `Type supplemental config "${typeName}" does not match a type declared by the document.`, + ); + } + + function getDocumentTypeConfigKey( + typeName: string, + ): SupplementalTypeConfigKey | undefined { + const typeDef = getTypeDef(typeName); + if (typeDef !== undefined) { + return typeConfigKeyByTypeDefinitionKind[typeDef.kind]; + } + + return ( + [ + [scalarExtensions, 'scalarTypes'], + [objectExtensions, 'objectTypes'], + [interfaceExtensions, 'interfaceTypes'], + [unionExtensions, 'unionTypes'], + [enumExtensions, 'enumTypes'], + [inputObjectExtensions, 'inputObjectTypes'], + ] as const + ).find(([extensions]) => extensions.get(typeName) !== undefined)?.[1]; + } + + function assertTypeDefinitionKind( + typeName: string, + typeDef: TypeDefinitionNode | undefined, + expectedKind: K, + expectedConfigKey: SupplementalTypeConfigKey, + ): asserts typeDef is Extract { + if (typeDef?.kind !== expectedKind) { + throwTypeSupplementalConfigMismatch(typeName, expectedConfigKey); + } + } + + const scalarTypeSupplements = config.scalarTypes; + if (scalarTypeSupplements !== undefined) { + for (const typeName of Object.keys(scalarTypeSupplements)) { + const typeSupplement = scalarTypeSupplements[typeName]; + const existingType = getExistingType(typeName); + + if (existingType !== undefined) { + if ( + isScalarType(existingType) && + scalarExtensions.get(typeName) !== undefined + ) { + assertTypeExtensionSupplementalConfigKeys(typeName, typeSupplement); + continue; + } + throwTypeSupplementalConfigCannotModifyExistingType( + typeName, + 'scalarTypes', + ); + } + + const typeDef = getTypeDef(typeName); + assertTypeDefinitionKind( + typeName, + typeDef, + Kind.SCALAR_TYPE_DEFINITION, + 'scalarTypes', + ); + } + } + + const objectTypeSupplements = config.objectTypes; + if (objectTypeSupplements !== undefined) { + for (const typeName of Object.keys(objectTypeSupplements)) { + const typeSupplement = objectTypeSupplements[typeName]; + const existingType = getExistingType(typeName); + + if (existingType !== undefined) { + const extensionNodes = objectExtensions.get(typeName); + if (isObjectType(existingType) && extensionNodes !== undefined) { + assertTypeExtensionSupplementalConfigKeys( + typeName, + typeSupplement, + 'fields', + ); + validateFieldSupplementalConfigs( + typeName, + typeSupplement.fields, + extensionNodes.flatMap((node) => node.fields ?? []), + existingType, + ); + continue; + } + throwTypeSupplementalConfigCannotModifyExistingType( + typeName, + 'objectTypes', + ); + } + + const typeDef = getTypeDef(typeName); + assertTypeDefinitionKind( + typeName, + typeDef, + Kind.OBJECT_TYPE_DEFINITION, + 'objectTypes', + ); + + validateFieldSupplementalConfigs( + typeName, + typeSupplement.fields, + [typeDef, ...(objectExtensions.get(typeName) ?? [])].flatMap( + (node) => node.fields ?? [], + ), + ); + } + } + + const interfaceTypeSupplements = config.interfaceTypes; + if (interfaceTypeSupplements !== undefined) { + for (const typeName of Object.keys(interfaceTypeSupplements)) { + const typeSupplement = interfaceTypeSupplements[typeName]; + const existingType = getExistingType(typeName); + + if (existingType !== undefined) { + const extensionNodes = interfaceExtensions.get(typeName); + if (isInterfaceType(existingType) && extensionNodes !== undefined) { + assertTypeExtensionSupplementalConfigKeys( + typeName, + typeSupplement, + 'fields', + ); + validateFieldSupplementalConfigs( + typeName, + typeSupplement.fields, + extensionNodes.flatMap((node) => node.fields ?? []), + existingType, + ); + continue; + } + throwTypeSupplementalConfigCannotModifyExistingType( + typeName, + 'interfaceTypes', + ); + } + + const typeDef = getTypeDef(typeName); + assertTypeDefinitionKind( + typeName, + typeDef, + Kind.INTERFACE_TYPE_DEFINITION, + 'interfaceTypes', + ); + + validateFieldSupplementalConfigs( + typeName, + typeSupplement.fields, + [typeDef, ...(interfaceExtensions.get(typeName) ?? [])].flatMap( + (node) => node.fields ?? [], + ), + ); + } + } + + const unionTypeSupplements = config.unionTypes; + if (unionTypeSupplements !== undefined) { + for (const typeName of Object.keys(unionTypeSupplements)) { + const typeSupplement = unionTypeSupplements[typeName]; + const existingType = getExistingType(typeName); + + if (existingType !== undefined) { + if ( + isUnionType(existingType) && + unionExtensions.get(typeName) !== undefined + ) { + assertTypeExtensionSupplementalConfigKeys(typeName, typeSupplement); + continue; + } + throwTypeSupplementalConfigCannotModifyExistingType( + typeName, + 'unionTypes', + ); + } + + const typeDef = getTypeDef(typeName); + assertTypeDefinitionKind( + typeName, + typeDef, + Kind.UNION_TYPE_DEFINITION, + 'unionTypes', + ); + } + } + + const enumTypeSupplements = config.enumTypes; + if (enumTypeSupplements !== undefined) { + for (const typeName of Object.keys(enumTypeSupplements)) { + const typeSupplement = enumTypeSupplements[typeName]; + const existingType = getExistingType(typeName); + + if (existingType !== undefined) { + const extensionNodes = enumExtensions.get(typeName); + if (isEnumType(existingType) && extensionNodes !== undefined) { + assertTypeExtensionSupplementalConfigKeys( + typeName, + typeSupplement, + 'values', + ); + validateEnumValueSupplementalConfigs( + typeName, + typeSupplement.values, + extensionNodes.flatMap((node) => node.values ?? []), + existingType, + ); + continue; + } + throwTypeSupplementalConfigCannotModifyExistingType( + typeName, + 'enumTypes', + ); + } + + const typeDef = getTypeDef(typeName); + assertTypeDefinitionKind( + typeName, + typeDef, + Kind.ENUM_TYPE_DEFINITION, + 'enumTypes', + ); + + validateEnumValueSupplementalConfigs( + typeName, + typeSupplement.values, + [typeDef, ...(enumExtensions.get(typeName) ?? [])].flatMap( + (node) => node.values ?? [], + ), + ); + } + } + + const inputObjectTypeSupplements = config.inputObjectTypes; + if (inputObjectTypeSupplements !== undefined) { + for (const typeName of Object.keys(inputObjectTypeSupplements)) { + const typeSupplement = inputObjectTypeSupplements[typeName]; + const existingType = getExistingType(typeName); + + if (existingType !== undefined) { + const extensionNodes = inputObjectExtensions.get(typeName); + if (isInputObjectType(existingType) && extensionNodes !== undefined) { + assertTypeExtensionSupplementalConfigKeys( + typeName, + typeSupplement, + 'fields', + ); + validateInputFieldSupplementalConfigs( + typeName, + typeSupplement.fields, + extensionNodes.flatMap((node) => node.fields ?? []), + existingType, + ); + continue; + } + throwTypeSupplementalConfigCannotModifyExistingType( + typeName, + 'inputObjectTypes', + ); + } + + const typeDef = getTypeDef(typeName); + assertTypeDefinitionKind( + typeName, + typeDef, + Kind.INPUT_OBJECT_TYPE_DEFINITION, + 'inputObjectTypes', + ); + + validateInputFieldSupplementalConfigs( + typeName, + typeSupplement.fields, + [typeDef, ...(inputObjectExtensions.get(typeName) ?? [])].flatMap( + (node) => node.fields ?? [], + ), + ); + } + } + + const directiveSupplements = config.directives; + if (directiveSupplements !== undefined) { + for (const directiveName of Object.keys(directiveSupplements)) { + const directiveSupplement = directiveSupplements[directiveName]; + const directiveDef = directiveDefs.find( + (def) => def.name.value === directiveName, + ); + + if (directiveDef === undefined) { + throw new Error( + `Directive supplemental config "@${directiveName}" does not match a directive declared by the document.`, + ); + } + + validateArgumentSupplementalConfigs( + directiveSupplement?.args, + directiveDef.arguments ?? [], + `@${directiveName}`, + ); + } + } + } + + function assertTypeExtensionSupplementalConfigKeys( + typeName: string, + typeSupplement: object, + allowedKey?: 'fields' | 'values', + ): void { + const typeSupplementMap = typeSupplement as Readonly>; + for (const key of Object.keys(typeSupplementMap)) { + if (key !== allowedKey && typeSupplementMap[key] !== undefined) { + throw new Error( + `Type supplemental config "${typeName}.${key}" cannot modify an existing type.`, + ); + } + } + } + + function validateFieldSupplementalConfigs( + typeName: string, + fieldSupplements: + | Readonly> + | undefined, + fieldNodes: ReadonlyArray, + existingType?: GraphQLObjectType | GraphQLInterfaceType, + ): void { + if (fieldSupplements === undefined) { + return; + } + + for (const fieldName of Object.keys(fieldSupplements)) { + const fieldNode = fieldNodes.find( + (field) => field.name.value === fieldName, + ); + + if (fieldNode === undefined) { + throw new Error( + `Field supplemental config "${typeName}.${fieldName}" does not match a field declared by the document.`, + ); + } + + if (existingType?.getFields()[fieldName] !== undefined) { + throw new Error( + `Field supplemental config "${typeName}.${fieldName}" cannot modify an existing field.`, + ); + } + + const fieldSupplement = fieldSupplements[fieldName]; + if (typeof fieldSupplement === 'function') { + continue; + } + + validateArgumentSupplementalConfigs( + fieldSupplement?.args, + fieldNode.arguments ?? [], + `${typeName}.${fieldName}`, + ); + } + } + + function validateInputFieldSupplementalConfigs( + typeName: string, + fieldSupplements: + | Readonly> + | undefined, + fieldNodes: ReadonlyArray, + existingType?: GraphQLInputObjectType, + ): void { + if (fieldSupplements === undefined) { + return; + } + + for (const fieldName of Object.keys(fieldSupplements)) { + if (!fieldNodes.some((field) => field.name.value === fieldName)) { + throw new Error( + `Input field supplemental config "${typeName}.${fieldName}" does not match an input field declared by the document.`, + ); + } + + if (existingType?.getFields()[fieldName] !== undefined) { + throw new Error( + `Input field supplemental config "${typeName}.${fieldName}" cannot modify an existing input field.`, + ); + } + } + } + + function validateEnumValueSupplementalConfigs( + typeName: string, + valueSupplements: + | Readonly> + | undefined, + valueNodes: ReadonlyArray, + existingType?: GraphQLEnumType, + ): void { + if (valueSupplements === undefined) { + return; + } + + for (const valueName of Object.keys(valueSupplements)) { + if (!valueNodes.some((value) => value.name.value === valueName)) { + throw new Error( + `Enum value supplemental config "${typeName}.${valueName}" does not match an enum value declared by the document.`, + ); + } + + if (existingType?.getValue(valueName) !== undefined) { + throw new Error( + `Enum value supplemental config "${typeName}.${valueName}" cannot modify an existing enum value.`, + ); + } + } + } + + function validateArgumentSupplementalConfigs( + argSupplements: + | Readonly> + | undefined, + argNodes: ReadonlyArray, + ownerCoordinate: string, + ): void { + if (argSupplements === undefined) { + return; + } + + for (const argName of Object.keys(argSupplements)) { + if (!argNodes.some((arg) => arg.name.value === argName)) { + throw new Error( + `Argument supplemental config "${ownerCoordinate}(${argName}:)" does not match an argument declared by the document.`, + ); + } + } + } + + return extendedConfig; } const stdTypeMap = new Map( @@ -708,12 +1550,3 @@ function getSpecifiedByURL( // @ts-expect-error validated by `getDirectiveValues` return specifiedBy?.url; } - -/** - * Given an input object node, returns if the node should be OneOf. - * - * @internal - */ -function isOneOf(node: InputObjectTypeDefinitionNode): boolean { - return Boolean(getDirectiveValues(GraphQLOneOfDirective, node)); -} diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 14b46b25e1..e54983e33b 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -49,6 +49,22 @@ export type { BuildSchemaOptions } from './buildASTSchema.ts'; // Extends an existing GraphQLSchema from a parsed GraphQL Schema language AST. export { extendSchema } from './extendSchema.ts'; +export type { ExtendSchemaOptions } from './extendSchema.ts'; +export type { + GraphQLSchemaSupplementalConfig, + GraphQLScalarTypeSupplementalConfig, + GraphQLObjectTypeSupplementalConfig, + GraphQLFieldSupplementalConfig, + GraphQLFieldSupplementalConfigMap, + GraphQLArgumentSupplementalConfig, + GraphQLInterfaceTypeSupplementalConfig, + GraphQLUnionTypeSupplementalConfig, + GraphQLEnumTypeSupplementalConfig, + GraphQLEnumValueSupplementalConfig, + GraphQLInputObjectTypeSupplementalConfig, + GraphQLInputFieldSupplementalConfig, + GraphQLDirectiveSupplementalConfig, +} from './extendSchema.ts'; // Sort a GraphQLSchema. export { lexicographicSortSchema } from './lexicographicSortSchema.ts'; diff --git a/src/utilities/validateInputValue.ts b/src/utilities/validateInputValue.ts index 322c4666ab..63bb158fc0 100644 --- a/src/utilities/validateInputValue.ts +++ b/src/utilities/validateInputValue.ts @@ -42,19 +42,19 @@ import { replaceVariables } from './replaceVariables.ts'; * @example * ```ts * // Collect validation errors with their input paths. - * import { - * GraphQLInputObjectType, - * GraphQLInt, - * GraphQLNonNull, - * } from 'graphql/type'; - * import { validateInputValue } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * import { buildSchema, validateInputValue } from 'graphql/utilities'; * - * const ReviewInput = new GraphQLInputObjectType({ - * name: 'ReviewInput', - * fields: { - * stars: { type: new GraphQLNonNull(GraphQLInt) }, - * }, - * }); + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); * const errors = []; * * validateInputValue({ stars: 'bad' }, ReviewInput, (error, path) => { @@ -66,15 +66,19 @@ import { replaceVariables } from './replaceVariables.ts'; * @example * ```ts * // This variant hides suggestion text for unknown input fields. - * import { GraphQLInputObjectType, GraphQLString } from 'graphql/type'; - * import { validateInputValue } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * import { buildSchema, validateInputValue } from 'graphql/utilities'; * - * const ReviewInput = new GraphQLInputObjectType({ - * name: 'ReviewInput', - * fields: { - * comment: { type: GraphQLString }, - * }, - * }); + * const schema = buildSchema(` + * input ReviewInput { + * comment: String + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); * const errors = []; * * validateInputValue( @@ -298,19 +302,19 @@ function reportInvalidValue( * ```ts * // Validate literal input values and collect literal paths. * import { parseValue } from 'graphql/language'; - * import { - * GraphQLInputObjectType, - * GraphQLInt, - * GraphQLNonNull, - * } from 'graphql/type'; - * import { validateInputLiteral } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * import { buildSchema, validateInputLiteral } from 'graphql/utilities'; * - * const ReviewInput = new GraphQLInputObjectType({ - * name: 'ReviewInput', - * fields: { - * stars: { type: new GraphQLNonNull(GraphQLInt) }, - * }, - * }); + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); * const errors = []; * * validateInputLiteral( diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts index d5ebc9e515..48fcd94f0e 100644 --- a/src/utilities/valueFromAST.ts +++ b/src/utilities/valueFromAST.ts @@ -45,22 +45,20 @@ import { * ```ts * // Coerce literal values without variables. * import { parseValue } from 'graphql/language'; - * import { - * GraphQLInputObjectType, - * GraphQLInt, - * GraphQLList, - * GraphQLNonNull, - * GraphQLString, - * } from 'graphql/type'; - * import { valueFromAST } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * import { buildSchema, valueFromAST } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * tags: [String] + * } * - * const ReviewInput = new GraphQLInputObjectType({ - * name: 'ReviewInput', - * fields: { - * stars: { type: new GraphQLNonNull(GraphQLInt) }, - * tags: { type: new GraphQLList(GraphQLString) }, - * }, - * }); + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); * * valueFromAST(parseValue('{ stars: 5, tags: ["featured"] }'), ReviewInput); // => { stars: 5, tags: ['featured'] } * valueFromAST(parseValue('{ stars: "bad" }'), ReviewInput); // => undefined diff --git a/src/utilities/valueToLiteral.ts b/src/utilities/valueToLiteral.ts index 2490433dc9..d0720700a4 100644 --- a/src/utilities/valueToLiteral.ts +++ b/src/utilities/valueToLiteral.ts @@ -33,22 +33,20 @@ import { * @example * ```ts * import { print } from 'graphql/language'; - * import { - * GraphQLInputObjectType, - * GraphQLInt, - * GraphQLList, - * GraphQLNonNull, - * GraphQLString, - * } from 'graphql/type'; - * import { valueToLiteral } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * import { buildSchema, valueToLiteral } from 'graphql/utilities'; * - * const ReviewInput = new GraphQLInputObjectType({ - * name: 'ReviewInput', - * fields: { - * stars: { type: new GraphQLNonNull(GraphQLInt) }, - * tags: { type: new GraphQLList(GraphQLString) }, - * }, - * }); + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * tags: [String] + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); * * const literal = valueToLiteral({ stars: 5, tags: ['featured'] }, ReviewInput); * diff --git a/src/validation/rules/custom/NoDeprecatedCustomRule.ts b/src/validation/rules/custom/NoDeprecatedCustomRule.ts index e59cfa290e..8bc035a6a1 100644 --- a/src/validation/rules/custom/NoDeprecatedCustomRule.ts +++ b/src/validation/rules/custom/NoDeprecatedCustomRule.ts @@ -21,27 +21,15 @@ import type { ValidationContext } from '../../ValidationContext.ts'; * @returns A visitor that reports validation errors for this rule. * @example * ```ts - * import { - * GraphQLObjectType, - * GraphQLSchema, - * GraphQLString, - * parse, - * validate, - * } from 'graphql'; + * import { buildSchema, parse, validate } from 'graphql'; * import { NoDeprecatedCustomRule } from 'graphql/validation'; * - * const schema = new GraphQLSchema({ - * query: new GraphQLObjectType({ - * name: 'Query', - * fields: { - * name: { type: GraphQLString }, - * oldName: { - * type: GraphQLString, - * deprecationReason: 'Use name instead.', - * }, - * }, - * }), - * }); + * const schema = buildSchema(` + * type Query { + * name: String + * oldName: String @deprecated(reason: "Use name instead.") + * } + * `); * * const invalidDocument = parse(` * { oldName } diff --git a/website/pages/api-v17/execution.mdx b/website/pages/api-v17/execution.mdx index 3355042eb8..28c8a5bbb2 100644 --- a/website/pages/api-v17/execution.mdx +++ b/website/pages/api-v17/execution.mdx @@ -228,18 +228,28 @@ import { parse } from 'graphql/language'; import { buildSchema } from 'graphql/utilities'; import { execute } from 'graphql/execution'; -const schema = buildSchema(` - type Query { - greeting(name: String!): String - } -`); +const schema = buildSchema( + ` + type Query { + greeting(name: String!): String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: (_source, { name }) => `Hello, ${name}!`, + }, + }, + }, + }, + }, +); const result = await execute({ schema, document: parse('query ($name: String!) { greeting(name: $name) }'), - rootValue: { - greeting: ({ name }) => `Hello, ${name}!`, - }, variableValues: { name: 'Ada' }, }); @@ -573,7 +583,6 @@ Accepts an object with named arguments.
Example 1
```ts -// Use a same-named rootValue function to provide the source event stream. import assert from 'node:assert'; import { parse } from 'graphql/language'; import { buildSchema } from 'graphql/utilities'; @@ -584,20 +593,34 @@ async function* greetings() { yield { greeting: 'Bonjour' }; } -const schema = buildSchema(` - type Query { - noop: String - } - - type Subscription { - greeting: String - } -`); +const schema = buildSchema( + ` + type Query { + noop: String + } + + type Subscription { + greeting: String + } + `, + { + supplementalConfig: { + objectTypes: { + Subscription: { + fields: { + greeting: { + subscribe: () => greetings(), + }, + }, + }, + }, + }, + }, +); const result = await subscribe({ schema, document: parse('subscription { greeting }'), - rootValue: { greeting: () => greetings() }, }); assert('next' in result); @@ -753,19 +776,33 @@ async function* greetings() { yield { greeting: 'Hello' }; } -const schema = buildSchema(` - type Query { - noop: String - } - - type Subscription { - greeting: String - } -`); +const schema = buildSchema( + ` + type Query { + noop: String + } + + type Subscription { + greeting: String + } + `, + { + supplementalConfig: { + objectTypes: { + Subscription: { + fields: { + greeting: { + subscribe: () => greetings(), + }, + }, + }, + }, + }, + }, +); const validatedArgs = validateSubscriptionArgs({ schema, document: parse('subscription { greeting }'), - rootValue: { greeting: () => greetings() }, }); assert('schema' in validatedArgs); @@ -1633,16 +1670,28 @@ import { parse } from 'graphql/language'; import { buildSchema } from 'graphql/utilities'; import { experimentalExecuteIncrementally } from 'graphql/execution'; -const schema = buildSchema(` - type Query { - greeting: String - } -`); +const schema = buildSchema( + ` + type Query { + greeting: String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: () => 'Hello', + }, + }, + }, + }, + }, +); const result = await experimentalExecuteIncrementally({ schema, document: parse('{ greeting }'), - rootValue: { greeting: 'Hello' }, }); result; // => { data: { greeting: 'Hello' } } diff --git a/website/pages/api-v17/graphql.mdx b/website/pages/api-v17/graphql.mdx index a338721cba..e6c0a58ddc 100644 --- a/website/pages/api-v17/graphql.mdx +++ b/website/pages/api-v17/graphql.mdx @@ -735,18 +735,28 @@ step and a server runtime step. // Execute a complete asynchronous request with variables. import { graphql, buildSchema } from 'graphql'; -const schema = buildSchema(` - type Query { - greeting(name: String!): String - } -`); +const schema = buildSchema( + ` + type Query { + greeting(name: String!): String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: (_source, { name }) => `Hello, ${name}!`, + }, + }, + }, + }, + }, +); const result = await graphql({ schema, source: 'query SayHello($name: String!) { greeting(name: $name) }', - rootValue: { - greeting: ({ name }) => `Hello, ${name}!`, - }, variableValues: { name: 'Ada' }, operationName: 'SayHello', }); @@ -801,11 +811,24 @@ result; // => { data: { viewer: { __typename: 'User', name: 'Ada' } } } // This variant customizes the request pipeline with a harness. import { buildSchema, defaultHarness, graphql } from 'graphql'; -const schema = buildSchema(` - type Query { - greeting: String - } -`); +const schema = buildSchema( + ` + type Query { + greeting: String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: () => 'Hello', + }, + }, + }, + }, + }, +); const stages = []; const abortController = new AbortController(); const harness = { @@ -830,7 +853,6 @@ const harness = { const result = await graphql({ schema, source: '{ greeting }', - rootValue: { greeting: 'Hello' }, rules: [], maxErrors: 25, hideSuggestions: true, @@ -906,18 +928,28 @@ validation fails. // Execute a complete synchronous request with variables. import { graphqlSync, buildSchema } from 'graphql'; -const schema = buildSchema(` - type Query { - greeting(name: String!): String - } -`); +const schema = buildSchema( + ` + type Query { + greeting(name: String!): String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: (_source, { name }) => `Hello, ${name}!`, + }, + }, + }, + }, + }, +); const result = graphqlSync({ schema, source: 'query SayHello($name: String!) { greeting(name: $name) }', - rootValue: { - greeting: ({ name }) => `Hello, ${name}!`, - }, variableValues: { name: 'Ada' }, operationName: 'SayHello', }); diff --git a/website/pages/api-v17/type.mdx b/website/pages/api-v17/type.mdx index 97c543fa3b..db1a3e3785 100644 --- a/website/pages/api-v17/type.mdx +++ b/website/pages/api-v17/type.mdx @@ -11324,15 +11324,17 @@ Returns true when the scalar type is one of the scalars specified by GraphQL.
Example
```ts -import { - GraphQLScalarType, - GraphQLString, - isSpecifiedScalarType, -} from 'graphql/type'; +import { assertScalarType, GraphQLString, isSpecifiedScalarType } from 'graphql/type'; +import { buildSchema } from 'graphql/utilities'; -const DateTime = new GraphQLScalarType({ - name: 'DateTime', -}); +const schema = buildSchema(` + scalar DateTime + + type Query { + now: DateTime + } +`); +const DateTime = assertScalarType(schema.getType('DateTime')); isSpecifiedScalarType(GraphQLString); // => true isSpecifiedScalarType(DateTime); // => false diff --git a/website/pages/api-v17/utilities.mdx b/website/pages/api-v17/utilities.mdx index e1642ba2b7..d3f923b778 100644 --- a/website/pages/api-v17/utilities.mdx +++ b/website/pages/api-v17/utilities.mdx @@ -1136,22 +1136,20 @@ instead, and take care to operate on external values. ```ts import { print } from 'graphql/language'; -import { - GraphQLInputObjectType, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLString, -} from 'graphql/type'; -import { astFromValue } from 'graphql/utilities'; +import { assertInputObjectType, GraphQLNonNull, GraphQLString } from 'graphql/type'; +import { astFromValue, buildSchema } from 'graphql/utilities'; -const ReviewInput = new GraphQLInputObjectType({ - name: 'ReviewInput', - fields: { - stars: { type: new GraphQLNonNull(GraphQLInt) }, - tags: { type: new GraphQLList(GraphQLString) }, - }, -}); +const schema = buildSchema(` + input ReviewInput { + stars: Int! + tags: [String] + } + + type Query { + reviews(filter: ReviewInput): [String] + } +`); +const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); const valueNode = astFromValue( { stars: 5, tags: ['featured', 'verified'] }, @@ -1228,22 +1226,20 @@ are needed. ```ts // Coerce runtime input values, returning undefined when coercion fails. -import { - GraphQLInputObjectType, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLString, -} from 'graphql/type'; -import { coerceInputValue } from 'graphql/utilities'; +import { assertInputObjectType } from 'graphql/type'; +import { buildSchema, coerceInputValue } from 'graphql/utilities'; -const ReviewInput = new GraphQLInputObjectType({ - name: 'ReviewInput', - fields: { - stars: { type: new GraphQLNonNull(GraphQLInt) }, - tags: { type: new GraphQLList(GraphQLString) }, - }, -}); +const schema = buildSchema(` + input ReviewInput { + stars: Int! + tags: [String] + } + + type Query { + reviews(filter: ReviewInput): [String] + } +`); +const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); coerceInputValue({ stars: '5', tags: ['featured'] }, ReviewInput); // => { stars: 5, tags: ['featured'] } coerceInputValue({ stars: 'bad' }, ReviewInput); // => undefined @@ -1324,21 +1320,20 @@ the provided type. ```ts // Coerce literal input values without variables. import { parseValue } from 'graphql/language'; -import { - GraphQLInputObjectType, - GraphQLInt, - GraphQLNonNull, - GraphQLString, -} from 'graphql/type'; -import { coerceInputLiteral } from 'graphql/utilities'; +import { assertInputObjectType } from 'graphql/type'; +import { buildSchema, coerceInputLiteral } from 'graphql/utilities'; -const ReviewInput = new GraphQLInputObjectType({ - name: 'ReviewInput', - fields: { - stars: { type: new GraphQLNonNull(GraphQLInt) }, - comment: { type: GraphQLString }, - }, -}); +const schema = buildSchema(` + input ReviewInput { + stars: Int! + comment: String + } + + type Query { + reviews(filter: ReviewInput): [String] + } +`); +const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); coerceInputLiteral( parseValue('{ stars: 5, comment: "Loved it" }'), @@ -1832,19 +1827,19 @@ all errors via a callback function. ```ts // Collect validation errors with their input paths. -import { - GraphQLInputObjectType, - GraphQLInt, - GraphQLNonNull, -} from 'graphql/type'; -import { validateInputValue } from 'graphql/utilities'; +import { assertInputObjectType } from 'graphql/type'; +import { buildSchema, validateInputValue } from 'graphql/utilities'; -const ReviewInput = new GraphQLInputObjectType({ - name: 'ReviewInput', - fields: { - stars: { type: new GraphQLNonNull(GraphQLInt) }, - }, -}); +const schema = buildSchema(` + input ReviewInput { + stars: Int! + } + + type Query { + reviews(filter: ReviewInput): [String] + } +`); +const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); const errors = []; validateInputValue({ stars: 'bad' }, ReviewInput, (error, path) => { @@ -1860,15 +1855,19 @@ errors; // => [ { message: 'Expected value of type "Int", found: "bad".', path: ```ts // This variant hides suggestion text for unknown input fields. -import { GraphQLInputObjectType, GraphQLString } from 'graphql/type'; -import { validateInputValue } from 'graphql/utilities'; +import { assertInputObjectType } from 'graphql/type'; +import { buildSchema, validateInputValue } from 'graphql/utilities'; -const ReviewInput = new GraphQLInputObjectType({ - name: 'ReviewInput', - fields: { - comment: { type: GraphQLString }, - }, -}); +const schema = buildSchema(` + input ReviewInput { + comment: String + } + + type Query { + reviews(filter: ReviewInput): [String] + } +`); +const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); const errors = []; validateInputValue( @@ -1950,19 +1949,19 @@ If variable values are not provided, the literal is validated statically ```ts // Validate literal input values and collect literal paths. import { parseValue } from 'graphql/language'; -import { - GraphQLInputObjectType, - GraphQLInt, - GraphQLNonNull, -} from 'graphql/type'; -import { validateInputLiteral } from 'graphql/utilities'; +import { assertInputObjectType } from 'graphql/type'; +import { buildSchema, validateInputLiteral } from 'graphql/utilities'; -const ReviewInput = new GraphQLInputObjectType({ - name: 'ReviewInput', - fields: { - stars: { type: new GraphQLNonNull(GraphQLInt) }, - }, -}); +const schema = buildSchema(` + input ReviewInput { + stars: Int! + } + + type Query { + reviews(filter: ReviewInput): [String] + } +`); +const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); const errors = []; validateInputLiteral( @@ -2100,22 +2099,20 @@ instead. ```ts // Coerce literal values without variables. import { parseValue } from 'graphql/language'; -import { - GraphQLInputObjectType, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLString, -} from 'graphql/type'; -import { valueFromAST } from 'graphql/utilities'; +import { assertInputObjectType } from 'graphql/type'; +import { buildSchema, valueFromAST } from 'graphql/utilities'; -const ReviewInput = new GraphQLInputObjectType({ - name: 'ReviewInput', - fields: { - stars: { type: new GraphQLNonNull(GraphQLInt) }, - tags: { type: new GraphQLList(GraphQLString) }, - }, -}); +const schema = buildSchema(` + input ReviewInput { + stars: Int! + tags: [String] + } + + type Query { + reviews(filter: ReviewInput): [String] + } +`); +const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); valueFromAST(parseValue('{ stars: 5, tags: ["featured"] }'), ReviewInput); // => { stars: 5, tags: ['featured'] } valueFromAST(parseValue('{ stars: "bad" }'), ReviewInput); // => undefined @@ -2286,22 +2283,20 @@ value. ```ts import { print } from 'graphql/language'; -import { - GraphQLInputObjectType, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLString, -} from 'graphql/type'; -import { valueToLiteral } from 'graphql/utilities'; +import { assertInputObjectType } from 'graphql/type'; +import { buildSchema, valueToLiteral } from 'graphql/utilities'; -const ReviewInput = new GraphQLInputObjectType({ - name: 'ReviewInput', - fields: { - stars: { type: new GraphQLNonNull(GraphQLInt) }, - tags: { type: new GraphQLList(GraphQLString) }, - }, -}); +const schema = buildSchema(` + input ReviewInput { + stars: Int! + tags: [String] + } + + type Query { + reviews(filter: ReviewInput): [String] + } +`); +const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); const literal = valueToLiteral({ stars: 5, tags: ['featured'] }, ReviewInput); @@ -2325,6 +2320,34 @@ valueToLiteral({ tags: ['missing stars'] }, ReviewInput); // => undefined

Types:
BuildSchemaOptions + + GraphQLSchemaSupplementalConfig + + GraphQLScalarTypeSupplementalConfig + + GraphQLObjectTypeSupplementalConfig + + GraphQLInterfaceTypeSupplementalConfig + + GraphQLUnionTypeSupplementalConfig + + GraphQLEnumTypeSupplementalConfig + + GraphQLEnumValueSupplementalConfig + + GraphQLInputObjectTypeSupplementalConfig + + GraphQLFieldSupplementalConfig + + GraphQLFieldSupplementalConfigMap + + GraphQLArgumentSupplementalConfig + + GraphQLInputFieldSupplementalConfig + + GraphQLDirectiveSupplementalConfig + + ExtendSchemaOptions

@@ -2337,8 +2360,8 @@ Builds a GraphQLSchema from a parsed schema definition language document. If no schema definition is provided, then it will look for types named Query, Mutation and Subscription. -The resulting schema has no resolver functions, so execution will use the -default field resolver. +The resulting schema uses the default field resolver unless resolver +functions are supplied through `supplementalConfig`. **Signature:** @@ -2494,6 +2517,42 @@ schema.getQueryType().name; // => 'Query'
Example 2
+```ts +// Supply resolver functions and other constructor-only config alongside SDL. +import { graphqlSync } from 'graphql'; +import { buildSchema } from 'graphql/utilities'; + +const schema = buildSchema( + ` + type Query { + greeting(name: String = "world"): String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: (_source, { name }) => `Hello, ${name}!`, + }, + }, + }, + }, + }, +); + +const result = graphqlSync({ + schema, + source: '{ greeting(name: "GraphQL") }', +}); + +result.data; // => { greeting: 'Hello, GraphQL!' } +``` + +
+ +
Example 3
+ ```ts // This variant enables parser options and omits source locations. import { buildSchema } from 'graphql/utilities'; @@ -2530,7 +2589,7 @@ producing the copy. The original schema remains unaltered. **Signature:** - +
@@ -2557,7 +2616,7 @@ producing the copy. The original schema remains unaltered. options? - + Optional configuration for this operation. @@ -2617,6 +2676,44 @@ Object.keys(extendedSchema.getQueryType().getFields()); // => ['greeting', 'fare
Example 2
+```ts +// Attach resolvers to fields newly added by an SDL extension. +import { graphqlSync } from 'graphql'; +import { parse } from 'graphql/language'; +import { buildSchema, extendSchema } from 'graphql/utilities'; + +const schema = buildSchema(` + type Query { + greeting: String + } +`); +const extensionAST = parse(` + extend type Query { + farewell: String + } +`); + +const extendedSchema = extendSchema(schema, extensionAST, { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + farewell: () => 'Goodbye!', + }, + }, + }, + }, +}); + +const result = graphqlSync({ schema: extendedSchema, source: '{ farewell }' }); + +result.data; // => { farewell: 'Goodbye!' } +``` + +
+ +
Example 3
+ ```ts // This variant bypasses validation for an otherwise invalid extension. import { parse } from 'graphql/language'; @@ -2757,6 +2854,522 @@ printSchema(sortedSchema); Set to true to assume the SDL is valid.
Default: false + + assumeValidSupplementalConfig? + + Set to true to assume the supplemental config is valid.
+Default: false + + + supplementalConfig? + + Supplemental schema constructor config not expressible in SDL. + + + + +
+ +#### GraphQLSchemaSupplementalConfig + +**Interface.** Supplemental schema constructor config for SDL-built elements when SDL cannot +express the config directly. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
scalarTypes?Supplemental config keyed by scalar type name.
objectTypes?Supplemental config keyed by object type name.
interfaceTypes?Supplemental config keyed by interface type name.
unionTypes?Supplemental config keyed by union type name.
enumTypes?Supplemental config keyed by enum type name.
inputObjectTypes?Supplemental config keyed by input object type name.
directives?Supplemental config keyed by directive name.
extensions?Custom extensions.
+ +
+ +#### GraphQLScalarTypeSupplementalConfig + +**Interface.** Supplemental config for a scalar type. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
serialize? Deprecated legacy serializer used to convert internal values for response
+output. Use {"coerceOutputValue()"} instead.
parseValue? Deprecated legacy parser used to convert externally provided input values.
+Use {"coerceInputValue()"} instead.
parseLiteral? Deprecated legacy parser used to convert externally provided input
+literals. Use {"replaceVariables()"} and {"coerceInputLiteral()"} instead.
coerceOutputValue?Coerces an internal value to include in a response.
coerceInputValue?Coerces an externally provided value to use as an input.
coerceInputLiteral?Coerces an externally provided const literal value to use as an input.
valueToLiteral?Translates an externally provided value to a literal (AST).
extensions?Custom extensions.
+ +
+ +#### GraphQLObjectTypeSupplementalConfig + +**Interface.** Supplemental config for an object type. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
fields?Supplemental config for fields declared by the document.
isTypeOf?Predicate used to determine whether a runtime value belongs to this object type.
extensions?Custom extensions.
+ +
+ +#### GraphQLInterfaceTypeSupplementalConfig + +**Interface.** Supplemental config for an interface type. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
fields?Supplemental config for fields declared by the document.
resolveType?Optionally provide a custom type resolver function. If one is not provided,
+the default implementation will call {"isTypeOf"} on each implementing
+Object type.
extensions?Custom extensions.
+ +
+ +#### GraphQLUnionTypeSupplementalConfig + +**Interface.** Supplemental config for a union type. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
resolveType?Optionally provide a custom type resolver function. If one is not provided,
+the default implementation will call {"isTypeOf"} on each implementing
+Object type.
extensions?Custom extensions.
+ +
+ +#### GraphQLEnumTypeSupplementalConfig + +**Interface.** Supplemental config for an enum type. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
values?Supplemental config for values declared by the document.
extensions?Custom extensions.
+ +
+ +#### GraphQLEnumValueSupplementalConfig + +**Interface.** Supplemental config for an enum value. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
value?Internal runtime value represented by this enum value.
extensions?Custom extensions.
+ +
+ +#### GraphQLInputObjectTypeSupplementalConfig + +**Interface.** Supplemental config for an input object type. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
fields?Supplemental config for fields declared by the document.
extensions?Custom extensions.
+ +
+ +#### GraphQLFieldSupplementalConfig + +**Type alias.** Supplemental config for a field declared by an object or interface. + + + +
+ +#### GraphQLFieldSupplementalConfigMap + +**Interface.** Supplemental config map for a field declared by an object or interface. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
args?Supplemental config for arguments declared by the document.
resolve?Resolver function used to produce this field value.
subscribe?Resolver function used to create a subscription event stream for this field.
extensions?Custom extensions.
+ +
+ +#### GraphQLArgumentSupplementalConfig + +**Interface.** Supplemental config for an argument declared by a field or directive. + +
+ +
Members
+ + + + + + + + + + + + + + + + +
NameTypeDescription
extensions?Custom extensions.
+ +
+ +#### GraphQLInputFieldSupplementalConfig + +**Interface.** Supplemental config for an input object field. + +
+ +
Members
+ + + + + + + + + + + + + + + + +
NameTypeDescription
extensions?Custom extensions.
+ +
+ +#### GraphQLDirectiveSupplementalConfig + +**Interface.** Supplemental config for a directive declared by SDL. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
args?Supplemental config for arguments declared by the document.
extensions?Custom extensions.
+ +
+ +#### ExtendSchemaOptions + +**Interface.** Options used when extending a schema from a parsed SDL document. + +
+ +
Members
+ + + + + + + + + + + + + + + + + + + +
NameTypeDescription
assumeValidSupplementalConfig?Set to true to assume the supplemental config is valid.
+Default: false
supplementalConfig?Supplemental schema constructor config not expressible in SDL.
+It applies only to elements newly added by the document, including fields
+added to an existing type by a type extension.
diff --git a/website/pages/api-v17/validation.mdx b/website/pages/api-v17/validation.mdx index a8a4bae7fe..a8d8381823 100644 --- a/website/pages/api-v17/validation.mdx +++ b/website/pages/api-v17/validation.mdx @@ -4505,27 +4505,15 @@ necessarily to forbid their use when querying a service.
Example
```ts -import { - GraphQLObjectType, - GraphQLSchema, - GraphQLString, - parse, - validate, -} from 'graphql'; +import { buildSchema, parse, validate } from 'graphql'; import { NoDeprecatedCustomRule } from 'graphql/validation'; -const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - name: { type: GraphQLString }, - oldName: { - type: GraphQLString, - deprecationReason: 'Use name instead.', - }, - }, - }), -}); +const schema = buildSchema(` + type Query { + name: String + oldName: String @deprecated(reason: "Use name instead.") + } +`); const invalidDocument = parse(` { oldName } diff --git a/website/pages/docs/abort-signals.mdx b/website/pages/docs/abort-signals.mdx index 5238a0871e..5acaa87950 100644 --- a/website/pages/docs/abort-signals.mdx +++ b/website/pages/docs/abort-signals.mdx @@ -3,7 +3,7 @@ title: Handling Abort Signals sidebarTitle: Abort Signals --- -import { Callout } from 'nextra/components'; +import { Callout, Tabs } from 'nextra/components'; # Handling Abort Signals @@ -72,27 +72,90 @@ resolver signal. This coarseness may change in future versions. Resolvers obtain the abort signal via `info.getAbortSignal()`. Pass that signal to downstream APIs that support cancellation. + + + ```js -const Query = new GraphQLObjectType({ - name: 'Query', +import { + GraphQLID, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, +} from 'graphql'; + +const User = new GraphQLObjectType({ + name: 'User', fields: { - user: { - type: User, - args: { id: { type: new GraphQLNonNull(GraphQLID) } }, - async resolve(_source, args, _context, info) { - const abortSignal = info.getAbortSignal(); + id: { type: new GraphQLNonNull(GraphQLID) }, + name: { type: GraphQLString }, + }, +}); + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + user: { + type: User, + args: { id: { type: new GraphQLNonNull(GraphQLID) } }, + async resolve(_source, args, _context, info) { + const abortSignal = info.getAbortSignal(); + + const response = await fetch(`https://users.example/${args.id}`, { + signal: abortSignal, + }); + + return response.json(); + }, + }, + }, + }), +}); +``` - const response = await fetch(`https://users.example/${args.id}`, { - signal: abortSignal, - }); + + - return response.json(); +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type User { + id: ID! + name: String + } + + type Query { + user(id: ID!): User + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + async user(_source, args, _context, info) { + const abortSignal = info.getAbortSignal(); + + const response = await fetch(`https://users.example/${args.id}`, { + signal: abortSignal, + }); + + return response.json(); + }, + }, + }, }, }, }, -}); +); ``` + + + As noted, this signal can be triggered by GraphQL.js itself. For example, suppose a query selects `user { profile recommendations }`, `profile` is non-null, and the `recommendations` resolver starts a slow downstream request. diff --git a/website/pages/docs/abstract-types.mdx b/website/pages/docs/abstract-types.mdx index 1762f9bb53..baa03b5944 100644 --- a/website/pages/docs/abstract-types.mdx +++ b/website/pages/docs/abstract-types.mdx @@ -4,12 +4,14 @@ title: Abstract types in GraphQL.js # Abstract types in GraphQL.js +import { Tabs } from 'nextra/components'; + GraphQL includes two kinds of abstract types: interfaces and unions. These types let a single field return values of different object types, while keeping your schema type-safe. -This guide covers how to define and resolve abstract types using GraphQL.js. It focuses on -constructing types in JavaScript using the GraphQL.js type system, not the schema definition -language (SDL). +This guide covers how to define and resolve abstract types using GraphQL.js. You can define +abstract types with code-first constructors or SDL. Runtime type resolution can come from +`resolveType`, `isTypeOf`, or from values that include `__typename`. ## What are abstract types? @@ -31,12 +33,16 @@ GraphQL provides two kinds of abstract types: ## Defining interfaces -To define an interface in GraphQL.js, use the `GraphQLInterfaceType` constructor. An interface -must include a `name`, definition of the shared `fields`, and should include a `resolveType` -function telling GraphQL which concrete type a given value corresponds to. +An interface must include a name and shared fields. In SDL, object types implement the interface +with `implements`. The `resolveType` function is JavaScript behavior, so provide it through +`supplementalConfig` or through the `GraphQLInterfaceType` constructor. If your returned values +include `__typename`, GraphQL.js can use that value for runtime type resolution. The following example defines a `ContentItem` interface for a publishing platform: + + + ```js filename="ContentItemInterface.js" import { GraphQLInterfaceType, GraphQLString, GraphQLNonNull } from 'graphql'; @@ -61,18 +67,80 @@ const ContentItemInterface = new GraphQLInterfaceType({ exports.ContentItemInterface = ContentItemInterface; ``` + + + +```js filename="schema.js" +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + interface ContentItem { + id: String! + title: String + publishedAt: String + } + + type Article implements ContentItem { + id: String! + title: String + publishedAt: String + bodyText: String + } + + type PodcastEpisode implements ContentItem { + id: String! + title: String + publishedAt: String + audioUrl: String + } + + type Query { + latest: ContentItem + } + `, + { + supplementalConfig: { + interfaceTypes: { + ContentItem: { + resolveType(value) { + if (value.audioUrl) { + return 'PodcastEpisode'; + } + if (value.bodyText) { + return 'Article'; + } + return null; + }, + }, + }, + }, + }, +); +``` + + + + The `resolveType` function must return either the string type name corresponding to the `GraphQLObjectType` of the given `value`, or `null` if the type could not be determined. +If returned values already include `__typename`, GraphQL.js can use that value +for default runtime type resolution instead of a schema-attached `resolveType`. ## Implementing interfaces with object types -To implement an interface, define a `GraphQLObjectType` and include the interface in its -`interfaces` array. The object type must implement all fields defined by the interface. +To implement an interface, declare the object type with `implements` in SDL or include the +interface in a `GraphQLObjectType` `interfaces` array. The object type must implement all fields +defined by the interface. `isTypeOf` is optional runtime behavior; it can be supplied through +`supplementalConfig` or in the object type constructor. The following example implements the `Article` and `PodcastEpisode` types that conform to the `ContentItem` interface: + + + ```js import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from 'graphql'; import { ContentItemInterface } from './ContentItemInterface.js'; @@ -102,20 +170,74 @@ const PodcastEpisodeType = new GraphQLObjectType({ }); ``` + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + interface ContentItem { + id: String! + title: String + publishedAt: String + } + + type Article implements ContentItem { + id: String! + title: String + publishedAt: String + bodyText: String + } + + type PodcastEpisode implements ContentItem { + id: String! + title: String + publishedAt: String + audioUrl: String + } + + type Query { + latest: ContentItem + } + `, + { + supplementalConfig: { + objectTypes: { + Article: { + isTypeOf: (value) => value.bodyText !== undefined, + }, + PodcastEpisode: { + isTypeOf: (value) => value.audioUrl !== undefined, + }, + }, + }, + }, +); +``` + + + + The `isTypeOf` function is optional. It provides a fallback when `resolveType` isn't defined, or when runtime values could match multiple types. If both `resolveType` and `isTypeOf` are defined, GraphQL uses `resolveType`. +In SDL, supply `isTypeOf` through `supplementalConfig` because it is schema configuration. ## Defining union types -Use the `GraphQLUnionType` constructor to define a union. A union allows a field to return one -of several object types that don't need to share fields. +Use `union` in SDL or the `GraphQLUnionType` constructor to define a union. A union allows a field +to return one of several object types that don't need to share fields. -A union requires a name and a list of object types (`types`). It should also be -provided a `resolveType` function the same as explained for interfaces above. +A union requires a name and a list of object types. It should also be provided a `resolveType` +function the same as explained for interfaces above. The following example defines a `SearchResult` union: + + + ```js import { GraphQLUnionType } from 'graphql'; @@ -137,6 +259,61 @@ const SearchResultType = new GraphQLUnionType({ }); ``` + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Book { + title: String + isbn: String + } + + type Author { + name: String + bio: String + } + + type Publisher { + name: String + catalogSize: Int + } + + union SearchResult = Book | Author | Publisher + + type Query { + search(term: String!): [SearchResult] + } + `, + { + supplementalConfig: { + unionTypes: { + SearchResult: { + resolveType(value) { + if (value.isbn) { + return 'Book'; + } + if (value.bio) { + return 'Author'; + } + if (value.catalogSize) { + return 'Publisher'; + } + return null; + }, + }, + }, + }, + }, +); +``` + + + + Unlike interfaces, unions don't declare any fields their members must implement. Clients use a fragment with a type condition to query fields from a concrete type. @@ -149,7 +326,7 @@ This function receives the following arguments: {/* prettier-ignore */} ```js -resolveType(value, context, info) +resolveType(value, context, info); ``` It can return: diff --git a/website/pages/docs/advanced-custom-scalars.mdx b/website/pages/docs/advanced-custom-scalars.mdx index c2eff04869..3eb9532a80 100644 --- a/website/pages/docs/advanced-custom-scalars.mdx +++ b/website/pages/docs/advanced-custom-scalars.mdx @@ -128,33 +128,39 @@ describe('DateTime scalar', () => { ### Test custom scalars in a schema Integrate the scalar into a schema and run real GraphQL queries to validate end-to-end behavior. +The scalar coercion functions must be part of the schema configuration. ```js -import { graphql, GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { graphql, buildSchema } from 'graphql'; import { DateTimeResolver as DateTime } from 'graphql-scalars'; -const Query = new GraphQLObjectType({ - name: 'Query', - fields: { - now: { - type: DateTime, - resolve() { - return new Date(); +const schema = buildSchema( + /* GraphQL */ ` + scalar DateTime + + type Query { + now: DateTime + } + `, + { + supplementalConfig: { + scalarTypes: { + DateTime: { + serialize: DateTime.serialize.bind(DateTime), + parseValue: DateTime.parseValue.bind(DateTime), + parseLiteral: DateTime.parseLiteral.bind(DateTime), + }, + }, + objectTypes: { + Query: { + fields: { + now: () => new Date(), + }, + }, }, }, }, -}); - -/* - scalar DateTime - - type Query { - now: DateTime - } -*/ -const schema = new GraphQLSchema({ - query: Query, -}); +); async function testQuery() { const response = await graphql({ diff --git a/website/pages/docs/authentication-and-express-middleware.mdx b/website/pages/docs/authentication-and-express-middleware.mdx index 5c493b7107..760af6cc55 100644 --- a/website/pages/docs/authentication-and-express-middleware.mdx +++ b/website/pages/docs/authentication-and-express-middleware.mdx @@ -11,36 +11,41 @@ It's simple to use any Express middleware in conjunction with `graphql-http`. In To use middleware with a GraphQL resolver, just use the middleware like you would with a normal Express app. The `request` object is then available as the second argument in any resolver. -For example, let's say we wanted our server to log the IP address of every request, and we also want to write an API that returns the IP address of the caller. We can do the former with middleware, and the latter by accessing the `request` object in a resolver. Here's server code that implements this: +For example, let's say we wanted our server to log the IP address of every request, and we also want to write an API that returns the IP address of the caller. We can do the former with middleware, and the latter by accessing the `request` object in a resolver. - + ```js import express from 'express'; import { createHandler } from 'graphql-http/lib/use/express'; -import { buildSchema } from 'graphql'; +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; -const schema = buildSchema(`type Query { ip: String }`); +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + ip: { + type: GraphQLString, + resolve: (_source, _args, context) => { + return context.ip; + }, + }, + }, + }), +}); function loggingMiddleware(req, res, next) { console.log('ip:', req.ip); next(); } -const root = { - ip(args, context) { - return context.ip; - }, -}; - const app = express(); app.use(loggingMiddleware); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, context: (req) => ({ ip: req.raw.ip, }), @@ -56,21 +61,28 @@ console.log('Running a GraphQL API server at localhost:4000/graphql'); ```js import express from 'express'; import { createHandler } from 'graphql-http/lib/use/express'; -import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; +import { buildSchema } from 'graphql'; -const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - ip: { - type: GraphQLString, - resolve: (_, args, context) => { - return context.ip; +const schema = buildSchema( + /* GraphQL */ ` + type Query { + ip: String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + ip: (_source, _args, context) => { + return context.ip; + }, + }, }, }, }, - }), -}); + }, +); function loggingMiddleware(req, res, next) { console.log('ip:', req.ip); diff --git a/website/pages/docs/basic-types.mdx b/website/pages/docs/basic-types.mdx index d051a1f767..18e3fa88ce 100644 --- a/website/pages/docs/basic-types.mdx +++ b/website/pages/docs/basic-types.mdx @@ -6,59 +6,17 @@ title: Basic Types import { Tabs } from 'nextra/components'; -In most situations, all you need to do is to specify the types for your API using the GraphQL schema language, taken as an argument to the `buildSchema` function. +In most situations, the first step is to specify the types for your API. You can do that with code-first constructors, or with the GraphQL schema language passed to the `buildSchema` function. -The GraphQL schema language supports the scalar types of `String`, `Int`, `Float`, `Boolean`, and `ID`, so you can use these directly in the schema you pass to `buildSchema`. +The GraphQL schema language supports the scalar types of `String`, `Int`, `Float`, `Boolean`, and `ID`, so you can use these directly in SDL schemas. By default, every type is nullable - it's legitimate to return `null` as any of the scalar types. Use an exclamation point to indicate a type cannot be nullable, so `String!` is a non-nullable string. To use a list type, surround the type in square brackets, so `[Int]` is a list of integers. -Each of these types maps straightforwardly to JavaScript, so you can just return plain old JavaScript objects in APIs that return these types. Here's an example that shows how to use some of these basic types: +Each of these types maps straightforwardly to JavaScript, so you can just return plain old JavaScript objects in APIs that return these types. Here's an example: - - - -```js -import express from 'express'; -import { createHandler } from 'graphql-http/lib/use/express'; -import { buildSchema } from 'graphql'; - -// Construct a schema, using GraphQL schema language -const schema = buildSchema(` - type Query { - quoteOfTheDay: String - random: Float! - rollThreeDice: [Int] - } -`); - -// The root provides a resolver function for each API endpoint -const root = { - quoteOfTheDay() { - return Math.random() < 0.5 ? 'Take it easy' : 'Salvation lies within'; - }, - random() { - return Math.random(); - }, - rollThreeDice() { - return [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)); - }, -}; - -const app = express(); -app.all( - '/graphql', - createHandler({ - schema: schema, - rootValue: root, - }), -); -app.listen(4000); -console.log('Running a GraphQL API server at localhost:4000/graphql'); -``` - - + ```js @@ -69,10 +27,11 @@ import { GraphQLSchema, GraphQLString, GraphQLFloat, + GraphQLInt, GraphQLList, } from 'graphql'; -// Construct a schema +// Construct a schema. const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', @@ -87,13 +46,57 @@ const schema = new GraphQLSchema({ resolve: () => Math.random(), }, rollThreeDice: { - type: new GraphQLList(GraphQLFloat), + type: new GraphQLList(GraphQLInt), resolve: () => [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)), }, }, }), }); +const app = express(); +app.all( + '/graphql', + createHandler({ + schema: schema, + }), +); +app.listen(4000); +console.log('Running a GraphQL API server at localhost:4000/graphql'); +``` + + + + +```js +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Query { + quoteOfTheDay: String + random: Float! + rollThreeDice: [Int] + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + quoteOfTheDay: () => + Math.random() < 0.5 ? 'Take it easy' : 'Salvation lies within', + random: () => Math.random(), + rollThreeDice: () => + [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)), + }, + }, + }, + }, + }, +); + const app = express(); app.all( '/graphql', diff --git a/website/pages/docs/constructing-types.mdx b/website/pages/docs/constructing-types.mdx index be6da1747c..bf61199589 100644 --- a/website/pages/docs/constructing-types.mdx +++ b/website/pages/docs/constructing-types.mdx @@ -6,30 +6,19 @@ title: Constructing Types import { Tabs } from 'nextra/components'; -For many apps, you can define a fixed schema when the application starts, and define it using GraphQL schema language. In some cases, it's useful to construct a schema programmatically. You can do this using the `GraphQLSchema` constructor. +GraphQL.js can construct a schema in code, from GraphQL schema language, or from schema language plus supplemental JavaScript config for behavior that SDL cannot represent directly, such as field resolvers, abstract type resolution, custom scalar coercion, and extension metadata. -When you are using the `GraphQLSchema` constructor to create a schema, instead of defining `Query` and `Mutation` types solely using schema language, you create them as separate object types. +When you are using the `GraphQLSchema` constructor to create a schema, instead of defining `Query` and `Mutation` types solely using schema language, you create them as separate object types. This can be a good fit when the schema is generated from JavaScript data structures or when you want native language composition to drive the schema shape. -For example, let's say we are building a simple API that lets you fetch user data for a few hardcoded users based on an id. Using `buildSchema` we could write a server with: +For example, let's say we are building a simple API that lets you fetch user data for a few hardcoded users based on an id. The same API can be written with code-first constructors or SDL. - + ```js import express from 'express'; import { createHandler } from 'graphql-http/lib/use/express'; -import { buildSchema } from 'graphql'; - -const schema = buildSchema(` - type User { - id: String - name: String - } - - type Query { - user(id: String): User - } -`); +import * as graphql from 'graphql'; // Maps id to User object const fakeDatabase = { @@ -43,18 +32,39 @@ const fakeDatabase = { }, }; -const root = { - user({ id }) { - return fakeDatabase[id]; +// Define the User type +const userType = new graphql.GraphQLObjectType({ + name: 'User', + fields: { + id: { type: graphql.GraphQLString }, + name: { type: graphql.GraphQLString }, }, -}; +}); + +// Define the Query type with inline resolver +const queryType = new graphql.GraphQLObjectType({ + name: 'Query', + fields: { + user: { + type: userType, + // `args` describes the arguments that the `user` query accepts + args: { + id: { type: graphql.GraphQLString }, + }, + resolve: (_, { id }) => { + return fakeDatabase[id]; + }, + }, + }, +}); + +const schema = new graphql.GraphQLSchema({ query: queryType }); const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000); @@ -67,7 +77,7 @@ console.log('Running a GraphQL API server at localhost:4000/graphql'); ```js import express from 'express'; import { createHandler } from 'graphql-http/lib/use/express'; -import * as graphql from 'graphql'; +import { buildSchema } from 'graphql'; // Maps id to User object const fakeDatabase = { @@ -81,33 +91,29 @@ const fakeDatabase = { }, }; -// Define the User type -const userType = new graphql.GraphQLObjectType({ - name: 'User', - fields: { - id: { type: graphql.GraphQLString }, - name: { type: graphql.GraphQLString }, - }, -}); - -// Define the Query type with inline resolver -const queryType = new graphql.GraphQLObjectType({ - name: 'Query', - fields: { - user: { - type: userType, - // `args` describes the arguments that the `user` query accepts - args: { - id: { type: graphql.GraphQLString }, - }, - resolve: (_, { id }) => { - return fakeDatabase[id]; +const schema = buildSchema( + /* GraphQL */ ` + type User { + id: String + name: String + } + + type Query { + user(id: String): User + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + user: (_source, { id }) => fakeDatabase[id], + }, + }, }, }, }, -}); - -const schema = new graphql.GraphQLSchema({ query: queryType }); +); const app = express(); app.all( @@ -123,6 +129,6 @@ console.log('Running a GraphQL API server at localhost:4000/graphql'); -When we use the `GraphQLSchema` constructor method of creating the API, the root level resolvers are implemented on the `Query` and `Mutation` types rather than on a `root` object. +When we use the `GraphQLSchema` constructor method of creating the API, the root level resolvers are implemented on the `Query` and `Mutation` types. With SDL, those resolvers are installed on the built `Query` and `Mutation` fields. -This can be particularly useful if you want to create a GraphQL schema automatically from something else, like a database schema. You might have a common format for something like creating and updating database records. This is also useful for implementing features like union types which don't map cleanly to ES6 classes and schema language. +Programmatic construction can be particularly useful if you want to create a GraphQL schema automatically from something else, like a database schema, or if native JavaScript composition makes the schema easier to maintain. SDL is useful when you want schema language and schema-attached JavaScript behavior together. diff --git a/website/pages/docs/cursor-based-pagination.mdx b/website/pages/docs/cursor-based-pagination.mdx index a663fe23e8..e9a093cc42 100644 --- a/website/pages/docs/cursor-based-pagination.mdx +++ b/website/pages/docs/cursor-based-pagination.mdx @@ -2,7 +2,7 @@ title: Implementing Cursor-based Pagination --- -import { Callout } from 'nextra/components'; +import { Callout, Tabs } from 'nextra/components'; # Implementing Cursor-based Pagination @@ -90,6 +90,9 @@ more data is available. To support this structure in your schema, define a few custom types: + + + ```js const PageInfoType = new GraphQLObjectType({ name: 'PageInfo', @@ -102,6 +105,21 @@ const PageInfoType = new GraphQLObjectType({ }); ``` + + + +```graphql +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} +``` + + + + The `PageInfo` type provides metadata about the current page of results. The `hasNextPage` and `hasPreviousPage` fields indicate whether more results are available in either direction. The `startCursor` and `endCursor` @@ -109,6 +127,9 @@ fields help clients resume pagination from a specific point. Next, define an edge type to represent individual items in the connection: + + + ```js const UserEdgeType = new GraphQLObjectType({ name: 'UserEdge', @@ -119,11 +140,27 @@ const UserEdgeType = new GraphQLObjectType({ }); ``` + + + +```graphql +type UserEdge { + node: User + cursor: String! +} +``` + + + + Each edge includes a `node` and a `cursor`, which marks its position in the list. Then, define the connection type itself: + + + ```js const UserConnectionType = new GraphQLObjectType({ name: 'UserConnection', @@ -138,11 +175,27 @@ const UserConnectionType = new GraphQLObjectType({ }); ``` + + + +```graphql +type UserConnection { + edges: [UserEdge!]! + pageInfo: PageInfo! +} +``` + + + + The connection type wraps a list of edges and includes the pagination metadata. Paginated fields typically accept the following arguments: + + + ```js const connectionArgs = { first: { type: GraphQLInt }, @@ -150,8 +203,25 @@ const connectionArgs = { last: { type: GraphQLInt }, before: { type: GraphQLString }, }; + +const usersField = { + type: UserConnectionType, + args: connectionArgs, +}; +``` + + + + +```graphql +type Query { + users(first: Int, after: String, last: Int, before: String): UserConnection +} ``` + + + Use `first` and `after` for forward pagination. The `last` and `before` arguments enable backward pagination if needed. diff --git a/website/pages/docs/custom-scalars.mdx b/website/pages/docs/custom-scalars.mdx index 064c6cbaf9..8b5793c2c2 100644 --- a/website/pages/docs/custom-scalars.mdx +++ b/website/pages/docs/custom-scalars.mdx @@ -2,7 +2,7 @@ title: Using Custom Scalars --- -import { Callout } from 'nextra/components'; +import { Callout, Tabs } from 'nextra/components'; # Custom Scalars: When and How to Use Them @@ -15,11 +15,14 @@ APIs often need. For example, you might want to represent a timestamp as an ISO ensure a user-submitted field is a valid email address. In these cases, you can define a custom scalar type. -In GraphQL.js, custom scalars are created using the `GraphQLScalarType` class. This gives you -full control over how values are serialized, parsed, and validated. +GraphQL.js can define custom scalars by constructing a `GraphQLScalarType`, or by declaring +the scalar in SDL and supplying JavaScript coercion functions through `supplementalConfig`. Here’s a simple example of a custom scalar that handles date-time strings: + + + ```js import { GraphQLScalarType, Kind } from 'graphql'; @@ -38,14 +41,60 @@ const DateTime = new GraphQLScalarType({ return value instanceof Date ? value.toISOString() : null; }, parseValue(value) { - return typeof value === 'string' ? new Date(value) : null; + return typeof value === 'string' ? parseDate(value) : null; }, parseLiteral(ast) { - return ast.kind === Kind.STRING ? new Date(ast.value) : null; + return ast.kind === Kind.STRING ? parseDate(ast.value) : null; }, }); ``` + + + +```js +import { Kind, buildSchema } from 'graphql'; + +function parseDate(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; +} + +const schema = buildSchema( + /* GraphQL */ ` + "An ISO-8601 encoded UTC date string." + scalar DateTime + + type Query { + now: DateTime + } + `, + { + supplementalConfig: { + scalarTypes: { + DateTime: { + serialize(value) { + return value instanceof Date ? value.toISOString() : null; + }, + parseValue(value) { + return typeof value === 'string' ? parseDate(value) : null; + }, + parseLiteral(ast) { + return ast.kind === Kind.STRING ? parseDate(ast.value) : null; + }, + }, + }, + }, + }, +); +``` + + + + Custom scalars offer flexibility, but they also shift responsibility onto you. You're defining not just the format of a value, but also how it is validated and how it moves through your schema. @@ -85,8 +134,9 @@ system, not to replace structured types altogether. ## How to define a custom scalar in GraphQL.js -In GraphQL.js, a custom scalar is defined by creating an instance of `GraphQLScalarType`, -providing a name, description, and three functions: +In code-first schemas, custom scalar behavior is supplied to `GraphQLScalarType`. With SDL, +the scalar is declared with `scalar`, and its JavaScript behavior is supplied by +`supplementalConfig`. In either style, provide a name, description, and coercion functions: - `serialize`: How the server sends internal values to clients. - `parseValue`: How the server parses incoming variable values. @@ -99,9 +149,20 @@ providing a name, description, and three functions: The following example is a custom `DateTime` scalar that handles ISO-8601 encoded date strings: + + + ```js import { GraphQLScalarType, Kind } from 'graphql'; +function parseDate(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; +} + const DateTime = new GraphQLScalarType({ name: 'DateTime', description: 'An ISO-8601 encoded UTC date string.', @@ -115,13 +176,7 @@ const DateTime = new GraphQLScalarType({ }, parseValue(value) { - const date = new Date(value); - if (isNaN(date.getTime())) { - throw new TypeError( - `DateTime cannot represent an invalid date: ${value}`, - ); - } - return date; + return parseDate(value); }, parseLiteral(ast) { @@ -130,17 +185,67 @@ const DateTime = new GraphQLScalarType({ `DateTime can only parse string values, but got: ${ast.kind}`, ); } - const date = new Date(ast.value); - if (isNaN(date.getTime())) { - throw new TypeError( - `DateTime cannot represent an invalid date: ${ast.value}`, - ); - } - return date; + return parseDate(ast.value); }, }); ``` + + + +```js +import { Kind, buildSchema } from 'graphql'; + +function parseDate(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; +} + +const schema = buildSchema( + /* GraphQL */ ` + scalar DateTime + @specifiedBy(url: "https://scalars.graphql.org/andimarek/date-time.html") + + type Query { + now: DateTime + } + `, + { + supplementalConfig: { + scalarTypes: { + DateTime: { + serialize(value) { + if (!(value instanceof Date)) { + throw new TypeError('DateTime can only serialize Date instances'); + } + return value.toISOString(); + }, + + parseValue(value) { + return parseDate(value); + }, + + parseLiteral(ast) { + if (ast.kind !== Kind.STRING) { + throw new TypeError( + `DateTime can only parse string values, but got: ${ast.kind}`, + ); + } + return parseDate(ast.value); + }, + }, + }, + }, + }, +); +``` + + + + These functions give you full control over validation and data flow. ## v17 scalar method names @@ -164,12 +269,22 @@ During execution, GraphQL.js replaces variables before calling coercion directly outside execution, it must call `replaceVariables()` itself before passing the literal to `coerceInputLiteral()`. + + + ```js import { GraphQLScalarType, Kind } from 'graphql'; +function parseDate(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; +} + const DateTime = new GraphQLScalarType({ name: 'DateTime', - description: 'An ISO-8601 encoded UTC date string.', coerceOutputValue(value) { if (!(value instanceof Date)) { @@ -199,6 +314,67 @@ const DateTime = new GraphQLScalarType({ }); ``` + + + +```js +import { Kind, buildSchema } from 'graphql'; + +function parseDate(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; +} + +const schema = buildSchema( + /* GraphQL */ ` + scalar DateTime + + type Query { + now: DateTime + } + `, + { + supplementalConfig: { + scalarTypes: { + DateTime: { + coerceOutputValue(value) { + if (!(value instanceof Date)) { + throw new TypeError('DateTime can only serialize Date instances'); + } + return value.toISOString(); + }, + + coerceInputValue(value) { + return parseDate(value); + }, + + coerceInputLiteral(ast) { + if (ast.kind !== Kind.STRING) { + throw new TypeError( + `DateTime can only parse string values, but got: ${ast.kind}`, + ); + } + return parseDate(ast.value); + }, + + valueToLiteral(value) { + if (typeof value === 'string') { + return { kind: Kind.STRING, value, block: false }; + } + }, + }, + }, + }, + }, +); +``` + + + + If you need one scalar definition that supports both v16 and v17, keep the v16 method names until your minimum GraphQL.js version is v17. diff --git a/website/pages/docs/getting-started.mdx b/website/pages/docs/getting-started.mdx index cb141cc4f3..ec2e533eb4 100644 --- a/website/pages/docs/getting-started.mdx +++ b/website/pages/docs/getting-started.mdx @@ -58,37 +58,11 @@ npm install graphql@16 --save ## Writing Code -To handle GraphQL queries, we need a schema that defines the `Query` type, and we need an API root with a function called a "resolver" for each API endpoint. For an API that just returns "Hello world!", create a file named `server.js` in your project root (the same directory as your `package.json`): +To handle GraphQL queries, we need a schema that defines the `Query` type, and we need resolver functions for the fields clients can execute. GraphQL.js supports code-first constructors and SDL. For an API that just returns "Hello world!", create a file named `server.js` in your project root (the same directory as your `package.json`): - + -```javascript -import { graphql, buildSchema } from 'graphql'; - -// Construct a schema, using GraphQL schema language -const schema = buildSchema(`type Query { hello: String } `); - -// The rootValue provides a resolver function for each API endpoint -const rootValue = { - hello() { - return 'Hello world!'; - }, -}; - -// Run the GraphQL query '{ hello }' and print out the response -graphql({ - schema, - source: '{ hello }', - rootValue, -}).then((response) => { - console.log(response); -}); -``` - - - - ```javascript import { graphql, @@ -97,7 +71,7 @@ import { GraphQLString, } from 'graphql'; -// Construct a schema +// Construct a schema. const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', @@ -118,7 +92,40 @@ graphql({ }); ``` - + + + +```javascript +import { graphql, buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Query { + hello: String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + hello: () => 'Hello world!', + }, + }, + }, + }, + }, +); + +graphql({ + schema, + source: '{ hello }', +}).then((response) => { + console.log(response); +}); +``` + + If you run this with: diff --git a/website/pages/docs/mutations-and-input-types.mdx b/website/pages/docs/mutations-and-input-types.mdx index 5fd5a07374..06f82a9545 100644 --- a/website/pages/docs/mutations-and-input-types.mdx +++ b/website/pages/docs/mutations-and-input-types.mdx @@ -8,22 +8,9 @@ import { Tabs } from 'nextra/components'; If you have an API endpoint that alters data, like inserting data into a database or altering data already in a database, you should make this endpoint a `Mutation` rather than a `Query`. This is as simple as making the API endpoint part of the top-level `Mutation` type instead of the top-level `Query` type. -Let's say we have a "message of the day" server, where anyone can update the message of the day, and anyone can read the current one. The GraphQL schema for this is simply: +Let's say we have a "message of the day" server, where anyone can update the message of the day, and anyone can read the current one. The same schema can be constructed in code, passed to `buildSchema`, or written as SDL: - - - -```graphql -type Mutation { - setMessage(message: String): String -} - -type Query { - getMessage: String -} -``` - - + ```js @@ -43,6 +30,36 @@ const schema = new GraphQLSchema({ }, }), }); +``` + + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema(/* GraphQL */ ` + type Mutation { + setMessage(message: String): String + } + + type Query { + getMessage: String + } +`); +``` + + + + +```graphql +type Mutation { + setMessage(message: String): String +} + +type Query { + getMessage: String +} ``` @@ -67,34 +84,9 @@ const root = { You don't need anything more than this to implement mutations. But in many cases, you will find a number of different mutations that all accept the same input parameters. A common example is that creating an object in a database and updating an object in a database often take the same parameters. To make your schema simpler, you can use "input types" for this, by using the `input` keyword instead of the `type` keyword. -For example, instead of a single message of the day, let's say we have many messages, indexed in a database by the `id` field, and each message has both a `content` string and an `author` string. We want a mutation API both for creating a new message and for updating an old message. We could use the schema: +For example, instead of a single message of the day, let's say we have many messages, indexed in a database by the `id` field, and each message has both a `content` string and an `author` string. We want a mutation API both for creating a new message and for updating an old message. The same schema and behavior can be represented with code-first constructors or SDL: - - - -```graphql -input MessageInput { - content: String - author: String -} - -type Message { - id: ID! - content: String - author: String -} - -type Query { - getMessage(id: ID!): Message -} - -type Mutation { - createMessage(input: MessageInput): Message - updateMessage(id: ID!, input: MessageInput): Message -} -``` - - + ```js @@ -193,6 +185,73 @@ const schema = new GraphQLSchema({ }, }), }); +``` + + + + +```js +import { randomBytes } from 'node:crypto'; +import { buildSchema } from 'graphql'; + +// Maps username to content +const fakeDatabase = {}; + +const schema = buildSchema( + /* GraphQL */ ` + input MessageInput { + content: String + author: String + } + + type Message { + id: ID! + content: String + author: String + } + + type Query { + getMessage(id: ID!): Message + } + + type Mutation { + createMessage(input: MessageInput): Message + updateMessage(id: ID!, input: MessageInput): Message + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + getMessage: (_source, { id }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + return { id, ...fakeDatabase[id] }; + }, + }, + }, + Mutation: { + fields: { + createMessage: (_source, { input }) => { + const id = randomBytes(10).toString('hex'); + fakeDatabase[id] = input; + return { id, ...input }; + }, + updateMessage: (_source, { id, input }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + fakeDatabase[id] = input; + return { id, ...input }; + }, + }, + }, + }, + }, + }, +); ``` @@ -239,85 +298,9 @@ literal form internally when building a schema from SDL. Invalid defaults are reported by schema validation instead of waiting until a query happens to use that field. -Here's some runnable code that implements this schema, keeping the data in memory: +Here's some runnable code that implements this schema, keeping the data in memory. The code-first version defines the schema and resolvers together with constructors. The SDL version defines the schema with schema language and installs resolver functions on the built schema. - - - -```js -import express from 'express'; -import { createHandler } from 'graphql-http/lib/use/express'; -import { buildSchema } from 'graphql'; - -const fakeDatabase = {}; - -// Construct a schema, using GraphQL schema language -const schema = buildSchema(` - input MessageInput { - content: String - author: String - } - - type Message { - id: ID! - content: String - author: String - } - - type Query { - getMessage(id: ID!): Message - getMessages: [Message] - } - - type Mutation { - createMessage(input: MessageInput): Message - updateMessage(id: ID!, input: MessageInput): Message - } -`); - -// If Message had any complex fields, we'd put them on this object. -class Message { - constructor(id, { content, author }) { - this.id = id; - this.content = content; - this.author = author; - } -} - -const root = { - getMessage: ({ id }) => { - return fakeDatabase[id]; - }, - getMessages: () => { - return Object.values(fakeDatabase); - }, - createMessage: ({ input }) => { - const id = String(Object.keys(fakeDatabase).length + 1); - const message = new Message(id, input); - fakeDatabase[id] = message; - return message; - }, - updateMessage: ({ id, input }) => { - const message = fakeDatabase[id]; - Object.assign(message, input); - return message; - }, -}; - -const app = express(); -app.all( - '/graphql', - createHandler({ - schema: schema, - rootValue: root, - }), -); -app.listen(4000, () => { - console.log('Running a GraphQL API server at localhost:4000/graphql'); -}); -``` - - + ```js @@ -330,6 +313,7 @@ import { GraphQLID, GraphQLInputObjectType, GraphQLNonNull, + GraphQLList, } from 'graphql'; // If Message had any complex fields, we'd put them on this object. @@ -370,18 +354,11 @@ const schema = new GraphQLSchema({ args: { id: { type: new GraphQLNonNull(GraphQLID) }, }, - resolve: (_, { id }) => { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - return fakeDatabase[id] - ? { - id, - content: fakeDatabase[id].content, - author: fakeDatabase[id].author, - } - : null; - }, + resolve: (_, { id }) => fakeDatabase[id], + }, + getMessages: { + type: new GraphQLList(Message), + resolve: () => Object.values(fakeDatabase), }, }, }), @@ -394,15 +371,10 @@ const schema = new GraphQLSchema({ input: { type: new GraphQLNonNull(MessageInput) }, }, resolve: (_, { input }) => { - // Create a random id for our "database". - import { randomBytes } from 'crypto'; - const id = randomBytes(10).toString('hex'); - fakeDatabase[id] = input; - return { - id, - content: input.content, - author: input.author, - }; + const id = String(Object.keys(fakeDatabase).length + 1); + const message = new Message(id, input); + fakeDatabase[id] = message; + return message; }, }, updateMessage: { @@ -412,22 +384,99 @@ const schema = new GraphQLSchema({ input: { type: new GraphQLNonNull(MessageInput) }, }, resolve: (_, { id, input }) => { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - // This replaces all old data, but some apps might want partial update. - fakeDatabase[id] = input; - return { - id, - content: input.content, - author: input.author, - }; + const message = fakeDatabase[id]; + Object.assign(message, input); + return message; }, }, }, }), }); +const app = express(); +app.all( + '/graphql', + createHandler({ + schema: schema, + }), +); +app.listen(4000, () => { + console.log('Running a GraphQL API server at localhost:4000/graphql'); +}); +``` + + + + +```js +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; + +// If Message had any complex fields, we'd put them on this object. +class Message { + constructor(id, { content, author }) { + this.id = id; + this.content = content; + this.author = author; + } +} + +// Maps username to content +const fakeDatabase = {}; + +const schema = buildSchema( + /* GraphQL */ ` + input MessageInput { + content: String + author: String + } + + type Message { + id: ID! + content: String + author: String + } + + type Query { + getMessage(id: ID!): Message + getMessages: [Message] + } + + type Mutation { + createMessage(input: MessageInput): Message + updateMessage(id: ID!, input: MessageInput): Message + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + getMessage: (_source, { id }) => fakeDatabase[id], + getMessages: () => Object.values(fakeDatabase), + }, + }, + Mutation: { + fields: { + createMessage: (_source, { input }) => { + const id = String(Object.keys(fakeDatabase).length + 1); + const message = new Message(id, input); + fakeDatabase[id] = message; + return message; + }, + updateMessage: (_source, { id, input }) => { + const message = fakeDatabase[id]; + Object.assign(message, input); + return message; + }, + }, + }, + }, + }, + }, +); + const app = express(); app.all( '/graphql', diff --git a/website/pages/docs/n1-dataloader.mdx b/website/pages/docs/n1-dataloader.mdx index e4d9a06840..abe90c3d8d 100644 --- a/website/pages/docs/n1-dataloader.mdx +++ b/website/pages/docs/n1-dataloader.mdx @@ -2,6 +2,8 @@ title: Solving the N+1 Problem with DataLoader --- +import { Tabs } from 'nextra/components'; + # Solving the N+1 Problem with DataLoader When building a server with GraphQL.js, it's common to encounter @@ -69,17 +71,20 @@ To use `DataLoader` in a `graphql-js` server: ### Example: Batching author lookups Suppose each `Post` has an `authorId`, and you have a `getUsersByIds(ids)` -function that can fetch multiple users in a single call: +function that can fetch multiple users in a single call. The SDL example attaches the +`Post.author` resolver through `supplementalConfig` so every `Post` value can load its +author through `context.userLoader`. + + + -{/* prettier-ignore */} -```js {14-17,37} +```js import { - graphql, + GraphQLID, + GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString, - GraphQLList, - GraphQLID } from 'graphql'; import DataLoader from 'dataloader'; import { getPosts, getUsersByIds } from './db.js'; @@ -88,46 +93,104 @@ function createContext() { return { userLoader: new DataLoader(async (userIds) => { const users = await getUsersByIds(userIds); - return userIds.map(id => users.find(user => user.id === id)); + return userIds.map((id) => users.find((user) => user.id === id)); }), }; } const UserType = new GraphQLObjectType({ name: 'User', - fields: () => ({ + fields: { id: { type: GraphQLID }, name: { type: GraphQLString }, - }), + }, }); const PostType = new GraphQLObjectType({ name: 'Post', - fields: () => ({ + fields: { id: { type: GraphQLID }, title: { type: GraphQLString }, author: { type: UserType, - resolve(post, args, context) { + resolve(post, _args, context) { return context.userLoader.load(post.authorId); }, }, - }), + }, }); -const QueryType = new GraphQLObjectType({ - name: 'Query', - fields: () => ({ - posts: { - type: GraphQLList(PostType), - resolve: () => getPosts(), +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + posts: { + type: new GraphQLList(PostType), + resolve: () => getPosts(), + }, }, }), }); +``` + + + + +```js +import { buildSchema } from 'graphql'; +import DataLoader from 'dataloader'; +import { getPosts, getUsersByIds } from './db.js'; + +function createContext() { + return { + userLoader: new DataLoader(async (userIds) => { + const users = await getUsersByIds(userIds); + return userIds.map((id) => users.find((user) => user.id === id)); + }), + }; +} + +const schema = buildSchema( + /* GraphQL */ ` + type User { + id: ID + name: String + } + + type Post { + id: ID + title: String + author: User + } -const schema = new GraphQLSchema({ query: QueryType }); + type Query { + posts: [Post] + } + `, + { + supplementalConfig: { + objectTypes: { + Post: { + fields: { + author(post, _args, context) { + return context.userLoader.load(post.authorId); + }, + }, + }, + Query: { + fields: { + posts: () => getPosts(), + }, + }, + }, + }, + }, +); ``` + + + With this setup, all `.load(authorId)` calls are automatically collected and batched into a single call to `getUsersByIds`. `DataLoader` also caches results for the duration of the request, so repeated `.load(id)` calls for the same ID don't trigger diff --git a/website/pages/docs/nullability.mdx b/website/pages/docs/nullability.mdx index 01a79e45fb..08afb3db75 100644 --- a/website/pages/docs/nullability.mdx +++ b/website/pages/docs/nullability.mdx @@ -5,6 +5,8 @@ sidebarTitle: Nullability in GraphQL.js # Nullability in GraphQL.js +import { Tabs } from 'nextra/components'; + Nullability is a core concept in GraphQL that affects how schemas are defined, how execution behaves, and how clients interpret results. In GraphQL.js, nullability plays a critical role in both schema construction and @@ -27,21 +29,17 @@ propagation" but more commonly as null bubbling. Understanding nullability requires familiarity with the GraphQL type system, execution semantics, and the trade-offs involved in schema design. -## The role of `GraphQLNonNull` +## The role of non-null types -GraphQL.js represents non-nullability using the `GraphQLNonNull` wrapper type. +SDL represents non-nullability with `!`. In code-first schemas, GraphQL.js represents +the same concept with the `GraphQLNonNull` wrapper type. By default, all fields are nullable: -```js -import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from 'graphql'; - -const UserType = new GraphQLObjectType({ - name: 'User', - fields: () => ({ - id: { type: new GraphQLNonNull(GraphQLString) }, - email: { type: GraphQLString }, - }), -}); +```graphql +type User { + id: String! + email: String +} ``` In this example, the `id` field is non-nullable, meaning it must always @@ -56,8 +54,8 @@ You can use `GraphQLNonNull` with: You can also combine it with other types to create more specific constraints. For example: -```js -new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(UserType))); +```graphql +[User!]! ``` This structure corresponds to [User!]! in SDL: a non-nullable list of non-null @@ -79,34 +77,70 @@ a non-nullable guarantee is violated. Here's an example that shows this in action: + + + ```js import { - GraphQLSchema, + GraphQLNonNull, GraphQLObjectType, + GraphQLSchema, GraphQLString, - GraphQLNonNull, } from 'graphql'; -const UserType = new GraphQLObjectType({ +const User = new GraphQLObjectType({ name: 'User', fields: { id: { type: new GraphQLNonNull(GraphQLString) }, }, }); -const QueryType = new GraphQLObjectType({ - name: 'Query', - fields: { - user: { - type: UserType, - resolve: () => ({ id: null }), +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + user: { + type: User, + resolve: () => ({ id: null }), + }, }, - }, + }), }); +``` + + + -const schema = new GraphQLSchema({ query: QueryType }); +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type User { + id: String! + } + + type Query { + user: User + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + user: () => ({ id: null }), + }, + }, + }, + }, + }, +); ``` + + + In this example, the `user` field returns an object with `id: null`. Because `id` is non-nullable, GraphQL can't return `user.id`, and instead nullifies the `user` field entirely. An error describing the violation is @@ -150,17 +184,36 @@ nullability. This example defines a `Product` type with a non-nullable `name` field: + + + ```js -import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from 'graphql'; +import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; -const ProductType = new GraphQLObjectType({ +const Product = new GraphQLObjectType({ name: 'Product', - fields: () => ({ + fields: { name: { type: new GraphQLNonNull(GraphQLString) }, - }), + }, }); ``` + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema(/* GraphQL */ ` + type Product { + name: String! + } +`); +``` + + + + This configuration guarantees that `name` must always be a string and never `null`. If a resolver returns `null` for this field, an error will be thrown. @@ -170,34 +223,70 @@ error will be thrown. In this example, the resolver returns an object with `name: null`, violating the non-null constraint: + + + ```js import { - GraphQLObjectType, - GraphQLString, GraphQLNonNull, + GraphQLObjectType, GraphQLSchema, + GraphQLString, } from 'graphql'; -const ProductType = new GraphQLObjectType({ +const Product = new GraphQLObjectType({ name: 'Product', fields: { name: { type: new GraphQLNonNull(GraphQLString) }, }, }); -const QueryType = new GraphQLObjectType({ - name: 'Query', - fields: { - product: { - type: ProductType, - resolve: () => ({ name: null }), +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + product: { + type: Product, + resolve: () => ({ name: null }), + }, }, - }, + }), }); +``` -const schema = new GraphQLSchema({ query: QueryType }); + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Product { + name: String! + } + + type Query { + product: Product + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + product: () => ({ name: null }), + }, + }, + }, + }, + }, +); ``` + + + In this example, the `product` resolver returns an object with `name: null`. Because the `name` field is non-nullable, GraphQL.js responds by nullifying the entire `product` field and appending a diff --git a/website/pages/docs/object-types.mdx b/website/pages/docs/object-types.mdx index 090d4bb0af..c0a7101174 100644 --- a/website/pages/docs/object-types.mdx +++ b/website/pages/docs/object-types.mdx @@ -10,16 +10,7 @@ In many cases, you don't want to return a number or a string from an API. You wa In GraphQL schema language, the way you define a new object type is the same way we have been defining the `Query` type in our examples. Each object can have fields that return a particular type, and methods that take arguments. For example, in the [Passing Arguments](./passing-arguments) documentation, we had a method to roll some random dice: - - - -```graphql -type Query { - rollDice(numDice: Int!, numSides: Int): [Int] -} -``` - - + ```js @@ -27,22 +18,20 @@ import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, - GraphQLString, GraphQLList, - GraphQLFloat, } from 'graphql'; new GraphQLObjectType({ name: 'Query', fields: { rollDice: { - type: new GraphQLList(GraphQLFloat), + type: new GraphQLList(GraphQLInt), args: { numDice: { type: new GraphQLNonNull(GraphQLInt), }, numSides: { - type: new GraphQLNonNull(GraphQLInt), + type: GraphQLInt, }, }, }, @@ -51,24 +40,33 @@ new GraphQLObjectType({ ``` - + -If we wanted to have more and more methods based on a random die over time, we could implement this with a `RandomDie` object type instead. +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema(/* GraphQL */ ` + type Query { + rollDice(numDice: Int!, numSides: Int): [Int] + } +`); +``` - + ```graphql -type RandomDie { - roll(numRolls: Int!): [Int] -} - type Query { - getDie(numSides: Int): RandomDie + rollDice(numDice: Int!, numSides: Int): [Int] } ``` + + +If we wanted to have more and more methods based on a random die over time, we could implement this with a `RandomDie` object type instead. + + ```js @@ -76,27 +74,13 @@ import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, - GraphQLString, GraphQLList, - GraphQLFloat, GraphQLSchema, } from 'graphql'; const RandomDie = new GraphQLObjectType({ name: 'RandomDie', fields: { - numSides: { - type: new GraphQLNonNull(GraphQLInt), - resolve: function (die) { - return die.numSides; - }, - }, - rollOnce: { - type: new GraphQLNonNull(GraphQLInt), - resolve: function (die) { - return 1 + Math.floor(Math.random() * die.numSides); - }, - }, roll: { type: new GraphQLList(GraphQLInt), args: { @@ -135,6 +119,47 @@ const schema = new GraphQLSchema({ }, }), }); +``` + + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type RandomDie { + roll(numRolls: Int!): [Int] + } + + type Query { + getDie(numSides: Int): RandomDie + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + getDie: (_source, { numSides }) => { + return { + numSides: numSides || 6, + roll({ numRolls }) { + const output = []; + for (let i = 0; i < numRolls; i++) { + output.push(1 + Math.floor(Math.random() * this.numSides)); + } + return output; + }, + }; + }, + }, + }, + }, + }, + }, +); ``` @@ -170,22 +195,7 @@ const root = { For fields that don't use any arguments, you can use either properties on the object or instance methods. So for the example code above, both `numSides` and `rollOnce` can actually be used to implement GraphQL fields, so that code also implements the schema of: - - - -```graphql -type RandomDie { - numSides: Int! - rollOnce: Int! - roll(numRolls: Int!): [Int] -} - -type Query { - getDie(numSides: Int): RandomDie -} -``` - - + ```js @@ -193,125 +203,173 @@ import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, - GraphQLString, GraphQLList, - GraphQLFloat, GraphQLSchema, } from 'graphql'; const RandomDie = new GraphQLObjectType({ - name: 'RandomDie', - fields: { - numSides: { - type: new GraphQLNonNull(GraphQLInt), - resolve: (die) => die.numSides, - }, - rollOnce: { - type: new GraphQLNonNull(GraphQLInt), - resolve: (die) => 1 + Math.floor(Math.random() * die.numSides), - }, - roll: { - type: new GraphQLList(GraphQLInt), - args: { - numRolls: { - type: new GraphQLNonNull(GraphQLInt), - }, - }, - resolve: (die, { numRolls }) => { - const output = []; - for (let i = 0; i < numRolls; i++) { - output.push(1 + Math.floor(Math.random() * die.numSides)); - } - return output; - }, - }, - }, +name: 'RandomDie', +fields: { +numSides: { +type: new GraphQLNonNull(GraphQLInt), +resolve: (die) => die.numSides, +}, +rollOnce: { +type: new GraphQLNonNull(GraphQLInt), +resolve: (die) => 1 + Math.floor(Math.random() _ die.numSides), +}, +roll: { +type: new GraphQLList(GraphQLInt), +args: { +numRolls: { +type: new GraphQLNonNull(GraphQLInt), +}, +}, +resolve: (die, { numRolls }) => { +const output = []; +for (let i = 0; i < numRolls; i++) { +output.push(1 + Math.floor(Math.random() _ die.numSides)); +} +return output; +}, +}, +}, }); const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - getDie: { - type: RandomDie, - args: { - numSides: { - type: GraphQLInt, +query: new GraphQLObjectType({ +name: 'Query', +fields: { +getDie: { +type: RandomDie, +args: { +numSides: { +type: GraphQLInt, +}, +}, +resolve: (_, { numSides }) => { +return { +numSides: numSides || 6, +}; +}, +}, +}, +}), +}); + +``` + + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type RandomDie { + numSides: Int! + rollOnce: Int! + roll(numRolls: Int!): [Int] + } + + type Query { + getDie(numSides: Int): RandomDie + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + getDie: (_source, { numSides }) => new RandomDie(numSides || 6), }, }, - resolve: (_, { numSides }) => { - return { - numSides: numSides || 6, - }; - }, }, }, - }), -}); + }, +); ``` -Putting this all together, here is some sample code that runs a server with this GraphQL API: +Putting this all together, here is some sample code that runs a server with this GraphQL API. The SDL versions rely on GraphQL.js's default field resolver for `RandomDie` instance properties and methods, while the code-first version attaches those same resolvers directly to field definitions: - + ```js import express from 'express'; import { createHandler } from 'graphql-http/lib/use/express'; -import { buildSchema } from 'graphql'; - -// Construct a schema, using GraphQL schema language -const schema = buildSchema(` - type RandomDie { - numSides: Int! - rollOnce: Int! - roll(numRolls: Int!): [Int] - } - - type Query { - getDie(numSides: Int): RandomDie - } -`); - -// This class implements the RandomDie GraphQL type -class RandomDie { - constructor(numSides) { - this.numSides = numSides; - } - - rollOnce() { - return 1 + Math.floor(Math.random() * this.numSides); - } +import { + GraphQLObjectType, + GraphQLNonNull, + GraphQLInt, + GraphQLList, + GraphQLSchema, +} from 'graphql'; - roll({ numRolls }) { - const output = []; - for (let i = 0; i < numRolls; i++) { - output.push(this.rollOnce()); - } - return output; - } +const RandomDie = new GraphQLObjectType({ +name: 'RandomDie', +fields: { +numSides: { +type: new GraphQLNonNull(GraphQLInt), +resolve: (die) => die.numSides, +}, +rollOnce: { +type: new GraphQLNonNull(GraphQLInt), +resolve: (die) => 1 + Math.floor(Math.random() _ die.numSides), +}, +roll: { +type: new GraphQLList(GraphQLInt), +args: { +numRolls: { +type: new GraphQLNonNull(GraphQLInt), +}, +}, +resolve: (die, { numRolls }) => { +const output = []; +for (let i = 0; i < numRolls; i++) { +output.push(1 + Math.floor(Math.random() _ die.numSides)); } +return output; +}, +}, +}, +}); -// The root provides the top-level API endpoints -const root = { - getDie({ numSides }) { - return new RandomDie(numSides || 6); - }, +const schema = new GraphQLSchema({ +query: new GraphQLObjectType({ +name: 'Query', +fields: { +getDie: { +type: RandomDie, +args: { +numSides: { +type: GraphQLInt, +}, +}, +resolve: (_, { numSides }) => { +return { +numSides: numSides || 6, }; +}, +}, +}, +}), +}); const app = express(); app.all( - '/graphql', - createHandler({ - schema: schema, - rootValue: root, - }), +'/graphql', +createHandler({ +schema: schema, +}), ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); + ``` @@ -320,64 +378,50 @@ console.log('Running a GraphQL API server at localhost:4000/graphql'); ```js import express from 'express'; import { createHandler } from 'graphql-http/lib/use/express'; -import { - GraphQLObjectType, - GraphQLNonNull, - GraphQLInt, - GraphQLList, - GraphQLFloat, - GraphQLSchema, -} from 'graphql'; +import { buildSchema } from 'graphql'; -const RandomDie = new GraphQLObjectType({ - name: 'RandomDie', - fields: { - numSides: { - type: new GraphQLNonNull(GraphQLInt), - resolve: (die) => die.numSides, - }, - rollOnce: { - type: new GraphQLNonNull(GraphQLInt), - resolve: (die) => 1 + Math.floor(Math.random() * die.numSides), - }, - roll: { - type: new GraphQLList(GraphQLInt), - args: { - numRolls: { - type: new GraphQLNonNull(GraphQLInt), - }, - }, - resolve: (die, { numRolls }) => { - const output = []; - for (let i = 0; i < numRolls; i++) { - output.push(1 + Math.floor(Math.random() * die.numSides)); - } - return output; - }, - }, - }, -}); +const schema = buildSchema( + /* GraphQL */ ` + type RandomDie { + numSides: Int! + rollOnce: Int! + roll(numRolls: Int!): [Int] + } -const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - getDie: { - type: RandomDie, - args: { - numSides: { - type: GraphQLInt, + type Query { + getDie(numSides: Int): RandomDie + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + getDie: (_source, { numSides }) => new RandomDie(numSides || 6), }, }, - resolve: (_, { numSides }) => { - return { - numSides: numSides || 6, - }; - }, }, }, - }), -}); + }, +); + +class RandomDie { + constructor(numSides) { + this.numSides = numSides; + } + + rollOnce() { + return 1 + Math.floor(Math.random() * this.numSides); + } + + roll({ numRolls }) { + const output = []; + for (let i = 0; i < numRolls; i++) { + output.push(this.rollOnce()); + } + return output; + } +} const app = express(); app.all( diff --git a/website/pages/docs/oneof-input-objects.mdx b/website/pages/docs/oneof-input-objects.mdx index e69c46d715..53110cf8ff 100644 --- a/website/pages/docs/oneof-input-objects.mdx +++ b/website/pages/docs/oneof-input-objects.mdx @@ -10,38 +10,10 @@ Some inputs will behave differently depending on what input we choose. Let's loo a field named `product`, we can fetch a `Product` by either its `id` or its `name`. Currently we'd make a tradeoff for this by introducing two arguments that are both nullable, now if both are passed as null (or both non-null) we'd have to handle that in code - the type system wouldn't indicate that exactly one was required. To fix this, the `@oneOf` directive was introduced so we -can create this "exactly one option" constraint without sacrificing the strictly typed nature of our GraphQL Schema. +can create this "exactly one option" constraint without sacrificing the strictly typed nature of our GraphQL Schema. In code-first schemas, this is expressed with `isOneOf: true`. In SDL, it is expressed with `@oneOf`. - - - -```js -const schema = buildSchema(` - type Product { - id: ID! - name: String! - } - - input ProductLocation { - aisleNumber: Int! - shelfNumber: Int! - positionOnShelf: Int! - } - - input ProductSpecifier @oneOf { - id: ID - name: String - location: ProductLocation - } - - type Query { - product(by: ProductSpecifier!): Product - } -`); -``` - - - + + ```js const Product = new GraphQLObjectType({ @@ -82,14 +54,55 @@ const schema = new GraphQLSchema({ product: { type: Product, args: { by: { type: new GraphQLNonNull(ProductSpecifier) } }, + resolve: (_source, { by }) => findProduct(by), }, }, }), }); ``` - + + + +```js +const schema = buildSchema( + /* GraphQL */ ` + type Product { + id: ID! + name: String! + } + + input ProductLocation { + aisleNumber: Int! + shelfNumber: Int! + positionOnShelf: Int! + } + + input ProductSpecifier @oneOf { + id: ID + name: String + location: ProductLocation + } + + type Query { + product(by: ProductSpecifier!): Product + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + product: (_source, { by }) => findProduct(by), + }, + }, + }, + }, + }, +); +``` + It doesn't matter whether you have 2 or more inputs here, all that matters is diff --git a/website/pages/docs/operation-complexity-controls.mdx b/website/pages/docs/operation-complexity-controls.mdx index 08282e0b85..1618c542cc 100644 --- a/website/pages/docs/operation-complexity-controls.mdx +++ b/website/pages/docs/operation-complexity-controls.mdx @@ -2,7 +2,7 @@ title: Operation Complexity Controls --- -import { Callout } from 'nextra/components'; +import { Callout, Tabs } from 'nextra/components'; # Operation Complexity Controls @@ -115,17 +115,36 @@ costly than a scalar field. You can define per-field costs using `fieldExtensionsEstimator`, a feature supported by some complexity tools. This estimator reads cost metadata from the field's `extensions.complexity` function in -your schema. For example: +your schema. SDL describes the field, while `supplementalConfig` can attach the extension +metadata used by the estimator: + + + ```js -import { GraphQLObjectType, GraphQLList, GraphQLInt } from 'graphql'; -import { PostType } from './post-type.js'; +import { + GraphQLID, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, +} from 'graphql'; + +const Post = new GraphQLObjectType({ + name: 'Post', + fields: { + id: { type: new GraphQLNonNull(GraphQLID) }, + title: { type: GraphQLString }, + }, +}); -const UserType = new GraphQLObjectType({ +const User = new GraphQLObjectType({ name: 'User', fields: { posts: { - type: GraphQLList(PostType), + type: new GraphQLList(Post), args: { limit: { type: GraphQLInt }, }, @@ -138,8 +157,64 @@ const UserType = new GraphQLObjectType({ }, }, }); + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + viewer: { + type: User, + }, + }, + }), +}); +``` + + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Post { + id: ID! + title: String + } + + type User { + posts(limit: Int): [Post] + } + + type Query { + viewer: User + } + `, + { + supplementalConfig: { + objectTypes: { + User: { + fields: { + posts: { + extensions: { + complexity: ({ args, childComplexity }) => { + const limit = args.limit ?? 10; + return childComplexity * limit; + }, + }, + }, + }, + }, + }, + }, + }, +); ``` + + + In this example, the cost of `posts` depends on the number of items requested (`limit`) and the complexity of each child field. diff --git a/website/pages/docs/passing-arguments.mdx b/website/pages/docs/passing-arguments.mdx index e2f9233ffd..3373f58a2c 100644 --- a/website/pages/docs/passing-arguments.mdx +++ b/website/pages/docs/passing-arguments.mdx @@ -14,28 +14,16 @@ type Query { } ``` -Instead of hard coding "three", we might want a more general function that rolls `numDice` dice, each of which have `numSides` sides. We can add arguments to the GraphQL schema language like this: +Instead of hard coding "three", we might want a more general function that rolls `numDice` dice, each of which have `numSides` sides: - - - -```graphql -type Query { - rollDice(numDice: Int!, numSides: Int): [Int] -} -``` - - + ```js -import express from 'express'; -import { createHandler } from 'graphql-http/lib/use/express'; import { GraphQLObjectType, GraphQLSchema, GraphQLList, - GraphQLFloat, GraphQLInt, GraphQLNonNull, } from 'graphql'; @@ -45,7 +33,7 @@ const schema = new GraphQLSchema({ name: 'Query', fields: { rollDice: { - type: new GraphQLList(GraphQLFloat), + type: new GraphQLList(GraphQLInt), args: { numDice: { type: new GraphQLNonNull(GraphQLInt) }, numSides: { type: GraphQLInt }, @@ -61,16 +49,38 @@ const schema = new GraphQLSchema({ }, }), }); +``` -const app = express(); -app.all( - '/graphql', - createHandler({ - schema: schema, - }), + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Query { + rollDice(numDice: Int!, numSides: Int): [Int] + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + rollDice: (_source, { numDice, numSides }) => { + const output = []; + for (let i = 0; i < numDice; i++) { + output.push(1 + Math.floor(Math.random() * (numSides || 6))); + } + return output; + }, + }, + }, + }, + }, + }, ); -app.listen(4000); -console.log('Running a GraphQL API server at localhost:4000/graphql'); ``` @@ -94,7 +104,7 @@ When constructing a schema in code, GraphQL.js v16 uses `defaultValue`: ```js const rollDiceField = { - type: new GraphQLList(GraphQLFloat), + type: new GraphQLList(GraphQLInt), args: { numDice: { type: new GraphQLNonNull(GraphQLInt) }, numSides: { type: GraphQLInt, defaultValue: 6 }, @@ -106,7 +116,7 @@ GraphQL.js v17 also supports the more explicit `default` shape for new code: ```js const rollDiceField = { - type: new GraphQLList(GraphQLFloat), + type: new GraphQLList(GraphQLInt), args: { numDice: { type: new GraphQLNonNull(GraphQLInt) }, numSides: { type: GraphQLInt, default: { value: 6 } }, @@ -155,47 +165,9 @@ const root = { If you're familiar with destructuring, this is a bit nicer because the line of code where `rollDice` is defined tells you about what the arguments are. -The entire code for a server that hosts this `rollDice` API is: - - - - -```js -import express from 'express'; -import { createHandler } from 'graphql-http/lib/use/express'; -import { buildSchema } from 'graphql'; - -// Construct a schema, using GraphQL schema language -const schema = buildSchema(/* GraphQL */ ` - type Query { - rollDice(numDice: Int!, numSides: Int): [Int] - } -`); - -// The root provides a resolver function for each API endpoint -const root = { - rollDice({ numDice, numSides }) { - const output = []; - for (let i = 0; i < numDice; i++) { - output.push(1 + Math.floor(Math.random() * (numSides || 6))); - } - return output; - }, -}; - -const app = express(); -app.all( - '/graphql', - createHandler({ - schema: schema, - rootValue: root, - }), -); -app.listen(4000); -console.log('Running a GraphQL API server at localhost:4000/graphql'); -``` +The entire code for a server that hosts this `rollDice` API can use code-first schema construction or SDL: - + ```js @@ -205,25 +177,23 @@ import { GraphQLObjectType, GraphQLNonNull, GraphQLInt, - GraphQLString, GraphQLList, - GraphQLFloat, GraphQLSchema, } from 'graphql'; -// Construct a schema, using GraphQL schema language +// Construct a schema in code. const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { rollDice: { - type: new GraphQLList(GraphQLFloat), + type: new GraphQLList(GraphQLInt), args: { numDice: { type: new GraphQLNonNull(GraphQLInt), }, numSides: { - type: new GraphQLNonNull(GraphQLInt), + type: GraphQLInt, }, }, resolve: (_, { numDice, numSides }) => { @@ -238,6 +208,50 @@ const schema = new GraphQLSchema({ }), }); +const app = express(); +app.all( + '/graphql', + createHandler({ + schema: schema, + }), +); +app.listen(4000); +console.log('Running a GraphQL API server at localhost:4000/graphql'); +``` + + + + +```js +import express from 'express'; +import { createHandler } from 'graphql-http/lib/use/express'; +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Query { + rollDice(numDice: Int!, numSides: Int): [Int] + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + rollDice: (_source, { numDice, numSides }) => { + const output = []; + for (let i = 0; i < numDice; i++) { + output.push(1 + Math.floor(Math.random() * (numSides || 6))); + } + return output; + }, + }, + }, + }, + }, + }, +); + const app = express(); app.all( '/graphql', diff --git a/website/pages/docs/resolver-anatomy.mdx b/website/pages/docs/resolver-anatomy.mdx index 28de4cd52a..308ef7f63b 100644 --- a/website/pages/docs/resolver-anatomy.mdx +++ b/website/pages/docs/resolver-anatomy.mdx @@ -4,6 +4,8 @@ title: Anatomy of a Resolver # Anatomy of a Resolver +import { Tabs } from 'nextra/components'; + In GraphQL.js, a resolver is a function that returns the value for a specific field in a schema. Resolvers connect a GraphQL query to the underlying data or logic needed to fulfill it. @@ -75,10 +77,11 @@ writing custom resolvers for every field. For example, if your `source` object already contains fields with matching names, GraphQL.js can resolve them automatically. -You can override the default behavior by specifying a `resolve` function when -defining a field in the schema. This is necessary when the field’s value -needs to be computed dynamically, fetched from an external service, or -otherwise requires custom logic. +You can override the default behavior by specifying a `resolve` function for a field. If the +schema is defined in SDL, add that resolver with `supplementalConfig`; if the schema is +code-first, add it to the field config. The default resolver can still read same-named +properties or methods from the source object, so custom resolvers are only needed when the +field behavior cannot live on the returned value. ## Writing a custom resolver @@ -86,10 +89,17 @@ A custom resolver is a function you define to control exactly how a field's value is fetched or computed. You can add a resolver by specifying a `resolve` function when defining a field in your schema: -```js {6-8} -const UserType = new GraphQLObjectType({ + + + +```js {19-21} +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; + +const User = new GraphQLObjectType({ name: 'User', fields: { + firstName: { type: GraphQLString }, + lastName: { type: GraphQLString }, fullName: { type: GraphQLString, resolve(source) { @@ -98,8 +108,54 @@ const UserType = new GraphQLObjectType({ }, }, }); + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + user: { type: User }, + }, + }), +}); ``` + + + +```js {6-8} +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type User { + firstName: String + lastName: String + fullName: String + } + + type Query { + user: User + } + `, + { + supplementalConfig: { + objectTypes: { + User: { + fields: { + fullName(source) { + return `${source.firstName} ${source.lastName}`; + }, + }, + }, + }, + }, + }, +); +``` + + + + Resolvers can be synchronous or asynchronous. If a resolver returns a Promise, GraphQL.js automatically waits for the Promise to resolve before continuing execution: diff --git a/website/pages/docs/running-an-express-graphql-server.mdx b/website/pages/docs/running-an-express-graphql-server.mdx index d47b0384a8..606368cc6b 100644 --- a/website/pages/docs/running-an-express-graphql-server.mdx +++ b/website/pages/docs/running-an-express-graphql-server.mdx @@ -15,23 +15,26 @@ npm install express graphql-http graphql --save Let's modify our "hello world" example so that it's an API server rather than a script that runs a single query. We can use the 'express' module to run a webserver, and instead of executing a query directly with the `graphql` function, we can use the `graphql-http` library to mount a GraphQL API server on the "/graphql" HTTP endpoint: - + ```javascript -import { buildSchema } from 'graphql'; +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; import { createHandler } from 'graphql-http/lib/use/express'; import express from 'express'; -// Construct a schema, using GraphQL schema language -const schema = buildSchema(`type Query { hello: String } `); - -// The root provides a resolver function for each API endpoint -const root = { - hello() { - return 'Hello world!'; - }, -}; +// Construct a schema. +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + hello: { + type: GraphQLString, + resolve: () => 'Hello world!', + }, + }, + }), +}); const app = express(); @@ -40,7 +43,6 @@ app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); @@ -49,26 +51,32 @@ app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); ``` - - + + ```javascript -import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; +import { buildSchema } from 'graphql'; import { createHandler } from 'graphql-http/lib/use/express'; import express from 'express'; -// Construct a schema -const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - hello: { - type: GraphQLString, - resolve: () => 'Hello world!', +const schema = buildSchema( + /* GraphQL */ ` + type Query { + hello: String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + hello: () => 'Hello world!', + }, + }, }, }, - }), -}); + }, +); const app = express(); @@ -85,7 +93,7 @@ app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); ``` - + You can run this GraphQL server with: diff --git a/website/pages/docs/scaling-graphql.mdx b/website/pages/docs/scaling-graphql.mdx index f22f9e0959..afd3fcc1ca 100644 --- a/website/pages/docs/scaling-graphql.mdx +++ b/website/pages/docs/scaling-graphql.mdx @@ -2,6 +2,8 @@ title: Scaling your GraphQL API --- +import { Tabs } from 'nextra/components'; + As your application grows, so does your GraphQL schema. What starts as a small, self-contained monolith may eventually need to support multiple teams, services, and domains. @@ -17,26 +19,58 @@ A monolithic schema is a single GraphQL schema served from a single service. All resolvers, and business logic are located and deployed together. This is the default approach when using GraphQL.js. You define the entire schema in one -place using the `GraphQLSchema` constructor and expose it through a single HTTP endpoint. +place and expose it through a single HTTP endpoint. The following example defines a minimal schema that serves a single `hello` field: + + + ```js -import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'; - -const QueryType = new GraphQLObjectType({ - name: 'Query', - fields: { - hello: { - type: GraphQLString, - resolve: () => 'Hello from a monolithic schema!', +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; + +export const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + hello: { + type: GraphQLString, + resolve: () => 'Hello from a monolithic schema!', + }, }, - }, + }), }); +``` + + + -export const schema = new GraphQLSchema({ query: QueryType }); +```js +import { buildSchema } from 'graphql'; + +export const schema = buildSchema( + /* GraphQL */ ` + type Query { + hello: String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + hello: () => 'Hello from a monolithic schema!', + }, + }, + }, + }, + }, +); ``` + + + This structure works well for small to medium projects, especially when a single team owns the entire graph. It's simple to test, deploy, and reason about. As long as the schema remains manageable in size and scope, there's often no need to introduce additional architectural complexity. diff --git a/website/pages/docs/subscriptions.mdx b/website/pages/docs/subscriptions.mdx index b193eb4ad0..cf93cd5529 100644 --- a/website/pages/docs/subscriptions.mdx +++ b/website/pages/docs/subscriptions.mdx @@ -4,6 +4,8 @@ title: Subscriptions # Subscriptions +import { Tabs } from 'nextra/components'; + Subscriptions allow a GraphQL server to push updates to clients in real time. Unlike queries and mutations, which use a request/response model, subscriptions maintain a long-lived connection between the client and server to stream data as events occur. This is useful for building features like chat apps, live dashboards, or collaborative tools that require real-time updates. This guide covers how to implement subscriptions in GraphQL.js, when to use them, and what to consider in production environments. @@ -142,13 +144,21 @@ broadcast and listen for events. ### Define a subscription type -Next, define your `Subscription` root type. Each field on this type should return -an `AsyncIterable`. Subscribe functions can also accept standard resolver arguments -such as `args`, `context`, and `info`, depending on your use case: +Next, define your `Subscription` root type: + + + ```js import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; +const QueryType = new GraphQLObjectType({ + name: 'Query', + fields: { + noop: { type: GraphQLString }, + }, +}); + const SubscriptionType = new GraphQLObjectType({ name: 'Subscription', fields: { @@ -158,8 +168,48 @@ const SubscriptionType = new GraphQLObjectType({ }, }, }); + +const schema = new GraphQLSchema({ + query: QueryType, + subscription: SubscriptionType, +}); +``` + + + + +```js +import { buildSchema } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Query { + noop: String + } + + type Subscription { + messageSent: String + } + `, + { + supplementalConfig: { + objectTypes: { + Subscription: { + fields: { + messageSent: { + subscribe: () => pubsub.asyncIterator(['MESSAGE_SENT']), + }, + }, + }, + }, + }, + }, +); ``` + + + This schema defines a `messageSent` field that listens for the `MESSAGE_SENT` event and returns a string. ### Publish events @@ -173,15 +223,7 @@ pubsub.publish('MESSAGE_SENT', { messageSent: 'Hello world!' }); Clients subscribed to the `messageSent` field will receive this message. -### Construct your schema - -Finally, build your schema and include the `Subscription` type: - -```js -const schema = new GraphQLSchema({ - subscription: SubscriptionType, -}); -``` +### Use your schema A client can then initiate a subscription like this: diff --git a/website/pages/docs/type-generation.mdx b/website/pages/docs/type-generation.mdx index f8ebb280ee..1cce83afde 100644 --- a/website/pages/docs/type-generation.mdx +++ b/website/pages/docs/type-generation.mdx @@ -40,6 +40,11 @@ If you're using this approach with TypeScript, you already get some built-in typ with the types exposed by `graphql-js`. For example, TypeScript can help ensure your resolver functions return values that match their expected shapes. +Another workflow is to define the schema in SDL and supply JavaScript-only behavior through +`supplementalConfig`. That gives type-generation tools SDL directly while still letting you +attach resolvers, custom scalar coercion, abstract type resolution, and extension metadata +in code. + However, code-first development has tradeoffs: - You won't get automatic type definitions for your resolvers unless you generate @@ -48,7 +53,8 @@ However, code-first development has tradeoffs: a description of the schema in SDL first. You can still use type generation tools like GraphQL Code Generator in a code-first setup. -You just need to convert your schema into SDL. +You just need to convert your schema into SDL. If the schema is already constructed from +SDL, that conversion step is unnecessary. To produce an SDL description of your schema: @@ -60,8 +66,7 @@ import { writeFileSync } from 'fs'; writeFileSync('./schema.graphql', printSchema(schema)); ``` -Once you've written the SDL, you can treat the project like an SDL-first project -for type generation. +Once you have SDL, you can use it as the input for type generation. ## Schema-first development diff --git a/website/pages/docs/using-directives.mdx b/website/pages/docs/using-directives.mdx index fe323e18ce..065898d79f 100644 --- a/website/pages/docs/using-directives.mdx +++ b/website/pages/docs/using-directives.mdx @@ -5,6 +5,8 @@ sidebarTitle: Using Directives # Using Directives in GraphQL.js +import { Tabs } from 'nextra/components'; + Directives let you customize query execution at a fine-grained level. They act like annotations in a GraphQL document, giving the server instructions about whether to include a field, how to format a response, or how to apply custom behavior. @@ -35,40 +37,90 @@ query ($shouldInclude: Boolean!) { ``` At runtime, GraphQL.js evaluates the `if` argument. If `shouldInclude` is `false`, the -`greeting` field in this example is skipped entirely and your resolver won't run. +`greeting` field in this example is skipped entirely and your resolver won't run. The same +runtime behavior works with code-first schema construction or SDL: + + + ```js -import { graphql, buildSchema } from 'graphql'; +import { + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + graphql, +} from 'graphql'; -const schema = buildSchema(` - type Query { - greeting: String - } -`); +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + greeting: { + type: GraphQLString, + resolve: () => 'Hello!', + }, + }, + }), +}); -const rootValue = { - greeting: () => 'Hello!', -}; +const source = ` query ($shouldInclude: Boolean!) { + greeting @include(if: $shouldInclude) + }`; -const query = ` - query($shouldInclude: Boolean!) { +const result = await graphql({ + schema, + source, + variableValues: { shouldInclude: true }, +}); + +console.log(result); +// => { data: { greeting: 'Hello!' } } +``` + + + + +```js +import { buildSchema, graphql } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + type Query { + greeting: String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: () => 'Hello!', + }, + }, + }, + }, + }, +); + +const source = ` + query ($shouldInclude: Boolean!) { greeting @include(if: $shouldInclude) } `; -const variables = { shouldInclude: true }; - const result = await graphql({ schema, - source: query, - rootValue, - variableValues: variables, + source, + variableValues: { shouldInclude: true }, }); console.log(result); -// → { data: { greeting: 'Hello!' } } +// => { data: { greeting: 'Hello!' } } ``` + + + If `shouldInclude` is `false`, the result would be `{ data: {} }`. The `@deprecated` directive is used in the schema to indicate that a field or enum @@ -93,20 +145,25 @@ You can still query deprecated fields unless you add validation rules yourself. ## Declaring custom directives in GraphQL.js -To use a custom directive, you first define it in your schema using the -`GraphQLDirective` class. This defines the directive's name, where it can -be applied, and any arguments it accepts. - -A directive in GraphQL.js is just metadata. It doesn't perform any behavior on its own. +To use a custom directive, you first define it in your schema. A directive in GraphQL.js is +metadata: it doesn't perform any behavior on its own. Define it with `GraphQLDirective` in +code-first schemas, or with a directive definition in SDL. Here's a basic example that declares an `@uppercase` directive that can be applied to fields: + + + ```js import { - GraphQLDirective, DirectiveLocation, - GraphQLNonNull, GraphQLBoolean, + GraphQLDirective, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + specifiedDirectives, } from 'graphql'; const UppercaseDirective = new GraphQLDirective({ @@ -115,26 +172,49 @@ const UppercaseDirective = new GraphQLDirective({ locations: [DirectiveLocation.FIELD], args: { enabled: { - type: GraphQLNonNull(GraphQLBoolean), + type: new GraphQLNonNull(GraphQLBoolean), defaultValue: true, description: 'Whether to apply the transformation.', }, }, }); + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + greeting: { + type: GraphQLString, + }, + }, + }), + directives: [...specifiedDirectives, UppercaseDirective], +}); ``` -To make the directive available to your schema, you must explicitly include it: + + ```js -import { GraphQLSchema } from 'graphql'; +import { buildSchema } from 'graphql'; -const schema = new GraphQLSchema({ - query: QueryType, - directives: [UppercaseDirective], -}); +const schema = buildSchema(/* GraphQL */ ` + "Converts the result of a field to uppercase." + directive @uppercase( + "Whether to apply the transformation." + enabled: Boolean! = true + ) on FIELD + + type Query { + greeting: String + } +`); ``` -Once added, tools like validation and introspection will recognize it. + + + +Once declared in the schema, tools like validation and introspection will recognize it. ## Applying directives in queries @@ -189,46 +269,114 @@ execution. There are two common approaches: Inside a resolver, use the `info` object to access AST nodes and inspect directives. You can check whether a directive is present and change behavior accordingly. + + + ```js -import { graphql, buildSchema, getDirectiveValues } from 'graphql'; +import { + DirectiveLocation, + GraphQLBoolean, + GraphQLDirective, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + getDirectiveValues, + graphql, + specifiedDirectives, +} from 'graphql'; + +const UppercaseDirective = new GraphQLDirective({ + name: 'uppercase', + locations: [DirectiveLocation.FIELD], + args: { + enabled: { + type: GraphQLBoolean, + defaultValue: true, + }, + }, +}); -const schema = buildSchema(` - directive @uppercase(enabled: Boolean = true) on FIELD +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + greeting: { + type: GraphQLString, + resolve: (_source, _args, _context, info) => { + const directive = getDirectiveValues( + UppercaseDirective, + info.fieldNodes[0], + info.variableValues, + ); + const result = 'Hello, world'; + + return directive?.enabled ? result.toUpperCase() : result; + }, + }, + }, + }), + directives: [...specifiedDirectives, UppercaseDirective], +}); - type Query { - greeting: String - } -`); +const source = ` query { + greeting @uppercase + }`; -const rootValue = { - greeting: (source, args, context, info) => { - const directive = getDirectiveValues( - schema.getDirective('uppercase'), - info.fieldNodes[0], - info.variableValues, - ); +const result = await graphql({ schema, source }); +console.log(result); +// => { data: { greeting: 'HELLO, WORLD' } } +``` - const result = 'Hello, world'; + + - if (directive?.enabled) { - return result.toUpperCase(); - } +```js +import { buildSchema, getDirectiveValues, graphql } from 'graphql'; - return result; +const schema = buildSchema( + /* GraphQL */ ` + directive @uppercase(enabled: Boolean = true) on FIELD + + type Query { + greeting: String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: (_source, _args, _context, info) => { + const directive = getDirectiveValues( + schema.getDirective('uppercase'), + info.fieldNodes[0], + info.variableValues, + ); + const result = 'Hello, world'; + + return directive?.enabled ? result.toUpperCase() : result; + }, + }, + }, + }, + }, }, -}; +); -const query = ` +const source = ` query { greeting @uppercase } `; -const result = await graphql({ schema, source: query, rootValue }); +const result = await graphql({ schema, source }); console.log(result); -// → { data: { greeting: 'HELLO, WORLD' } } +// => { data: { greeting: 'HELLO, WORLD' } } ``` + + + ### 2. Use AST visitors or schema wrappers For more complex logic, you can preprocess the schema or query using AST visitors or wrap @@ -260,46 +408,106 @@ normalized caches) and are less well understood by tooling such as GraphQL; by using arguments instead we maximize compatibility and consistency. For example, our `@uppercase` directive would fit naturally as a field argument: + + + ```js -import { graphql, buildSchema } from 'graphql'; +import { + GraphQLEnumType, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + graphql, +} from 'graphql'; -const schema = buildSchema(` - enum Format { - VERBATIM - UPPERCASE - } - type Query { - greeting(format: Format! = VERBATIM): String - } -`); +const Format = new GraphQLEnumType({ + name: 'Format', + values: { + VERBATIM: {}, + UPPERCASE: {}, + }, +}); + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + greeting: { + type: GraphQLString, + args: { + format: { + type: new GraphQLNonNull(Format), + default: { value: 'VERBATIM' }, + }, + }, + resolve: (_source, args) => { + const result = 'Hello, world'; + + return args.format === 'UPPERCASE' ? result.toUpperCase() : result; + }, + }, + }, + }), +}); + +const source = ` query { + greeting(format: UPPERCASE) + }`; + +const result = await graphql({ schema, source }); +console.log(result); +``` -const rootValue = { - greeting: (source, args) => { - const result = 'Hello, world'; + + - if (args.format === 'UPPERCASE') { - return result.toUpperCase(); +```js +import { buildSchema, graphql } from 'graphql'; + +const schema = buildSchema( + /* GraphQL */ ` + enum Format { + VERBATIM + UPPERCASE } - return result; + type Query { + greeting(format: Format! = VERBATIM): String + } + `, + { + supplementalConfig: { + objectTypes: { + Query: { + fields: { + greeting: (_source, args) => { + const result = 'Hello, world'; + + return args.format === 'UPPERCASE' + ? result.toUpperCase() + : result; + }, + }, + }, + }, + }, }, -}; +); -const query = ` +const source = ` query { greeting(format: UPPERCASE) } `; -const result = await graphql({ - schema, - source: query, - rootValue, -}); - +const result = await graphql({ schema, source }); console.log(result); ``` + + + ## Best practices When working with custom directives in GraphQL.js, keep the following best practices in mind: