Skip to content

Commit 70ad2f5

Browse files
JoviDeCroockn1ru4l
andauthored
Support symbol properties on extension objects (#4234)
Supersedes #3511 This adds support for `Symbol` property keys on the extension property of AST nodes. Had to tweak `toObjMap` here to support symbol keys by adding `getOwnPropertySymbols` which is the only way to enumerate over them. --------- Co-authored-by: n1ru4l <[email protected]>
1 parent a9b18b9 commit 70ad2f5

File tree

6 files changed

+84
-28
lines changed

6 files changed

+84
-28
lines changed

src/jsutils/ObjMap.ts

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export interface ReadOnlyObjMap<T> {
88
readonly [key: string]: T;
99
}
1010

11+
export interface ReadOnlyObjMapWithSymbol<T> {
12+
readonly [key: string | symbol]: T;
13+
}
14+
1115
export type ReadOnlyObjMapLike<T> =
1216
| ReadOnlyObjMap<T>
1317
| { readonly [key: string]: T };
18+
19+
export type ReadOnlyObjMapSymbolLike<T> =
20+
| ReadOnlyObjMapWithSymbol<T>
21+
| { readonly [key: string | symbol]: T };

src/jsutils/toObjMap.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { Maybe } from './Maybe.js';
2-
import type { ReadOnlyObjMap, ReadOnlyObjMapLike } from './ObjMap.js';
2+
import type {
3+
ReadOnlyObjMap,
4+
ReadOnlyObjMapLike,
5+
ReadOnlyObjMapSymbolLike,
6+
ReadOnlyObjMapWithSymbol,
7+
} from './ObjMap.js';
38

49
export function toObjMap<T>(
510
obj: Maybe<ReadOnlyObjMapLike<T>>,
@@ -16,5 +21,29 @@ export function toObjMap<T>(
1621
for (const [key, value] of Object.entries(obj)) {
1722
map[key] = value;
1823
}
24+
25+
return map;
26+
}
27+
28+
export function toObjMapWithSymbols<T>(
29+
obj: Maybe<ReadOnlyObjMapSymbolLike<T>>,
30+
): ReadOnlyObjMapWithSymbol<T> {
31+
if (obj == null) {
32+
return Object.create(null);
33+
}
34+
35+
if (Object.getPrototypeOf(obj) === null) {
36+
return obj;
37+
}
38+
39+
const map = Object.create(null);
40+
for (const [key, value] of Object.entries(obj)) {
41+
map[key] = value;
42+
}
43+
44+
for (const key of Object.getOwnPropertySymbols(obj)) {
45+
map[key] = obj[key];
46+
}
47+
1948
return map;
2049
}

src/type/__tests__/definition-test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ describe('Type System: Scalars', () => {
8686
expect(someScalar.toConfig()).to.deep.equal(someScalarConfig);
8787
});
8888

89+
it('supports symbol extensions', () => {
90+
const test = Symbol.for('test');
91+
const someScalarConfig: GraphQLScalarTypeConfig<unknown, unknown> = {
92+
name: 'SomeScalar',
93+
description: 'SomeScalar description.',
94+
specifiedByURL: 'https://example.com/foo_spec',
95+
serialize: passThroughFunc,
96+
parseValue: passThroughFunc,
97+
parseLiteral: passThroughFunc,
98+
coerceInputLiteral: passThroughFunc,
99+
valueToLiteral: passThroughFunc,
100+
extensions: { [test]: 'extension' },
101+
astNode: dummyAny,
102+
extensionASTNodes: [dummyAny],
103+
};
104+
const someScalar = new GraphQLScalarType(someScalarConfig);
105+
expect(someScalar.toConfig()).to.deep.equal(someScalarConfig);
106+
});
107+
89108
it('provides default methods if omitted', () => {
90109
const scalar = new GraphQLScalarType({ name: 'Foo' });
91110

src/type/definition.ts

+21-21
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { ObjMap } from '../jsutils/ObjMap.js';
1111
import type { Path } from '../jsutils/Path.js';
1212
import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js';
1313
import { suggestionList } from '../jsutils/suggestionList.js';
14-
import { toObjMap } from '../jsutils/toObjMap.js';
14+
import { toObjMapWithSymbols } from '../jsutils/toObjMap.js';
1515

1616
import { GraphQLError } from '../error/GraphQLError.js';
1717

@@ -511,7 +511,7 @@ export function resolveObjMapThunk<T>(thunk: ThunkObjMap<T>): ObjMap<T> {
511511
* an object which can contain all the values you need.
512512
*/
513513
export interface GraphQLScalarTypeExtensions {
514-
[attributeName: string]: unknown;
514+
[attributeName: string | symbol]: unknown;
515515
}
516516

517517
/**
@@ -610,7 +610,7 @@ export class GraphQLScalarType<TInternal = unknown, TExternal = TInternal> {
610610
((node, variables) => parseValue(valueFromASTUntyped(node, variables)));
611611
this.coerceInputLiteral = config.coerceInputLiteral;
612612
this.valueToLiteral = config.valueToLiteral;
613-
this.extensions = toObjMap(config.extensions);
613+
this.extensions = toObjMapWithSymbols(config.extensions);
614614
this.astNode = config.astNode;
615615
this.extensionASTNodes = config.extensionASTNodes ?? [];
616616

@@ -725,7 +725,7 @@ interface GraphQLScalarTypeNormalizedConfig<TInternal, TExternal>
725725
* you may find them useful.
726726
*/
727727
export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> {
728-
[attributeName: string]: unknown;
728+
[attributeName: string | symbol]: unknown;
729729
}
730730

731731
/**
@@ -783,7 +783,7 @@ export class GraphQLObjectType<TSource = any, TContext = any> {
783783
this.name = assertName(config.name);
784784
this.description = config.description;
785785
this.isTypeOf = config.isTypeOf;
786-
this.extensions = toObjMap(config.extensions);
786+
this.extensions = toObjMapWithSymbols(config.extensions);
787787
this.astNode = config.astNode;
788788
this.extensionASTNodes = config.extensionASTNodes ?? [];
789789
this._fields = (defineFieldMap<TSource, TContext>).bind(
@@ -854,7 +854,7 @@ function defineFieldMap<TSource, TContext>(
854854
resolve: fieldConfig.resolve,
855855
subscribe: fieldConfig.subscribe,
856856
deprecationReason: fieldConfig.deprecationReason,
857-
extensions: toObjMap(fieldConfig.extensions),
857+
extensions: toObjMapWithSymbols(fieldConfig.extensions),
858858
astNode: fieldConfig.astNode,
859859
};
860860
});
@@ -869,7 +869,7 @@ export function defineArguments(
869869
type: argConfig.type,
870870
defaultValue: defineDefaultValue(argName, argConfig),
871871
deprecationReason: argConfig.deprecationReason,
872-
extensions: toObjMap(argConfig.extensions),
872+
extensions: toObjMapWithSymbols(argConfig.extensions),
873873
astNode: argConfig.astNode,
874874
}));
875875
}
@@ -980,7 +980,7 @@ export interface GraphQLResolveInfo {
980980
* you may find them useful.
981981
*/
982982
export interface GraphQLFieldExtensions<_TSource, _TContext, _TArgs = any> {
983-
[attributeName: string]: unknown;
983+
[attributeName: string | symbol]: unknown;
984984
}
985985

986986
export interface GraphQLFieldConfig<TSource, TContext, TArgs = any> {
@@ -1008,7 +1008,7 @@ export type GraphQLFieldConfigArgumentMap = ObjMap<GraphQLArgumentConfig>;
10081008
* an object which can contain all the values you need.
10091009
*/
10101010
export interface GraphQLArgumentExtensions {
1011-
[attributeName: string]: unknown;
1011+
[attributeName: string | symbol]: unknown;
10121012
}
10131013

10141014
export interface GraphQLArgumentConfig {
@@ -1085,7 +1085,7 @@ export function defineDefaultValue(
10851085
* an object which can contain all the values you need.
10861086
*/
10871087
export interface GraphQLInterfaceTypeExtensions {
1088-
[attributeName: string]: unknown;
1088+
[attributeName: string | symbol]: unknown;
10891089
}
10901090

10911091
/**
@@ -1122,7 +1122,7 @@ export class GraphQLInterfaceType<TSource = any, TContext = any> {
11221122
this.name = assertName(config.name);
11231123
this.description = config.description;
11241124
this.resolveType = config.resolveType;
1125-
this.extensions = toObjMap(config.extensions);
1125+
this.extensions = toObjMapWithSymbols(config.extensions);
11261126
this.astNode = config.astNode;
11271127
this.extensionASTNodes = config.extensionASTNodes ?? [];
11281128
this._fields = (defineFieldMap<TSource, TContext>).bind(
@@ -1206,7 +1206,7 @@ interface GraphQLInterfaceTypeNormalizedConfig<TSource, TContext>
12061206
* an object which can contain all the values you need.
12071207
*/
12081208
export interface GraphQLUnionTypeExtensions {
1209-
[attributeName: string]: unknown;
1209+
[attributeName: string | symbol]: unknown;
12101210
}
12111211

12121212
/**
@@ -1247,7 +1247,7 @@ export class GraphQLUnionType {
12471247
this.name = assertName(config.name);
12481248
this.description = config.description;
12491249
this.resolveType = config.resolveType;
1250-
this.extensions = toObjMap(config.extensions);
1250+
this.extensions = toObjMapWithSymbols(config.extensions);
12511251
this.astNode = config.astNode;
12521252
this.extensionASTNodes = config.extensionASTNodes ?? [];
12531253

@@ -1324,7 +1324,7 @@ interface GraphQLUnionTypeNormalizedConfig
13241324
* an object which can contain all the values you need.
13251325
*/
13261326
export interface GraphQLEnumTypeExtensions {
1327-
[attributeName: string]: unknown;
1327+
[attributeName: string | symbol]: unknown;
13281328
}
13291329

13301330
function enumValuesFromConfig(values: GraphQLEnumValueConfigMap) {
@@ -1333,7 +1333,7 @@ function enumValuesFromConfig(values: GraphQLEnumValueConfigMap) {
13331333
description: valueConfig.description,
13341334
value: valueConfig.value !== undefined ? valueConfig.value : valueName,
13351335
deprecationReason: valueConfig.deprecationReason,
1336-
extensions: toObjMap(valueConfig.extensions),
1336+
extensions: toObjMapWithSymbols(valueConfig.extensions),
13371337
astNode: valueConfig.astNode,
13381338
}));
13391339
}
@@ -1378,7 +1378,7 @@ export class GraphQLEnumType /* <T> */ {
13781378
constructor(config: Readonly<GraphQLEnumTypeConfig /* <T> */>) {
13791379
this.name = assertName(config.name);
13801380
this.description = config.description;
1381-
this.extensions = toObjMap(config.extensions);
1381+
this.extensions = toObjMapWithSymbols(config.extensions);
13821382
this.astNode = config.astNode;
13831383
this.extensionASTNodes = config.extensionASTNodes ?? [];
13841384

@@ -1559,7 +1559,7 @@ export type GraphQLEnumValueConfigMap /* <T> */ =
15591559
* an object which can contain all the values you need.
15601560
*/
15611561
export interface GraphQLEnumValueExtensions {
1562-
[attributeName: string]: unknown;
1562+
[attributeName: string | symbol]: unknown;
15631563
}
15641564

15651565
export interface GraphQLEnumValueConfig {
@@ -1589,7 +1589,7 @@ export interface GraphQLEnumValue {
15891589
* an object which can contain all the values you need.
15901590
*/
15911591
export interface GraphQLInputObjectTypeExtensions {
1592-
[attributeName: string]: unknown;
1592+
[attributeName: string | symbol]: unknown;
15931593
}
15941594

15951595
/**
@@ -1626,7 +1626,7 @@ export class GraphQLInputObjectType {
16261626
constructor(config: Readonly<GraphQLInputObjectTypeConfig>) {
16271627
this.name = assertName(config.name);
16281628
this.description = config.description;
1629-
this.extensions = toObjMap(config.extensions);
1629+
this.extensions = toObjMapWithSymbols(config.extensions);
16301630
this.astNode = config.astNode;
16311631
this.extensionASTNodes = config.extensionASTNodes ?? [];
16321632
this.isOneOf = config.isOneOf ?? false;
@@ -1686,7 +1686,7 @@ function defineInputFieldMap(
16861686
type: fieldConfig.type,
16871687
defaultValue: defineDefaultValue(fieldName, fieldConfig),
16881688
deprecationReason: fieldConfig.deprecationReason,
1689-
extensions: toObjMap(fieldConfig.extensions),
1689+
extensions: toObjMapWithSymbols(fieldConfig.extensions),
16901690
astNode: fieldConfig.astNode,
16911691
}));
16921692
}
@@ -1718,7 +1718,7 @@ interface GraphQLInputObjectTypeNormalizedConfig
17181718
* an object which can contain all the values you need.
17191719
*/
17201720
export interface GraphQLInputFieldExtensions {
1721-
[attributeName: string]: unknown;
1721+
[attributeName: string | symbol]: unknown;
17221722
}
17231723

17241724
export interface GraphQLInputFieldConfig {

src/type/directives.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { inspect } from '../jsutils/inspect.js';
22
import { instanceOf } from '../jsutils/instanceOf.js';
33
import type { Maybe } from '../jsutils/Maybe.js';
4-
import { toObjMap } from '../jsutils/toObjMap.js';
4+
import { toObjMapWithSymbols } from '../jsutils/toObjMap.js';
55

66
import type { DirectiveDefinitionNode } from '../language/ast.js';
77
import { DirectiveLocation } from '../language/directiveLocation.js';
@@ -44,7 +44,7 @@ export function assertDirective(directive: unknown): GraphQLDirective {
4444
* an object which can contain all the values you need.
4545
*/
4646
export interface GraphQLDirectiveExtensions {
47-
[attributeName: string]: unknown;
47+
[attributeName: string | symbol]: unknown;
4848
}
4949

5050
/**
@@ -65,7 +65,7 @@ export class GraphQLDirective {
6565
this.description = config.description;
6666
this.locations = config.locations;
6767
this.isRepeatable = config.isRepeatable ?? false;
68-
this.extensions = toObjMap(config.extensions);
68+
this.extensions = toObjMapWithSymbols(config.extensions);
6969
this.astNode = config.astNode;
7070

7171
const args = config.args ?? {};

src/type/schema.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { inspect } from '../jsutils/inspect.js';
22
import { instanceOf } from '../jsutils/instanceOf.js';
33
import type { Maybe } from '../jsutils/Maybe.js';
44
import type { ObjMap } from '../jsutils/ObjMap.js';
5-
import { toObjMap } from '../jsutils/toObjMap.js';
5+
import { toObjMapWithSymbols } from '../jsutils/toObjMap.js';
66

77
import type { GraphQLError } from '../error/GraphQLError.js';
88

@@ -61,7 +61,7 @@ export function assertSchema(schema: unknown): GraphQLSchema {
6161
* an object which can contain all the values you need.
6262
*/
6363
export interface GraphQLSchemaExtensions {
64-
[attributeName: string]: unknown;
64+
[attributeName: string | symbol]: unknown;
6565
}
6666

6767
/**
@@ -162,7 +162,7 @@ export class GraphQLSchema {
162162
this.__validationErrors = config.assumeValid === true ? [] : undefined;
163163

164164
this.description = config.description;
165-
this.extensions = toObjMap(config.extensions);
165+
this.extensions = toObjMapWithSymbols(config.extensions);
166166
this.astNode = config.astNode;
167167
this.extensionASTNodes = config.extensionASTNodes ?? [];
168168

0 commit comments

Comments
 (0)