UNPKG

@graphql-codegen/typescript-apollo-angular

Version:

GraphQL Code Generator plugin for generating ready-to-use Angular Components based on GraphQL operations

295 lines (292 loc) • 13.5 kB
import { ClientSideBaseVisitor, DocumentMode, indentMultiline, getConfigValue, indent, } from '@graphql-codegen/visitor-plugin-common'; import autoBind from 'auto-bind'; import { print, visit, Kind } from 'graphql'; import { camelCase } from 'change-case-all'; const R_MOD = /module:\s*"([^"]+)"/; // matches: module: "..." const R_NAME = /name:\s*"([^"]+)"/; // matches: name: "..." function R_DEF(directive) { return new RegExp(`\\s+\\@${directive}\\([^)]+\\)`, 'gm'); } export class ApolloAngularVisitor extends ClientSideBaseVisitor { constructor(schema, fragments, _allOperations, rawConfig, documents) { super(schema, fragments, rawConfig, { sdkClass: rawConfig.sdkClass, ngModule: rawConfig.ngModule, namedClient: rawConfig.namedClient, serviceName: rawConfig.serviceName, serviceProvidedIn: rawConfig.serviceProvidedIn, serviceProvidedInRoot: rawConfig.serviceProvidedInRoot, querySuffix: rawConfig.querySuffix, mutationSuffix: rawConfig.mutationSuffix, subscriptionSuffix: rawConfig.subscriptionSuffix, additionalDI: getConfigValue(rawConfig.additionalDI, []), apolloAngularPackage: getConfigValue(rawConfig.apolloAngularPackage, 'apollo-angular'), apolloAngularVersion: getConfigValue(rawConfig.apolloAngularVersion, 2), gqlImport: getConfigValue(rawConfig.gqlImport, !rawConfig.apolloAngularVersion || rawConfig.apolloAngularVersion === 2 ? `apollo-angular#gql` : null), addExplicitOverride: getConfigValue(rawConfig.addExplicitOverride, false), }, documents); this._allOperations = _allOperations; this._externalImportPrefix = ''; this._operationsToInclude = []; this.dependencyInjections = ''; this.dependencyInjectionArgs = ''; if (this.config.importOperationTypesFrom) { this._externalImportPrefix = `${this.config.importOperationTypesFrom}.`; if (this.config.documentMode !== DocumentMode.external || !this.config.importDocumentNodeExternallyFrom) { // eslint-disable-next-line no-console console.warn('"importOperationTypesFrom" should be used with "documentMode=external" and "importDocumentNodeExternallyFrom"'); } if (this.config.importOperationTypesFrom !== 'Operations') { // eslint-disable-next-line no-console console.warn('importOperationTypesFrom only works correctly when left empty or set to "Operations"'); } } const dependencyInjections = ['apollo: Apollo.Apollo'].concat(this.config.additionalDI); const dependencyInjectionArgs = dependencyInjections.map(content => { return content.split(':')[0]; }); this.dependencyInjections = dependencyInjections.join(', '); this.dependencyInjectionArgs = dependencyInjectionArgs.join(', '); autoBind(this); } getImports() { const baseImports = super.getImports(); const hasOperations = this._collectedOperations.length > 0; if (!hasOperations) { return baseImports; } const imports = [ `import { Injectable } from '@angular/core';`, `import * as Apollo from '${this.config.apolloAngularPackage}';`, ]; if (this.config.sdkClass) { const corePackage = this.config.apolloAngularVersion > 1 ? '@apollo/client/core' : 'apollo-client'; imports.push(`import * as ApolloCore from '${corePackage}';`); } const defs = {}; this._allOperations .filter(op => this._operationHasDirective(op, 'NgModule') || !!this.config.ngModule) .forEach(op => { const def = this._operationHasDirective(op, 'NgModule') ? this._extractNgModule(op) : this._parseNgModule(this.config.ngModule); // by setting key as link we easily get rid of duplicated imports // every path should be relative to the output file defs[def.link] = { path: def.path, module: def.module, }; }); if (this.config.serviceProvidedIn) { const ngModule = this._parseNgModule(this.config.serviceProvidedIn); defs[ngModule.link] = { path: ngModule.path, module: ngModule.module, }; } Object.keys(defs).forEach(key => { const def = defs[key]; // Every Angular Module that I've seen in my entire life use named exports imports.push(`import { ${def.module} } from '${def.path}';`); }); return [...baseImports, ...imports]; } _extractNgModule(operation) { const [, link] = print(operation).match(R_MOD); return this._parseNgModule(link); } _parseNgModule(link) { const [path, module] = link.split('#'); return { path, module, link, }; } _operationHasDirective(operation, directive) { if (typeof operation === 'string') { return operation.includes(`@${directive}`); } let found = false; visit(operation, { Directive(node) { if (node.name.value === directive) { found = true; } }, }); return found; } _removeDirective(document, directive) { if (this._operationHasDirective(document, directive)) { return document.replace(R_DEF(directive), ''); } return document; } _removeDirectives(document, directives) { return directives.reduce((doc, directive) => this._removeDirective(doc, directive), document); } _extractDirective(operation, directive) { const directives = print(operation).match(R_DEF(directive)); if (directives.length > 1) { throw new Error(`The ${directive} directive used multiple times in '${operation.name}' operation`); } return directives[0]; } _prepareDocument(documentStr) { return this._removeDirectives(documentStr, ['NgModule', 'namedClient']); } _namedClient(operation) { let name; if (this._operationHasDirective(operation, 'namedClient')) { name = this._extractNamedClient(operation); } else if (this.config.namedClient) { name = this.config.namedClient; } return name ? `client = '${name}';` : ''; } // tries to find namedClient directive and extract {name} _extractNamedClient(operation) { const [, name] = this._extractDirective(operation, 'namedClient').match(R_NAME); return name; } _providedIn(operation) { if (this._operationHasDirective(operation, 'NgModule')) { return this._extractNgModule(operation).module; } if (this.config.ngModule) { return this._parseNgModule(this.config.ngModule).module; } return `'root'`; } _getDocumentNodeVariable(node, documentVariableName) { if (this.config.importDocumentNodeExternallyFrom === 'near-operation-file') { return `Operations.${documentVariableName}`; } if (this.config.importOperationTypesFrom) { return `${this.config.importOperationTypesFrom}.${documentVariableName}`; } return documentVariableName; } _operationSuffix(operationType) { const defaultSuffix = 'GQL'; switch (operationType) { case 'Query': return this.config.querySuffix || defaultSuffix; case 'Mutation': return this.config.mutationSuffix || defaultSuffix; case 'Subscription': return this.config.subscriptionSuffix || defaultSuffix; default: return defaultSuffix; } } buildOperation(node, documentVariableName, operationType, operationResultType, operationVariablesTypes) { const serviceName = `${this.convertName(node)}${this._operationSuffix(operationType)}`; this._operationsToInclude.push({ node, documentVariableName, operationType, operationResultType, operationVariablesTypes, serviceName, }); const namedClient = this._namedClient(node); operationResultType = this._externalImportPrefix + operationResultType; operationVariablesTypes = this._externalImportPrefix + operationVariablesTypes; const content = ` @Injectable({ providedIn: ${this._providedIn(node)} }) export class ${serviceName} extends Apollo.${operationType}<${operationResultType}, ${operationVariablesTypes}> { ${this.config.addExplicitOverride ? 'override ' : ''}document = ${this._getDocumentNodeVariable(node, documentVariableName)}; ${namedClient !== '' ? (this.config.addExplicitOverride ? 'override ' : '') + namedClient : ''} constructor(${this.dependencyInjections}) { super(${this.dependencyInjectionArgs}); } }`; return content; } get sdkClass() { const actionType = operation => { switch (operation) { case 'Mutation': return 'mutate'; case 'Subscription': return 'subscribe'; default: return 'fetch'; } }; const hasMutations = this._operationsToInclude.find(o => o.operationType === 'Mutation'); const hasSubscriptions = this._operationsToInclude.find(o => o.operationType === 'Subscription'); const hasQueries = this._operationsToInclude.find(o => o.operationType === 'Query'); const allPossibleActions = this._operationsToInclude .map(o => { const operationResultType = this._externalImportPrefix + o.operationResultType; const operationVariablesTypes = this._externalImportPrefix + o.operationVariablesTypes; const optionalVariables = !o.node.variableDefinitions || o.node.variableDefinitions.length === 0 || o.node.variableDefinitions.every(v => v.type.kind !== Kind.NON_NULL_TYPE || !!v.defaultValue); const options = o.operationType === 'Mutation' ? `${o.operationType}OptionsAlone<${operationResultType}, ${operationVariablesTypes}>` : `${o.operationType}OptionsAlone<${operationVariablesTypes}>`; const method = ` ${camelCase(o.node.name.value)}(variables${optionalVariables ? '?' : ''}: ${operationVariablesTypes}, options?: ${options}) { return this.${camelCase(o.serviceName)}.${actionType(o.operationType)}(variables, options) }`; let watchMethod; if (o.operationType === 'Query') { watchMethod = ` ${camelCase(o.node.name.value)}Watch(variables${optionalVariables ? '?' : ''}: ${operationVariablesTypes}, options?: WatchQueryOptionsAlone<${operationVariablesTypes}>) { return this.${camelCase(o.serviceName)}.watch(variables, options) }`; } return [method, watchMethod].join(''); }) .map(s => indentMultiline(s, 2)); // Inject the generated services in the constructor const injectString = (service) => `private ${camelCase(service)}: ${service}`; const injections = this._operationsToInclude .map(op => injectString(op.serviceName)) .map(s => indentMultiline(s, 3)) .join(',\n'); const serviceName = this.config.serviceName || 'ApolloAngularSDK'; const providedIn = this.config.serviceProvidedIn ? `{ providedIn: ${this._parseNgModule(this.config.serviceProvidedIn).module} }` : this.config.serviceProvidedInRoot === false ? '' : `{ providedIn: 'root' }`; // Generate these types only if they're going to be used, // to avoid "unused variable" compile errors in generated code const omitType = hasQueries || hasMutations || hasSubscriptions ? `type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;` : ''; const watchType = hasQueries ? `interface WatchQueryOptionsAlone<V> extends Omit<ApolloCore.WatchQueryOptions<V>, 'query' | 'variables'> {}` : ''; const queryType = hasQueries ? `interface QueryOptionsAlone<V> extends Omit<ApolloCore.QueryOptions<V>, 'query' | 'variables'> {}` : ''; const mutationType = hasMutations ? `interface MutationOptionsAlone<T, V> extends Omit<ApolloCore.MutationOptions<T, V>, 'mutation' | 'variables'> {}` : ''; const subscriptionType = hasSubscriptions ? `interface SubscriptionOptionsAlone<V> extends Omit<ApolloCore.SubscriptionOptions<V>, 'query' | 'variables'> {}` : ''; const types = [omitType, watchType, queryType, mutationType, subscriptionType] .filter(s => s) .map(s => indent(s, 1)) .join('\n\n'); return ` ${types} @Injectable(${providedIn}) export class ${serviceName} { constructor( ${injections} ) {} ${allPossibleActions.join('\n')} }`; } }