diff --git a/src/jsutils/ObjMap.js b/src/jsutils/ObjMap.js index 1835d98378..63a8251084 100644 --- a/src/jsutils/ObjMap.js +++ b/src/jsutils/ObjMap.js @@ -1,3 +1,9 @@ // @flow strict export type ObjMap = { [key: string]: T, __proto__: null, ... }; +export type ObjMapLike = ObjMap | { [key: string]: T, ... }; + +export type ReadOnlyObjMap = { +[key: string]: T, __proto__: null, ... }; +export type ReadOnlyObjMapLike = + | ReadOnlyObjMap + | { +[key: string]: T, ... }; diff --git a/src/jsutils/__tests__/toObjMap-test.js b/src/jsutils/__tests__/toObjMap-test.js new file mode 100644 index 0000000000..51b6f81128 --- /dev/null +++ b/src/jsutils/__tests__/toObjMap-test.js @@ -0,0 +1,51 @@ +// @flow strict + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import toObjMap from '../toObjMap'; +import { type ObjMapLike } from '../ObjMap'; + +// Workaround to make both ESLint and Flow happy +const __proto__: string = '__proto__'; + +describe('toObjMap', () => { + it('convert empty object to ObjMap', () => { + const result = toObjMap({}); + expect(result).to.deep.equal({}); + expect(Object.getPrototypeOf(result)).to.equal(null); + }); + + it('convert object with own properties to ObjMap', () => { + const obj: ObjMapLike = Object.freeze({ foo: 'bar' }); + + const result = toObjMap(obj); + expect(result).to.deep.equal(obj); + expect(Object.getPrototypeOf(result)).to.equal(null); + }); + + it('convert object with __proto__ property to ObjMap', () => { + const protoObj = Object.freeze({ toString: false }); + const obj = Object.create(null); + obj[__proto__] = protoObj; + Object.freeze(obj); + + const result = toObjMap(obj); + expect(Object.keys(result)).to.deep.equal(['__proto__']); + expect(Object.getPrototypeOf(result)).to.equal(null); + expect(result[__proto__]).to.equal(protoObj); + }); + + it('passthrough empty ObjMap', () => { + const objMap = Object.create(null); + expect(toObjMap(objMap)).to.deep.equal(objMap); + }); + + it('passthrough ObjMap with properties', () => { + const objMap = Object.freeze({ + __proto__: null, + foo: 'bar', + }); + expect(toObjMap(objMap)).to.deep.equal(objMap); + }); +}); diff --git a/src/jsutils/toObjMap.js b/src/jsutils/toObjMap.js new file mode 100644 index 0000000000..a7a2d6c37e --- /dev/null +++ b/src/jsutils/toObjMap.js @@ -0,0 +1,26 @@ +// @flow strict + +import objectEntries from '../polyfills/objectEntries'; +import { + type ObjMap, + type ObjMapLike, + type ReadOnlyObjMap, + type ReadOnlyObjMapLike, +} from './ObjMap'; + +/* eslint-disable no-redeclare */ +declare function toObjMap(obj: ObjMapLike): ObjMap; +declare function toObjMap(obj: ReadOnlyObjMapLike): ReadOnlyObjMap; + +export default function toObjMap(obj) { + /* eslint-enable no-redeclare */ + if (Object.getPrototypeOf(obj) === null) { + return obj; + } + + const map = Object.create(null); + for (const [key, value] of objectEntries(obj)) { + map[key] = value; + } + return map; +} diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index a67297f6b9..77cee03a0f 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -195,6 +195,7 @@ describe('Type System: Objects', () => { subscribe: undefined, isDeprecated: true, deprecationReason: 'A terrible reason', + extensions: undefined, astNode: undefined, }); }); @@ -216,6 +217,7 @@ describe('Type System: Objects', () => { subscribe: undefined, isDeprecated: false, deprecationReason: undefined, + extensions: undefined, astNode: undefined, }, }); @@ -244,6 +246,7 @@ describe('Type System: Objects', () => { description: null, type: ScalarType, defaultValue: undefined, + extensions: undefined, astNode: undefined, }, ], @@ -251,6 +254,7 @@ describe('Type System: Objects', () => { subscribe: undefined, isDeprecated: false, deprecationReason: undefined, + extensions: undefined, astNode: undefined, }, }); @@ -524,6 +528,7 @@ describe('Type System: Enums', () => { isDeprecated: true, deprecationReason: 'Just because', value: 'foo', + extensions: undefined, astNode: undefined, }); }); @@ -544,6 +549,7 @@ describe('Type System: Enums', () => { value: null, isDeprecated: false, deprecationReason: undefined, + extensions: undefined, astNode: undefined, }, { @@ -552,6 +558,7 @@ describe('Type System: Enums', () => { value: undefined, isDeprecated: false, deprecationReason: undefined, + extensions: undefined, astNode: undefined, }, ]); @@ -649,6 +656,7 @@ describe('Type System: Input Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + extensions: undefined, astNode: undefined, }, }); @@ -667,6 +675,7 @@ describe('Type System: Input Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + extensions: undefined, astNode: undefined, }, }); diff --git a/src/type/__tests__/directive-test.js b/src/type/__tests__/directive-test.js index a638e7ac6b..640b236613 100644 --- a/src/type/__tests__/directive-test.js +++ b/src/type/__tests__/directive-test.js @@ -39,6 +39,7 @@ describe('Type System: Directive', () => { description: null, type: GraphQLString, defaultValue: undefined, + extensions: undefined, astNode: undefined, }, { @@ -46,6 +47,7 @@ describe('Type System: Directive', () => { description: null, type: GraphQLInt, defaultValue: undefined, + extensions: undefined, astNode: undefined, }, ], diff --git a/src/type/__tests__/enumType-test.js b/src/type/__tests__/enumType-test.js index 6e35fc41e3..b650339f5b 100644 --- a/src/type/__tests__/enumType-test.js +++ b/src/type/__tests__/enumType-test.js @@ -345,6 +345,7 @@ describe('Type System: Enum Values', () => { value: Complex1, isDeprecated: false, deprecationReason: undefined, + extensions: undefined, astNode: undefined, }, { @@ -353,6 +354,7 @@ describe('Type System: Enum Values', () => { value: Complex2, isDeprecated: false, deprecationReason: undefined, + extensions: undefined, astNode: undefined, }, ]); diff --git a/src/type/__tests__/extensions-test.js b/src/type/__tests__/extensions-test.js new file mode 100644 index 0000000000..64bd583245 --- /dev/null +++ b/src/type/__tests__/extensions-test.js @@ -0,0 +1,383 @@ +// @flow strict + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import invariant from '../../jsutils/invariant'; + +import { GraphQLSchema } from '../schema'; +import { GraphQLDirective } from '../directives'; +import { + GraphQLScalarType, + GraphQLObjectType, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, +} from '../definition'; + +const dummyType = new GraphQLScalarType({ name: 'DummyScalar' }); + +describe('Type System: Extensions', () => { + describe('GraphQLScalarType', () => { + it('without extensions', () => { + const someScalar = new GraphQLScalarType({ name: 'SomeScalar' }); + expect(someScalar.extensions).to.equal(undefined); + + const config = someScalar.toConfig(); + expect(config.extensions).to.equal(undefined); + }); + + it('with extensions', () => { + const scalarExtensions = Object.freeze({ SomeScalarExt: 'scalar' }); + const someScalar = new GraphQLScalarType({ + name: 'SomeScalar', + extensions: scalarExtensions, + }); + + expect(someScalar.extensions).to.deep.equal(scalarExtensions); + + const config = someScalar.toConfig(); + expect(config.extensions).to.deep.equal(scalarExtensions); + }); + }); + + describe('GraphQLObjectType', () => { + it('without extensions', () => { + const someObject = new GraphQLObjectType({ + name: 'SomeObject', + fields: { + someField: { + type: dummyType, + args: { + someArg: { + type: dummyType, + }, + }, + }, + }, + }); + + expect(someObject.extensions).to.equal(undefined); + const someField = someObject.getFields().someField; + expect(someField.extensions).to.equal(undefined); + const someArg = someField.args[0]; + expect(someArg.extensions).to.equal(undefined); + + const config = someObject.toConfig(); + expect(config.extensions).to.equal(undefined); + const someFieldConfig = config.fields.someField; + expect(someFieldConfig.extensions).to.equal(undefined); + invariant(someFieldConfig.args); + const someArgConfig = someFieldConfig.args.someArg; + expect(someArgConfig.extensions).to.equal(undefined); + }); + + it('with extensions', () => { + const objectExtensions = Object.freeze({ SomeObjectExt: 'object' }); + const fieldExtensions = Object.freeze({ SomeFieldExt: 'field' }); + const argExtensions = Object.freeze({ SomeArgExt: 'arg' }); + + const someObject = new GraphQLObjectType({ + name: 'SomeObject', + fields: { + someField: { + type: dummyType, + args: { + someArg: { + type: dummyType, + extensions: argExtensions, + }, + }, + extensions: fieldExtensions, + }, + }, + extensions: objectExtensions, + }); + + expect(someObject.extensions).to.deep.equal(objectExtensions); + const someField = someObject.getFields().someField; + expect(someField.extensions).to.deep.equal(fieldExtensions); + const someArg = someField.args[0]; + expect(someArg.extensions).to.deep.equal(argExtensions); + + const config = someObject.toConfig(); + expect(config.extensions).to.deep.equal(objectExtensions); + const someFieldConfig = config.fields.someField; + expect(someFieldConfig.extensions).to.deep.equal(fieldExtensions); + invariant(someFieldConfig.args); + const someArgConfig = someFieldConfig.args.someArg; + expect(someArgConfig.extensions).to.deep.equal(argExtensions); + }); + }); + + describe('GraphQLInterfaceType', () => { + it('without extensions', () => { + const someInterface = new GraphQLInterfaceType({ + name: 'SomeInterface', + fields: { + someField: { + type: dummyType, + args: { + someArg: { + type: dummyType, + }, + }, + }, + }, + }); + + expect(someInterface.extensions).to.equal(undefined); + const someField = someInterface.getFields().someField; + expect(someField.extensions).to.equal(undefined); + const someArg = someField.args[0]; + expect(someArg.extensions).to.equal(undefined); + + const config = someInterface.toConfig(); + expect(config.extensions).to.equal(undefined); + const someFieldConfig = config.fields.someField; + expect(someFieldConfig.extensions).to.equal(undefined); + invariant(someFieldConfig.args); + const someArgConfig = someFieldConfig.args.someArg; + expect(someArgConfig.extensions).to.equal(undefined); + }); + + it('with extensions', () => { + const interfaceExtensions = Object.freeze({ SomeObjectExt: 'object' }); + const fieldExtensions = Object.freeze({ SomeFieldExt: 'field' }); + const argExtensions = Object.freeze({ SomeArgExt: 'arg' }); + + const someInterface = new GraphQLObjectType({ + name: 'SomeInterface', + fields: { + someField: { + type: dummyType, + args: { + someArg: { + type: dummyType, + extensions: argExtensions, + }, + }, + extensions: fieldExtensions, + }, + }, + extensions: interfaceExtensions, + }); + + expect(someInterface.extensions).to.deep.equal(interfaceExtensions); + const someField = someInterface.getFields().someField; + expect(someField.extensions).to.deep.equal(fieldExtensions); + const someArg = someField.args[0]; + expect(someArg.extensions).to.deep.equal(argExtensions); + + const config = someInterface.toConfig(); + expect(config.extensions).to.deep.equal(interfaceExtensions); + const someFieldConfig = config.fields.someField; + expect(someFieldConfig.extensions).to.deep.equal(fieldExtensions); + invariant(someFieldConfig.args); + const someArgConfig = someFieldConfig.args.someArg; + expect(someArgConfig.extensions).to.deep.equal(argExtensions); + }); + }); + + describe('GraphQLUnionType', () => { + it('without extensions', () => { + const someUnion = new GraphQLUnionType({ + name: 'SomeUnion', + types: [], + }); + + expect(someUnion.extensions).to.equal(undefined); + + const config = someUnion.toConfig(); + expect(config.extensions).to.equal(undefined); + }); + + it('with extensions', () => { + const unionExtensions = Object.freeze({ SomeScalarExt: 'union' }); + + const someUnion = new GraphQLUnionType({ + name: 'SomeUnion', + types: [], + extensions: unionExtensions, + }); + + expect(someUnion.extensions).to.deep.equal(unionExtensions); + + const config = someUnion.toConfig(); + expect(config.extensions).to.deep.equal(unionExtensions); + }); + }); + + describe('GraphQLEnumType', () => { + it('without extensions', () => { + const someEnum = new GraphQLEnumType({ + name: 'SomeEnum', + values: { + SOME_VALUE: {}, + }, + }); + + expect(someEnum.extensions).to.equal(undefined); + const someValue = someEnum.getValues()[0]; + expect(someValue.extensions).to.equal(undefined); + + const config = someEnum.toConfig(); + expect(config.extensions).to.equal(undefined); + const someValueConfig = config.values.SOME_VALUE; + expect(someValueConfig.extensions).to.equal(undefined); + }); + + it('with extensions', () => { + const enumExtensions = Object.freeze({ SomeEnumExt: 'enum' }); + const valueExtensions = Object.freeze({ SomeValueExt: 'value' }); + + const someEnum = new GraphQLEnumType({ + name: 'SomeEnum', + values: { + SOME_VALUE: { + extensions: valueExtensions, + }, + }, + extensions: enumExtensions, + }); + + expect(someEnum.extensions).to.deep.equal(enumExtensions); + const someValue = someEnum.getValues()[0]; + expect(someValue.extensions).to.deep.equal(valueExtensions); + + const config = someEnum.toConfig(); + expect(config.extensions).to.deep.equal(enumExtensions); + const someValueConfig = config.values.SOME_VALUE; + expect(someValueConfig.extensions).to.deep.equal(valueExtensions); + }); + }); + + describe('GraphQLInputObjectType', () => { + it('without extensions', () => { + const someInputObject = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + someInputField: { + type: dummyType, + }, + }, + }); + + expect(someInputObject.extensions).to.equal(undefined); + const someInputField = someInputObject.getFields().someInputField; + expect(someInputField.extensions).to.equal(undefined); + + const config = someInputObject.toConfig(); + expect(config.extensions).to.equal(undefined); + const someInputFieldConfig = config.fields.someInputField; + expect(someInputFieldConfig.extensions).to.equal(undefined); + }); + + it('with extensions', () => { + const inputObjectExtensions = Object.freeze({ + SomeInputObjectExt: 'inputObject', + }); + const inputFieldExtensions = Object.freeze({ + SomeInputFieldExt: 'inputField', + }); + + const someInputObject = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + someInputField: { + type: dummyType, + extensions: inputFieldExtensions, + }, + }, + extensions: inputObjectExtensions, + }); + + expect(someInputObject.extensions).to.deep.equal(inputObjectExtensions); + const someInputField = someInputObject.getFields().someInputField; + expect(someInputField.extensions).to.deep.equal(inputFieldExtensions); + + const config = someInputObject.toConfig(); + expect(config.extensions).to.deep.equal(inputObjectExtensions); + const someInputFieldConfig = config.fields.someInputField; + expect(someInputFieldConfig.extensions).to.deep.equal( + inputFieldExtensions, + ); + }); + }); + + describe('GraphQLDirective', () => { + it('without extensions', () => { + const someDirective = new GraphQLDirective({ + name: 'SomeDirective', + args: { + someArg: { + type: dummyType, + }, + }, + locations: [], + }); + + expect(someDirective.extensions).to.equal(undefined); + const someArg = someDirective.args[0]; + expect(someArg.extensions).to.equal(undefined); + + const config = someDirective.toConfig(); + expect(config.extensions).to.equal(undefined); + const someArgConfig = config.args.someArg; + expect(someArgConfig.extensions).to.equal(undefined); + }); + + it('with extensions', () => { + const directiveExtensions = Object.freeze({ + SomeDirectiveExt: 'directive', + }); + const argExtensions = Object.freeze({ SomeArgExt: 'arg' }); + + const someDirective = new GraphQLDirective({ + name: 'SomeDirective', + args: { + someArg: { + type: dummyType, + extensions: argExtensions, + }, + }, + locations: [], + extensions: directiveExtensions, + }); + + expect(someDirective.extensions).to.deep.equal(directiveExtensions); + const someArg = someDirective.args[0]; + expect(someArg.extensions).to.deep.equal(argExtensions); + + const config = someDirective.toConfig(); + expect(config.extensions).to.deep.equal(directiveExtensions); + const someArgConfig = config.args.someArg; + expect(someArgConfig.extensions).to.deep.equal(argExtensions); + }); + }); + + describe('GraphQLSchema', () => { + it('without extensions', () => { + const schema = new GraphQLSchema({}); + + expect(schema.extensions).to.equal(undefined); + + const config = schema.toConfig(); + expect(config.extensions).to.equal(undefined); + }); + + it('with extensions', () => { + const schemaExtensions = Object.freeze({ + schemaExtension: 'schema', + }); + + const schema = new GraphQLSchema({ extensions: schemaExtensions }); + + expect(schema.extensions).to.deep.equal(schemaExtensions); + + const config = schema.toConfig(); + expect(config.extensions).to.deep.equal(schemaExtensions); + }); + }); +}); diff --git a/src/type/__tests__/predicate-test.js b/src/type/__tests__/predicate-test.js index 2a5f885b62..a825db0e6c 100644 --- a/src/type/__tests__/predicate-test.js +++ b/src/type/__tests__/predicate-test.js @@ -578,6 +578,7 @@ describe('Type predicates', () => { name: 'someArg', description: undefined, defaultValue: undefined, + extensions: undefined, astNode: undefined, ...config, }; @@ -621,6 +622,7 @@ describe('Type predicates', () => { name: 'someInputField', description: undefined, defaultValue: undefined, + extensions: undefined, astNode: undefined, ...config, }; diff --git a/src/type/definition.js b/src/type/definition.js index 4c15ff69e5..2de4ded64e 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -8,16 +8,21 @@ import objectEntries from '../polyfills/objectEntries'; import inspect from '../jsutils/inspect'; import keyMap from '../jsutils/keyMap'; import mapValue from '../jsutils/mapValue'; +import toObjMap from '../jsutils/toObjMap'; import { type Path } from '../jsutils/Path'; import devAssert from '../jsutils/devAssert'; import keyValMap from '../jsutils/keyValMap'; -import { type ObjMap } from '../jsutils/ObjMap'; import instanceOf from '../jsutils/instanceOf'; import isObjectLike from '../jsutils/isObjectLike'; import identityFunc from '../jsutils/identityFunc'; import defineToJSON from '../jsutils/defineToJSON'; import defineToStringTag from '../jsutils/defineToStringTag'; import { type PromiseOrValue } from '../jsutils/PromiseOrValue'; +import { + type ObjMap, + type ReadOnlyObjMap, + type ReadOnlyObjMapLike, +} from '../jsutils/ObjMap'; import { Kind } from '../language/kinds'; import { @@ -547,6 +552,7 @@ export class GraphQLScalarType { serialize: GraphQLScalarSerializer<*>; parseValue: GraphQLScalarValueParser<*>; parseLiteral: GraphQLScalarLiteralParser<*>; + extensions: ?ReadOnlyObjMap; astNode: ?ScalarTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -558,6 +564,7 @@ export class GraphQLScalarType { this.parseValue = parseValue; this.parseLiteral = config.parseLiteral || (node => parseValue(valueFromASTUntyped(node))); + this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); @@ -581,6 +588,7 @@ export class GraphQLScalarType { serialize: GraphQLScalarSerializer<*>, parseValue: GraphQLScalarValueParser<*>, parseLiteral: GraphQLScalarLiteralParser<*>, + extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |} { return { @@ -589,6 +597,7 @@ export class GraphQLScalarType { serialize: this.serialize, parseValue: this.parseValue, parseLiteral: this.parseLiteral, + extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes || [], }; @@ -619,6 +628,7 @@ export type GraphQLScalarTypeConfig = {| parseValue?: GraphQLScalarValueParser, // Parses an externally provided literal value to use as an input. parseLiteral?: GraphQLScalarLiteralParser, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?ScalarTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, |}; @@ -664,6 +674,7 @@ export class GraphQLObjectType { name: string; description: ?string; isTypeOf: ?GraphQLIsTypeOfFn<*, *>; + extensions: ?ReadOnlyObjMap; astNode: ?ObjectTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -674,6 +685,7 @@ export class GraphQLObjectType { this.name = config.name; this.description = config.description; this.isTypeOf = config.isTypeOf; + this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); @@ -705,6 +717,7 @@ export class GraphQLObjectType { ...GraphQLObjectTypeConfig<*, *>, interfaces: Array, fields: GraphQLFieldConfigMap<*, *>, + extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |} { return { @@ -713,6 +726,7 @@ export class GraphQLObjectType { interfaces: this.getInterfaces(), fields: fieldsToFieldsConfig(this.getFields()), isTypeOf: this.isTypeOf, + extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes || [], }; @@ -775,6 +789,7 @@ function defineFieldMap( description: arg.description === undefined ? null : arg.description, type: arg.type, defaultValue: arg.defaultValue, + extensions: arg.extensions && toObjMap(arg.extensions), astNode: arg.astNode, })); @@ -787,6 +802,7 @@ function defineFieldMap( subscribe: fieldConfig.subscribe, isDeprecated: Boolean(fieldConfig.deprecationReason), deprecationReason: fieldConfig.deprecationReason, + extensions: fieldConfig.extensions && toObjMap(fieldConfig.extensions), astNode: fieldConfig.astNode, }; }); @@ -804,6 +820,7 @@ function fieldsToFieldsConfig(fields) { resolve: field.resolve, subscribe: field.subscribe, deprecationReason: field.deprecationReason, + extensions: field.extensions, astNode: field.astNode, })); } @@ -818,6 +835,7 @@ export function argsToArgsConfig( description: arg.description, type: arg.type, defaultValue: arg.defaultValue, + extensions: arg.extensions, astNode: arg.astNode, }), ); @@ -829,6 +847,7 @@ export type GraphQLObjectTypeConfig = {| interfaces?: Thunk>, fields: Thunk>, isTypeOf?: ?GraphQLIsTypeOfFn, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?ObjectTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, |}; @@ -881,6 +900,7 @@ export type GraphQLFieldConfig< resolve?: GraphQLFieldResolver, subscribe?: GraphQLFieldResolver, deprecationReason?: ?string, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?FieldDefinitionNode, |}; @@ -890,6 +910,7 @@ export type GraphQLArgumentConfig = {| description?: ?string, type: GraphQLInputType, defaultValue?: mixed, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?InputValueDefinitionNode, |}; @@ -910,6 +931,7 @@ export type GraphQLField< subscribe?: GraphQLFieldResolver, isDeprecated?: boolean, deprecationReason: ?string, + extensions: ?ReadOnlyObjMap, astNode: ?FieldDefinitionNode, |}; @@ -918,6 +940,7 @@ export type GraphQLArgument = {| description: ?string, type: GraphQLInputType, defaultValue: mixed, + extensions: ?ReadOnlyObjMap, astNode: ?InputValueDefinitionNode, |}; @@ -951,6 +974,7 @@ export class GraphQLInterfaceType { name: string; description: ?string; resolveType: ?GraphQLTypeResolver<*, *>; + extensions: ?ReadOnlyObjMap; astNode: ?InterfaceTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -960,6 +984,7 @@ export class GraphQLInterfaceType { this.name = config.name; this.description = config.description; this.resolveType = config.resolveType; + this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); @@ -982,6 +1007,7 @@ export class GraphQLInterfaceType { toConfig(): {| ...GraphQLInterfaceTypeConfig<*, *>, fields: GraphQLFieldConfigMap<*, *>, + extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |} { return { @@ -989,6 +1015,7 @@ export class GraphQLInterfaceType { description: this.description, fields: fieldsToFieldsConfig(this.getFields()), resolveType: this.resolveType, + extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes || [], }; @@ -1013,6 +1040,7 @@ export type GraphQLInterfaceTypeConfig = {| * Object type. */ resolveType?: ?GraphQLTypeResolver, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?InterfaceTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, |}; @@ -1044,6 +1072,7 @@ export class GraphQLUnionType { name: string; description: ?string; resolveType: ?GraphQLTypeResolver<*, *>; + extensions: ?ReadOnlyObjMap; astNode: ?UnionTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -1053,6 +1082,7 @@ export class GraphQLUnionType { this.name = config.name; this.description = config.description; this.resolveType = config.resolveType; + this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); @@ -1075,6 +1105,7 @@ export class GraphQLUnionType { toConfig(): {| ...GraphQLUnionTypeConfig<*, *>, types: Array, + extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |} { return { @@ -1082,6 +1113,7 @@ export class GraphQLUnionType { description: this.description, types: this.getTypes(), resolveType: this.resolveType, + extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes || [], }; @@ -1117,6 +1149,7 @@ export type GraphQLUnionTypeConfig = {| * Object type. */ resolveType?: ?GraphQLTypeResolver, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?UnionTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, |}; @@ -1145,6 +1178,7 @@ export type GraphQLUnionTypeConfig = {| export class GraphQLEnumType /* */ { name: string; description: ?string; + extensions: ?ReadOnlyObjMap; astNode: ?EnumTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -1155,6 +1189,7 @@ export class GraphQLEnumType /* */ { constructor(config: GraphQLEnumTypeConfig /* */): void { this.name = config.name; this.description = config.description; + this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); @@ -1203,6 +1238,7 @@ export class GraphQLEnumType /* */ { toConfig(): {| ...GraphQLEnumTypeConfig, + extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |} { const values = keyValMap( @@ -1212,6 +1248,7 @@ export class GraphQLEnumType /* */ { description: value.description, value: value.value, deprecationReason: value.deprecationReason, + extensions: value.extensions, astNode: value.astNode, }), ); @@ -1220,6 +1257,7 @@ export class GraphQLEnumType /* */ { name: this.name, description: this.description, values, + extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes || [], }; @@ -1258,6 +1296,7 @@ function defineEnumValues( value: 'value' in value ? value.value : valueName, isDeprecated: Boolean(value.deprecationReason), deprecationReason: value.deprecationReason, + extensions: value.extensions && toObjMap(value.extensions), astNode: value.astNode, }; }); @@ -1267,6 +1306,7 @@ export type GraphQLEnumTypeConfig /* */ = {| name: string, description?: ?string, values: GraphQLEnumValueConfigMap /* */, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?EnumTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, |}; @@ -1277,6 +1317,7 @@ export type GraphQLEnumValueConfig /* */ = {| description?: ?string, value?: any /* T */, deprecationReason?: ?string, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?EnumValueDefinitionNode, |}; @@ -1286,6 +1327,7 @@ export type GraphQLEnumValue /* */ = {| value: any /* T */, isDeprecated: boolean, deprecationReason: ?string, + extensions: ?ReadOnlyObjMap, astNode: ?EnumValueDefinitionNode, |}; @@ -1312,6 +1354,7 @@ export type GraphQLEnumValue /* */ = {| export class GraphQLInputObjectType { name: string; description: ?string; + extensions: ?ReadOnlyObjMap; astNode: ?InputObjectTypeDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -1320,6 +1363,7 @@ export class GraphQLInputObjectType { constructor(config: GraphQLInputObjectTypeConfig): void { this.name = config.name; this.description = config.description; + this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = undefineIfEmpty(config.extensionASTNodes); @@ -1337,12 +1381,14 @@ export class GraphQLInputObjectType { toConfig(): {| ...GraphQLInputObjectTypeConfig, fields: GraphQLInputFieldConfigMap, + extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |} { const fields = mapValue(this.getFields(), field => ({ description: field.description, type: field.type, defaultValue: field.defaultValue, + extensions: field.extensions, astNode: field.astNode, })); @@ -1350,6 +1396,7 @@ export class GraphQLInputObjectType { name: this.name, description: this.description, fields, + extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes || [], }; @@ -1383,6 +1430,7 @@ function defineInputFieldMap( description: fieldConfig.description, type: fieldConfig.type, defaultValue: fieldConfig.defaultValue, + extensions: fieldConfig.extensions && toObjMap(fieldConfig.extensions), astNode: fieldConfig.astNode, }; }); @@ -1392,6 +1440,7 @@ export type GraphQLInputObjectTypeConfig = {| name: string, description?: ?string, fields: Thunk, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?InputObjectTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, |}; @@ -1400,6 +1449,7 @@ export type GraphQLInputFieldConfig = {| description?: ?string, type: GraphQLInputType, defaultValue?: mixed, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?InputValueDefinitionNode, |}; @@ -1410,6 +1460,7 @@ export type GraphQLInputField = {| description: ?string, type: GraphQLInputType, defaultValue: mixed, + extensions: ?ReadOnlyObjMap, astNode: ?InputValueDefinitionNode, |}; diff --git a/src/type/directives.js b/src/type/directives.js index 9691994eba..14e60185ca 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -3,11 +3,16 @@ import objectEntries from '../polyfills/objectEntries'; import inspect from '../jsutils/inspect'; +import toObjMap from '../jsutils/toObjMap'; import devAssert from '../jsutils/devAssert'; import instanceOf from '../jsutils/instanceOf'; import defineToJSON from '../jsutils/defineToJSON'; import isObjectLike from '../jsutils/isObjectLike'; import defineToStringTag from '../jsutils/defineToStringTag'; +import { + type ReadOnlyObjMap, + type ReadOnlyObjMapLike, +} from '../jsutils/ObjMap'; import { type DirectiveDefinitionNode } from '../language/ast'; import { @@ -53,6 +58,7 @@ export class GraphQLDirective { locations: Array; isRepeatable: boolean; args: Array; + extensions: ?ReadOnlyObjMap; astNode: ?DirectiveDefinitionNode; constructor(config: GraphQLDirectiveConfig): void { @@ -60,6 +66,7 @@ export class GraphQLDirective { this.description = config.description; this.locations = config.locations; this.isRepeatable = config.isRepeatable != null && config.isRepeatable; + this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; devAssert(config.name, 'Directive must be named.'); @@ -79,6 +86,7 @@ export class GraphQLDirective { description: arg.description === undefined ? null : arg.description, type: arg.type, defaultValue: arg.defaultValue, + extensions: arg.extensions, astNode: arg.astNode, })); } @@ -90,6 +98,7 @@ export class GraphQLDirective { toConfig(): {| ...GraphQLDirectiveConfig, args: GraphQLFieldConfigArgumentMap, + extensions: ?ReadOnlyObjMap, isRepeatable: boolean, |} { return { @@ -98,6 +107,7 @@ export class GraphQLDirective { locations: this.locations, args: argsToArgsConfig(this.args), isRepeatable: this.isRepeatable, + extensions: this.extensions, astNode: this.astNode, }; } @@ -113,6 +123,7 @@ export type GraphQLDirectiveConfig = {| locations: Array, args?: ?GraphQLFieldConfigArgumentMap, isRepeatable?: ?boolean, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?DirectiveDefinitionNode, |}; diff --git a/src/type/introspection.js b/src/type/introspection.js index d74354e471..d899648bac 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -439,6 +439,7 @@ export const SchemaMetaFieldDef: GraphQLField = { args: [], resolve: (source, args, context, { schema }) => schema, deprecationReason: undefined, + extensions: undefined, astNode: undefined, }; @@ -452,11 +453,13 @@ export const TypeMetaFieldDef: GraphQLField = { description: undefined, type: GraphQLNonNull(GraphQLString), defaultValue: undefined, + extensions: undefined, astNode: undefined, }, ], resolve: (source, { name }, context, { schema }) => schema.getType(name), deprecationReason: undefined, + extensions: undefined, astNode: undefined, }; @@ -467,6 +470,7 @@ export const TypeNameMetaFieldDef: GraphQLField = { args: [], resolve: (source, args, context, { parentType }) => parentType.name, deprecationReason: undefined, + extensions: undefined, astNode: undefined, }; diff --git a/src/type/schema.js b/src/type/schema.js index 3c0fac47c3..d357288743 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -4,11 +4,16 @@ import find from '../polyfills/find'; import objectValues from '../polyfills/objectValues'; import inspect from '../jsutils/inspect'; +import toObjMap from '../jsutils/toObjMap'; import devAssert from '../jsutils/devAssert'; import instanceOf from '../jsutils/instanceOf'; -import { type ObjMap } from '../jsutils/ObjMap'; import isObjectLike from '../jsutils/isObjectLike'; import defineToStringTag from '../jsutils/defineToStringTag'; +import { + type ObjMap, + type ReadOnlyObjMap, + type ReadOnlyObjMapLike, +} from '../jsutils/ObjMap'; import { type GraphQLError } from '../error/GraphQLError'; import { @@ -115,6 +120,7 @@ export function assertSchema(schema: mixed): GraphQLSchema { * */ export class GraphQLSchema { + extensions: ?ReadOnlyObjMap; astNode: ?SchemaDefinitionNode; extensionASTNodes: ?$ReadOnlyArray; @@ -157,6 +163,7 @@ export class GraphQLSchema { ); } + this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes; @@ -263,6 +270,7 @@ export class GraphQLSchema { ...GraphQLSchemaConfig, types: Array, directives: Array, + extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, assumeValid: boolean, allowedLegacyNames: $ReadOnlyArray, @@ -273,6 +281,7 @@ export class GraphQLSchema { subscription: this.getSubscriptionType(), types: objectValues(this.getTypeMap()), directives: this.getDirectives().slice(), + extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes || [], assumeValid: this.__validationErrors !== undefined, @@ -312,6 +321,7 @@ export type GraphQLSchemaConfig = {| subscription?: ?GraphQLObjectType, types?: ?Array, directives?: ?Array, + extensions?: ?ReadOnlyObjMapLike, astNode?: ?SchemaDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, ...GraphQLSchemaValidationOptions, diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 86dabfe4da..33e5b21fca 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -362,6 +362,7 @@ describe('Type System: build schema from introspection', () => { value: 'VEGETABLES', isDeprecated: false, deprecationReason: null, + extensions: undefined, astNode: undefined, }, { @@ -370,6 +371,7 @@ describe('Type System: build schema from introspection', () => { value: 'FRUITS', isDeprecated: false, deprecationReason: null, + extensions: undefined, astNode: undefined, }, { @@ -378,6 +380,7 @@ describe('Type System: build schema from introspection', () => { value: 'OILS', isDeprecated: false, deprecationReason: null, + extensions: undefined, astNode: undefined, }, { @@ -386,6 +389,7 @@ describe('Type System: build schema from introspection', () => { value: 'DAIRY', isDeprecated: false, deprecationReason: null, + extensions: undefined, astNode: undefined, }, { @@ -394,6 +398,7 @@ describe('Type System: build schema from introspection', () => { value: 'MEAT', isDeprecated: false, deprecationReason: null, + extensions: undefined, astNode: undefined, }, ]);