1
0

graphql-errors-plugin.ts 9.4 KB

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