diff --git a/src/jsutils/ObjMap.ts b/src/jsutils/ObjMap.ts index 2c202821..c717da60 100644 --- a/src/jsutils/ObjMap.ts +++ b/src/jsutils/ObjMap.ts @@ -8,6 +8,14 @@ export interface ReadOnlyObjMap { readonly [key: string]: T; } +export interface ReadOnlyObjMapWithSymbol { + readonly [key: string | symbol]: T; +} + export type ReadOnlyObjMapLike = | ReadOnlyObjMap | { readonly [key: string]: T }; + +export type ReadOnlyObjMapSymbolLike = + | ReadOnlyObjMapWithSymbol + | { readonly [key: string | symbol]: T }; diff --git a/src/jsutils/toObjMap.ts b/src/jsutils/toObjMap.ts index c7adab7b..ef1d475e 100644 --- a/src/jsutils/toObjMap.ts +++ b/src/jsutils/toObjMap.ts @@ -1,5 +1,10 @@ import type { Maybe } from './Maybe.js'; -import type { ReadOnlyObjMap, ReadOnlyObjMapLike } from './ObjMap.js'; +import type { + ReadOnlyObjMap, + ReadOnlyObjMapLike, + ReadOnlyObjMapSymbolLike, + ReadOnlyObjMapWithSymbol, +} from './ObjMap.js'; export function toObjMap( obj: Maybe>, @@ -16,5 +21,29 @@ export function toObjMap( for (const [key, value] of Object.entries(obj)) { map[key] = value; } + + return map; +} + +export function toObjMapWithSymbols( + obj: Maybe>, +): ReadOnlyObjMapWithSymbol { + if (obj == null) { + return Object.create(null); + } + + if (Object.getPrototypeOf(obj) === null) { + return obj; + } + + const map = Object.create(null); + for (const [key, value] of Object.entries(obj)) { + map[key] = value; + } + + for (const key of Object.getOwnPropertySymbols(obj)) { + map[key] = obj[key]; + } + return map; } diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index 768ccab4..00ef6dfc 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -86,6 +86,25 @@ describe('Type System: Scalars', () => { expect(someScalar.toConfig()).to.deep.equal(someScalarConfig); }); + it('supports symbol extensions', () => { + const test = Symbol.for('test'); + const someScalarConfig: GraphQLScalarTypeConfig = { + name: 'SomeScalar', + description: 'SomeScalar description.', + specifiedByURL: 'https://example.com/foo_spec', + serialize: passThroughFunc, + parseValue: passThroughFunc, + parseLiteral: passThroughFunc, + coerceInputLiteral: passThroughFunc, + valueToLiteral: passThroughFunc, + extensions: { [test]: 'extension' }, + astNode: dummyAny, + extensionASTNodes: [dummyAny], + }; + const someScalar = new GraphQLScalarType(someScalarConfig); + expect(someScalar.toConfig()).to.deep.equal(someScalarConfig); + }); + it('provides default methods if omitted', () => { const scalar = new GraphQLScalarType({ name: 'Foo' }); diff --git a/src/type/definition.ts b/src/type/definition.ts index 695d4623..ec40bf42 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -11,7 +11,7 @@ import type { ObjMap } from '../jsutils/ObjMap.js'; import type { Path } from '../jsutils/Path.js'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js'; import { suggestionList } from '../jsutils/suggestionList.js'; -import { toObjMap } from '../jsutils/toObjMap.js'; +import { toObjMapWithSymbols } from '../jsutils/toObjMap.js'; import { GraphQLError } from '../error/GraphQLError.js'; @@ -511,7 +511,7 @@ export function resolveObjMapThunk(thunk: ThunkObjMap): ObjMap { * an object which can contain all the values you need. */ export interface GraphQLScalarTypeExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } /** @@ -610,7 +610,7 @@ export class GraphQLScalarType { ((node, variables) => parseValue(valueFromASTUntyped(node, variables))); this.coerceInputLiteral = config.coerceInputLiteral; this.valueToLiteral = config.valueToLiteral; - this.extensions = toObjMap(config.extensions); + this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; @@ -725,7 +725,7 @@ interface GraphQLScalarTypeNormalizedConfig * you may find them useful. */ export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } /** @@ -783,7 +783,7 @@ export class GraphQLObjectType { this.name = assertName(config.name); this.description = config.description; this.isTypeOf = config.isTypeOf; - this.extensions = toObjMap(config.extensions); + this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; this._fields = (defineFieldMap).bind( @@ -854,7 +854,7 @@ function defineFieldMap( resolve: fieldConfig.resolve, subscribe: fieldConfig.subscribe, deprecationReason: fieldConfig.deprecationReason, - extensions: toObjMap(fieldConfig.extensions), + extensions: toObjMapWithSymbols(fieldConfig.extensions), astNode: fieldConfig.astNode, }; }); @@ -869,7 +869,7 @@ export function defineArguments( type: argConfig.type, defaultValue: defineDefaultValue(argName, argConfig), deprecationReason: argConfig.deprecationReason, - extensions: toObjMap(argConfig.extensions), + extensions: toObjMapWithSymbols(argConfig.extensions), astNode: argConfig.astNode, })); } @@ -980,7 +980,7 @@ export interface GraphQLResolveInfo { * you may find them useful. */ export interface GraphQLFieldExtensions<_TSource, _TContext, _TArgs = any> { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } export interface GraphQLFieldConfig { @@ -1008,7 +1008,7 @@ export type GraphQLFieldConfigArgumentMap = ObjMap; * an object which can contain all the values you need. */ export interface GraphQLArgumentExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } export interface GraphQLArgumentConfig { @@ -1085,7 +1085,7 @@ export function defineDefaultValue( * an object which can contain all the values you need. */ export interface GraphQLInterfaceTypeExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } /** @@ -1122,7 +1122,7 @@ export class GraphQLInterfaceType { this.name = assertName(config.name); this.description = config.description; this.resolveType = config.resolveType; - this.extensions = toObjMap(config.extensions); + this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; this._fields = (defineFieldMap).bind( @@ -1206,7 +1206,7 @@ interface GraphQLInterfaceTypeNormalizedConfig * an object which can contain all the values you need. */ export interface GraphQLUnionTypeExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } /** @@ -1247,7 +1247,7 @@ export class GraphQLUnionType { this.name = assertName(config.name); this.description = config.description; this.resolveType = config.resolveType; - this.extensions = toObjMap(config.extensions); + this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; @@ -1324,7 +1324,7 @@ interface GraphQLUnionTypeNormalizedConfig * an object which can contain all the values you need. */ export interface GraphQLEnumTypeExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } function enumValuesFromConfig(values: GraphQLEnumValueConfigMap) { @@ -1333,7 +1333,7 @@ function enumValuesFromConfig(values: GraphQLEnumValueConfigMap) { description: valueConfig.description, value: valueConfig.value !== undefined ? valueConfig.value : valueName, deprecationReason: valueConfig.deprecationReason, - extensions: toObjMap(valueConfig.extensions), + extensions: toObjMapWithSymbols(valueConfig.extensions), astNode: valueConfig.astNode, })); } @@ -1378,7 +1378,7 @@ export class GraphQLEnumType /* */ { constructor(config: Readonly */>) { this.name = assertName(config.name); this.description = config.description; - this.extensions = toObjMap(config.extensions); + this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; @@ -1559,7 +1559,7 @@ export type GraphQLEnumValueConfigMap /* */ = * an object which can contain all the values you need. */ export interface GraphQLEnumValueExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } export interface GraphQLEnumValueConfig { @@ -1589,7 +1589,7 @@ export interface GraphQLEnumValue { * an object which can contain all the values you need. */ export interface GraphQLInputObjectTypeExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } /** @@ -1626,7 +1626,7 @@ export class GraphQLInputObjectType { constructor(config: Readonly) { this.name = assertName(config.name); this.description = config.description; - this.extensions = toObjMap(config.extensions); + this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; this.isOneOf = config.isOneOf ?? false; @@ -1686,7 +1686,7 @@ function defineInputFieldMap( type: fieldConfig.type, defaultValue: defineDefaultValue(fieldName, fieldConfig), deprecationReason: fieldConfig.deprecationReason, - extensions: toObjMap(fieldConfig.extensions), + extensions: toObjMapWithSymbols(fieldConfig.extensions), astNode: fieldConfig.astNode, })); } @@ -1718,7 +1718,7 @@ interface GraphQLInputObjectTypeNormalizedConfig * an object which can contain all the values you need. */ export interface GraphQLInputFieldExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } export interface GraphQLInputFieldConfig { diff --git a/src/type/directives.ts b/src/type/directives.ts index 48e90c55..23faa337 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -1,7 +1,7 @@ import { inspect } from '../jsutils/inspect.js'; import { instanceOf } from '../jsutils/instanceOf.js'; import type { Maybe } from '../jsutils/Maybe.js'; -import { toObjMap } from '../jsutils/toObjMap.js'; +import { toObjMapWithSymbols } from '../jsutils/toObjMap.js'; import type { DirectiveDefinitionNode } from '../language/ast.js'; import { DirectiveLocation } from '../language/directiveLocation.js'; @@ -44,7 +44,7 @@ export function assertDirective(directive: unknown): GraphQLDirective { * an object which can contain all the values you need. */ export interface GraphQLDirectiveExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } /** @@ -65,7 +65,7 @@ export class GraphQLDirective { this.description = config.description; this.locations = config.locations; this.isRepeatable = config.isRepeatable ?? false; - this.extensions = toObjMap(config.extensions); + this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; const args = config.args ?? {}; diff --git a/src/type/schema.ts b/src/type/schema.ts index 694454fa..2dd324b5 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -2,7 +2,7 @@ import { inspect } from '../jsutils/inspect.js'; import { instanceOf } from '../jsutils/instanceOf.js'; import type { Maybe } from '../jsutils/Maybe.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; -import { toObjMap } from '../jsutils/toObjMap.js'; +import { toObjMapWithSymbols } from '../jsutils/toObjMap.js'; import type { GraphQLError } from '../error/GraphQLError.js'; @@ -61,7 +61,7 @@ export function assertSchema(schema: unknown): GraphQLSchema { * an object which can contain all the values you need. */ export interface GraphQLSchemaExtensions { - [attributeName: string]: unknown; + [attributeName: string | symbol]: unknown; } /** @@ -162,7 +162,7 @@ export class GraphQLSchema { this.__validationErrors = config.assumeValid === true ? [] : undefined; this.description = config.description; - this.extensions = toObjMap(config.extensions); + this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? [];