1
0

graphql-errors-plugin.ts 8.7 KB

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