diff --git a/src/error/GraphQLError.ts b/src/error/GraphQLError.ts index 7fec960883..2bb3ccb7cd 100644 --- a/src/error/GraphQLError.ts +++ b/src/error/GraphQLError.ts @@ -29,6 +29,14 @@ export interface GraphQLErrorOptions { extensions?: Maybe; } +const isGraphQLErrorSymbol = Symbol.for('GraphQLError'); + +export function isGraphQLError(error: unknown): error is GraphQLError { + return ( + typeof error === 'object' && error != null && isGraphQLErrorSymbol in error + ); +} + /** * A GraphQLError describes an Error found during the parse, validate, or * execute phases of performing a GraphQL operation. In addition to a message @@ -36,6 +44,7 @@ export interface GraphQLErrorOptions { * GraphQL document and/or execution result that correspond to the Error. */ export class GraphQLError extends Error { + [isGraphQLErrorSymbol]: true = true; /** * An array of `{ line, column }` locations within the source GraphQL document * which correspond to this error. diff --git a/src/jsutils/__tests__/instanceOf-test.ts b/src/jsutils/__tests__/instanceOf-test.ts deleted file mode 100644 index cbd649bfa8..0000000000 --- a/src/jsutils/__tests__/instanceOf-test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { instanceOf } from '../instanceOf'; - -describe('instanceOf', () => { - it('do not throw on values without prototype', () => { - class Foo { - get [Symbol.toStringTag]() { - return 'Foo'; - } - } - - expect(instanceOf(true, Foo)).to.equal(false); - expect(instanceOf(null, Foo)).to.equal(false); - expect(instanceOf(Object.create(null), Foo)).to.equal(false); - }); - - it('detect name clashes with older versions of this lib', () => { - function oldVersion() { - class Foo {} - return Foo; - } - - function newVersion() { - class Foo { - get [Symbol.toStringTag]() { - return 'Foo'; - } - } - return Foo; - } - - const NewClass = newVersion(); - const OldClass = oldVersion(); - expect(instanceOf(new NewClass(), NewClass)).to.equal(true); - expect(() => instanceOf(new OldClass(), NewClass)).to.throw(); - }); - - it('allows instances to have share the same constructor name', () => { - function getMinifiedClass(tag: string) { - class SomeNameAfterMinification { - get [Symbol.toStringTag]() { - return tag; - } - } - return SomeNameAfterMinification; - } - - const Foo = getMinifiedClass('Foo'); - const Bar = getMinifiedClass('Bar'); - expect(instanceOf(new Foo(), Bar)).to.equal(false); - expect(instanceOf(new Bar(), Foo)).to.equal(false); - - const DuplicateOfFoo = getMinifiedClass('Foo'); - expect(() => instanceOf(new DuplicateOfFoo(), Foo)).to.throw(); - expect(() => instanceOf(new Foo(), DuplicateOfFoo)).to.throw(); - }); - - it('fails with descriptive error message', () => { - function getFoo() { - class Foo { - get [Symbol.toStringTag]() { - return 'Foo'; - } - } - return Foo; - } - const Foo1 = getFoo(); - const Foo2 = getFoo(); - - expect(() => instanceOf(new Foo1(), Foo2)).to.throw( - /^Cannot use Foo "{}" from another module or realm./m, - ); - expect(() => instanceOf(new Foo2(), Foo1)).to.throw( - /^Cannot use Foo "{}" from another module or realm./m, - ); - }); -}); diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts deleted file mode 100644 index b917cfc3bf..0000000000 --- a/src/jsutils/instanceOf.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { inspect } from './inspect'; - -/** - * A replacement for instanceof which includes an error warning when multi-realm - * constructors are detected. - * See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production - * See: https://webpack.js.org/guides/production/ - */ -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' - ? function instanceOf(value: unknown, constructor: Constructor): boolean { - return value instanceof constructor; - } - : function instanceOf(value: unknown, constructor: Constructor): boolean { - if (value instanceof constructor) { - return true; - } - if (typeof value === 'object' && value !== null) { - // Prefer Symbol.toStringTag since it is immune to minification. - const className = constructor.prototype[Symbol.toStringTag]; - const valueClassName = - // We still need to support constructor's name to detect conflicts with older versions of this library. - Symbol.toStringTag in value - ? // @ts-expect-error TS bug see, https://github.com/microsoft/TypeScript/issues/38009 - value[Symbol.toStringTag] - : value.constructor?.name; - if (className === valueClassName) { - const stringifiedValue = inspect(value); - throw new Error( - `Cannot use ${className} "${stringifiedValue}" from another module or realm. - -Ensure that there is only one instance of "graphql" in the node_modules -directory. If different versions of "graphql" are the dependencies of other -relied on modules, use "resolutions" to ensure only one version is installed. - -https://yarnpkg.com/en/docs/selective-version-resolutions - -Duplicate "graphql" modules cannot be used at the same time since different -versions may have different capabilities and behavior. The data from one -version used in the function from another could produce confusing and -spurious results.`, - ); - } - } - return false; - }; - -interface Constructor extends Function { - prototype: { - [Symbol.toStringTag]: string; - }; -} diff --git a/src/language/source.ts b/src/language/source.ts index 49c1567938..8f6fed590a 100644 --- a/src/language/source.ts +++ b/src/language/source.ts @@ -1,11 +1,12 @@ import { devAssert } from '../jsutils/devAssert'; -import { instanceOf } from '../jsutils/instanceOf'; interface Location { line: number; column: number; } +const isSourceSymbol = Symbol.for('Source'); + /** * A representation of source input to GraphQL. The `name` and `locationOffset` parameters are * optional, but they are useful for clients who store GraphQL documents in source files. @@ -14,6 +15,7 @@ interface Location { * The `line` and `column` properties in `locationOffset` are 1-indexed. */ export class Source { + [isSourceSymbol]: true = true; body: string; name: string; locationOffset: Location; @@ -47,5 +49,7 @@ export class Source { * @internal */ export function isSource(source: unknown): source is Source { - return instanceOf(source, Source); + return ( + typeof source === 'object' && source != null && isSourceSymbol in source + ); } diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index 7e8914449a..7b311ba1f9 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -643,6 +643,27 @@ describe('Type System: Non-Null', () => { expectNonNull(ListOfScalarsType).to.not.throw(); expectNonNull(ListOfNonNullScalarsType).to.not.throw(); }); + + it('rejects a non-type as nullable type of non-null', () => { + // @ts-expect-error + expectNonNull(NonNullScalarType).to.throw( + 'Expected Scalar! to be a GraphQL nullable type.', + ); + // @ts-expect-error + expectNonNull({}).to.throw('Expected {} to be a GraphQL nullable type.'); + // @ts-expect-error + expectNonNull(String).to.throw( + 'Expected [function String] to be a GraphQL nullable type.', + ); + // @ts-expect-error (must provide type) + expectNonNull(null).to.throw( + 'Expected null to be a GraphQL nullable type.', + ); + // @ts-expect-error (must provide type) + expectNonNull(undefined).to.throw( + 'Expected undefined to be a GraphQL nullable type.', + ); + }); }); describe('Type System: test utility methods', () => { diff --git a/src/type/definition.ts b/src/type/definition.ts index f4cb1a90fe..60ac712d41 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -2,7 +2,6 @@ import { devAssert } from '../jsutils/devAssert'; import { didYouMean } from '../jsutils/didYouMean'; import { identityFunc } from '../jsutils/identityFunc'; import { inspect } from '../jsutils/inspect'; -import { instanceOf } from '../jsutils/instanceOf'; import { keyMap } from '../jsutils/keyMap'; import { keyValMap } from '../jsutils/keyValMap'; import { mapValue } from '../jsutils/mapValue'; @@ -71,11 +70,17 @@ export function assertType(type: unknown): GraphQLType { return type; } +const isGraphQLScalarTypeSymbol = Symbol.for('GraphQLScalarType'); + /** * There are predicates for each kind of GraphQL type. */ export function isScalarType(type: unknown): type is GraphQLScalarType { - return instanceOf(type, GraphQLScalarType); + return ( + typeof type === 'object' && + type != null && + isGraphQLScalarTypeSymbol in type + ); } export function assertScalarType(type: unknown): GraphQLScalarType { @@ -85,8 +90,14 @@ export function assertScalarType(type: unknown): GraphQLScalarType { return type; } +const isGraphQLObjectTypeSymbol = Symbol.for('GraphQLObjectType'); + export function isObjectType(type: unknown): type is GraphQLObjectType { - return instanceOf(type, GraphQLObjectType); + return ( + typeof type === 'object' && + type != null && + isGraphQLObjectTypeSymbol in type + ); } export function assertObjectType(type: unknown): GraphQLObjectType { @@ -96,8 +107,14 @@ export function assertObjectType(type: unknown): GraphQLObjectType { return type; } +const isGraphQLInterfaceTypeSymbol = Symbol.for('GraphQLInterfaceType'); + export function isInterfaceType(type: unknown): type is GraphQLInterfaceType { - return instanceOf(type, GraphQLInterfaceType); + return ( + typeof type === 'object' && + type != null && + isGraphQLInterfaceTypeSymbol in type + ); } export function assertInterfaceType(type: unknown): GraphQLInterfaceType { @@ -109,8 +126,12 @@ export function assertInterfaceType(type: unknown): GraphQLInterfaceType { return type; } +const isGraphQLUnionTypeSymbol = Symbol.for('GraphQLUnionType'); + export function isUnionType(type: unknown): type is GraphQLUnionType { - return instanceOf(type, GraphQLUnionType); + return ( + typeof type === 'object' && type != null && isGraphQLUnionTypeSymbol in type + ); } export function assertUnionType(type: unknown): GraphQLUnionType { @@ -120,8 +141,12 @@ export function assertUnionType(type: unknown): GraphQLUnionType { return type; } +const isGraphQLEnumTypeSymbol = Symbol.for('GraphQLEnumType'); + export function isEnumType(type: unknown): type is GraphQLEnumType { - return instanceOf(type, GraphQLEnumType); + return ( + typeof type === 'object' && type != null && isGraphQLEnumTypeSymbol in type + ); } export function assertEnumType(type: unknown): GraphQLEnumType { @@ -131,10 +156,16 @@ export function assertEnumType(type: unknown): GraphQLEnumType { return type; } +const isGraphQLInputObjectTypeSymbol = Symbol.for('GraphQLInputObjectType'); + export function isInputObjectType( type: unknown, ): type is GraphQLInputObjectType { - return instanceOf(type, GraphQLInputObjectType); + return ( + typeof type === 'object' && + type != null && + isGraphQLInputObjectTypeSymbol in type + ); } export function assertInputObjectType(type: unknown): GraphQLInputObjectType { @@ -146,6 +177,8 @@ export function assertInputObjectType(type: unknown): GraphQLInputObjectType { return type; } +const isGraphQLListTypeSymbol = Symbol.for('GraphQLListType'); + export function isListType( type: GraphQLInputType, ): type is GraphQLList; @@ -154,7 +187,9 @@ export function isListType( ): type is GraphQLList; export function isListType(type: unknown): type is GraphQLList; export function isListType(type: unknown): type is GraphQLList { - return instanceOf(type, GraphQLList); + return ( + typeof type === 'object' && type != null && isGraphQLListTypeSymbol in type + ); } export function assertListType(type: unknown): GraphQLList { @@ -164,6 +199,8 @@ export function assertListType(type: unknown): GraphQLList { return type; } +const isGraphQLNonNullTypeSymbol = Symbol.for('GraphQLNonNullType'); + export function isNonNullType( type: GraphQLInputType, ): type is GraphQLNonNull; @@ -176,7 +213,11 @@ export function isNonNullType( export function isNonNullType( type: unknown, ): type is GraphQLNonNull { - return instanceOf(type, GraphQLNonNull); + return ( + typeof type === 'object' && + type != null && + isGraphQLNonNullTypeSymbol in type + ); } export function assertNonNullType( @@ -317,6 +358,7 @@ export function assertAbstractType(type: unknown): GraphQLAbstractType { * ``` */ export class GraphQLList { + readonly [isGraphQLListTypeSymbol]: true = true; readonly ofType: T; constructor(ofType: T) { @@ -358,6 +400,7 @@ export class GraphQLList { * Note: the enforcement of non-nullability occurs within the executor. */ export class GraphQLNonNull { + readonly [isGraphQLNonNullTypeSymbol]: true = true; readonly ofType: T; constructor(ofType: T) { @@ -543,6 +586,7 @@ export interface GraphQLScalarTypeExtensions { * ``` */ export class GraphQLScalarType { + readonly [isGraphQLScalarTypeSymbol]: true = true; name: string; description: Maybe; specifiedByURL: Maybe; @@ -701,6 +745,7 @@ export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> { * ``` */ export class GraphQLObjectType { + readonly [isGraphQLObjectTypeSymbol]: true = true; name: string; description: Maybe; isTypeOf: Maybe>; @@ -1021,6 +1066,7 @@ export interface GraphQLInterfaceTypeExtensions { * ``` */ export class GraphQLInterfaceType { + readonly [isGraphQLInterfaceTypeSymbol]: true = true; name: string; description: Maybe; resolveType: Maybe>; @@ -1145,6 +1191,7 @@ export interface GraphQLUnionTypeExtensions { * ``` */ export class GraphQLUnionType { + readonly [isGraphQLUnionTypeSymbol]: true = true; name: string; description: Maybe; resolveType: Maybe>; @@ -1262,6 +1309,7 @@ export interface GraphQLEnumTypeExtensions { * will be used as its internal value. */ export class GraphQLEnumType /* */ { + readonly [isGraphQLEnumTypeSymbol]: true = true; name: string; description: Maybe; extensions: Readonly; @@ -1486,6 +1534,7 @@ export interface GraphQLInputObjectTypeExtensions { * ``` */ export class GraphQLInputObjectType { + readonly [isGraphQLInputObjectTypeSymbol]: true = true; name: string; description: Maybe; extensions: Readonly; diff --git a/src/type/directives.ts b/src/type/directives.ts index 13f4ad2721..440cc432d3 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -1,5 +1,4 @@ import { inspect } from '../jsutils/inspect'; -import { instanceOf } from '../jsutils/instanceOf'; import type { Maybe } from '../jsutils/Maybe'; import { toObjMap } from '../jsutils/toObjMap'; @@ -22,7 +21,11 @@ import { GraphQLBoolean, GraphQLString } from './scalars'; * Test if the given value is a GraphQL directive. */ export function isDirective(directive: unknown): directive is GraphQLDirective { - return instanceOf(directive, GraphQLDirective); + return ( + typeof directive === 'object' && + directive != null && + isGraphQLDirectiveSymbol in directive + ); } export function assertDirective(directive: unknown): GraphQLDirective { @@ -47,11 +50,14 @@ export interface GraphQLDirectiveExtensions { [attributeName: string]: unknown; } +const isGraphQLDirectiveSymbol = Symbol.for('GraphQLDirective'); + /** * Directives are used by the GraphQL runtime as a way of modifying execution * behavior. Type system creators will usually not create these directly. */ export class GraphQLDirective { + [isGraphQLDirectiveSymbol]: true = true; name: string; description: Maybe; locations: ReadonlyArray; diff --git a/src/type/schema.ts b/src/type/schema.ts index 456c1772ff..61b11a9fbd 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -1,5 +1,4 @@ import { inspect } from '../jsutils/inspect'; -import { instanceOf } from '../jsutils/instanceOf'; import type { Maybe } from '../jsutils/Maybe'; import type { ObjMap } from '../jsutils/ObjMap'; import { toObjMap } from '../jsutils/toObjMap'; @@ -41,7 +40,9 @@ import { * Test if the given value is a GraphQL schema. */ export function isSchema(schema: unknown): schema is GraphQLSchema { - return instanceOf(schema, GraphQLSchema); + return ( + typeof schema === 'object' && schema != null && isSchemaSymbol in schema + ); } export function assertSchema(schema: unknown): GraphQLSchema { @@ -64,6 +65,8 @@ export interface GraphQLSchemaExtensions { [attributeName: string]: unknown; } +const isSchemaSymbol = Symbol.for('GraphQLSchema'); + /** * Schema Definition * @@ -137,6 +140,7 @@ export class GraphQLSchema { extensions: Readonly; astNode: Maybe; extensionASTNodes: ReadonlyArray; + [isSchemaSymbol]: true = true; // Used as a cache for validateSchema(). __validationErrors: Maybe>; diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 136bee63c9..3c15162308 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -8,7 +8,7 @@ import { addPath, pathToArray } from '../jsutils/Path'; import { printPathArray } from '../jsutils/printPathArray'; import { suggestionList } from '../jsutils/suggestionList'; -import { GraphQLError } from '../error/GraphQLError'; +import { GraphQLError, isGraphQLError } from '../error/GraphQLError'; import type { GraphQLInputType } from '../type/definition'; import { @@ -154,7 +154,7 @@ function coerceInputValueImpl( try { parseResult = type.parseValue(inputValue); } catch (error) { - if (error instanceof GraphQLError) { + if (isGraphQLError(error)) { onError(pathToArray(path), inputValue, error); } else { onError( diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index e69b24bef9..aab0ee5554 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -42,6 +42,7 @@ import type { GraphQLFieldConfigMap, GraphQLInputFieldConfigMap, GraphQLNamedType, + GraphQLNullableType, GraphQLType, } from '../type/definition'; import { @@ -429,7 +430,9 @@ export function extendSchemaImpl( return new GraphQLList(getWrappedType(node.type)); } if (node.kind === Kind.NON_NULL_TYPE) { - return new GraphQLNonNull(getWrappedType(node.type)); + return new GraphQLNonNull( + getWrappedType(node.type) as GraphQLNullableType, + ); } return getNamedType(node); } diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts index 7510df1046..37e8045983 100644 --- a/src/utilities/typeFromAST.ts +++ b/src/utilities/typeFromAST.ts @@ -6,7 +6,11 @@ import type { } from '../language/ast'; import { Kind } from '../language/kinds'; -import type { GraphQLNamedType, GraphQLType } from '../type/definition'; +import type { + GraphQLNamedType, + GraphQLNullableType, + GraphQLType, +} from '../type/definition'; import { GraphQLList, GraphQLNonNull } from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; @@ -43,7 +47,9 @@ export function typeFromAST( return innerType && new GraphQLList(innerType); } case Kind.NON_NULL_TYPE: { - const innerType = typeFromAST(schema, typeNode.type); + const innerType = typeFromAST(schema, typeNode.type) as + | GraphQLNullableType + | undefined; return innerType && new GraphQLNonNull(innerType); } case Kind.NAMED_TYPE: diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.ts b/src/validation/rules/ValuesOfCorrectTypeRule.ts index 5d81a3833a..cee2148919 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.ts +++ b/src/validation/rules/ValuesOfCorrectTypeRule.ts @@ -3,7 +3,7 @@ import { inspect } from '../../jsutils/inspect'; import { keyMap } from '../../jsutils/keyMap'; import { suggestionList } from '../../jsutils/suggestionList'; -import { GraphQLError } from '../../error/GraphQLError'; +import { GraphQLError, isGraphQLError } from '../../error/GraphQLError'; import type { ValueNode } from '../../language/ast'; import { print } from '../../language/printer'; @@ -138,7 +138,7 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void { } } catch (error) { const typeStr = inspect(locationType); - if (error instanceof GraphQLError) { + if (isGraphQLError(error)) { context.reportError(error); } else { context.reportError(