UNPKG

apollo-codegen-typescript

Version:

TypeScript generator module for Apollo Codegen

606 lines (511 loc) 17.2 kB
import path from "path"; import * as t from "@babel/types"; import { stripIndent } from "common-tags"; import { GraphQLEnumType, GraphQLInputObjectType } from "graphql"; import { CompilerContext, Operation, Fragment, Selection, SelectionSet, Field, FragmentSpread, } from "apollo-codegen-core/lib/compiler"; import { typeCaseForSelectionSet, Variant, } from "apollo-codegen-core/lib/compiler/visitors/typeCase"; import { collectAndMergeFields } from "apollo-codegen-core/lib/compiler/visitors/collectAndMergeFields"; import { BasicGeneratedFile } from "apollo-codegen-core/lib/utilities/CodeGenerator"; import { CompilerOptions } from "apollo-codegen-core/lib/compiler"; import TypescriptGenerator, { ObjectProperty } from "./language"; import Printer from "./printer"; import { DEFAULT_FILE_EXTENSION } from "./helpers"; import { GraphQLType, isListType, isObjectType, isNonNullType, isEnumType, isInputObjectType, } from "graphql/type/definition"; import { GraphQLOutputType, getNullableType } from "graphql"; import { maybePush } from "apollo-codegen-core/lib/utilities/array"; import { unifyPaths } from "apollo-codegen-core/lib/utilities/printing"; class TypescriptGeneratedFile implements BasicGeneratedFile { fileContents: string; constructor(fileContents: string) { this.fileContents = fileContents; } get output() { return this.fileContents; } } function printEnumsAndInputObjects( generator: TypescriptAPIGenerator, typesUsed: GraphQLType[] ) { generator.printer.enqueue(stripIndent` //============================================================== // START Enums and Input Objects //============================================================== `); typesUsed .filter(isEnumType) .sort() .forEach((enumType) => { generator.typeAliasForEnumType(enumType); }); typesUsed .filter(isInputObjectType) .sort() .forEach((inputObjectType) => { generator.typeAliasForInputObjectType(inputObjectType); }); generator.printer.enqueue(stripIndent` //============================================================== // END Enums and Input Objects //============================================================== `); } function printGlobalImport( generator: TypescriptAPIGenerator, typesUsed: GraphQLType[], outputPath: string, tsFileExtension: string, globalSourcePath: string ) { if (typesUsed.length > 0) { const relative = path.relative( path.dirname(outputPath), path.join( path.dirname(globalSourcePath), path.basename(globalSourcePath, `.${tsFileExtension}`) ) ); generator.printer.enqueue( generator.import(typesUsed, "./" + unifyPaths(relative)) ); } } // TODO: deprecate this, use generateLocalSource and generateGlobalSource instead. export function generateSource(context: CompilerContext) { const generator = new TypescriptAPIGenerator(context); const generatedFiles: { sourcePath: string; fileName: string; content: TypescriptGeneratedFile; }[] = []; Object.values(context.operations).forEach((operation) => { generator.fileHeader(); generator.interfacesForOperation(operation); const output = generator.printer.printAndClear(); generatedFiles.push({ sourcePath: operation.filePath, fileName: `${operation.operationName}.${ context.options.tsFileExtension || DEFAULT_FILE_EXTENSION }`, content: new TypescriptGeneratedFile(output), }); }); Object.values(context.fragments).forEach((fragment) => { generator.fileHeader(); generator.interfacesForFragment(fragment); const output = generator.printer.printAndClear(); generatedFiles.push({ sourcePath: fragment.filePath, fileName: `${fragment.fragmentName}.ts`, content: new TypescriptGeneratedFile(output), }); }); generator.fileHeader(); printEnumsAndInputObjects(generator, context.typesUsed); const common = generator.printer.printAndClear(); return { generatedFiles, common, }; } interface IGeneratedFileOptions { outputPath?: string; globalSourcePath?: string; } interface IGeneratedFile { sourcePath: string; fileName: string; content: (options?: IGeneratedFileOptions) => TypescriptGeneratedFile; } export function generateLocalSource( context: CompilerContext ): IGeneratedFile[] { const generator = new TypescriptAPIGenerator(context); const operations = Object.values(context.operations).map((operation) => ({ sourcePath: operation.filePath, fileName: `${operation.operationName}.${ context.options.tsFileExtension || DEFAULT_FILE_EXTENSION }`, content: (options?: IGeneratedFileOptions) => { generator.fileHeader(); if (options && options.outputPath && options.globalSourcePath) { printGlobalImport( generator, generator.getGlobalTypesUsedForOperation(operation), options.outputPath, context.options.tsFileExtension || DEFAULT_FILE_EXTENSION, options.globalSourcePath ); } generator.interfacesForOperation(operation); const output = generator.printer.printAndClear(); return new TypescriptGeneratedFile(output); }, })); const fragments = Object.values(context.fragments).map((fragment) => ({ sourcePath: fragment.filePath, fileName: `${fragment.fragmentName}.${ context.options.tsFileExtension || DEFAULT_FILE_EXTENSION }`, content: (options?: IGeneratedFileOptions) => { generator.fileHeader(); if (options && options.outputPath && options.globalSourcePath) { printGlobalImport( generator, generator.getGlobalTypesUsedForFragment(fragment), options.outputPath, context.options.tsFileExtension || DEFAULT_FILE_EXTENSION, options.globalSourcePath ); } generator.interfacesForFragment(fragment); const output = generator.printer.printAndClear(); return new TypescriptGeneratedFile(output); }, })); return operations.concat(fragments); } export function generateGlobalSource( context: CompilerContext ): TypescriptGeneratedFile { const generator = new TypescriptAPIGenerator(context); generator.fileHeader(); printEnumsAndInputObjects(generator, context.typesUsed); const output = generator.printer.printAndClear(); return new TypescriptGeneratedFile(output); } export class TypescriptAPIGenerator extends TypescriptGenerator { context: CompilerContext; printer: Printer; scopeStack: string[]; constructor(context: CompilerContext) { super(context.options as CompilerOptions); this.context = context; this.printer = new Printer(); this.scopeStack = []; } fileHeader() { this.printer.enqueue( stripIndent` /* tslint:disable */ /* eslint-disable */ // @generated // This file was automatically generated and should not be edited. ` ); } public typeAliasForEnumType(enumType: GraphQLEnumType) { this.printer.enqueue(this.enumerationDeclaration(enumType)); } public typeAliasForInputObjectType(inputObjectType: GraphQLInputObjectType) { this.printer.enqueue(this.inputObjectDeclaration(inputObjectType)); } public interfacesForOperation(operation: Operation) { const { operationType, operationName, variables, selectionSet } = operation; this.scopeStackPush(operationName); this.printer.enqueue(stripIndent` // ==================================================== // GraphQL ${operationType} operation: ${operationName} // ==================================================== `); // The root operation only has one variant // Do we need to get exhaustive variants anyway? const variants = this.getVariantsForSelectionSet(selectionSet); const variant = variants[0]; const properties = this.getPropertiesForVariant(variant); const exportedTypeAlias = this.exportDeclaration( this.interface(operationName, properties) ); this.printer.enqueue(exportedTypeAlias); this.scopeStackPop(); // Generate the variables interface if the operation has any variables if (variables.length > 0) { const interfaceName = operationName + "Variables"; this.scopeStackPush(interfaceName); this.printer.enqueue( this.exportDeclaration( this.interface( interfaceName, variables.map((variable) => ({ name: variable.name, type: this.typeFromGraphQLType(variable.type), })), { keyInheritsNullability: true } ) ) ); this.scopeStackPop(); } } public interfacesForFragment(fragment: Fragment) { const { fragmentName, selectionSet } = fragment; this.scopeStackPush(fragmentName); this.printer.enqueue(stripIndent` // ==================================================== // GraphQL fragment: ${fragmentName} // ==================================================== `); const variants = this.getVariantsForSelectionSet(selectionSet); if (variants.length === 1) { const properties = this.getPropertiesForVariant(variants[0]); const name = this.nameFromScopeStack(this.scopeStack); const exportedTypeAlias = this.exportDeclaration( this.interface(name, properties) ); this.printer.enqueue(exportedTypeAlias); } else { const unionMembers: t.Identifier[] = []; variants.forEach((variant) => { this.scopeStackPush(variant.possibleTypes[0].toString()); const properties = this.getPropertiesForVariant(variant); const name = this.nameFromScopeStack(this.scopeStack); const exportedTypeAlias = this.exportDeclaration( this.interface(name, properties) ); this.printer.enqueue(exportedTypeAlias); unionMembers.push( t.identifier(this.nameFromScopeStack(this.scopeStack)) ); this.scopeStackPop(); }); this.printer.enqueue( this.exportDeclaration( this.typeAliasGenericUnion( this.nameFromScopeStack(this.scopeStack), unionMembers.map((id) => t.TSTypeReference(id)) ) ) ); } this.scopeStackPop(); } public getGlobalTypesUsedForOperation = (doc: Operation) => { const typesUsed = doc.variables.reduce( (acc: GraphQLType[], { type }: { type: GraphQLType }) => { const t = this.getUnderlyingType(type); if (this.isGlobalType(t)) { return maybePush(acc, t); } return acc; }, [] ); return doc.selectionSet.selections.reduce(this.reduceSelection, typesUsed); }; public getGlobalTypesUsedForFragment = (doc: Fragment) => { return doc.selectionSet.selections.reduce(this.reduceSelection, []); }; private reduceSelection = ( acc: GraphQLType[], selection: Selection ): GraphQLType[] => { if (selection.kind === "Field" || selection.kind === "TypeCondition") { const type = this.getUnderlyingType(selection.type); if (this.isGlobalType(type)) { acc = maybePush(acc, type); } } if (selection.selectionSet) { return selection.selectionSet.selections.reduce( this.reduceSelection, acc ); } return acc; }; private isGlobalType = (type: GraphQLType) => { return isEnumType(type) || isInputObjectType(type); }; private getUnderlyingType = (type: GraphQLType): GraphQLType => { if (isNonNullType(type)) { return this.getUnderlyingType(getNullableType(type)); } if (isListType(type)) { return this.getUnderlyingType(type.ofType); } return type; }; public getTypesUsedForOperation( doc: Operation | Fragment, context: CompilerContext ) { let docTypesUsed: GraphQLType[] = []; if (doc.hasOwnProperty("operationName")) { const operation = doc as Operation; docTypesUsed = operation.variables.map(({ type }) => type); } const reduceTypesForDocument = ( nestDoc: Operation | Fragment | FragmentSpread, acc: GraphQLType[] ) => { const { selectionSet: { possibleTypes, selections }, } = nestDoc; acc = possibleTypes.reduce(maybePush, acc); acc = selections.reduce((selectionAcc, selection) => { switch (selection.kind) { case "Field": case "TypeCondition": selectionAcc = maybePush(selectionAcc, selection.type); break; case "FragmentSpread": selectionAcc = reduceTypesForDocument(selection, selectionAcc); break; default: break; } return selectionAcc; }, acc); return acc; }; docTypesUsed = reduceTypesForDocument(doc, docTypesUsed).reduce( this.reduceTypesUsed, [] ); return context.typesUsed.filter((type) => { return docTypesUsed.find((typeUsed) => type === typeUsed); }); } private reduceTypesUsed = ( acc: (GraphQLType | GraphQLOutputType)[], type: GraphQLType ) => { if (isNonNullType(type)) { type = getNullableType(type); } if (isListType(type)) { type = type.ofType; } if (isInputObjectType(type) || isObjectType(type)) { acc = maybePush(acc, type); const fields = type.getFields(); acc = Object.keys(fields) .map((key) => fields[key] && fields[key].type) .reduce(this.reduceTypesUsed, acc); } else { acc = maybePush(acc, type); } return acc; }; private getVariantsForSelectionSet(selectionSet: SelectionSet) { return this.getTypeCasesForSelectionSet(selectionSet).exhaustiveVariants; } private getTypeCasesForSelectionSet(selectionSet: SelectionSet) { return typeCaseForSelectionSet( selectionSet, this.context.options.mergeInFieldsFromFragmentSpreads ); } private getPropertiesForVariant(variant: Variant): ObjectProperty[] { const fields = collectAndMergeFields( variant, this.context.options.mergeInFieldsFromFragmentSpreads ); return fields.map((field) => { const fieldName = field.alias !== undefined ? field.alias : field.name; this.scopeStackPush(fieldName); let res: ObjectProperty; if (field.selectionSet) { res = this.handleFieldSelectionSetValue( t.identifier(this.nameFromScopeStack(this.scopeStack)), field ); } else { res = this.handleFieldValue(field, variant); } this.scopeStackPop(); return res; }); } private handleFieldSelectionSetValue( generatedIdentifier: t.Identifier, field: Field ): ObjectProperty { const { selectionSet } = field; const type = this.typeFromGraphQLType(field.type, generatedIdentifier.name); const typeCase = this.getTypeCasesForSelectionSet( selectionSet as SelectionSet ); const variants = typeCase.exhaustiveVariants; let exportedTypeAlias; if (variants.length === 1) { const variant = variants[0]; const properties = this.getPropertiesForVariant(variant); exportedTypeAlias = this.exportDeclaration( this.interface(this.nameFromScopeStack(this.scopeStack), properties) ); } else { const identifiers = variants.map((variant) => { this.scopeStackPush(variant.possibleTypes[0].toString()); const properties = this.getPropertiesForVariant(variant); const identifierName = this.nameFromScopeStack(this.scopeStack); this.printer.enqueue( this.exportDeclaration(this.interface(identifierName, properties)) ); this.scopeStackPop(); return t.identifier(identifierName); }); exportedTypeAlias = this.exportDeclaration( this.typeAliasGenericUnion( generatedIdentifier.name, identifiers.map((i) => t.TSTypeReference(i)) ) ); } this.printer.enqueue(exportedTypeAlias); return { name: field.alias ? field.alias : field.name, description: field.description, type, }; } private handleFieldValue(field: Field, variant: Variant): ObjectProperty { let res: ObjectProperty; if (field.name === "__typename") { const types = variant.possibleTypes.map((type) => { return t.TSLiteralType(t.stringLiteral(type.toString())); }); res = { name: field.alias ? field.alias : field.name, description: field.description, type: t.TSUnionType(types), }; } else { // TODO: Double check that this works res = { name: field.alias ? field.alias : field.name, description: field.description, type: this.typeFromGraphQLType(field.type), }; } return res; } public get output(): string { return this.printer.print(); } scopeStackPush(name: string) { this.scopeStack.push(name); } scopeStackPop() { const popped = this.scopeStack.pop(); return popped; } }