graphql-errors-plugin.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import { PluginFunction } from '@graphql-codegen/plugin-helpers';
  2. import { buildScalars } from '@graphql-codegen/visitor-plugin-common';
  3. import {
  4. ASTNode,
  5. ASTVisitor,
  6. FieldDefinitionNode,
  7. getNamedType,
  8. GraphQLFieldMap,
  9. GraphQLNamedType,
  10. GraphQLObjectType,
  11. GraphQLSchema,
  12. GraphQLType,
  13. GraphQLUnionType,
  14. InterfaceTypeDefinitionNode,
  15. isNamedType,
  16. isObjectType,
  17. isTypeDefinitionNode,
  18. isUnionType,
  19. Kind,
  20. ListTypeNode,
  21. NonNullTypeNode,
  22. ObjectTypeDefinitionNode,
  23. parse,
  24. printSchema,
  25. UnionTypeDefinitionNode,
  26. visit,
  27. } from 'graphql';
  28. import { ASTVisitFn } from 'graphql/language/visitor';
  29. // This plugin generates classes for all GraphQL types which implement the `ErrorResult` interface.
  30. // This means that when returning an error result from a GraphQL operation, you can use one of
  31. // the generated classes rather than constructing the object by hand.
  32. // It also generates type resolvers to be used by Apollo Server to discriminate between
  33. // members of returned union types.
  34. export const ERROR_INTERFACE_NAME = 'ErrorResult';
  35. const empty = () => '';
  36. type TransformedField = { name: string; type: string };
  37. const errorsVisitor: ASTVisitFn<ASTNode> = (node, key, parent) => {
  38. switch (node.kind) {
  39. case Kind.NON_NULL_TYPE: {
  40. return node.type.kind === 'NamedType'
  41. ? node.type.name.value
  42. : node.type.kind === 'ListType'
  43. ? node.type
  44. : '';
  45. }
  46. case Kind.FIELD_DEFINITION: {
  47. const type = (node.type.kind === 'ListType' ? node.type.type : node.type) as unknown as string;
  48. const tsType = isScalar(type) ? `Scalars['${type}']` : 'any';
  49. const listPart = node.type.kind === 'ListType' ? `[]` : ``;
  50. return { name: node.name.value, type: `${tsType}${listPart}` };
  51. }
  52. case Kind.SCALAR_TYPE_DEFINITION: {
  53. return '';
  54. }
  55. case Kind.INPUT_OBJECT_TYPE_DEFINITION: {
  56. return '';
  57. }
  58. case Kind.ENUM_TYPE_DEFINITION: {
  59. return '';
  60. }
  61. case Kind.UNION_TYPE_DEFINITION: {
  62. return '';
  63. }
  64. case Kind.INTERFACE_TYPE_DEFINITION: {
  65. if (node.name.value !== ERROR_INTERFACE_NAME) {
  66. return '';
  67. }
  68. return [
  69. `export class ${ERROR_INTERFACE_NAME} {`,
  70. ` readonly __typename: string;`,
  71. ` readonly errorCode: string;`,
  72. ...node.fields
  73. .filter(f => (f as any as TransformedField).name !== 'errorCode')
  74. .map(f => ` readonly ${f.name}: ${f.type};`),
  75. `}`,
  76. ].join('\n');
  77. }
  78. case Kind.OBJECT_TYPE_DEFINITION: {
  79. if (!inheritsFromErrorResult(node)) {
  80. return '';
  81. }
  82. const originalNode = parent[key] as ObjectTypeDefinitionNode;
  83. const constructorArgs = (node.fields as any as TransformedField[]).filter(
  84. f => f.name !== 'errorCode' && f.name !== 'message',
  85. );
  86. return [
  87. `export class ${node.name.value} extends ${ERROR_INTERFACE_NAME} {`,
  88. ` readonly __typename = '${node.name.value}';`,
  89. // We cast this to "any" otherwise we need to specify it as type "ErrorCode",
  90. // which means shared ErrorResult classes e.g. OrderStateTransitionError
  91. // will not be compatible between the admin and shop variations.
  92. ` readonly errorCode = '${camelToUpperSnakeCase(node.name.value)}' as any;`,
  93. ` readonly message = '${camelToUpperSnakeCase(node.name.value)}';`,
  94. ...constructorArgs.map(f => ` readonly ${f.name}: ${f.type};`),
  95. ` constructor(`,
  96. constructorArgs.length
  97. ? ` input: { ${constructorArgs.map(f => `${f.name}: ${f.type}`).join(', ')} }`
  98. : '',
  99. ` ) {`,
  100. ` super();`,
  101. ...(constructorArgs.length
  102. ? constructorArgs.map(f => ` this.${f.name} = input.${f.name}`)
  103. : []),
  104. ` }`,
  105. `}`,
  106. ].join('\n');
  107. }
  108. }
  109. };
  110. export const plugin: PluginFunction<any> = (schema, documents, config, info) => {
  111. const printedSchema = printSchema(schema); // Returns a string representation of the schema
  112. const astNode = parse(printedSchema); // Transforms the string into ASTNode
  113. const result = visit(astNode, { leave: errorsVisitor });
  114. const defs = result.definitions
  115. .filter(d => !!d)
  116. // Ensure the ErrorResult base class is first
  117. .sort((a, b) => ((a as any).includes('class ErrorResult') ? -1 : 1));
  118. return {
  119. content: [
  120. `/** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */`,
  121. generateScalars(schema, config),
  122. ...defs,
  123. defs.length ? generateIsErrorFunction(schema) : '',
  124. generateTypeResolvers(schema),
  125. ].join('\n\n'),
  126. };
  127. };
  128. function generateScalars(schema: GraphQLSchema, config: any): string {
  129. const scalarMap = buildScalars(schema, config.scalars);
  130. const allScalars = Object.keys(scalarMap)
  131. .map(scalarName => {
  132. const scalarValue = scalarMap[scalarName].type;
  133. const scalarType = schema.getType(scalarName);
  134. return ` ${scalarName}: ${scalarValue};`;
  135. })
  136. .join('\n');
  137. return `export type Scalars = {\n${allScalars}\n};`;
  138. }
  139. function generateErrorClassSource(node: ObjectTypeDefinitionNode) {
  140. let source = `export class ${node.name.value} {`;
  141. for (const field of node.fields) {
  142. source += ` ${1}`;
  143. }
  144. }
  145. function generateIsErrorFunction(schema: GraphQLSchema) {
  146. const errorNodes = Object.values(schema.getTypeMap())
  147. .map(type => type.astNode)
  148. .filter(isObjectTypeDefinition)
  149. .filter(node => inheritsFromErrorResult(node));
  150. return `
  151. const errorTypeNames = new Set([${errorNodes.map(n => `'${n.name.value}'`).join(', ')}]);
  152. function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').${ERROR_INTERFACE_NAME} {
  153. return input instanceof ${ERROR_INTERFACE_NAME} || errorTypeNames.has(input.__typename);
  154. }`;
  155. }
  156. function generateTypeResolvers(schema: GraphQLSchema) {
  157. const mutations = getOperationsThatReturnErrorUnions(schema, schema.getMutationType().getFields());
  158. const queries = getOperationsThatReturnErrorUnions(schema, schema.getQueryType().getFields());
  159. const operations = [...mutations, ...queries];
  160. const varName = isAdminApi(schema)
  161. ? `adminErrorOperationTypeResolvers`
  162. : `shopErrorOperationTypeResolvers`;
  163. const result = [`export const ${varName} = {`];
  164. const typesHandled = new Set<string>();
  165. for (const operation of operations) {
  166. const returnType = unwrapType(operation.type) as GraphQLUnionType;
  167. if (!typesHandled.has(returnType.name)) {
  168. typesHandled.add(returnType.name);
  169. const nonErrorResult = returnType.getTypes().find(t => !inheritsFromErrorResult(t));
  170. result.push(
  171. ` ${returnType.name}: {`,
  172. ` __resolveType(value: any) {`,
  173. // tslint:disable-next-line:no-non-null-assertion
  174. ` return isGraphQLError(value) ? (value as any).__typename : '${nonErrorResult!.name}';`,
  175. ` },`,
  176. ` },`,
  177. );
  178. }
  179. }
  180. result.push(`};`);
  181. return result.join('\n');
  182. }
  183. function getOperationsThatReturnErrorUnions(schema: GraphQLSchema, fields: GraphQLFieldMap<any, any>) {
  184. return Object.values(fields).filter(operation => {
  185. const innerType = unwrapType(operation.type);
  186. if (innerType.astNode?.kind === 'UnionTypeDefinition') {
  187. return isUnionOfResultAndErrors(schema, innerType.astNode);
  188. }
  189. return false;
  190. });
  191. }
  192. function isUnionOfResultAndErrors(schema: GraphQLSchema, node: UnionTypeDefinitionNode) {
  193. const errorResultTypes = node.types.filter(namedType => {
  194. const type = schema.getType(namedType.name.value);
  195. if (isObjectType(type)) {
  196. if (inheritsFromErrorResult(type)) {
  197. return true;
  198. }
  199. }
  200. return false;
  201. });
  202. return (errorResultTypes.length = node.types.length - 1);
  203. }
  204. function isObjectTypeDefinition(node: any): node is ObjectTypeDefinitionNode {
  205. return node && isTypeDefinitionNode(node) && node.kind === 'ObjectTypeDefinition';
  206. }
  207. function inheritsFromErrorResult(node: ObjectTypeDefinitionNode | GraphQLObjectType): boolean {
  208. const interfaceNames = isObjectType(node)
  209. ? node.getInterfaces().map(i => i.name)
  210. : node.interfaces.map(i => i.name.value);
  211. return interfaceNames.includes(ERROR_INTERFACE_NAME);
  212. }
  213. /**
  214. * Unwraps the inner type from a higher-order type, e.g. [Address!]! => Address
  215. */
  216. function unwrapType(type: GraphQLType): GraphQLNamedType {
  217. return getNamedType(type);
  218. }
  219. function isAdminApi(schema: GraphQLSchema): boolean {
  220. return !!schema.getType('UpdateGlobalSettingsInput');
  221. }
  222. function camelToUpperSnakeCase(input: string): string {
  223. return input.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
  224. }
  225. function isScalar(type: string): boolean {
  226. return ['ID', 'String', 'Boolean', 'Int', 'Float', 'JSON', 'DateTime', 'Upload'].includes(type);
  227. }