graphql-errors-plugin.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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. } from 'graphql';
  24. // This plugin generates classes for all GraphQL types which implement the `ErrorResult` interface.
  25. // This means that when returning an error result from a GraphQL operation, you can use one of
  26. // the generated classes rather than constructing the object by hand.
  27. // It also generates type resolvers to be used by Apollo Server to discriminate between
  28. // members of returned union types.
  29. export const ERROR_INTERFACE_NAME = 'ErrorResult';
  30. const empty = () => '';
  31. const errorsVisitor: Visitor<any> = {
  32. NonNullType(node: NonNullTypeNode): string {
  33. return node.type.kind === 'NamedType' ? node.type.name.value : '';
  34. },
  35. FieldDefinition(node: FieldDefinitionNode): string {
  36. return ` ${node.name.value}: Scalars['${node.type}']`;
  37. },
  38. ScalarTypeDefinition: empty,
  39. InputObjectTypeDefinition: empty,
  40. EnumTypeDefinition: empty,
  41. UnionTypeDefinition: empty,
  42. InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode) {
  43. if (node.name.value !== ERROR_INTERFACE_NAME) {
  44. return '';
  45. }
  46. return [
  47. `export class ${ERROR_INTERFACE_NAME} {`,
  48. ` readonly __typename: string;`,
  49. ` readonly code: ErrorCode;`,
  50. ...node.fields.filter(f => !(f as any).includes('code:')).map(f => `${f};`),
  51. `}`,
  52. ].join('\n');
  53. },
  54. ObjectTypeDefinition(
  55. node: ObjectTypeDefinitionNode,
  56. key: number | string | undefined,
  57. parent: any,
  58. ): string {
  59. if (!inheritsFromErrorResult(node)) {
  60. return '';
  61. }
  62. const originalNode = parent[key] as ObjectTypeDefinitionNode;
  63. return [
  64. `export class ${node.name.value} extends ${ERROR_INTERFACE_NAME} {`,
  65. ` readonly __typename = '${node.name.value}';`,
  66. ` readonly code = ErrorCode.${node.name.value};`,
  67. ` constructor(`,
  68. ...node.fields.filter(f => !(f as any).includes('code:')).map(f => ` public ${f},`),
  69. ` ) {`,
  70. ` super();`,
  71. ` }`,
  72. `}`,
  73. ].join('\n');
  74. },
  75. };
  76. export const plugin: PluginFunction<any> = (schema, documents, config, info) => {
  77. const printedSchema = printSchema(schema); // Returns a string representation of the schema
  78. const astNode = parse(printedSchema); // Transforms the string into ASTNode
  79. const result = visit(astNode, { leave: errorsVisitor });
  80. const defs = result.definitions.filter(d => !!d);
  81. return {
  82. content: [
  83. `/** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */`,
  84. `import { ErrorCode } from '@vendure/common/lib/generated-types';`,
  85. generateScalars(schema, config),
  86. ...defs,
  87. defs.length ? generateIsErrorFunction(schema) : '',
  88. generateTypeResolvers(schema),
  89. ].join('\n\n'),
  90. };
  91. };
  92. function generateScalars(schema: GraphQLSchema, config: any): string {
  93. const scalarMap = buildScalars(schema, config.scalars);
  94. const allScalars = Object.keys(scalarMap)
  95. .map(scalarName => {
  96. const scalarValue = scalarMap[scalarName].type;
  97. const scalarType = schema.getType(scalarName);
  98. return ` ${scalarName}: ${scalarValue};`;
  99. })
  100. .join('\n');
  101. return `export type Scalars = {\n${allScalars}\n};`;
  102. }
  103. function generateErrorClassSource(node: ObjectTypeDefinitionNode) {
  104. let source = `export class ${node.name.value} {`;
  105. for (const field of node.fields) {
  106. source += ` ${1}`;
  107. }
  108. }
  109. function generateIsErrorFunction(schema: GraphQLSchema) {
  110. const errorNodes = Object.values(schema.getTypeMap())
  111. .map(type => type.astNode)
  112. .filter(isObjectTypeDefinition)
  113. .filter(node => inheritsFromErrorResult(node));
  114. return `
  115. const errorTypeNames = new Set([${errorNodes.map(n => `'${n.name.value}'`).join(', ')}]);
  116. export function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').${ERROR_INTERFACE_NAME} {
  117. return input instanceof ${ERROR_INTERFACE_NAME} || errorTypeNames.has(input.__typename);
  118. }`;
  119. }
  120. function generateTypeResolvers(schema: GraphQLSchema) {
  121. const mutations = getOperationsThatReturnErrorUnions(schema, schema.getMutationType().getFields());
  122. const queries = getOperationsThatReturnErrorUnions(schema, schema.getQueryType().getFields());
  123. const operations = [...mutations, ...queries];
  124. const isAdminApi = !!schema.getType('UpdateGlobalSettingsInput');
  125. const varName = isAdminApi ? `adminErrorOperationTypeResolvers` : `shopErrorOperationTypeResolvers`;
  126. const result = [`export const ${varName} = {`];
  127. for (const operation of operations) {
  128. const returnType = unwrapType(operation.type) as GraphQLUnionType;
  129. const nonErrorResult = returnType.getTypes().find(t => !inheritsFromErrorResult(t));
  130. result.push(
  131. ` ${returnType.name}: {`,
  132. ` __resolveType(value: any) {`,
  133. // tslint:disable-next-line:no-non-null-assertion
  134. ` return isGraphQLError(value) ? (value as any).__typename : '${nonErrorResult!.name}';`,
  135. ` },`,
  136. ` },`,
  137. );
  138. }
  139. result.push(`};`);
  140. return result.join('\n');
  141. }
  142. function getOperationsThatReturnErrorUnions(schema: GraphQLSchema, fields: GraphQLFieldMap<any, any>) {
  143. return Object.values(fields).filter(operation => {
  144. const innerType = unwrapType(operation.type);
  145. if (innerType.astNode?.kind === 'UnionTypeDefinition') {
  146. return isUnionOfResultAndErrors(schema, innerType.astNode);
  147. }
  148. return false;
  149. });
  150. }
  151. function isUnionOfResultAndErrors(schema: GraphQLSchema, node: UnionTypeDefinitionNode) {
  152. const errorResultTypes = node.types.filter(namedType => {
  153. const type = schema.getType(namedType.name.value);
  154. if (isObjectType(type)) {
  155. if (inheritsFromErrorResult(type)) {
  156. return true;
  157. }
  158. }
  159. return false;
  160. });
  161. return (errorResultTypes.length = node.types.length - 1);
  162. }
  163. function isObjectTypeDefinition(node: any): node is ObjectTypeDefinitionNode {
  164. return node && isTypeDefinitionNode(node) && node.kind === 'ObjectTypeDefinition';
  165. }
  166. function inheritsFromErrorResult(node: ObjectTypeDefinitionNode | GraphQLObjectType): boolean {
  167. const interfaceNames = isObjectType(node)
  168. ? node.getInterfaces().map(i => i.name)
  169. : node.interfaces.map(i => i.name.value);
  170. return interfaceNames.includes(ERROR_INTERFACE_NAME);
  171. }
  172. /**
  173. * Unwraps the inner type from a higher-order type, e.g. [Address!]! => Address
  174. */
  175. function unwrapType(type: GraphQLType): GraphQLNamedType {
  176. if (isNamedType(type)) {
  177. return type;
  178. }
  179. let innerType = type;
  180. while (!isNamedType(innerType)) {
  181. innerType = innerType.ofType;
  182. }
  183. return innerType;
  184. }