schemaPrinter.js.flow 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. // @flow strict
  2. import flatMap from '../polyfills/flatMap';
  3. import objectValues from '../polyfills/objectValues';
  4. import inspect from '../jsutils/inspect';
  5. import invariant from '../jsutils/invariant';
  6. import { print } from '../language/printer';
  7. import { printBlockString } from '../language/blockString';
  8. import { type GraphQLSchema } from '../type/schema';
  9. import { isIntrospectionType } from '../type/introspection';
  10. import { GraphQLString, isSpecifiedScalarType } from '../type/scalars';
  11. import {
  12. GraphQLDirective,
  13. DEFAULT_DEPRECATION_REASON,
  14. isSpecifiedDirective,
  15. } from '../type/directives';
  16. import {
  17. type GraphQLNamedType,
  18. type GraphQLScalarType,
  19. type GraphQLEnumType,
  20. type GraphQLObjectType,
  21. type GraphQLInterfaceType,
  22. type GraphQLUnionType,
  23. type GraphQLInputObjectType,
  24. isScalarType,
  25. isObjectType,
  26. isInterfaceType,
  27. isUnionType,
  28. isEnumType,
  29. isInputObjectType,
  30. } from '../type/definition';
  31. import { astFromValue } from '../utilities/astFromValue';
  32. type Options = {|
  33. /**
  34. * Descriptions are defined as preceding string literals, however an older
  35. * experimental version of the SDL supported preceding comments as
  36. * descriptions. Set to true to enable this deprecated behavior.
  37. * This option is provided to ease adoption and will be removed in v16.
  38. *
  39. * Default: false
  40. */
  41. commentDescriptions?: boolean,
  42. |};
  43. /**
  44. * Accepts options as a second argument:
  45. *
  46. * - commentDescriptions:
  47. * Provide true to use preceding comments as the description.
  48. *
  49. */
  50. export function printSchema(schema: GraphQLSchema, options?: Options): string {
  51. return printFilteredSchema(
  52. schema,
  53. n => !isSpecifiedDirective(n),
  54. isDefinedType,
  55. options,
  56. );
  57. }
  58. export function printIntrospectionSchema(
  59. schema: GraphQLSchema,
  60. options?: Options,
  61. ): string {
  62. return printFilteredSchema(
  63. schema,
  64. isSpecifiedDirective,
  65. isIntrospectionType,
  66. options,
  67. );
  68. }
  69. function isDefinedType(type: GraphQLNamedType): boolean {
  70. return !isSpecifiedScalarType(type) && !isIntrospectionType(type);
  71. }
  72. function printFilteredSchema(
  73. schema: GraphQLSchema,
  74. directiveFilter: (type: GraphQLDirective) => boolean,
  75. typeFilter: (type: GraphQLNamedType) => boolean,
  76. options,
  77. ): string {
  78. const directives = schema.getDirectives().filter(directiveFilter);
  79. const typeMap = schema.getTypeMap();
  80. const types = objectValues(typeMap)
  81. .sort((type1, type2) => type1.name.localeCompare(type2.name))
  82. .filter(typeFilter);
  83. return (
  84. [printSchemaDefinition(schema)]
  85. .concat(
  86. directives.map(directive => printDirective(directive, options)),
  87. types.map(type => printType(type, options)),
  88. )
  89. .filter(Boolean)
  90. .join('\n\n') + '\n'
  91. );
  92. }
  93. function printSchemaDefinition(schema: GraphQLSchema): ?string {
  94. if (isSchemaOfCommonNames(schema)) {
  95. return;
  96. }
  97. const operationTypes = [];
  98. const queryType = schema.getQueryType();
  99. if (queryType) {
  100. operationTypes.push(` query: ${queryType.name}`);
  101. }
  102. const mutationType = schema.getMutationType();
  103. if (mutationType) {
  104. operationTypes.push(` mutation: ${mutationType.name}`);
  105. }
  106. const subscriptionType = schema.getSubscriptionType();
  107. if (subscriptionType) {
  108. operationTypes.push(` subscription: ${subscriptionType.name}`);
  109. }
  110. return `schema {\n${operationTypes.join('\n')}\n}`;
  111. }
  112. /**
  113. * GraphQL schema define root types for each type of operation. These types are
  114. * the same as any other type and can be named in any manner, however there is
  115. * a common naming convention:
  116. *
  117. * schema {
  118. * query: Query
  119. * mutation: Mutation
  120. * }
  121. *
  122. * When using this naming convention, the schema description can be omitted.
  123. */
  124. function isSchemaOfCommonNames(schema: GraphQLSchema): boolean {
  125. const queryType = schema.getQueryType();
  126. if (queryType && queryType.name !== 'Query') {
  127. return false;
  128. }
  129. const mutationType = schema.getMutationType();
  130. if (mutationType && mutationType.name !== 'Mutation') {
  131. return false;
  132. }
  133. const subscriptionType = schema.getSubscriptionType();
  134. if (subscriptionType && subscriptionType.name !== 'Subscription') {
  135. return false;
  136. }
  137. return true;
  138. }
  139. export function printType(type: GraphQLNamedType, options?: Options): string {
  140. if (isScalarType(type)) {
  141. return printScalar(type, options);
  142. } else if (isObjectType(type)) {
  143. return printObject(type, options);
  144. } else if (isInterfaceType(type)) {
  145. return printInterface(type, options);
  146. } else if (isUnionType(type)) {
  147. return printUnion(type, options);
  148. } else if (isEnumType(type)) {
  149. return printEnum(type, options);
  150. } else if (isInputObjectType(type)) {
  151. return printInputObject(type, options);
  152. }
  153. // Not reachable. All possible types have been considered.
  154. invariant(false, 'Unexpected type: ' + inspect((type: empty)));
  155. }
  156. function printScalar(type: GraphQLScalarType, options): string {
  157. return printDescription(options, type) + `scalar ${type.name}`;
  158. }
  159. function printObject(type: GraphQLObjectType, options): string {
  160. const interfaces = type.getInterfaces();
  161. const implementedInterfaces = interfaces.length
  162. ? ' implements ' + interfaces.map(i => i.name).join(' & ')
  163. : '';
  164. return (
  165. printDescription(options, type) +
  166. `type ${type.name}${implementedInterfaces}` +
  167. printFields(options, type)
  168. );
  169. }
  170. function printInterface(type: GraphQLInterfaceType, options): string {
  171. return (
  172. printDescription(options, type) +
  173. `interface ${type.name}` +
  174. printFields(options, type)
  175. );
  176. }
  177. function printUnion(type: GraphQLUnionType, options): string {
  178. const types = type.getTypes();
  179. const possibleTypes = types.length ? ' = ' + types.join(' | ') : '';
  180. return printDescription(options, type) + 'union ' + type.name + possibleTypes;
  181. }
  182. function printEnum(type: GraphQLEnumType, options): string {
  183. const values = type
  184. .getValues()
  185. .map(
  186. (value, i) =>
  187. printDescription(options, value, ' ', !i) +
  188. ' ' +
  189. value.name +
  190. printDeprecated(value),
  191. );
  192. return (
  193. printDescription(options, type) + `enum ${type.name}` + printBlock(values)
  194. );
  195. }
  196. function printInputObject(type: GraphQLInputObjectType, options): string {
  197. const fields = objectValues(type.getFields()).map(
  198. (f, i) =>
  199. printDescription(options, f, ' ', !i) + ' ' + printInputValue(f),
  200. );
  201. return (
  202. printDescription(options, type) + `input ${type.name}` + printBlock(fields)
  203. );
  204. }
  205. function printFields(options, type) {
  206. const fields = objectValues(type.getFields()).map(
  207. (f, i) =>
  208. printDescription(options, f, ' ', !i) +
  209. ' ' +
  210. f.name +
  211. printArgs(options, f.args, ' ') +
  212. ': ' +
  213. String(f.type) +
  214. printDeprecated(f),
  215. );
  216. return printBlock(fields);
  217. }
  218. function printBlock(items) {
  219. return items.length !== 0 ? ' {\n' + items.join('\n') + '\n}' : '';
  220. }
  221. function printArgs(options, args, indentation = '') {
  222. if (args.length === 0) {
  223. return '';
  224. }
  225. // If every arg does not have a description, print them on one line.
  226. if (args.every(arg => !arg.description)) {
  227. return '(' + args.map(printInputValue).join(', ') + ')';
  228. }
  229. return (
  230. '(\n' +
  231. args
  232. .map(
  233. (arg, i) =>
  234. printDescription(options, arg, ' ' + indentation, !i) +
  235. ' ' +
  236. indentation +
  237. printInputValue(arg),
  238. )
  239. .join('\n') +
  240. '\n' +
  241. indentation +
  242. ')'
  243. );
  244. }
  245. function printInputValue(arg) {
  246. const defaultAST = astFromValue(arg.defaultValue, arg.type);
  247. let argDecl = arg.name + ': ' + String(arg.type);
  248. if (defaultAST) {
  249. argDecl += ` = ${print(defaultAST)}`;
  250. }
  251. return argDecl;
  252. }
  253. function printDirective(directive, options) {
  254. return (
  255. printDescription(options, directive) +
  256. 'directive @' +
  257. directive.name +
  258. printArgs(options, directive.args) +
  259. (directive.isRepeatable ? ' repeatable' : '') +
  260. ' on ' +
  261. directive.locations.join(' | ')
  262. );
  263. }
  264. function printDeprecated(fieldOrEnumVal) {
  265. if (!fieldOrEnumVal.isDeprecated) {
  266. return '';
  267. }
  268. const reason = fieldOrEnumVal.deprecationReason;
  269. const reasonAST = astFromValue(reason, GraphQLString);
  270. if (reasonAST && reason !== '' && reason !== DEFAULT_DEPRECATION_REASON) {
  271. return ' @deprecated(reason: ' + print(reasonAST) + ')';
  272. }
  273. return ' @deprecated';
  274. }
  275. function printDescription(
  276. options,
  277. def,
  278. indentation = '',
  279. firstInBlock = true,
  280. ): string {
  281. if (!def.description) {
  282. return '';
  283. }
  284. const lines = descriptionLines(def.description, 120 - indentation.length);
  285. if (options && options.commentDescriptions) {
  286. return printDescriptionWithComments(lines, indentation, firstInBlock);
  287. }
  288. const text = lines.join('\n');
  289. const preferMultipleLines = text.length > 70;
  290. const blockString = printBlockString(text, '', preferMultipleLines);
  291. const prefix =
  292. indentation && !firstInBlock ? '\n' + indentation : indentation;
  293. return prefix + blockString.replace(/\n/g, '\n' + indentation) + '\n';
  294. }
  295. function printDescriptionWithComments(lines, indentation, firstInBlock) {
  296. let description = indentation && !firstInBlock ? '\n' : '';
  297. for (const line of lines) {
  298. if (line === '') {
  299. description += indentation + '#\n';
  300. } else {
  301. description += indentation + '# ' + line + '\n';
  302. }
  303. }
  304. return description;
  305. }
  306. function descriptionLines(description: string, maxLen: number): Array<string> {
  307. const rawLines = description.split('\n');
  308. return flatMap(rawLines, line => {
  309. if (line.length < maxLen + 5) {
  310. return line;
  311. }
  312. // For > 120 character long lines, cut at space boundaries into sublines
  313. // of ~80 chars.
  314. return breakLine(line, maxLen);
  315. });
  316. }
  317. function breakLine(line: string, maxLen: number): Array<string> {
  318. const parts = line.split(new RegExp(`((?: |^).{15,${maxLen - 40}}(?= |$))`));
  319. if (parts.length < 4) {
  320. return [line];
  321. }
  322. const sublines = [parts[0] + parts[1] + parts[2]];
  323. for (let i = 3; i < parts.length; i += 2) {
  324. sublines.push(parts[i].slice(1) + parts[i + 1]);
  325. }
  326. return sublines;
  327. }