From 1112b4d58ec755dbde4f4734f3a4f8a6d6459681 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 9 May 2022 19:30:19 +0300 Subject: [PATCH 01/29] Expose GraphQLErrorOptions type (#3554) (#3565) --- src/error/GraphQLError.ts | 26 ++++++++++++++------------ src/error/index.ts | 1 + src/index.ts | 1 + 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/error/GraphQLError.ts b/src/error/GraphQLError.ts index 3c73e85759..b88d27a56c 100644 --- a/src/error/GraphQLError.ts +++ b/src/error/GraphQLError.ts @@ -20,7 +20,7 @@ export interface GraphQLErrorExtensions { [attributeName: string]: unknown; } -export interface GraphQLErrorArgs { +export interface GraphQLErrorOptions { nodes?: ReadonlyArray | ASTNode | null; source?: Maybe; positions?: Maybe>; @@ -30,17 +30,19 @@ export interface GraphQLErrorArgs { } type BackwardsCompatibleArgs = - | [args?: GraphQLErrorArgs] + | [options?: GraphQLErrorOptions] | [ - nodes?: GraphQLErrorArgs['nodes'], - source?: GraphQLErrorArgs['source'], - positions?: GraphQLErrorArgs['positions'], - path?: GraphQLErrorArgs['path'], - originalError?: GraphQLErrorArgs['originalError'], - extensions?: GraphQLErrorArgs['extensions'], + nodes?: GraphQLErrorOptions['nodes'], + source?: GraphQLErrorOptions['source'], + positions?: GraphQLErrorOptions['positions'], + path?: GraphQLErrorOptions['path'], + originalError?: GraphQLErrorOptions['originalError'], + extensions?: GraphQLErrorOptions['extensions'], ]; -function toNormalizedArgs(args: BackwardsCompatibleArgs): GraphQLErrorArgs { +function toNormalizedOptions( + args: BackwardsCompatibleArgs, +): GraphQLErrorOptions { const firstArg = args[0]; if (firstArg == null || 'kind' in firstArg || 'length' in firstArg) { return { @@ -111,9 +113,9 @@ export class GraphQLError extends Error { */ readonly extensions: GraphQLErrorExtensions; - constructor(message: string, args?: GraphQLErrorArgs); + constructor(message: string, options?: GraphQLErrorOptions); /** - * @deprecated Please use the `GraphQLErrorArgs` constructor overload instead. + * @deprecated Please use the `GraphQLErrorOptions` constructor overload instead. */ constructor( message: string, @@ -126,7 +128,7 @@ export class GraphQLError extends Error { ); constructor(message: string, ...rawArgs: BackwardsCompatibleArgs) { const { nodes, source, positions, path, originalError, extensions } = - toNormalizedArgs(rawArgs); + toNormalizedOptions(rawArgs); super(message); this.name = 'GraphQLError'; diff --git a/src/error/index.ts b/src/error/index.ts index 41ad0d6f09..dd92e48364 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -1,5 +1,6 @@ export { GraphQLError, printError, formatError } from './GraphQLError'; export type { + GraphQLErrorOptions, GraphQLFormattedError, GraphQLErrorExtensions, } from './GraphQLError'; diff --git a/src/index.ts b/src/index.ts index 7fbf4d6d68..bb82b58164 100644 --- a/src/index.ts +++ b/src/index.ts @@ -392,6 +392,7 @@ export { } from './error/index'; export type { + GraphQLErrorOptions, GraphQLFormattedError, GraphQLErrorExtensions, } from './error/index'; From 1f8ba95c662118452bd969c6b26ba4e9050c55da Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 9 May 2022 19:31:51 +0300 Subject: [PATCH 02/29] 16.5.0 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82bf501fa8..83faedcadf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "16.4.0", + "version": "16.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "16.4.0", + "version": "16.5.0", "license": "MIT", "devDependencies": { "@babel/core": "7.17.9", diff --git a/package.json b/package.json index baf0eab582..5457b6e2ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "16.4.0", + "version": "16.5.0", "description": "A Query Language and Runtime which can target any service.", "license": "MIT", "private": true, diff --git a/src/version.ts b/src/version.ts index 121ce9b597..2ccaf094d9 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,14 +4,14 @@ /** * A string containing the version of the GraphQL.js library */ -export const version = '16.4.0' as string; +export const version = '16.5.0' as string; /** * An object containing the components of the GraphQL.js version string */ export const versionInfo = Object.freeze({ major: 16 as number, - minor: 4 as number, + minor: 5 as number, patch: 0 as number, preReleaseTag: null as string | null, }); From 59a73d64ee8a7a717c73e7dfdc1ea627a12a283e Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 15 Jun 2022 15:55:50 +0300 Subject: [PATCH 03/29] createSourceEventStream: introduce named arguments and deprecate positional arguments (#3645) BACKPORT OF #3634 Deprecates the positional arguments to createSourceEventStream, to be removed in the next major version, in favor of named arguments. Motivation: 1. aligns createSourceEventStream with the other exported entrypoints graphql, execute, and subscribe 2. allows simplification of mapSourceToResponse suggested by @IvanGoncharov --- src/execution/__tests__/subscribe-test.ts | 61 ++++++++++++++++- src/execution/subscribe.ts | 81 +++++++++++++---------- 2 files changed, 105 insertions(+), 37 deletions(-) diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 54c5019ab0..e9ea0d0ace 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -1,4 +1,4 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; @@ -377,6 +377,65 @@ describe('Subscription Initialization Phase', () => { ); }); + it('Deprecated: allows positional arguments to createSourceEventStream', async () => { + async function* fooGenerator() { + /* c8 ignore next 2 */ + yield { foo: 'FooValue' }; + } + + const schema = new GraphQLSchema({ + query: DummyQueryType, + subscription: new GraphQLObjectType({ + name: 'Subscription', + fields: { + foo: { type: GraphQLString, subscribe: fooGenerator }, + }, + }), + }); + const document = parse('subscription { foo }'); + + const eventStream = await createSourceEventStream(schema, document); + assert(isAsyncIterable(eventStream)); + }); + + it('Deprecated: throws an error if document is missing when using positional arguments', async () => { + const document = parse('subscription { foo }'); + const schema = new GraphQLSchema({ + query: DummyQueryType, + subscription: new GraphQLObjectType({ + name: 'Subscription', + fields: { + foo: { type: GraphQLString }, + }, + }), + }); + + // @ts-expect-error (schema must not be null) + (await expectPromise(createSourceEventStream(null, document))).toRejectWith( + 'Expected null to be a GraphQL schema.', + ); + + ( + await expectPromise( + createSourceEventStream( + // @ts-expect-error + undefined, + document, + ), + ) + ).toRejectWith('Expected undefined to be a GraphQL schema.'); + + // @ts-expect-error (document must not be null) + (await expectPromise(createSourceEventStream(schema, null))).toRejectWith( + 'Must provide document.', + ); + + // @ts-expect-error + (await expectPromise(createSourceEventStream(schema))).toRejectWith( + 'Must provide document.', + ); + }); + it('resolves to an error if schema does not support subscriptions', async () => { const schema = new GraphQLSchema({ query: DummyQueryType }); const document = parse('subscription { unknownField }'); diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts index 91a8231538..8b20ec3374 100644 --- a/src/execution/subscribe.ts +++ b/src/execution/subscribe.ts @@ -58,26 +58,7 @@ export async function subscribe( 'graphql@16 dropped long-deprecated support for positional arguments, please pass an object instead.', ); - const { - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - subscribeFieldResolver, - } = args; - - const resultOrStream = await createSourceEventStream( - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - subscribeFieldResolver, - ); + const resultOrStream = await createSourceEventStream(args); if (!isAsyncIterable(resultOrStream)) { return resultOrStream; @@ -91,19 +72,44 @@ export async function subscribe( // "ExecuteQuery" algorithm, for which `execute` is also used. const mapSourceToResponse = (payload: unknown) => execute({ - schema, - document, + ...args, rootValue: payload, - contextValue, - variableValues, - operationName, - fieldResolver, }); // Map every source value to a ExecutionResult value as described above. return mapAsyncIterator(resultOrStream, mapSourceToResponse); } +type BackwardsCompatibleArgs = + | [options: ExecutionArgs] + | [ + schema: ExecutionArgs['schema'], + document: ExecutionArgs['document'], + rootValue?: ExecutionArgs['rootValue'], + contextValue?: ExecutionArgs['contextValue'], + variableValues?: ExecutionArgs['variableValues'], + operationName?: ExecutionArgs['operationName'], + subscribeFieldResolver?: ExecutionArgs['subscribeFieldResolver'], + ]; + +function toNormalizedArgs(args: BackwardsCompatibleArgs): ExecutionArgs { + const firstArg = args[0]; + if (firstArg && 'document' in firstArg) { + return firstArg; + } + + return { + schema: firstArg, + // FIXME: when underlying TS bug fixed, see https://github.com/microsoft/TypeScript/issues/31613 + document: args[1] as DocumentNode, + rootValue: args[2], + contextValue: args[3], + variableValues: args[4], + operationName: args[5], + subscribeFieldResolver: args[6], + }; +} + /** * Implements the "CreateSourceEventStream" algorithm described in the * GraphQL specification, resolving the subscription source event stream. @@ -132,6 +138,10 @@ export async function subscribe( * or otherwise separating these two steps. For more on this, see the * "Supporting Subscriptions at Scale" information in the GraphQL specification. */ +export async function createSourceEventStream( + args: ExecutionArgs, +): Promise | ExecutionResult>; +/** @deprecated will be removed in next major version in favor of named arguments */ export async function createSourceEventStream( schema: GraphQLSchema, document: DocumentNode, @@ -140,22 +150,21 @@ export async function createSourceEventStream( variableValues?: Maybe<{ readonly [variable: string]: unknown }>, operationName?: Maybe, subscribeFieldResolver?: Maybe>, -): Promise | ExecutionResult> { +): Promise | ExecutionResult>; +export async function createSourceEventStream( + ...rawArgs: BackwardsCompatibleArgs +) { + const args = toNormalizedArgs(rawArgs); + + const { schema, document, variableValues } = args; + // If arguments are missing or incorrectly typed, this is an internal // developer mistake which should throw an early error. assertValidExecutionArguments(schema, document, variableValues); // If a valid execution context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. - const exeContext = buildExecutionContext({ - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - subscribeFieldResolver, - }); + const exeContext = buildExecutionContext(args); // Return early errors if execution context failed. if (!('schema' in exeContext)) { From af8221a6504b66a95b9bc0c20935e8f18b23d7d2 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Tue, 16 Aug 2022 21:51:23 +0300 Subject: [PATCH 04/29] Workaround for codesandbox having bug with TS enums (#3686) --- src/language/ast.ts | 3 ++- src/language/directiveLocation.ts | 3 ++- src/language/kinds.ts | 3 ++- src/language/tokenKind.ts | 3 ++- src/type/introspection.ts | 3 ++- src/utilities/findBreakingChanges.ts | 6 ++++-- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/language/ast.ts b/src/language/ast.ts index 0b30366df0..29029342a1 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -323,11 +323,12 @@ export interface OperationDefinitionNode { readonly selectionSet: SelectionSetNode; } -export enum OperationTypeNode { +enum OperationTypeNode { QUERY = 'query', MUTATION = 'mutation', SUBSCRIPTION = 'subscription', } +export { OperationTypeNode }; export interface VariableDefinitionNode { readonly kind: Kind.VARIABLE_DEFINITION; diff --git a/src/language/directiveLocation.ts b/src/language/directiveLocation.ts index e98ddf6d75..5c8aeb7240 100644 --- a/src/language/directiveLocation.ts +++ b/src/language/directiveLocation.ts @@ -1,7 +1,7 @@ /** * The set of allowed directive location values. */ -export enum DirectiveLocation { +enum DirectiveLocation { /** Request Definitions */ QUERY = 'QUERY', MUTATION = 'MUTATION', @@ -24,6 +24,7 @@ export enum DirectiveLocation { INPUT_OBJECT = 'INPUT_OBJECT', INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION', } +export { DirectiveLocation }; /** * The enum type representing the directive location values. diff --git a/src/language/kinds.ts b/src/language/kinds.ts index 39b2a8e675..cd05f66a3b 100644 --- a/src/language/kinds.ts +++ b/src/language/kinds.ts @@ -1,7 +1,7 @@ /** * The set of allowed kind values for AST nodes. */ -export enum Kind { +enum Kind { /** Name */ NAME = 'Name', @@ -67,6 +67,7 @@ export enum Kind { ENUM_TYPE_EXTENSION = 'EnumTypeExtension', INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension', } +export { Kind }; /** * The enum type representing the possible kind values of AST nodes. diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts index 4878d697b0..0c260df99e 100644 --- a/src/language/tokenKind.ts +++ b/src/language/tokenKind.ts @@ -2,7 +2,7 @@ * An exported enum describing the different kinds of tokens that the * lexer emits. */ -export enum TokenKind { +enum TokenKind { SOF = '', EOF = '', BANG = '!', @@ -26,6 +26,7 @@ export enum TokenKind { BLOCK_STRING = 'BlockString', COMMENT = 'Comment', } +export { TokenKind }; /** * The enum type representing the token kinds values. diff --git a/src/type/introspection.ts b/src/type/introspection.ts index e5fce6f241..f5e4b07ea7 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -435,7 +435,7 @@ export const __EnumValue: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap), }); -export enum TypeKind { +enum TypeKind { SCALAR = 'SCALAR', OBJECT = 'OBJECT', INTERFACE = 'INTERFACE', @@ -445,6 +445,7 @@ export enum TypeKind { LIST = 'LIST', NON_NULL = 'NON_NULL', } +export { TypeKind }; export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({ name: '__TypeKind', diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index 0bf0d453b4..2489af9d62 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -34,7 +34,7 @@ import type { GraphQLSchema } from '../type/schema'; import { astFromValue } from './astFromValue'; import { sortValueNode } from './sortValueNode'; -export enum BreakingChangeType { +enum BreakingChangeType { TYPE_REMOVED = 'TYPE_REMOVED', TYPE_CHANGED_KIND = 'TYPE_CHANGED_KIND', TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION', @@ -52,8 +52,9 @@ export enum BreakingChangeType { DIRECTIVE_REPEATABLE_REMOVED = 'DIRECTIVE_REPEATABLE_REMOVED', DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED', } +export { BreakingChangeType }; -export enum DangerousChangeType { +enum DangerousChangeType { VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM', TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION', OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED', @@ -61,6 +62,7 @@ export enum DangerousChangeType { IMPLEMENTED_INTERFACE_ADDED = 'IMPLEMENTED_INTERFACE_ADDED', ARG_DEFAULT_VALUE_CHANGE = 'ARG_DEFAULT_VALUE_CHANGE', } +export { DangerousChangeType }; export interface BreakingChange { type: BreakingChangeType; From 6c6508bd0d74587d7d264f6ab2258df5aeccc6af Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Tue, 16 Aug 2022 22:12:00 +0300 Subject: [PATCH 05/29] Parser: allow 'options' to explicitly accept undefined (#3701) --- src/language/parser.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/language/parser.ts b/src/language/parser.ts index 282ee16859..ef19af7417 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -102,7 +102,7 @@ export interface ParseOptions { */ export function parse( source: string | Source, - options?: ParseOptions, + options?: ParseOptions | undefined, ): DocumentNode { const parser = new Parser(source, options); return parser.parseDocument(); @@ -120,7 +120,7 @@ export function parse( */ export function parseValue( source: string | Source, - options?: ParseOptions, + options?: ParseOptions | undefined, ): ValueNode { const parser = new Parser(source, options); parser.expectToken(TokenKind.SOF); @@ -135,7 +135,7 @@ export function parseValue( */ export function parseConstValue( source: string | Source, - options?: ParseOptions, + options?: ParseOptions | undefined, ): ConstValueNode { const parser = new Parser(source, options); parser.expectToken(TokenKind.SOF); @@ -156,7 +156,7 @@ export function parseConstValue( */ export function parseType( source: string | Source, - options?: ParseOptions, + options?: ParseOptions | undefined, ): TypeNode { const parser = new Parser(source, options); parser.expectToken(TokenKind.SOF); @@ -177,10 +177,10 @@ export function parseType( * @internal */ export class Parser { - protected _options: Maybe; + protected _options: ParseOptions; protected _lexer: Lexer; - constructor(source: string | Source, options?: ParseOptions) { + constructor(source: string | Source, options: ParseOptions = {}) { const sourceObj = isSource(source) ? source : new Source(source); this._lexer = new Lexer(sourceObj); @@ -510,7 +510,7 @@ export class Parser { // Legacy support for defining variables within fragments changes // the grammar of FragmentDefinition: // - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet - if (this._options?.allowLegacyFragmentVariables === true) { + if (this._options.allowLegacyFragmentVariables === true) { return this.node(start, { kind: Kind.FRAGMENT_DEFINITION, name: this.parseFragmentName(), @@ -1387,7 +1387,7 @@ export class Parser { * given parsed object. */ node(startToken: Token, node: T): T { - if (this._options?.noLocation !== true) { + if (this._options.noLocation !== true) { node.loc = new Location( startToken, this._lexer.lastToken, From f0a0a4dadffe41dae541ab297f95997435b27c57 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Tue, 16 Aug 2022 22:20:22 +0300 Subject: [PATCH 06/29] parser: limit maximum number of tokens (#3702) Co-authored-by: Yaacov Rydzinski --- src/language/__tests__/parser-test.ts | 13 ++++++++ src/language/parser.ts | 43 ++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 3571b75700..87e7b92c34 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -84,6 +84,19 @@ describe('Parser', () => { `); }); + it('limit maximum number of tokens', () => { + expect(() => parse('{ foo }', { maxTokens: 3 })).to.not.throw(); + expect(() => parse('{ foo }', { maxTokens: 2 })).to.throw( + 'Syntax Error: Document contains more that 2 tokens. Parsing aborted.', + ); + + expect(() => parse('{ foo(bar: "baz") }', { maxTokens: 8 })).to.not.throw(); + + expect(() => parse('{ foo(bar: "baz") }', { maxTokens: 7 })).to.throw( + 'Syntax Error: Document contains more that 7 tokens. Parsing aborted.', + ); + }); + it('parses variable inline values', () => { expect(() => parse('{ field(complex: { a: { b: [ $var ] } }) }'), diff --git a/src/language/parser.ts b/src/language/parser.ts index ef19af7417..eb54a0376b 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -78,6 +78,15 @@ export interface ParseOptions { */ noLocation?: boolean; + /** + * Parser CPU and memory usage is linear to the number of tokens in a document + * however in extreme cases it becomes quadratic due to memory exhaustion. + * Parsing happens before validation so even invalid queries can burn lots of + * CPU time and memory. + * To prevent this you can set a maximum number of tokens allowed within a document. + */ + maxTokens?: number | undefined; + /** * @deprecated will be removed in the v17.0.0 * @@ -179,12 +188,14 @@ export function parseType( export class Parser { protected _options: ParseOptions; protected _lexer: Lexer; + protected _tokenCounter: number; constructor(source: string | Source, options: ParseOptions = {}) { const sourceObj = isSource(source) ? source : new Source(source); this._lexer = new Lexer(sourceObj); this._options = options; + this._tokenCounter = 0; } /** @@ -569,13 +580,13 @@ export class Parser { case TokenKind.BRACE_L: return this.parseObject(isConst); case TokenKind.INT: - this._lexer.advance(); + this.advanceLexer(); return this.node(token, { kind: Kind.INT, value: token.value, }); case TokenKind.FLOAT: - this._lexer.advance(); + this.advanceLexer(); return this.node(token, { kind: Kind.FLOAT, value: token.value, @@ -584,7 +595,7 @@ export class Parser { case TokenKind.BLOCK_STRING: return this.parseStringLiteral(); case TokenKind.NAME: - this._lexer.advance(); + this.advanceLexer(); switch (token.value) { case 'true': return this.node(token, { @@ -630,7 +641,7 @@ export class Parser { parseStringLiteral(): StringValueNode { const token = this._lexer.token; - this._lexer.advance(); + this.advanceLexer(); return this.node(token, { kind: Kind.STRING, value: token.value, @@ -1411,7 +1422,7 @@ export class Parser { expectToken(kind: TokenKind): Token { const token = this._lexer.token; if (token.kind === kind) { - this._lexer.advance(); + this.advanceLexer(); return token; } @@ -1429,7 +1440,7 @@ export class Parser { expectOptionalToken(kind: TokenKind): boolean { const token = this._lexer.token; if (token.kind === kind) { - this._lexer.advance(); + this.advanceLexer(); return true; } return false; @@ -1442,7 +1453,7 @@ export class Parser { expectKeyword(value: string): void { const token = this._lexer.token; if (token.kind === TokenKind.NAME && token.value === value) { - this._lexer.advance(); + this.advanceLexer(); } else { throw syntaxError( this._lexer.source, @@ -1459,7 +1470,7 @@ export class Parser { expectOptionalKeyword(value: string): boolean { const token = this._lexer.token; if (token.kind === TokenKind.NAME && token.value === value) { - this._lexer.advance(); + this.advanceLexer(); return true; } return false; @@ -1548,6 +1559,22 @@ export class Parser { } while (this.expectOptionalToken(delimiterKind)); return nodes; } + + advanceLexer(): void { + const { maxTokens } = this._options; + const token = this._lexer.advance(); + + if (maxTokens !== undefined && token.kind !== TokenKind.EOF) { + ++this._tokenCounter; + if (this._tokenCounter > maxTokens) { + throw syntaxError( + this._lexer.source, + token.start, + `Document contains more that ${maxTokens} tokens. Parsing aborted.`, + ); + } + } + } } /** From 3a51ecade74a0198847e8b1ab1bcdc129485b79b Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Tue, 16 Aug 2022 22:23:05 +0300 Subject: [PATCH 07/29] 16.6.0 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83faedcadf..1a7bfea8b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "16.5.0", + "version": "16.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "16.5.0", + "version": "16.6.0", "license": "MIT", "devDependencies": { "@babel/core": "7.17.9", diff --git a/package.json b/package.json index 5457b6e2ec..8ac82541b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "16.5.0", + "version": "16.6.0", "description": "A Query Language and Runtime which can target any service.", "license": "MIT", "private": true, diff --git a/src/version.ts b/src/version.ts index 2ccaf094d9..d5662a10f5 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,14 +4,14 @@ /** * A string containing the version of the GraphQL.js library */ -export const version = '16.5.0' as string; +export const version = '16.6.0' as string; /** * An object containing the components of the GraphQL.js version string */ export const versionInfo = Object.freeze({ major: 16 as number, - minor: 5 as number, + minor: 6 as number, patch: 0 as number, preReleaseTag: null as string | null, }); From 4a82557ae6d3b3c6cd72bcd528254296ecf7e9e8 Mon Sep 17 00:00:00 2001 From: Chris Karcher Date: Mon, 17 Oct 2022 12:07:43 -0500 Subject: [PATCH 08/29] Fix crash in node when mixing sync/async resolvers (backport of #3706) (#3707) Co-authored-by: Ivan Goncharov --- src/execution/__tests__/executor-test.ts | 51 ++++++++++++++++++++++++ src/execution/execute.ts | 36 +++++++++++------ 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 116334aded..c758d3e426 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick'; import { inspect } from '../../jsutils/inspect'; import { invariant } from '../../jsutils/invariant'; @@ -625,6 +626,56 @@ describe('Execute: Handles basic execution tasks', () => { }); }); + it('handles sync errors combined with rejections', async () => { + let isAsyncResolverFinished = false; + + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + syncNullError: { + type: new GraphQLNonNull(GraphQLString), + resolve: () => null, + }, + asyncNullError: { + type: new GraphQLNonNull(GraphQLString), + async resolve() { + await resolveOnNextTick(); + await resolveOnNextTick(); + await resolveOnNextTick(); + isAsyncResolverFinished = true; + return null; + }, + }, + }, + }), + }); + + // Order is important here, as the promise has to be created before the synchronous error is thrown + const document = parse(` + { + asyncNullError + syncNullError + } + `); + + const result = execute({ schema, document }); + + expect(isAsyncResolverFinished).to.equal(false); + expectJSON(await result).toDeepEqual({ + data: null, + errors: [ + { + message: + 'Cannot return null for non-nullable field Query.syncNullError.', + locations: [{ line: 4, column: 9 }], + path: ['syncNullError'], + }, + ], + }); + expect(isAsyncResolverFinished).to.equal(true); + }); + it('Full response path is included for non-nullable fields', () => { const A: GraphQLObjectType = new GraphQLObjectType({ name: 'A', diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 4b8cf3a6f7..55c22ea9de 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -445,22 +445,32 @@ function executeFields( const results = Object.create(null); let containsPromise = false; - for (const [responseName, fieldNodes] of fields.entries()) { - const fieldPath = addPath(path, responseName, parentType.name); - const result = executeField( - exeContext, - parentType, - sourceValue, - fieldNodes, - fieldPath, - ); + try { + for (const [responseName, fieldNodes] of fields.entries()) { + const fieldPath = addPath(path, responseName, parentType.name); + const result = executeField( + exeContext, + parentType, + sourceValue, + fieldNodes, + fieldPath, + ); - if (result !== undefined) { - results[responseName] = result; - if (isPromise(result)) { - containsPromise = true; + if (result !== undefined) { + results[responseName] = result; + if (isPromise(result)) { + containsPromise = true; + } } } + } catch (error) { + if (containsPromise) { + // Ensure that any promises returned by other fields are handled, as they may also reject. + return promiseForObject(results).finally(() => { + throw error; + }); + } + throw error; } // If there are no promises, we can just return the object From 076972e9c1944c9fe43a42046ed9d8be08d974dc Mon Sep 17 00:00:00 2001 From: Sten Reijers <37755871+stenreijers@users.noreply.github.com> Date: Wed, 15 Feb 2023 10:19:03 +0100 Subject: [PATCH 09/29] Fix/invalid error propagation custom scalars (backport for 16.x.x) (#3838) --- src/execution/__tests__/variables-test.ts | 59 +++++++++++++++++++++++ src/execution/values.ts | 2 +- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index 786a4810c2..3a859a0bdc 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -6,6 +6,8 @@ import { expectJSON } from '../../__testUtils__/expectJSON'; import { inspect } from '../../jsutils/inspect'; import { invariant } from '../../jsutils/invariant'; +import { GraphQLError } from '../../error/GraphQLError'; + import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; @@ -27,6 +29,25 @@ import { GraphQLSchema } from '../../type/schema'; import { executeSync } from '../execute'; import { getVariableValues } from '../values'; +const TestFaultyScalarGraphQLError = new GraphQLError( + 'FaultyScalarErrorMessage', + { + extensions: { + code: 'FaultyScalarErrorMessageExtensionCode', + }, + }, +); + +const TestFaultyScalar = new GraphQLScalarType({ + name: 'FaultyScalar', + parseValue() { + throw TestFaultyScalarGraphQLError; + }, + parseLiteral() { + throw TestFaultyScalarGraphQLError; + }, +}); + const TestComplexScalar = new GraphQLScalarType({ name: 'ComplexScalar', parseValue(value) { @@ -46,6 +67,7 @@ const TestInputObject = new GraphQLInputObjectType({ b: { type: new GraphQLList(GraphQLString) }, c: { type: new GraphQLNonNull(GraphQLString) }, d: { type: TestComplexScalar }, + e: { type: TestFaultyScalar }, }, }); @@ -228,6 +250,27 @@ describe('Execute: Handles inputs', () => { }); }); + it('errors on faulty scalar type input', () => { + const result = executeQuery(` + { + fieldWithObjectInput(input: {c: "foo", e: "bar"}) + } + `); + + expectJSON(result).toDeepEqual({ + data: { + fieldWithObjectInput: null, + }, + errors: [ + { + message: 'Argument "input" has invalid value {c: "foo", e: "bar"}.', + path: ['fieldWithObjectInput'], + locations: [{ line: 3, column: 39 }], + }, + ], + }); + }); + describe('using variables', () => { const doc = ` query ($input: TestInputObject) { @@ -367,6 +410,22 @@ describe('Execute: Handles inputs', () => { }); }); + it('errors on faulty scalar type input', () => { + const params = { input: { c: 'foo', e: 'SerializedValue' } }; + const result = executeQuery(doc, params); + + expectJSON(result).toDeepEqual({ + errors: [ + { + message: + 'Variable "$input" got invalid value "SerializedValue" at "input.e"; FaultyScalarErrorMessage', + locations: [{ line: 2, column: 16 }], + extensions: { code: 'FaultyScalarErrorMessageExtensionCode' }, + }, + ], + }); + }); + it('errors on null for nested non-null', () => { const params = { input: { a: 'foo', b: 'bar', c: null } }; const result = executeQuery(doc, params); diff --git a/src/execution/values.ts b/src/execution/values.ts index 023e028109..d65ea9cf20 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -131,7 +131,7 @@ function coerceVariableValues( onError( new GraphQLError(prefix + '; ' + error.message, { nodes: varDefNode, - originalError: error.originalError, + originalError: error, }), ); }, From 84bb146e644e78ba75faf0ba173de9b4434807c5 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 2 May 2023 21:45:50 +0200 Subject: [PATCH 10/29] check "globalThis.process" before accessing it (#3887) Closes #3758 --- src/jsutils/instanceOf.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts index a4456c679d..b917cfc3bf 100644 --- a/src/jsutils/instanceOf.ts +++ b/src/jsutils/instanceOf.ts @@ -9,8 +9,7 @@ import { inspect } from './inspect'; export const instanceOf: (value: unknown, constructor: Constructor) => boolean = /* c8 ignore next 6 */ // FIXME: https://github.com/graphql/graphql-js/issues/2317 - // eslint-disable-next-line no-undef - process.env.NODE_ENV === 'production' + globalThis.process?.env.NODE_ENV === 'production' ? function instanceOf(value: unknown, constructor: Constructor): boolean { return value instanceof constructor; } From 1519fda27376bcdd26b433aecfb9e7b485da71f8 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Wed, 21 Jun 2023 19:27:33 +0300 Subject: [PATCH 11/29] 16.7.0 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a7bfea8b3..fc7c4c9680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "16.6.0", + "version": "16.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "16.6.0", + "version": "16.7.0", "license": "MIT", "devDependencies": { "@babel/core": "7.17.9", diff --git a/package.json b/package.json index 8ac82541b7..f3466e2735 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "16.6.0", + "version": "16.7.0", "description": "A Query Language and Runtime which can target any service.", "license": "MIT", "private": true, diff --git a/src/version.ts b/src/version.ts index d5662a10f5..b7cf1e898c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,14 +4,14 @@ /** * A string containing the version of the GraphQL.js library */ -export const version = '16.6.0' as string; +export const version = '16.7.0' as string; /** * An object containing the components of the GraphQL.js version string */ export const versionInfo = Object.freeze({ major: 16 as number, - minor: 6 as number, + minor: 7 as number, patch: 0 as number, preReleaseTag: null as string | null, }); From a08aaeea584a326c7d1a40cbcbd1b28b64c4e08c Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 22 Jun 2023 19:57:02 +0300 Subject: [PATCH 12/29] instanceOf: workaround bundler issue with `process.env` (#3923) Fixes: #3919 #3920 #3921 --- src/jsutils/instanceOf.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts index b917cfc3bf..05eac2ca89 100644 --- a/src/jsutils/instanceOf.ts +++ b/src/jsutils/instanceOf.ts @@ -9,7 +9,7 @@ import { inspect } from './inspect'; export const instanceOf: (value: unknown, constructor: Constructor) => boolean = /* c8 ignore next 6 */ // FIXME: https://github.com/graphql/graphql-js/issues/2317 - globalThis.process?.env.NODE_ENV === 'production' + globalThis.process && globalThis.process.env.NODE_ENV === 'production' ? function instanceOf(value: unknown, constructor: Constructor): boolean { return value instanceof constructor; } From bf6a9f0e1cc8721de6675fb7bff470137635266f Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 22 Jun 2023 20:01:35 +0300 Subject: [PATCH 13/29] 16.7.1 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc7c4c9680..92d12a2926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "16.7.0", + "version": "16.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "16.7.0", + "version": "16.7.1", "license": "MIT", "devDependencies": { "@babel/core": "7.17.9", diff --git a/package.json b/package.json index f3466e2735..6ba956ff75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "16.7.0", + "version": "16.7.1", "description": "A Query Language and Runtime which can target any service.", "license": "MIT", "private": true, diff --git a/src/version.ts b/src/version.ts index b7cf1e898c..623bb83c25 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,7 +4,7 @@ /** * A string containing the version of the GraphQL.js library */ -export const version = '16.7.0' as string; +export const version = '16.7.1' as string; /** * An object containing the components of the GraphQL.js version string @@ -12,6 +12,6 @@ export const version = '16.7.0' as string; export const versionInfo = Object.freeze({ major: 16 as number, minor: 7 as number, - patch: 0 as number, + patch: 1 as number, preReleaseTag: null as string | null, }); From bec1b497fdfba69937b958e80676b585124bf0c5 Mon Sep 17 00:00:00 2001 From: Gunnar Schulze Date: Mon, 14 Aug 2023 12:19:47 +0200 Subject: [PATCH 14/29] Support fourfold nested lists (#3950) --- src/utilities/__tests__/buildClientSchema-test.ts | 14 +++++++------- src/utilities/getIntrospectionQuery.ts | 8 ++++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 7d17811515..c6c619be42 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -879,10 +879,10 @@ describe('Type System: build schema from introspection', () => { }); describe('very deep decorators are not supported', () => { - it('fails on very deep (> 7 levels) lists', () => { + it('fails on very deep (> 8 levels) lists', () => { const schema = buildSchema(` type Query { - foo: [[[[[[[[String]]]]]]]] + foo: [[[[[[[[[[String]]]]]]]]]] } `); @@ -892,10 +892,10 @@ describe('Type System: build schema from introspection', () => { ); }); - it('fails on a very deep (> 7 levels) non-null', () => { + it('fails on a very deep (> 8 levels) non-null', () => { const schema = buildSchema(` type Query { - foo: [[[[String!]!]!]!] + foo: [[[[[String!]!]!]!]!] } `); @@ -905,11 +905,11 @@ describe('Type System: build schema from introspection', () => { ); }); - it('succeeds on deep (<= 7 levels) types', () => { - // e.g., fully non-null 3D matrix + it('succeeds on deep (<= 8 levels) types', () => { + // e.g., fully non-null 4D matrix const sdl = dedent` type Query { - foo: [[[String!]!]!]! + foo: [[[[String!]!]!]!]! } `; diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index c21fe9a1bb..b58fb083e4 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -152,6 +152,14 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { ofType { kind name + ofType { + kind + name + ofType { + kind + name + } + } } } } From e4f759dba1a9b19c8a189b803657ee4abe0efe11 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 14 Aug 2023 13:26:04 +0300 Subject: [PATCH 15/29] 16.8.0 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92d12a2926..9b616c3d84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "16.7.1", + "version": "16.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "16.7.1", + "version": "16.8.0", "license": "MIT", "devDependencies": { "@babel/core": "7.17.9", diff --git a/package.json b/package.json index 6ba956ff75..2fedf014a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "16.7.1", + "version": "16.8.0", "description": "A Query Language and Runtime which can target any service.", "license": "MIT", "private": true, diff --git a/src/version.ts b/src/version.ts index 623bb83c25..a6e2e5cc5c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,14 +4,14 @@ /** * A string containing the version of the GraphQL.js library */ -export const version = '16.7.1' as string; +export const version = '16.8.0' as string; /** * An object containing the components of the GraphQL.js version string */ export const versionInfo = Object.freeze({ major: 16 as number, - minor: 7 as number, - patch: 1 as number, + minor: 8 as number, + patch: 0 as number, preReleaseTag: null as string | null, }); From 8f4c64eb6a7112a929ffeef00caa67529b3f2fcf Mon Sep 17 00:00:00 2001 From: Aaron Moat <2937187+AaronMoat@users.noreply.github.com> Date: Mon, 11 Sep 2023 05:03:35 +1000 Subject: [PATCH 16/29] OverlappingFieldsCanBeMergedRule: Fix performance degradation (#3967) fixes from https://github.com/graphql/graphql-js/pull/3958 into --- benchmark/repeated-fields-benchmark.js | 15 ++++++ .../rules/OverlappingFieldsCanBeMergedRule.ts | 53 +++++++++++++------ 2 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 benchmark/repeated-fields-benchmark.js diff --git a/benchmark/repeated-fields-benchmark.js b/benchmark/repeated-fields-benchmark.js new file mode 100644 index 0000000000..7dd5b179b7 --- /dev/null +++ b/benchmark/repeated-fields-benchmark.js @@ -0,0 +1,15 @@ +'use strict'; + +const { graphqlSync } = require('graphql/graphql.js'); +const { buildSchema } = require('graphql/utilities/buildASTSchema.js'); + +const schema = buildSchema('type Query { hello: String! }'); +const source = `{ ${'hello '.repeat(250)}}`; + +module.exports = { + name: 'Many repeated fields', + count: 5, + measure() { + graphqlSync({ schema, source }); + }, +}; diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts index bdf6eb874e..4305064a6f 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts +++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts @@ -5,10 +5,11 @@ import type { ObjMap } from '../../jsutils/ObjMap'; import { GraphQLError } from '../../error/GraphQLError'; import type { + DirectiveNode, FieldNode, FragmentDefinitionNode, - ObjectValueNode, SelectionSetNode, + ValueNode, } from '../../language/ast'; import { Kind } from '../../language/kinds'; import { print } from '../../language/printer'; @@ -588,7 +589,7 @@ function findConflict( } // Two field calls must have the same arguments. - if (stringifyArguments(node1) !== stringifyArguments(node2)) { + if (!sameArguments(node1, node2)) { return [ [responseName, 'they have differing arguments'], [node1], @@ -634,19 +635,41 @@ function findConflict( } } -function stringifyArguments(fieldNode: FieldNode): string { - // FIXME https://github.com/graphql/graphql-js/issues/2203 - const args = /* c8 ignore next */ fieldNode.arguments ?? []; - - const inputObjectWithArgs: ObjectValueNode = { - kind: Kind.OBJECT, - fields: args.map((argNode) => ({ - kind: Kind.OBJECT_FIELD, - name: argNode.name, - value: argNode.value, - })), - }; - return print(sortValueNode(inputObjectWithArgs)); +function sameArguments( + node1: FieldNode | DirectiveNode, + node2: FieldNode | DirectiveNode, +): boolean { + const args1 = node1.arguments; + const args2 = node2.arguments; + + if (args1 === undefined || args1.length === 0) { + return args2 === undefined || args2.length === 0; + } + if (args2 === undefined || args2.length === 0) { + return false; + } + + /* c8 ignore next */ + if (args1.length !== args2.length) { + /* c8 ignore next */ + return false; + /* c8 ignore next */ + } + + const values2 = new Map(args2.map(({ name, value }) => [name.value, value])); + return args1.every((arg1) => { + const value1 = arg1.value; + const value2 = values2.get(arg1.name.value); + if (value2 === undefined) { + return false; + } + + return stringifyValue(value1) === stringifyValue(value2); + }); +} + +function stringifyValue(value: ValueNode): string | null { + return print(sortValueNode(value)); } // Two types conflict if both types could not apply to a value simultaneously. From 8a95335f545024c09abfa0f07cc326f73a0e466f Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Tue, 19 Sep 2023 10:25:23 +0300 Subject: [PATCH 17/29] 16.8.1 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b616c3d84..df6969202d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "16.8.0", + "version": "16.8.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "16.8.0", + "version": "16.8.1", "license": "MIT", "devDependencies": { "@babel/core": "7.17.9", diff --git a/package.json b/package.json index 2fedf014a8..c779267657 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "16.8.0", + "version": "16.8.1", "description": "A Query Language and Runtime which can target any service.", "license": "MIT", "private": true, diff --git a/src/version.ts b/src/version.ts index a6e2e5cc5c..275c3e4a67 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,7 +4,7 @@ /** * A string containing the version of the GraphQL.js library */ -export const version = '16.8.0' as string; +export const version = '16.8.1' as string; /** * An object containing the components of the GraphQL.js version string @@ -12,6 +12,6 @@ export const version = '16.8.0' as string; export const versionInfo = Object.freeze({ major: 16 as number, minor: 8 as number, - patch: 0 as number, + patch: 1 as number, preReleaseTag: null as string | null, }); From 0d12b0654a44f7bb9e35a2c612ed28c7bfe1e69e Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 29 May 2024 23:27:36 +0200 Subject: [PATCH 18/29] fix: remove `globalThis` check and align with what bundlers can accept (#4022) As surfaced in [Discord](https://discord.com/channels/625400653321076807/862957336082645006/1206980831915282532) this currently is a breaking change in the 16.x.x release line which is preventing folks from upgrading towards a security fix. This PR should result in a patch release on the 16 release line. This change was originally introduced to support CFW and browser environments which should still be supported with the `typeof` check CC @n1ru4l This also adds a check whether `.env` is present as in the DOM using `id="process"` defines that as a global which we don't want to access on accident. as shown in https://github.com/graphql/graphql-js/pull/4017 Bundles also target `process.env.NODE_ENV` specifically which fails when it replaces `globalThis.process.env.NODE_ENV` as this becomes `globalThis."production"` which is invalid syntax. Fixes https://github.com/graphql/graphql-js/issues/3978 Fixes https://github.com/graphql/graphql-js/issues/3918 Fixes https://github.com/graphql/graphql-js/issues/3928 Fixes https://github.com/graphql/graphql-js/issues/3758 Fixes https://github.com/graphql/graphql-js/issues/3934 This purposefully does not account for https://github.com/graphql/graphql-js/issues/3925 as we can't address this without breaking CF/plain browsers so the small byte-size increase will be expected for bundled browser environments. As a middle ground we did optimise the performance here. We can revisit this for v17. Most bundlers will be able to tree-shake this with a little help, in https://github.com/graphql/graphql-js/issues/4075#issuecomment-2094052098 you can find a conclusion with a repo where we discuss a few. - Next.JS by default replaces [`process.env.NODE_ENV`](https://github.com/vercel/next.js/blob/b0ab0fe85fe8c93792051b058e060724ff373cc2/packages/next/webpack.config.js#L182) you can add `typeof process` linearly - Vite allows you to specify [`config.define`](https://vitejs.dev/config/shared-options.html#define) - ESBuild by default will replace `process.env.NODE_ENV` but does not support replacing `typeof process` - Rollup has a plugin for this https://www.npmjs.com/package/@rollup/plugin-replace Supersedes https://github.com/graphql/graphql-js/pull/4021 Supersedes https://github.com/graphql/graphql-js/pull/4019 Supersedes https://github.com/graphql/graphql-js/pull/3927 > This now also adds a documentation page on how to remove all of these --- cspell.yml | 4 + src/jsutils/instanceOf.ts | 8 +- website/docs/tutorials/going-to-production.md | 127 ++++++++++++++++++ website/sidebars.js | 5 + 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 website/docs/tutorials/going-to-production.md diff --git a/cspell.yml b/cspell.yml index 8770a4d781..88780fc80b 100644 --- a/cspell.yml +++ b/cspell.yml @@ -19,6 +19,10 @@ overrides: - clsx - infima - noopener + - Vite + - craco + - esbuild + - swcrc - noreferrer - xlink diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts index 05eac2ca89..27c4ab4d12 100644 --- a/src/jsutils/instanceOf.ts +++ b/src/jsutils/instanceOf.ts @@ -1,5 +1,11 @@ import { inspect } from './inspect'; +/* c8 ignore next 3 */ +const isProduction = + globalThis.process && + // eslint-disable-next-line no-undef + process.env.NODE_ENV === 'production'; + /** * A replacement for instanceof which includes an error warning when multi-realm * constructors are detected. @@ -9,7 +15,7 @@ import { inspect } from './inspect'; export const instanceOf: (value: unknown, constructor: Constructor) => boolean = /* c8 ignore next 6 */ // FIXME: https://github.com/graphql/graphql-js/issues/2317 - globalThis.process && globalThis.process.env.NODE_ENV === 'production' + isProduction ? function instanceOf(value: unknown, constructor: Constructor): boolean { return value instanceof constructor; } diff --git a/website/docs/tutorials/going-to-production.md b/website/docs/tutorials/going-to-production.md new file mode 100644 index 0000000000..fcc4a9ca37 --- /dev/null +++ b/website/docs/tutorials/going-to-production.md @@ -0,0 +1,127 @@ +--- +title: Going to production +category: FAQ +--- + +GraphQL.JS contains a few development checks which in production will cause slower performance and +an increase in bundle-size. Every bundler goes about these changes different, in here we'll list +out the most popular ones. + +## Bundler-specific configuration + +Here are some bundler-specific suggestions for configuring your bundler to remove `globalThis.process` and `process.env.NODE_ENV` on build time. + +### Vite + +```js +export default defineConfig({ + // ... + define: { + 'globalThis.process': JSON.stringify(true), + 'process.env.NODE_ENV': JSON.stringify('production'), + }, +}); +``` + +### Next.js + +```js +// ... +/** @type {import('next').NextConfig} */ +const nextConfig = { + webpack(config, { webpack }) { + config.plugins.push( + new webpack.DefinePlugin({ + 'globalThis.process': JSON.stringify(true), + 'process.env.NODE_ENV': JSON.stringify('production'), + }), + ); + return config; + }, +}; + +module.exports = nextConfig; +``` + +### create-react-app + +With `create-react-app`, you need to use a third-party package like [`craco`](https://craco.js.org/) to modify the bundler configuration. + +```js +const webpack = require('webpack'); +module.exports = { + webpack: { + plugins: [ + new webpack.DefinePlugin({ + 'globalThis.process': JSON.stringify(true), + 'process.env.NODE_ENV': JSON.stringify('production'), + }), + ], + }, +}; +``` + +### esbuild + +```json +{ + "define": { + "globalThis.process": true, + "process.env.NODE_ENV": "production" + } +} +``` + +### Webpack + +```js +config.plugins.push( + new webpack.DefinePlugin({ + 'globalThis.process': JSON.stringify(true), + 'process.env.NODE_ENV': JSON.stringify('production'), + }), +); +``` + +### Rollup + +```js +export default [ + { + // ... input, output, etc. + plugins: [ + minify({ + mangle: { + toplevel: true, + }, + compress: { + toplevel: true, + global_defs: { + '@globalThis.process': JSON.stringify(true), + '@process.env.NODE_ENV': JSON.stringify('production'), + }, + }, + }), + ], + }, +]; +``` + +### SWC + +```json title=".swcrc" +{ + "jsc": { + "transform": { + "optimizer": { + "globals": { + "vars": { + "globalThis.process": true, + "process.env.NODE_ENV": "production" + } + } + } + } + } +} +``` diff --git a/website/sidebars.js b/website/sidebars.js index 79fe5e9d8b..7b4722bd7c 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -15,6 +15,11 @@ module.exports = { label: 'Advanced', items: ['tutorials/constructing-types'], }, + { + type: 'category', + label: 'FAQ', + items: ['tutorials/going-to-production'], + }, 'tutorials/express-graphql', ], }; From c82609e8224d80888c9704b47bac90098fbfa176 Mon Sep 17 00:00:00 2001 From: Benjie Date: Wed, 12 Jun 2024 15:16:59 +0100 Subject: [PATCH 19/29] Fix publish scripts (#4104) --- package.json | 2 +- resources/checkgit.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c779267657..1ed4f378fb 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" }, "scripts": { - "preversion": ". ./resources/checkgit.sh && npm ci --ignore-scripts", + "preversion": "bash -c '. ./resources/checkgit.sh && npm ci --ignore-scripts'", "version": "node resources/gen-version.js && npm test && git add src/version.ts", "fuzzonly": "mocha --full-trace src/**/__tests__/**/*-fuzz.ts", "changelog": "node resources/gen-changelog.js", diff --git a/resources/checkgit.sh b/resources/checkgit.sh index 49449ecdf2..e5e4c67cf2 100644 --- a/resources/checkgit.sh +++ b/resources/checkgit.sh @@ -1,4 +1,5 @@ # Exit immediately if any subcommand terminated +set -e trap "exit 1" ERR # From 08779a044b84881fb36377272777cf42a82eb262 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 12 Jun 2024 15:19:41 +0100 Subject: [PATCH 20/29] 16.8.2 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index df6969202d..74682729c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "16.8.1", + "version": "16.8.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "16.8.1", + "version": "16.8.2", "license": "MIT", "devDependencies": { "@babel/core": "7.17.9", diff --git a/package.json b/package.json index 1ed4f378fb..199d3c7045 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "16.8.1", + "version": "16.8.2", "description": "A Query Language and Runtime which can target any service.", "license": "MIT", "private": true, diff --git a/src/version.ts b/src/version.ts index 275c3e4a67..0ab817db6b 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,7 +4,7 @@ /** * A string containing the version of the GraphQL.js library */ -export const version = '16.8.1' as string; +export const version = '16.8.2' as string; /** * An object containing the components of the GraphQL.js version string @@ -12,6 +12,6 @@ export const version = '16.8.1' as string; export const versionInfo = Object.freeze({ major: 16 as number, minor: 8 as number, - patch: 1 as number, + patch: 2 as number, preReleaseTag: null as string | null, }); From c985c2733d92c8c60b51345e3b15b7d6517b80eb Mon Sep 17 00:00:00 2001 From: Benjie Date: Fri, 21 Jun 2024 14:02:33 +0100 Subject: [PATCH 21/29] backport[v16]: Introduce "recommended" validation rules (#4119) Co-authored-by: enisdenjo Co-authored-by: Denis Badurina --- src/index.ts | 2 + .../MaxIntrospectionDepthRule-test.ts | 554 ++++++++++++++++++ src/validation/index.ts | 4 +- .../rules/MaxIntrospectionDepthRule.ts | 91 +++ src/validation/specifiedRules.ts | 9 + 5 files changed, 659 insertions(+), 1 deletion(-) create mode 100644 src/validation/__tests__/MaxIntrospectionDepthRule-test.ts create mode 100644 src/validation/rules/MaxIntrospectionDepthRule.ts diff --git a/src/index.ts b/src/index.ts index bb82b58164..6185d9f444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -339,6 +339,7 @@ export { ValidationContext, // All validation rules in the GraphQL Specification. specifiedRules, + recommendedRules, // Individual validation rules. ExecutableDefinitionsRule, FieldsOnCorrectTypeRule, @@ -366,6 +367,7 @@ export { ValuesOfCorrectTypeRule, VariablesAreInputTypesRule, VariablesInAllowedPositionRule, + MaxIntrospectionDepthRule, // SDL-specific validation rules LoneSchemaDefinitionRule, UniqueOperationTypesRule, diff --git a/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts new file mode 100644 index 0000000000..d6609941d4 --- /dev/null +++ b/src/validation/__tests__/MaxIntrospectionDepthRule-test.ts @@ -0,0 +1,554 @@ +import { describe, it } from 'mocha'; + +import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery'; + +import { MaxIntrospectionDepthRule } from '../rules/MaxIntrospectionDepthRule'; + +import { expectValidationErrors } from './harness'; + +function expectErrors(queryStr: string) { + return expectValidationErrors(MaxIntrospectionDepthRule, queryStr); +} + +function expectValid(queryStr: string) { + expectErrors(queryStr).toDeepEqual([]); +} + +describe('Validate: Max introspection nodes rule', () => { + it('default introspection query', () => { + expectValid(getIntrospectionQuery()); + }); + + it('all options introspection query', () => { + expectValid( + getIntrospectionQuery({ + descriptions: true, + specifiedByUrl: true, + directiveIsRepeatable: true, + schemaDescription: true, + inputValueDeprecation: true, + }), + ); + }); + + it('3 flat fields introspection query', () => { + expectValid(` + { + __type(name: "Query") { + trueFields: fields(includeDeprecated: true) { + name + } + falseFields: fields(includeDeprecated: false) { + name + } + omittedFields: fields { + name + } + } + } + `); + }); + + it('3 fields deep introspection query from __schema', () => { + expectErrors(` + { + __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 interfaces deep introspection query from __schema', () => { + expectErrors(` + { + __schema { + types { + interfaces { + interfaces { + interfaces { + name + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 possibleTypes deep introspection query from __schema', () => { + expectErrors(` + { + __schema { + types { + possibleTypes { + possibleTypes { + possibleTypes { + name + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 inputFields deep introspection query from __schema', () => { + expectErrors(` + { + __schema { + types { + inputFields { + type { + inputFields { + type { + inputFields { + type { + name + } + } + } + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 fields deep introspection query from multiple __schema', () => { + expectErrors(` + { + one: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + two: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + three: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + { + locations: [ + { + column: 7, + line: 18, + }, + ], + message: 'Maximum introspection depth exceeded', + }, + { + locations: [ + { + column: 7, + line: 33, + }, + ], + message: 'Maximum introspection depth exceeded', + }, + ]); + }); + + it('3 fields deep introspection query from __type', () => { + expectErrors(` + { + __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 fields deep introspection query from multiple __type', () => { + expectErrors(` + { + one: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + two: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + three: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + { + locations: [ + { + column: 7, + line: 18, + }, + ], + message: 'Maximum introspection depth exceeded', + }, + { + locations: [ + { + column: 7, + line: 33, + }, + ], + message: 'Maximum introspection depth exceeded', + }, + ]); + }); + + it('1 fields deep with 3 fields introspection query', () => { + expectValid(` + { + __schema { + types { + fields { + type { + oneFields: fields { + name + } + twoFields: fields { + name + } + threeFields: fields { + name + } + } + } + } + } + } + `); + }); + + it('3 fields deep from varying parents introspection query', () => { + expectErrors(` + { + __schema { + types { + fields { + type { + fields { + type { + ofType { + fields { + name + } + } + } + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 fields deep introspection query with inline fragments', () => { + expectErrors(` + query test { + __schema { + types { + ... on __Type { + fields { + type { + ... on __Type { + ofType { + fields { + type { + ... on __Type { + fields { + name + } + } + } + } + } + } + } + } + } + } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 fields deep introspection query with fragments', () => { + expectErrors(` + query test { + __schema { + types { + ...One + } + } + } + + fragment One on __Type { + fields { + type { + ...Two + } + } + } + + fragment Two on __Type { + fields { + type { + ...Three + } + } + } + + fragment Three on __Type { + fields { + name + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 7, + line: 3, + }, + ], + }, + ]); + }); + + it('3 fields deep inside inline fragment on query', () => { + expectErrors(` + { + ... { + __schema { types { fields { type { fields { type { fields { name } } } } } } } + } + } + `).toDeepEqual([ + { + message: 'Maximum introspection depth exceeded', + locations: [ + { + column: 9, + line: 4, + }, + ], + }, + ]); + }); + + it('opts out if fragment is missing', () => { + expectValid(` + query test { + __schema { + types { + ...Missing + } + } + } + `); + }); + + it("doesn't infinitely recurse on fragment cycle", () => { + expectValid(` + query test { + __schema { + types { + ...Cycle + } + } + } + fragment Cycle on __Type { + ...Cycle + } + `); + }); +}); diff --git a/src/validation/index.ts b/src/validation/index.ts index 58cc012ee8..587479e351 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -4,7 +4,7 @@ export { ValidationContext } from './ValidationContext'; export type { ValidationRule } from './ValidationContext'; // All validation rules in the GraphQL Specification. -export { specifiedRules } from './specifiedRules'; +export { specifiedRules, recommendedRules } from './specifiedRules'; // Spec Section: "Executable Definitions" export { ExecutableDefinitionsRule } from './rules/ExecutableDefinitionsRule'; @@ -84,6 +84,8 @@ export { VariablesAreInputTypesRule } from './rules/VariablesAreInputTypesRule'; // Spec Section: "All Variable Usages Are Allowed" export { VariablesInAllowedPositionRule } from './rules/VariablesInAllowedPositionRule'; +export { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule'; + // SDL-specific validation rules export { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule'; export { UniqueOperationTypesRule } from './rules/UniqueOperationTypesRule'; diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts new file mode 100644 index 0000000000..0c2dbd3879 --- /dev/null +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -0,0 +1,91 @@ +import { GraphQLError } from '../../error/GraphQLError'; + +import type { ASTNode } from '../../language/ast'; +import { Kind } from '../../language/kinds'; +import type { ASTVisitor } from '../../language/visitor'; + +import type { ASTValidationContext } from '../ValidationContext'; + +const MAX_LISTS_DEPTH = 3; + +export function MaxIntrospectionDepthRule( + context: ASTValidationContext, +): ASTVisitor { + /** + * Counts the depth of list fields in "__Type" recursively and + * returns `true` if the limit has been reached. + */ + function checkDepth( + node: ASTNode, + visitedFragments: { + [fragmentName: string]: true | undefined; + } = Object.create(null), + depth: number = 0, + ): boolean { + if (node.kind === Kind.FRAGMENT_SPREAD) { + const fragmentName = node.name.value; + if (visitedFragments[fragmentName] === true) { + // Fragment cycles are handled by `NoFragmentCyclesRule`. + return false; + } + const fragment = context.getFragment(fragmentName); + if (!fragment) { + // Missing fragments checks are handled by `KnownFragmentNamesRule`. + return false; + } + + // Rather than following an immutable programming pattern which has + // significant memory and garbage collection overhead, we've opted to + // take a mutable approach for efficiency's sake. Importantly visiting a + // fragment twice is fine, so long as you don't do one visit inside the + // other. + try { + visitedFragments[fragmentName] = true; + return checkDepth(fragment, visitedFragments, depth); + } finally { + visitedFragments[fragmentName] = undefined; + } + } + + if ( + node.kind === Kind.FIELD && + // check all introspection lists + (node.name.value === 'fields' || + node.name.value === 'interfaces' || + node.name.value === 'possibleTypes' || + node.name.value === 'inputFields') + ) { + // eslint-disable-next-line no-param-reassign + depth++; + if (depth >= MAX_LISTS_DEPTH) { + return true; + } + } + + // handles fields and inline fragments + if ('selectionSet' in node && node.selectionSet) { + for (const child of node.selectionSet.selections) { + if (checkDepth(child, visitedFragments, depth)) { + return true; + } + } + } + + return false; + } + + return { + Field(node) { + if (node.name.value === '__schema' || node.name.value === '__type') { + if (checkDepth(node)) { + context.reportError( + new GraphQLError('Maximum introspection depth exceeded', { + nodes: [node], + }), + ); + return false; + } + } + }, + }; +} diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index 16e555db8a..c312c9839c 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -19,6 +19,8 @@ import { KnownTypeNamesRule } from './rules/KnownTypeNamesRule'; import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule'; // SDL-specific validation rules import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule'; +// TODO: Spec Section +import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule'; // Spec Section: "Fragments must not form cycles" import { NoFragmentCyclesRule } from './rules/NoFragmentCyclesRule'; // Spec Section: "All Variable Used Defined" @@ -67,6 +69,12 @@ import { VariablesAreInputTypesRule } from './rules/VariablesAreInputTypesRule'; import { VariablesInAllowedPositionRule } from './rules/VariablesInAllowedPositionRule'; import type { SDLValidationRule, ValidationRule } from './ValidationContext'; +/** + * Technically these aren't part of the spec but they are strongly encouraged + * validation rules. + */ +export const recommendedRules = Object.freeze([MaxIntrospectionDepthRule]); + /** * This set includes all validation rules defined by the GraphQL spec. * @@ -100,6 +108,7 @@ export const specifiedRules: ReadonlyArray = Object.freeze([ VariablesInAllowedPositionRule, OverlappingFieldsCanBeMergedRule, UniqueInputFieldNamesRule, + ...recommendedRules, ]); /** From 29c1bff40c7cc035052aae0f5e4dfb6ac738e7a2 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 21 Jun 2024 15:34:54 +0200 Subject: [PATCH 22/29] feat: allow defining symbol error extensions (#3730) --- src/error/GraphQLError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/error/GraphQLError.ts b/src/error/GraphQLError.ts index b88d27a56c..c7cde504dc 100644 --- a/src/error/GraphQLError.ts +++ b/src/error/GraphQLError.ts @@ -17,7 +17,7 @@ import type { Source } from '../language/source'; * an object which can contain all the values you need. */ export interface GraphQLErrorExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } export interface GraphQLErrorOptions { From c35130e4c1b196a63cb0f8b4a74b873f921eaba5 Mon Sep 17 00:00:00 2001 From: Benjie Date: Fri, 21 Jun 2024 15:14:51 +0100 Subject: [PATCH 23/29] Revert error extension symbol (#4123) --- src/error/GraphQLError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/error/GraphQLError.ts b/src/error/GraphQLError.ts index c7cde504dc..b88d27a56c 100644 --- a/src/error/GraphQLError.ts +++ b/src/error/GraphQLError.ts @@ -17,7 +17,7 @@ import type { Source } from '../language/source'; * an object which can contain all the values you need. */ export interface GraphQLErrorExtensions { - [attributeName: string | symbol]: unknown; + [attributeName: string]: unknown; } export interface GraphQLErrorOptions { From 29144f73452f17ef669c6902f610f28afe61dbd1 Mon Sep 17 00:00:00 2001 From: Benjie Date: Fri, 21 Jun 2024 15:46:46 +0100 Subject: [PATCH 24/29] backport[v16]: Implement OneOf Input Objects via `@oneOf` directive (#4124) Co-authored-by: Erik Kessler Co-authored-by: Benedikt Franke Co-authored-by: Michael Hayes Co-authored-by: Mike Ciesielka --- src/__testUtils__/kitchenSinkSDL.ts | 6 + src/execution/__tests__/oneof-test.ts | 185 ++++++++++++++++++ src/index.ts | 1 + src/language/__tests__/schema-printer-test.ts | 6 + src/type/__tests__/introspection-test.ts | 106 ++++++++++ src/type/__tests__/validation-test.ts | 43 +++- src/type/definition.ts | 4 + src/type/directives.ts | 12 ++ src/type/index.ts | 1 + src/type/introspection.ts | 8 + src/type/validate.ts | 24 +++ .../__tests__/buildASTSchema-test.ts | 13 +- .../__tests__/buildClientSchema-test.ts | 15 ++ .../__tests__/coerceInputValue-test.ts | 105 ++++++++++ .../__tests__/findBreakingChanges-test.ts | 2 + .../__tests__/getIntrospectionQuery-test.ts | 8 + src/utilities/__tests__/printSchema-test.ts | 23 +++ src/utilities/__tests__/valueFromAST-test.ts | 24 +++ src/utilities/buildClientSchema.ts | 1 + src/utilities/coerceInputValue.ts | 24 +++ src/utilities/extendSchema.ts | 9 + src/utilities/getIntrospectionQuery.ts | 10 + src/utilities/introspectionFromSchema.ts | 1 + src/utilities/printSchema.ts | 7 +- src/utilities/valueFromAST.ts | 12 ++ .../__tests__/ValuesOfCorrectTypeRule-test.ts | 86 ++++++++ src/validation/__tests__/harness.ts | 6 + .../rules/ValuesOfCorrectTypeRule.ts | 79 +++++++- 28 files changed, 814 insertions(+), 7 deletions(-) create mode 100644 src/execution/__tests__/oneof-test.ts diff --git a/src/__testUtils__/kitchenSinkSDL.ts b/src/__testUtils__/kitchenSinkSDL.ts index cdf2f9afce..7b7a537783 100644 --- a/src/__testUtils__/kitchenSinkSDL.ts +++ b/src/__testUtils__/kitchenSinkSDL.ts @@ -27,6 +27,7 @@ type Foo implements Bar & Baz & Two { five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type seven(argument: Int = null): Type + eight(argument: OneOfInputType): Type } type AnnotatedObject @onObject(arg: "value") { @@ -116,6 +117,11 @@ input InputType { answer: Int = 42 } +input OneOfInputType @oneOf { + string: String + int: Int +} + input AnnotatedInput @onInputObject { annotatedField: Type @onInputFieldDefinition } diff --git a/src/execution/__tests__/oneof-test.ts b/src/execution/__tests__/oneof-test.ts new file mode 100644 index 0000000000..82965afc24 --- /dev/null +++ b/src/execution/__tests__/oneof-test.ts @@ -0,0 +1,185 @@ +import { describe, it } from 'mocha'; + +import { expectJSON } from '../../__testUtils__/expectJSON'; + +import { parse } from '../../language/parser'; + +import { buildSchema } from '../../utilities/buildASTSchema'; + +import type { ExecutionResult } from '../execute'; +import { execute } from '../execute'; + +const schema = buildSchema(` + type Query { + test(input: TestInputObject!): TestObject + } + + input TestInputObject @oneOf { + a: String + b: Int + } + + type TestObject { + a: String + b: Int + } +`); + +function executeQuery( + query: string, + rootValue: unknown, + variableValues?: { [variable: string]: unknown }, +): ExecutionResult | Promise { + return execute({ schema, document: parse(query), rootValue, variableValues }); +} + +describe('Execute: Handles OneOf Input Objects', () => { + describe('OneOf Input Objects', () => { + const rootValue = { + test({ input }: { input: { a?: string; b?: number } }) { + return input; + }, + }; + + it('accepts a good default value', () => { + const query = ` + query ($input: TestInputObject! = {a: "abc"}) { + test(input: $input) { + a + b + } + } + `; + const result = executeQuery(query, rootValue); + + expectJSON(result).toDeepEqual({ + data: { + test: { + a: 'abc', + b: null, + }, + }, + }); + }); + + it('rejects a bad default value', () => { + const query = ` + query ($input: TestInputObject! = {a: "abc", b: 123}) { + test(input: $input) { + a + b + } + } + `; + const result = executeQuery(query, rootValue); + + expectJSON(result).toDeepEqual({ + data: { + test: null, + }, + errors: [ + { + locations: [{ column: 23, line: 3 }], + message: + // This type of error would be caught at validation-time + // hence the vague error message here. + 'Argument "input" of non-null type "TestInputObject!" must not be null.', + path: ['test'], + }, + ], + }); + }); + + it('accepts a good variable', () => { + const query = ` + query ($input: TestInputObject!) { + test(input: $input) { + a + b + } + } + `; + const result = executeQuery(query, rootValue, { input: { a: 'abc' } }); + + expectJSON(result).toDeepEqual({ + data: { + test: { + a: 'abc', + b: null, + }, + }, + }); + }); + + it('accepts a good variable with an undefined key', () => { + const query = ` + query ($input: TestInputObject!) { + test(input: $input) { + a + b + } + } + `; + const result = executeQuery(query, rootValue, { + input: { a: 'abc', b: undefined }, + }); + + expectJSON(result).toDeepEqual({ + data: { + test: { + a: 'abc', + b: null, + }, + }, + }); + }); + + it('rejects a variable with multiple non-null keys', () => { + const query = ` + query ($input: TestInputObject!) { + test(input: $input) { + a + b + } + } + `; + const result = executeQuery(query, rootValue, { + input: { a: 'abc', b: 123 }, + }); + + expectJSON(result).toDeepEqual({ + errors: [ + { + locations: [{ column: 16, line: 2 }], + message: + 'Variable "$input" got invalid value { a: "abc", b: 123 }; Exactly one key must be specified for OneOf type "TestInputObject".', + }, + ], + }); + }); + + it('rejects a variable with multiple nullable keys', () => { + const query = ` + query ($input: TestInputObject!) { + test(input: $input) { + a + b + } + } + `; + const result = executeQuery(query, rootValue, { + input: { a: 'abc', b: null }, + }); + + expectJSON(result).toDeepEqual({ + errors: [ + { + locations: [{ column: 16, line: 2 }], + message: + 'Variable "$input" got invalid value { a: "abc", b: null }; Exactly one key must be specified for OneOf type "TestInputObject".', + }, + ], + }); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index 6185d9f444..877939d879 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,6 +64,7 @@ export { GraphQLSkipDirective, GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, // "Enum" of Type Kinds TypeKind, // Constant Deprecation Reason diff --git a/src/language/__tests__/schema-printer-test.ts b/src/language/__tests__/schema-printer-test.ts index 49a32693ff..41cf6c5419 100644 --- a/src/language/__tests__/schema-printer-test.ts +++ b/src/language/__tests__/schema-printer-test.ts @@ -61,6 +61,7 @@ describe('Printer: SDL document', () => { five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type seven(argument: Int = null): Type + eight(argument: OneOfInputType): Type } type AnnotatedObject @onObject(arg: "value") { @@ -143,6 +144,11 @@ describe('Printer: SDL document', () => { answer: Int = 42 } + input OneOfInputType @oneOf { + string: String + int: Int + } + input AnnotatedInput @onInputObject { annotatedField: Type @onInputFieldDefinition } diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 741d9c0076..29994c76ed 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -372,6 +372,17 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'isOneOf', + args: [], + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + isDeprecated: false, + deprecationReason: null, + }, ], inputFields: null, interfaces: [], @@ -989,6 +1000,12 @@ describe('Introspection', () => { }, ], }, + { + name: 'oneOf', + isRepeatable: false, + locations: ['INPUT_OBJECT'], + args: [], + }, ], }, }, @@ -1519,6 +1536,95 @@ describe('Introspection', () => { }); }); + it('identifies oneOf for input objects', () => { + const schema = buildSchema(` + input SomeInputObject @oneOf { + a: String + } + + input AnotherInputObject { + a: String + b: String + } + + type Query { + someField(someArg: SomeInputObject): String + anotherField(anotherArg: AnotherInputObject): String + } + `); + + const source = ` + { + oneOfInputObject: __type(name: "SomeInputObject") { + isOneOf + } + inputObject: __type(name: "AnotherInputObject") { + isOneOf + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + oneOfInputObject: { + isOneOf: true, + }, + inputObject: { + isOneOf: false, + }, + }, + }); + }); + + it('returns null for oneOf for other types', () => { + const schema = buildSchema(` + type SomeObject implements SomeInterface { + fieldA: String + } + enum SomeEnum { + SomeObject + } + interface SomeInterface { + fieldA: String + } + union SomeUnion = SomeObject + type Query { + someField(enum: SomeEnum): SomeUnion + anotherField(enum: SomeEnum): SomeInterface + } + `); + + const source = ` + { + object: __type(name: "SomeObject") { + isOneOf + } + enum: __type(name: "SomeEnum") { + isOneOf + } + interface: __type(name: "SomeInterface") { + isOneOf + } + scalar: __type(name: "String") { + isOneOf + } + union: __type(name: "SomeUnion") { + isOneOf + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + object: { isOneOf: null }, + enum: { isOneOf: null }, + interface: { isOneOf: null }, + scalar: { isOneOf: null }, + union: { isOneOf: null }, + }, + }); + }); + it('fails as expected on the __type root field without an arg', () => { const schema = buildSchema(` type Query { diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index 629e0b8c3c..c7e3a15449 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -336,7 +336,7 @@ describe('Type System: A Schema must have Object root types', () => { ]); }); - it('rejects a schema extended with invalid root types', () => { + it('rejects a Schema extended with invalid root types', () => { let schema = buildSchema(` input SomeInputObject { test: String @@ -1663,6 +1663,47 @@ describe('Type System: Input Object fields must have input types', () => { }); }); +describe('Type System: OneOf Input Object fields must be nullable', () => { + it('rejects non-nullable fields', () => { + const schema = buildSchema(` + type Query { + test(arg: SomeInputObject): String + } + + input SomeInputObject @oneOf { + a: String + b: String! + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: 'OneOf input field SomeInputObject.b must be nullable.', + locations: [{ line: 8, column: 12 }], + }, + ]); + }); + + it('rejects fields with default values', () => { + const schema = buildSchema(` + type Query { + test(arg: SomeInputObject): String + } + + input SomeInputObject @oneOf { + a: String + b: String = "foo" + } + `); + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'OneOf input field SomeInputObject.b cannot have a default value.', + locations: [{ line: 8, column: 9 }], + }, + ]); + }); +}); + describe('Objects must adhere to Interface they implement', () => { it('accepts an Object which implements an Interface', () => { const schema = buildSchema(` diff --git a/src/type/definition.ts b/src/type/definition.ts index d9192c723a..32159a39a5 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -1611,6 +1611,7 @@ export class GraphQLInputObjectType { extensions: Readonly; astNode: Maybe; extensionASTNodes: ReadonlyArray; + isOneOf: boolean; private _fields: ThunkObjMap; @@ -1620,6 +1621,7 @@ export class GraphQLInputObjectType { this.extensions = toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; + this.isOneOf = config.isOneOf ?? false; this._fields = defineInputFieldMap.bind(undefined, config); } @@ -1652,6 +1654,7 @@ export class GraphQLInputObjectType { extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, + isOneOf: this.isOneOf, }; } @@ -1697,6 +1700,7 @@ export interface GraphQLInputObjectTypeConfig { extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; + isOneOf?: boolean; } interface GraphQLInputObjectTypeNormalizedConfig diff --git a/src/type/directives.ts b/src/type/directives.ts index bb3e441a43..6881f20532 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -209,6 +209,17 @@ export const GraphQLSpecifiedByDirective: GraphQLDirective = }, }); +/** + * Used to indicate an Input Object is a OneOf Input Object. + */ +export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({ + name: 'oneOf', + description: + 'Indicates exactly one field must be supplied and this field must not be `null`.', + locations: [DirectiveLocation.INPUT_OBJECT], + args: {}, +}); + /** * The full list of specified directives. */ @@ -218,6 +229,7 @@ export const specifiedDirectives: ReadonlyArray = GraphQLSkipDirective, GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, ]); export function isSpecifiedDirective(directive: GraphQLDirective): boolean { diff --git a/src/type/index.ts b/src/type/index.ts index 270dd67d35..cf276d1e02 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -133,6 +133,7 @@ export { GraphQLSkipDirective, GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, // Constant Deprecation Reason DEFAULT_DEPRECATION_REASON, } from './directives'; diff --git a/src/type/introspection.ts b/src/type/introspection.ts index f5e4b07ea7..2c66ca5098 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -323,6 +323,14 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ type: __Type, resolve: (type) => ('ofType' in type ? type.ofType : undefined), }, + isOneOf: { + type: GraphQLBoolean, + resolve: (type) => { + if (isInputObjectType(type)) { + return type.isOneOf; + } + }, + }, } as GraphQLFieldConfigMap), }); diff --git a/src/type/validate.ts b/src/type/validate.ts index 126e97d980..94ef92d05b 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -531,6 +531,30 @@ function validateInputFields( [getDeprecatedDirectiveNode(field.astNode), field.astNode?.type], ); } + + if (inputObj.isOneOf) { + validateOneOfInputObjectField(inputObj, field, context); + } + } +} + +function validateOneOfInputObjectField( + type: GraphQLInputObjectType, + field: GraphQLInputField, + context: SchemaValidationContext, +): void { + if (isNonNullType(field.type)) { + context.reportError( + `OneOf input field ${type.name}.${field.name} must be nullable.`, + field.astNode?.type, + ); + } + + if (field.defaultValue !== undefined) { + context.reportError( + `OneOf input field ${type.name}.${field.name} cannot have a default value.`, + field.astNode, + ); } } diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 7427ebb507..29280474ec 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -23,6 +23,7 @@ import { assertDirective, GraphQLDeprecatedDirective, GraphQLIncludeDirective, + GraphQLOneOfDirective, GraphQLSkipDirective, GraphQLSpecifiedByDirective, } from '../../type/directives'; @@ -222,7 +223,7 @@ describe('Schema Builder', () => { it('Maintains @include, @skip & @specifiedBy', () => { const schema = buildSchema('type Query'); - expect(schema.getDirectives()).to.have.lengthOf(4); + expect(schema.getDirectives()).to.have.lengthOf(5); expect(schema.getDirective('skip')).to.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.equal(GraphQLIncludeDirective); expect(schema.getDirective('deprecated')).to.equal( @@ -231,6 +232,7 @@ describe('Schema Builder', () => { expect(schema.getDirective('specifiedBy')).to.equal( GraphQLSpecifiedByDirective, ); + expect(schema.getDirective('oneOf')).to.equal(GraphQLOneOfDirective); }); it('Overriding directives excludes specified', () => { @@ -239,9 +241,10 @@ describe('Schema Builder', () => { directive @include on FIELD directive @deprecated on FIELD_DEFINITION directive @specifiedBy on FIELD_DEFINITION + directive @oneOf on OBJECT `); - expect(schema.getDirectives()).to.have.lengthOf(4); + expect(schema.getDirectives()).to.have.lengthOf(5); expect(schema.getDirective('skip')).to.not.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.not.equal( GraphQLIncludeDirective, @@ -252,18 +255,20 @@ describe('Schema Builder', () => { expect(schema.getDirective('specifiedBy')).to.not.equal( GraphQLSpecifiedByDirective, ); + expect(schema.getDirective('oneOf')).to.not.equal(GraphQLOneOfDirective); }); - it('Adding directives maintains @include, @skip & @specifiedBy', () => { + it('Adding directives maintains @include, @skip, @deprecated, @specifiedBy, and @oneOf', () => { const schema = buildSchema(` directive @foo(arg: Int) on FIELD `); - expect(schema.getDirectives()).to.have.lengthOf(5); + expect(schema.getDirectives()).to.have.lengthOf(6); expect(schema.getDirective('skip')).to.not.equal(undefined); expect(schema.getDirective('include')).to.not.equal(undefined); expect(schema.getDirective('deprecated')).to.not.equal(undefined); expect(schema.getDirective('specifiedBy')).to.not.equal(undefined); + expect(schema.getDirective('oneOf')).to.not.equal(undefined); }); it('Type modifiers', () => { diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index c6c619be42..d106d63a38 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -573,6 +573,21 @@ describe('Type System: build schema from introspection', () => { expect(cycleIntrospection(sdl)).to.equal(sdl); }); + it('builds a schema with @oneOf directive', () => { + const sdl = dedent` + type Query { + someField(someArg: SomeInputObject): String + } + + input SomeInputObject @oneOf { + someInputField1: String + someInputField2: String + } + `; + + expect(cycleIntrospection(sdl)).to.equal(sdl); + }); + it('can use client schema for limited execution', () => { const schema = buildSchema(` scalar CustomScalar diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index a73cdc6116..3692ce26b7 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -273,6 +273,111 @@ describe('coerceInputValue', () => { }); }); + describe('for GraphQLInputObject that isOneOf', () => { + const TestInputObject = new GraphQLInputObjectType({ + name: 'TestInputObject', + fields: { + foo: { type: GraphQLInt }, + bar: { type: GraphQLInt }, + }, + isOneOf: true, + }); + + it('returns no error for a valid input', () => { + const result = coerceValue({ foo: 123 }, TestInputObject); + expectValue(result).to.deep.equal({ foo: 123 }); + }); + + it('returns an error if more than one field is specified', () => { + const result = coerceValue({ foo: 123, bar: null }, TestInputObject); + expectErrors(result).to.deep.equal([ + { + error: + 'Exactly one key must be specified for OneOf type "TestInputObject".', + path: [], + value: { foo: 123, bar: null }, + }, + ]); + }); + + it('returns an error the one field is null', () => { + const result = coerceValue({ bar: null }, TestInputObject); + expectErrors(result).to.deep.equal([ + { + error: 'Field "bar" must be non-null.', + path: ['bar'], + value: null, + }, + ]); + }); + + it('returns an error for an invalid field', () => { + const result = coerceValue({ foo: NaN }, TestInputObject); + expectErrors(result).to.deep.equal([ + { + error: 'Int cannot represent non-integer value: NaN', + path: ['foo'], + value: NaN, + }, + ]); + }); + + it('returns multiple errors for multiple invalid fields', () => { + const result = coerceValue({ foo: 'abc', bar: 'def' }, TestInputObject); + expectErrors(result).to.deep.equal([ + { + error: 'Int cannot represent non-integer value: "abc"', + path: ['foo'], + value: 'abc', + }, + { + error: 'Int cannot represent non-integer value: "def"', + path: ['bar'], + value: 'def', + }, + { + error: + 'Exactly one key must be specified for OneOf type "TestInputObject".', + path: [], + value: { foo: 'abc', bar: 'def' }, + }, + ]); + }); + + it('returns error for an unknown field', () => { + const result = coerceValue( + { foo: 123, unknownField: 123 }, + TestInputObject, + ); + expectErrors(result).to.deep.equal([ + { + error: + 'Field "unknownField" is not defined by type "TestInputObject".', + path: [], + value: { foo: 123, unknownField: 123 }, + }, + ]); + }); + + it('returns error for a misspelled field', () => { + const result = coerceValue({ bart: 123 }, TestInputObject); + expectErrors(result).to.deep.equal([ + { + error: + 'Field "bart" is not defined by type "TestInputObject". Did you mean "bar"?', + path: [], + value: { bart: 123 }, + }, + { + error: + 'Exactly one key must be specified for OneOf type "TestInputObject".', + path: [], + value: { bart: 123 }, + }, + ]); + }); + }); + describe('for GraphQLInputObject with default value', () => { const makeTestInputObject = (defaultValue: any) => new GraphQLInputObjectType({ diff --git a/src/utilities/__tests__/findBreakingChanges-test.ts b/src/utilities/__tests__/findBreakingChanges-test.ts index 5a7956ae5d..ba526deb48 100644 --- a/src/utilities/__tests__/findBreakingChanges-test.ts +++ b/src/utilities/__tests__/findBreakingChanges-test.ts @@ -4,6 +4,7 @@ import { describe, it } from 'mocha'; import { GraphQLDeprecatedDirective, GraphQLIncludeDirective, + GraphQLOneOfDirective, GraphQLSkipDirective, GraphQLSpecifiedByDirective, } from '../../type/directives'; @@ -802,6 +803,7 @@ describe('findBreakingChanges', () => { GraphQLSkipDirective, GraphQLIncludeDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, ], }); diff --git a/src/utilities/__tests__/getIntrospectionQuery-test.ts b/src/utilities/__tests__/getIntrospectionQuery-test.ts index e2f5595b3c..86d1c549db 100644 --- a/src/utilities/__tests__/getIntrospectionQuery-test.ts +++ b/src/utilities/__tests__/getIntrospectionQuery-test.ts @@ -117,6 +117,14 @@ describe('getIntrospectionQuery', () => { ); }); + it('include "isOneOf" field on input objects', () => { + expectIntrospectionQuery().toNotMatch('isOneOf'); + + expectIntrospectionQuery({ oneOf: true }).toMatch('isOneOf', 1); + + expectIntrospectionQuery({ oneOf: false }).toNotMatch('isOneOf'); + }); + it('include deprecated input field and args', () => { expectIntrospectionQuery().toMatch('includeDeprecated: true', 2); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index d09153a2e6..37af4a60f7 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -503,6 +503,23 @@ describe('Type System Printer', () => { `); }); + it('Print Input Type with @oneOf directive', () => { + const InputType = new GraphQLInputObjectType({ + name: 'InputType', + isOneOf: true, + fields: { + int: { type: GraphQLInt }, + }, + }); + + const schema = new GraphQLSchema({ types: [InputType] }); + expectPrintedSchema(schema).to.equal(dedent` + input InputType @oneOf { + int: Int + } + `); + }); + it('Custom Scalar', () => { const OddType = new GraphQLScalarType({ name: 'Odd' }); @@ -672,6 +689,11 @@ describe('Type System Printer', () => { url: String! ) on SCALAR + """ + Indicates exactly one field must be supplied and this field must not be \`null\`. + """ + directive @oneOf on INPUT_OBJECT + """ A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. """ @@ -714,6 +736,7 @@ describe('Type System Printer', () => { enumValues(includeDeprecated: Boolean = false): [__EnumValue!] inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type + isOneOf: Boolean } """An enum describing what kind of type a given \`__Type\` is.""" diff --git a/src/utilities/__tests__/valueFromAST-test.ts b/src/utilities/__tests__/valueFromAST-test.ts index a01ed95772..05924f3c56 100644 --- a/src/utilities/__tests__/valueFromAST-test.ts +++ b/src/utilities/__tests__/valueFromAST-test.ts @@ -196,6 +196,14 @@ describe('valueFromAST', () => { requiredBool: { type: nonNullBool }, }, }); + const testOneOfInputObj = new GraphQLInputObjectType({ + name: 'TestOneOfInput', + fields: { + a: { type: GraphQLString }, + b: { type: GraphQLString }, + }, + isOneOf: true, + }); it('coerces input objects according to input coercion rules', () => { expectValueFrom('null', testInputObj).to.equal(null); @@ -221,6 +229,22 @@ describe('valueFromAST', () => { ); expectValueFrom('{ requiredBool: null }', testInputObj).to.equal(undefined); expectValueFrom('{ bool: true }', testInputObj).to.equal(undefined); + expectValueFrom('{ a: "abc" }', testOneOfInputObj).to.deep.equal({ + a: 'abc', + }); + expectValueFrom('{ b: "def" }', testOneOfInputObj).to.deep.equal({ + b: 'def', + }); + expectValueFrom('{ a: "abc", b: null }', testOneOfInputObj).to.deep.equal( + undefined, + ); + expectValueFrom('{ a: null }', testOneOfInputObj).to.equal(undefined); + expectValueFrom('{ a: 1 }', testOneOfInputObj).to.equal(undefined); + expectValueFrom('{ a: "abc", b: "def" }', testOneOfInputObj).to.equal( + undefined, + ); + expectValueFrom('{}', testOneOfInputObj).to.equal(undefined); + expectValueFrom('{ c: "abc" }', testOneOfInputObj).to.equal(undefined); }); it('accepts variable values assuming already coerced', () => { diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 18e6110b2b..83f6abada8 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -304,6 +304,7 @@ export function buildClientSchema( name: inputObjectIntrospection.name, description: inputObjectIntrospection.description, fields: () => buildInputValueDefMap(inputObjectIntrospection.inputFields), + isOneOf: inputObjectIntrospection.isOneOf, }); } diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 136bee63c9..eeab0614f8 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -142,6 +142,30 @@ function coerceInputValueImpl( ); } } + + if (type.isOneOf) { + const keys = Object.keys(coercedValue); + if (keys.length !== 1) { + onError( + pathToArray(path), + inputValue, + new GraphQLError( + `Exactly one key must be specified for OneOf type "${type.name}".`, + ), + ); + } + + const key = keys[0]; + const value = coercedValue[key]; + if (value === null) { + onError( + pathToArray(path).concat(key), + value, + new GraphQLError(`Field "${key}" must be non-null.`), + ); + } + } + return coercedValue; } diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 998847e9f1..d53752d919 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -66,6 +66,7 @@ import { import { GraphQLDeprecatedDirective, GraphQLDirective, + GraphQLOneOfDirective, GraphQLSpecifiedByDirective, } from '../type/directives'; import { introspectionTypes, isIntrospectionType } from '../type/introspection'; @@ -646,6 +647,7 @@ export function extendSchemaImpl( fields: () => buildInputFieldMap(allNodes), astNode, extensionASTNodes, + isOneOf: isOneOf(astNode), }); } } @@ -682,3 +684,10 @@ function getSpecifiedByURL( // @ts-expect-error validated by `getDirectiveValues` return specifiedBy?.url; } + +/** + * Given an input object node, returns if the node should be OneOf. + */ +function isOneOf(node: InputObjectTypeDefinitionNode): boolean { + return Boolean(getDirectiveValues(GraphQLOneOfDirective, node)); +} diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index b58fb083e4..c502f0f7b4 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -32,6 +32,12 @@ export interface IntrospectionOptions { * Default: false */ inputValueDeprecation?: boolean; + + /** + * Whether target GraphQL server supports `@oneOf` input objects. + * Default: false + */ + oneOf?: boolean; } /** @@ -45,6 +51,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { directiveIsRepeatable: false, schemaDescription: false, inputValueDeprecation: false, + oneOf: false, ...options, }; @@ -62,6 +69,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { function inputDeprecation(str: string) { return optionsWithDefault.inputValueDeprecation ? str : ''; } + const oneOf = optionsWithDefault.oneOf ? 'isOneOf' : ''; return ` query IntrospectionQuery { @@ -90,6 +98,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { name ${descriptions} ${specifiedByUrl} + ${oneOf} fields(includeDeprecated: true) { name ${descriptions} @@ -259,6 +268,7 @@ export interface IntrospectionInputObjectType { readonly name: string; readonly description?: Maybe; readonly inputFields: ReadonlyArray; + readonly isOneOf: boolean; } export interface IntrospectionListTypeRef< diff --git a/src/utilities/introspectionFromSchema.ts b/src/utilities/introspectionFromSchema.ts index 5b933c2191..5f58248363 100644 --- a/src/utilities/introspectionFromSchema.ts +++ b/src/utilities/introspectionFromSchema.ts @@ -30,6 +30,7 @@ export function introspectionFromSchema( directiveIsRepeatable: true, schemaDescription: true, inputValueDeprecation: true, + oneOf: true, ...options, }; diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 83859feee8..edac6262c5 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -209,7 +209,12 @@ function printInputObject(type: GraphQLInputObjectType): string { const fields = Object.values(type.getFields()).map( (f, i) => printDescription(f, ' ', !i) + ' ' + printInputValue(f), ); - return printDescription(type) + `input ${type.name}` + printBlock(fields); + return ( + printDescription(type) + + `input ${type.name}` + + (type.isOneOf ? ' @oneOf' : '') + + printBlock(fields) + ); } function printFields(type: GraphQLObjectType | GraphQLInterfaceType): string { diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts index 4f0cee6b29..2e6cc1c613 100644 --- a/src/utilities/valueFromAST.ts +++ b/src/utilities/valueFromAST.ts @@ -125,6 +125,18 @@ export function valueFromAST( } coercedObj[field.name] = fieldValue; } + + if (type.isOneOf) { + const keys = Object.keys(coercedObj); + if (keys.length !== 1) { + return; // Invalid: not exactly one key, intentionally return no value. + } + + if (coercedObj[keys[0]] === null) { + return; // Invalid: value not non-null, intentionally return no value. + } + } + return coercedObj; } diff --git a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts index 76b03035da..3610fa648b 100644 --- a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts +++ b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts @@ -874,6 +874,28 @@ describe('Validate: Values of correct type', () => { }); }); + describe('Valid oneOf input object value', () => { + it('Exactly one field', () => { + expectValid(` + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: "abc" }) + } + } + `); + }); + + it('Exactly one non-nullable variable', () => { + expectValid(` + query ($string: String!) { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $string }) + } + } + `); + }); + }); + describe('Invalid input object value', () => { it('Partial object, missing required', () => { expectErrors(` @@ -1041,6 +1063,70 @@ describe('Validate: Values of correct type', () => { }); }); + describe('Invalid oneOf input object value', () => { + it('Invalid field type', () => { + expectErrors(` + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: 2 }) + } + } + `).toDeepEqual([ + { + message: 'String cannot represent a non string value: 2', + locations: [{ line: 4, column: 52 }], + }, + ]); + }); + + it('Exactly one null field', () => { + expectErrors(` + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: null }) + } + } + `).toDeepEqual([ + { + message: 'Field "OneOfInput.stringField" must be non-null.', + locations: [{ line: 4, column: 37 }], + }, + ]); + }); + + it('Exactly one nullable variable', () => { + expectErrors(` + query ($string: String) { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $string }) + } + } + `).toDeepEqual([ + { + message: + 'Variable "string" must be non-nullable to be used for OneOf Input Object "OneOfInput".', + locations: [{ line: 4, column: 37 }], + }, + ]); + }); + + it('More than one field', () => { + expectErrors(` + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: "abc", intField: 123 }) + } + } + `).toDeepEqual([ + { + message: + 'OneOf Input Object "OneOfInput" must specify exactly one key.', + locations: [{ line: 4, column: 37 }], + }, + ]); + }); + }); + describe('Directive arguments', () => { it('with directives of valid types', () => { expectValid(` diff --git a/src/validation/__tests__/harness.ts b/src/validation/__tests__/harness.ts index 661256c56d..ea4840341c 100644 --- a/src/validation/__tests__/harness.ts +++ b/src/validation/__tests__/harness.ts @@ -79,6 +79,11 @@ export const testSchema: GraphQLSchema = buildSchema(` stringListField: [String] } + input OneOfInput @oneOf { + stringField: String + intField: Int + } + type ComplicatedArgs { # TODO List # TODO Coercion @@ -93,6 +98,7 @@ export const testSchema: GraphQLSchema = buildSchema(` stringListArgField(stringListArg: [String]): String stringListNonNullArgField(stringListNonNullArg: [String!]): String complexArgField(complexArg: ComplexInput): String + oneOfArgField(oneOfArg: OneOfInput): String multipleReqs(req1: Int!, req2: Int!): String nonNullFieldWithDefault(arg: Int! = 0): String multipleOpts(opt1: Int = 0, opt2: Int = 0): String diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.ts b/src/validation/rules/ValuesOfCorrectTypeRule.ts index 5d81a3833a..3f284d7103 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.ts +++ b/src/validation/rules/ValuesOfCorrectTypeRule.ts @@ -1,14 +1,22 @@ import { didYouMean } from '../../jsutils/didYouMean'; import { inspect } from '../../jsutils/inspect'; import { keyMap } from '../../jsutils/keyMap'; +import type { ObjMap } from '../../jsutils/ObjMap'; import { suggestionList } from '../../jsutils/suggestionList'; import { GraphQLError } from '../../error/GraphQLError'; -import type { ValueNode } from '../../language/ast'; +import type { + ObjectFieldNode, + ObjectValueNode, + ValueNode, + VariableDefinitionNode, +} from '../../language/ast'; +import { Kind } from '../../language/kinds'; import { print } from '../../language/printer'; import type { ASTVisitor } from '../../language/visitor'; +import type { GraphQLInputObjectType } from '../../type/definition'; import { getNamedType, getNullableType, @@ -32,7 +40,17 @@ import type { ValidationContext } from '../ValidationContext'; export function ValuesOfCorrectTypeRule( context: ValidationContext, ): ASTVisitor { + let variableDefinitions: { [key: string]: VariableDefinitionNode } = {}; + return { + OperationDefinition: { + enter() { + variableDefinitions = {}; + }, + }, + VariableDefinition(definition) { + variableDefinitions[definition.variable.name.value] = definition; + }, ListValue(node) { // Note: TypeInfo will traverse into a list's item type, so look to the // parent input type to check if it is a list. @@ -62,6 +80,16 @@ export function ValuesOfCorrectTypeRule( ); } } + + if (type.isOneOf) { + validateOneOfInputObject( + context, + node, + type, + fieldNodeMap, + variableDefinitions, + ); + } }, ObjectField(node) { const parentType = getNamedType(context.getParentInputType()); @@ -151,3 +179,52 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void { } } } + +function validateOneOfInputObject( + context: ValidationContext, + node: ObjectValueNode, + type: GraphQLInputObjectType, + fieldNodeMap: ObjMap, + variableDefinitions: { [key: string]: VariableDefinitionNode }, +): void { + const keys = Object.keys(fieldNodeMap); + const isNotExactlyOneField = keys.length !== 1; + + if (isNotExactlyOneField) { + context.reportError( + new GraphQLError( + `OneOf Input Object "${type.name}" must specify exactly one key.`, + { nodes: [node] }, + ), + ); + return; + } + + const value = fieldNodeMap[keys[0]]?.value; + const isNullLiteral = !value || value.kind === Kind.NULL; + const isVariable = value?.kind === Kind.VARIABLE; + + if (isNullLiteral) { + context.reportError( + new GraphQLError(`Field "${type.name}.${keys[0]}" must be non-null.`, { + nodes: [node], + }), + ); + return; + } + + if (isVariable) { + const variableName = value.name.value; + const definition = variableDefinitions[variableName]; + const isNullableVariable = definition.type.kind !== Kind.NON_NULL_TYPE; + + if (isNullableVariable) { + context.reportError( + new GraphQLError( + `Variable "${variableName}" must be non-nullable to be used for OneOf Input Object "${type.name}".`, + { nodes: [node] }, + ), + ); + } + } +} From 6a1614cbf22f4036150d4354fc200c368a29939f Mon Sep 17 00:00:00 2001 From: Benjie Date: Fri, 21 Jun 2024 15:49:17 +0100 Subject: [PATCH 25/29] backport[v16]: Enable passing values configuration to GraphQLEnumType as a thunk (#4122) --- src/type/__tests__/enumType-test.ts | 25 +++++++++++++++++++++ src/type/definition.ts | 34 +++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index 35e9f94b00..d5cfadc635 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -31,6 +31,14 @@ const ComplexEnum = new GraphQLEnumType({ }, }); +const ThunkValuesEnum = new GraphQLEnumType({ + name: 'ThunkValues', + values: () => ({ + A: { value: 'a' }, + B: { value: 'b' }, + }), +}); + const QueryType = new GraphQLObjectType({ name: 'Query', fields: { @@ -84,6 +92,15 @@ const QueryType = new GraphQLObjectType({ return fromEnum; }, }, + thunkValuesString: { + type: GraphQLString, + args: { + fromEnum: { type: ThunkValuesEnum }, + }, + resolve(_source, { fromEnum }) { + return fromEnum; + }, + }, }, }); @@ -400,6 +417,14 @@ describe('Type System: Enum Values', () => { }); }); + it('may have values specified via a callback', () => { + const result = executeQuery('{ thunkValuesString(fromEnum: B) }'); + + expect(result).to.deep.equal({ + data: { thunkValuesString: 'b' }, + }); + }); + it('can be introspected without error', () => { expect(() => introspectionFromSchema(schema)).to.not.throw(); }); diff --git a/src/type/definition.ts b/src/type/definition.ts index 32159a39a5..7eaac560dc 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -1372,9 +1372,12 @@ export class GraphQLEnumType /* */ { astNode: Maybe; extensionASTNodes: ReadonlyArray; - private _values: ReadonlyArray */>; - private _valueLookup: ReadonlyMap; - private _nameLookup: ObjMap; + private _values: + | ReadonlyArray */> + | (() => GraphQLEnumValueConfigMap); + + private _valueLookup: ReadonlyMap | null; + private _nameLookup: ObjMap | null; constructor(config: Readonly */>) { this.name = assertName(config.name); @@ -1383,11 +1386,12 @@ export class GraphQLEnumType /* */ { this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; - this._values = defineEnumValues(this.name, config.values); - this._valueLookup = new Map( - this._values.map((enumValue) => [enumValue.value, enumValue]), - ); - this._nameLookup = keyMap(this._values, (value) => value.name); + this._values = + typeof config.values === 'function' + ? config.values + : defineEnumValues(this.name, config.values); + this._valueLookup = null; + this._nameLookup = null; } get [Symbol.toStringTag]() { @@ -1395,14 +1399,25 @@ export class GraphQLEnumType /* */ { } getValues(): ReadonlyArray */> { + if (typeof this._values === 'function') { + this._values = defineEnumValues(this.name, this._values()); + } return this._values; } getValue(name: string): Maybe { + if (this._nameLookup === null) { + this._nameLookup = keyMap(this.getValues(), (value) => value.name); + } return this._nameLookup[name]; } serialize(outputValue: unknown /* T */): Maybe { + if (this._valueLookup === null) { + this._valueLookup = new Map( + this.getValues().map((enumValue) => [enumValue.value, enumValue]), + ); + } const enumValue = this._valueLookup.get(outputValue); if (enumValue === undefined) { throw new GraphQLError( @@ -1527,13 +1542,14 @@ function defineEnumValues( export interface GraphQLEnumTypeConfig { name: string; description?: Maybe; - values: GraphQLEnumValueConfigMap /* */; + values: ThunkObjMap */>; extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; } interface GraphQLEnumTypeNormalizedConfig extends GraphQLEnumTypeConfig { + values: ObjMap */>; extensions: Readonly; extensionASTNodes: ReadonlyArray; } From 556a01e33ec2dad5529d7d6f84aab3f8f68f1f1d Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 21 Jun 2024 15:54:48 +0100 Subject: [PATCH 26/29] 16.9.0 --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74682729c6..f26f98ef19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "16.8.2", + "version": "16.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "16.8.2", + "version": "16.9.0", "license": "MIT", "devDependencies": { "@babel/core": "7.17.9", diff --git a/package.json b/package.json index 199d3c7045..85a6688a83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "16.8.2", + "version": "16.9.0", "description": "A Query Language and Runtime which can target any service.", "license": "MIT", "private": true, diff --git a/src/version.ts b/src/version.ts index 0ab817db6b..05919be704 100644 --- a/src/version.ts +++ b/src/version.ts @@ -4,14 +4,14 @@ /** * A string containing the version of the GraphQL.js library */ -export const version = '16.8.2' as string; +export const version = '16.9.0' as string; /** * An object containing the components of the GraphQL.js version string */ export const versionInfo = Object.freeze({ major: 16 as number, - minor: 8 as number, - patch: 2 as number, + minor: 9 as number, + patch: 0 as number, preReleaseTag: null as string | null, }); From 04cba8873e953ed316af5a77e60012bff0e26af7 Mon Sep 17 00:00:00 2001 From: Benjie Date: Mon, 1 Jul 2024 14:32:19 +0100 Subject: [PATCH 27/29] Upgrade codecov action and pass token (#4138) --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index adfa1c5bd7..fbfe8488d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,10 +144,11 @@ jobs: - name: Upload coverage to Codecov if: ${{ always() }} - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: file: ./coverage/coverage-final.json fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} test: name: Run tests on Node v${{ matrix.node_version_to_setup }} From e1ceb8f4032113d483b2d75f25c865a0034501e4 Mon Sep 17 00:00:00 2001 From: Benjie Date: Mon, 1 Jul 2024 16:30:57 +0100 Subject: [PATCH 28/29] Fix codecov workflow (#4139) --- .github/workflows/ci.yml | 8 ++++++-- .github/workflows/pull_request.yml | 2 ++ .github/workflows/push.yml | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbfe8488d7..e94e48af3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,9 @@ name: CI -on: workflow_call +on: + workflow_call: + secrets: + codecov_token: + required: true jobs: lint: name: Lint source files @@ -148,7 +152,7 @@ jobs: with: file: ./coverage/coverage-final.json fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.codecov_token }} test: name: Run tests on Node v${{ matrix.node_version_to_setup }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index f7e0f14bbb..2f62c4fb7d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,6 +3,8 @@ on: pull_request jobs: ci: uses: ./.github/workflows/ci.yml + secrets: + codecov_token: ${{ secrets.CODECOV_TOKEN }} diff-npm-package: name: Diff content of NPM package diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 30e42d1379..0df1bac4ec 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -3,6 +3,8 @@ on: push jobs: ci: uses: ./.github/workflows/ci.yml + secrets: + codecov_token: ${{ secrets.CODECOV_TOKEN }} deploy-to-npm-branch: name: Deploy to `npm` branch needs: ci From 9a91e338101b94fb1cc5669dd00e1ba15e0f21b3 Mon Sep 17 00:00:00 2001 From: Jeff Auriemma Date: Mon, 5 Aug 2024 14:59:08 -0400 Subject: [PATCH 29/29] Add GraphQLConf 2024 banner (#4157) Co-authored-by: Saihajpreet Singh --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 34753472f9..773635ff7b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![GraphQLConf 2024 Banner: September 10-12, San Francisco. Hosted by the GraphQL Foundation](https://github.com/user-attachments/assets/2d048502-e5b2-4e9d-a02a-50b841824de6)](https://graphql.org/conf/2024/?utm_source=github&utm_medium=graphql_js&utm_campaign=readme) + # GraphQL.js The JavaScript reference implementation for GraphQL, a query language for APIs created by Facebook.