extendSchema.js.flow 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. // @flow strict
  2. import flatMap from '../polyfills/flatMap';
  3. import objectValues from '../polyfills/objectValues';
  4. import inspect from '../jsutils/inspect';
  5. import mapValue from '../jsutils/mapValue';
  6. import invariant from '../jsutils/invariant';
  7. import devAssert from '../jsutils/devAssert';
  8. import keyValMap from '../jsutils/keyValMap';
  9. import { Kind } from '../language/kinds';
  10. import {
  11. isTypeDefinitionNode,
  12. isTypeExtensionNode,
  13. } from '../language/predicates';
  14. import {
  15. type DocumentNode,
  16. type DirectiveDefinitionNode,
  17. type SchemaExtensionNode,
  18. type SchemaDefinitionNode,
  19. } from '../language/ast';
  20. import { assertValidSDLExtension } from '../validation/validate';
  21. import { GraphQLDirective } from '../type/directives';
  22. import { isSpecifiedScalarType } from '../type/scalars';
  23. import { isIntrospectionType } from '../type/introspection';
  24. import {
  25. type GraphQLSchemaValidationOptions,
  26. assertSchema,
  27. GraphQLSchema,
  28. } from '../type/schema';
  29. import {
  30. type GraphQLNamedType,
  31. isScalarType,
  32. isObjectType,
  33. isInterfaceType,
  34. isUnionType,
  35. isListType,
  36. isNonNullType,
  37. isEnumType,
  38. isInputObjectType,
  39. GraphQLList,
  40. GraphQLNonNull,
  41. GraphQLScalarType,
  42. GraphQLObjectType,
  43. GraphQLInterfaceType,
  44. GraphQLUnionType,
  45. GraphQLEnumType,
  46. GraphQLInputObjectType,
  47. } from '../type/definition';
  48. import { ASTDefinitionBuilder } from './buildASTSchema';
  49. type Options = {|
  50. ...GraphQLSchemaValidationOptions,
  51. /**
  52. * Descriptions are defined as preceding string literals, however an older
  53. * experimental version of the SDL supported preceding comments as
  54. * descriptions. Set to true to enable this deprecated behavior.
  55. * This option is provided to ease adoption and will be removed in v16.
  56. *
  57. * Default: false
  58. */
  59. commentDescriptions?: boolean,
  60. /**
  61. * Set to true to assume the SDL is valid.
  62. *
  63. * Default: false
  64. */
  65. assumeValidSDL?: boolean,
  66. |};
  67. /**
  68. * Produces a new schema given an existing schema and a document which may
  69. * contain GraphQL type extensions and definitions. The original schema will
  70. * remain unaltered.
  71. *
  72. * Because a schema represents a graph of references, a schema cannot be
  73. * extended without effectively making an entire copy. We do not know until it's
  74. * too late if subgraphs remain unchanged.
  75. *
  76. * This algorithm copies the provided schema, applying extensions while
  77. * producing the copy. The original schema remains unaltered.
  78. *
  79. * Accepts options as a third argument:
  80. *
  81. * - commentDescriptions:
  82. * Provide true to use preceding comments as the description.
  83. *
  84. */
  85. export function extendSchema(
  86. schema: GraphQLSchema,
  87. documentAST: DocumentNode,
  88. options?: Options,
  89. ): GraphQLSchema {
  90. assertSchema(schema);
  91. devAssert(
  92. documentAST && documentAST.kind === Kind.DOCUMENT,
  93. 'Must provide valid Document AST',
  94. );
  95. if (!options || !(options.assumeValid || options.assumeValidSDL)) {
  96. assertValidSDLExtension(documentAST, schema);
  97. }
  98. // Collect the type definitions and extensions found in the document.
  99. const typeDefs = [];
  100. const typeExtsMap = Object.create(null);
  101. // New directives and types are separate because a directives and types can
  102. // have the same name. For example, a type named "skip".
  103. const directiveDefs: Array<DirectiveDefinitionNode> = [];
  104. let schemaDef: ?SchemaDefinitionNode;
  105. // Schema extensions are collected which may add additional operation types.
  106. const schemaExts: Array<SchemaExtensionNode> = [];
  107. for (const def of documentAST.definitions) {
  108. if (def.kind === Kind.SCHEMA_DEFINITION) {
  109. schemaDef = def;
  110. } else if (def.kind === Kind.SCHEMA_EXTENSION) {
  111. schemaExts.push(def);
  112. } else if (isTypeDefinitionNode(def)) {
  113. typeDefs.push(def);
  114. } else if (isTypeExtensionNode(def)) {
  115. const extendedTypeName = def.name.value;
  116. const existingTypeExts = typeExtsMap[extendedTypeName];
  117. typeExtsMap[extendedTypeName] = existingTypeExts
  118. ? existingTypeExts.concat([def])
  119. : [def];
  120. } else if (def.kind === Kind.DIRECTIVE_DEFINITION) {
  121. directiveDefs.push(def);
  122. }
  123. }
  124. // If this document contains no new types, extensions, or directives then
  125. // return the same unmodified GraphQLSchema instance.
  126. if (
  127. Object.keys(typeExtsMap).length === 0 &&
  128. typeDefs.length === 0 &&
  129. directiveDefs.length === 0 &&
  130. schemaExts.length === 0 &&
  131. !schemaDef
  132. ) {
  133. return schema;
  134. }
  135. const schemaConfig = schema.toConfig();
  136. const astBuilder = new ASTDefinitionBuilder(options, typeName => {
  137. const type = typeMap[typeName];
  138. if (type === undefined) {
  139. throw new Error(`Unknown type: "${typeName}".`);
  140. }
  141. return type;
  142. });
  143. const typeMap = keyValMap(
  144. typeDefs,
  145. node => node.name.value,
  146. node => astBuilder.buildType(node),
  147. );
  148. for (const existingType of schemaConfig.types) {
  149. typeMap[existingType.name] = extendNamedType(existingType);
  150. }
  151. // Get the extended root operation types.
  152. const operationTypes = {
  153. query: schemaConfig.query && schemaConfig.query.name,
  154. mutation: schemaConfig.mutation && schemaConfig.mutation.name,
  155. subscription: schemaConfig.subscription && schemaConfig.subscription.name,
  156. };
  157. if (schemaDef) {
  158. for (const { operation, type } of schemaDef.operationTypes) {
  159. operationTypes[operation] = type.name.value;
  160. }
  161. }
  162. // Then, incorporate schema definition and all schema extensions.
  163. for (const schemaExt of schemaExts) {
  164. if (schemaExt.operationTypes) {
  165. for (const { operation, type } of schemaExt.operationTypes) {
  166. operationTypes[operation] = type.name.value;
  167. }
  168. }
  169. }
  170. // Support both original legacy names and extended legacy names.
  171. const allowedLegacyNames = schemaConfig.allowedLegacyNames.concat(
  172. (options && options.allowedLegacyNames) || [],
  173. );
  174. // Then produce and return a Schema with these types.
  175. return new GraphQLSchema({
  176. // Note: While this could make early assertions to get the correctly
  177. // typed values, that would throw immediately while type system
  178. // validation with validateSchema() will produce more actionable results.
  179. query: (getMaybeTypeByName(operationTypes.query): any),
  180. mutation: (getMaybeTypeByName(operationTypes.mutation): any),
  181. subscription: (getMaybeTypeByName(operationTypes.subscription): any),
  182. types: objectValues(typeMap),
  183. directives: getMergedDirectives(),
  184. astNode: schemaDef || schemaConfig.astNode,
  185. extensionASTNodes: schemaConfig.extensionASTNodes.concat(schemaExts),
  186. allowedLegacyNames,
  187. });
  188. // Below are functions used for producing this schema that have closed over
  189. // this scope and have access to the schema, cache, and newly defined types.
  190. function replaceType(type) {
  191. if (isListType(type)) {
  192. return new GraphQLList(replaceType(type.ofType));
  193. } else if (isNonNullType(type)) {
  194. return new GraphQLNonNull(replaceType(type.ofType));
  195. }
  196. return replaceNamedType(type);
  197. }
  198. function replaceNamedType<T: GraphQLNamedType>(type: T): T {
  199. return ((typeMap[type.name]: any): T);
  200. }
  201. function getMaybeTypeByName(typeName: ?string): ?GraphQLNamedType {
  202. return typeName ? typeMap[typeName] : null;
  203. }
  204. function getMergedDirectives(): Array<GraphQLDirective> {
  205. const existingDirectives = schema.getDirectives().map(extendDirective);
  206. devAssert(existingDirectives, 'schema must have default directives');
  207. return existingDirectives.concat(
  208. directiveDefs.map(node => astBuilder.buildDirective(node)),
  209. );
  210. }
  211. function extendNamedType(type: GraphQLNamedType): GraphQLNamedType {
  212. if (isIntrospectionType(type) || isSpecifiedScalarType(type)) {
  213. // Builtin types are not extended.
  214. return type;
  215. } else if (isScalarType(type)) {
  216. return extendScalarType(type);
  217. } else if (isObjectType(type)) {
  218. return extendObjectType(type);
  219. } else if (isInterfaceType(type)) {
  220. return extendInterfaceType(type);
  221. } else if (isUnionType(type)) {
  222. return extendUnionType(type);
  223. } else if (isEnumType(type)) {
  224. return extendEnumType(type);
  225. } else if (isInputObjectType(type)) {
  226. return extendInputObjectType(type);
  227. }
  228. // Not reachable. All possible types have been considered.
  229. invariant(false, 'Unexpected type: ' + inspect((type: empty)));
  230. }
  231. function extendDirective(directive: GraphQLDirective): GraphQLDirective {
  232. const config = directive.toConfig();
  233. return new GraphQLDirective({
  234. ...config,
  235. args: mapValue(config.args, extendArg),
  236. });
  237. }
  238. function extendInputObjectType(
  239. type: GraphQLInputObjectType,
  240. ): GraphQLInputObjectType {
  241. const config = type.toConfig();
  242. const extensions = typeExtsMap[config.name] || [];
  243. const fieldNodes = flatMap(extensions, node => node.fields || []);
  244. return new GraphQLInputObjectType({
  245. ...config,
  246. fields: () => ({
  247. ...mapValue(config.fields, field => ({
  248. ...field,
  249. type: replaceType(field.type),
  250. })),
  251. ...keyValMap(
  252. fieldNodes,
  253. field => field.name.value,
  254. field => astBuilder.buildInputField(field),
  255. ),
  256. }),
  257. extensionASTNodes: config.extensionASTNodes.concat(extensions),
  258. });
  259. }
  260. function extendEnumType(type: GraphQLEnumType): GraphQLEnumType {
  261. const config = type.toConfig();
  262. const extensions = typeExtsMap[type.name] || [];
  263. const valueNodes = flatMap(extensions, node => node.values || []);
  264. return new GraphQLEnumType({
  265. ...config,
  266. values: {
  267. ...config.values,
  268. ...keyValMap(
  269. valueNodes,
  270. value => value.name.value,
  271. value => astBuilder.buildEnumValue(value),
  272. ),
  273. },
  274. extensionASTNodes: config.extensionASTNodes.concat(extensions),
  275. });
  276. }
  277. function extendScalarType(type: GraphQLScalarType): GraphQLScalarType {
  278. const config = type.toConfig();
  279. const extensions = typeExtsMap[config.name] || [];
  280. return new GraphQLScalarType({
  281. ...config,
  282. extensionASTNodes: config.extensionASTNodes.concat(extensions),
  283. });
  284. }
  285. function extendObjectType(type: GraphQLObjectType): GraphQLObjectType {
  286. const config = type.toConfig();
  287. const extensions = typeExtsMap[config.name] || [];
  288. const interfaceNodes = flatMap(extensions, node => node.interfaces || []);
  289. const fieldNodes = flatMap(extensions, node => node.fields || []);
  290. return new GraphQLObjectType({
  291. ...config,
  292. interfaces: () => [
  293. ...type.getInterfaces().map(replaceNamedType),
  294. // Note: While this could make early assertions to get the correctly
  295. // typed values, that would throw immediately while type system
  296. // validation with validateSchema() will produce more actionable results.
  297. ...interfaceNodes.map(node => (astBuilder.getNamedType(node): any)),
  298. ],
  299. fields: () => ({
  300. ...mapValue(config.fields, extendField),
  301. ...keyValMap(
  302. fieldNodes,
  303. node => node.name.value,
  304. node => astBuilder.buildField(node),
  305. ),
  306. }),
  307. extensionASTNodes: config.extensionASTNodes.concat(extensions),
  308. });
  309. }
  310. function extendInterfaceType(
  311. type: GraphQLInterfaceType,
  312. ): GraphQLInterfaceType {
  313. const config = type.toConfig();
  314. const extensions = typeExtsMap[config.name] || [];
  315. const fieldNodes = flatMap(extensions, node => node.fields || []);
  316. return new GraphQLInterfaceType({
  317. ...config,
  318. fields: () => ({
  319. ...mapValue(config.fields, extendField),
  320. ...keyValMap(
  321. fieldNodes,
  322. node => node.name.value,
  323. node => astBuilder.buildField(node),
  324. ),
  325. }),
  326. extensionASTNodes: config.extensionASTNodes.concat(extensions),
  327. });
  328. }
  329. function extendUnionType(type: GraphQLUnionType): GraphQLUnionType {
  330. const config = type.toConfig();
  331. const extensions = typeExtsMap[config.name] || [];
  332. const typeNodes = flatMap(extensions, node => node.types || []);
  333. return new GraphQLUnionType({
  334. ...config,
  335. types: () => [
  336. ...type.getTypes().map(replaceNamedType),
  337. // Note: While this could make early assertions to get the correctly
  338. // typed values, that would throw immediately while type system
  339. // validation with validateSchema() will produce more actionable results.
  340. ...typeNodes.map(node => (astBuilder.getNamedType(node): any)),
  341. ],
  342. extensionASTNodes: config.extensionASTNodes.concat(extensions),
  343. });
  344. }
  345. function extendField(field) {
  346. return {
  347. ...field,
  348. type: replaceType(field.type),
  349. args: mapValue(field.args, extendArg),
  350. };
  351. }
  352. function extendArg(arg) {
  353. return {
  354. ...arg,
  355. type: replaceType(arg.type),
  356. };
  357. }
  358. }