import { GenerateTypescriptOptions } from './types'; import { versionMajorMinor as TSVersion } from 'typescript'; import { isBuiltinType, descriptionToJSDoc, createFieldRef, pascalCase, } from './utils'; import { IntrospectionType, IntrospectionScalarType, IntrospectionEnumType, IntrospectionObjectType, IntrospectionUnionType, IntrospectionInputObjectType, IntrospectionInterfaceType, IntrospectionQuery, IntrospectionField, IntrospectionInputValue, } from 'graphql'; export class TypeScriptGenerator { constructor( protected options: GenerateTypescriptOptions, protected introspectResult: IntrospectionQuery, protected outputPath: string ) {} public async generate(): Promise { const { introspectResult } = this; const gqlTypes = introspectResult.__schema.types.filter( (type) => !isBuiltinType(type) ); return gqlTypes.reduce((prevTypescriptDefs, gqlType) => { const jsDoc = descriptionToJSDoc({ description: gqlType.description }); let typeScriptDefs: string[] = [].concat(jsDoc); switch (gqlType.kind) { case 'SCALAR': { typeScriptDefs = typeScriptDefs.concat( this.generateCustomScalarType(gqlType) ); break; } case 'ENUM': { typeScriptDefs = typeScriptDefs.concat( this.generateEnumType(gqlType) ); break; } case 'OBJECT': case 'INPUT_OBJECT': case 'INTERFACE': { typeScriptDefs = typeScriptDefs.concat( this.generateObjectType(gqlType, gqlTypes) ); break; } case 'UNION': { typeScriptDefs = typeScriptDefs.concat( this.generateUnionType(gqlType) ); break; } default: { throw new Error(`Unknown type kind ${(gqlType as any).kind}`); } } typeScriptDefs.push(''); return prevTypescriptDefs.concat(typeScriptDefs); }, []); } private generateCustomScalarType( scalarType: IntrospectionScalarType ): string[] { const customScalarType = this.options.customScalarType || {}; if (customScalarType[scalarType.name]) { return [ `export type ${this.options.typePrefix}${scalarType.name} = ${ customScalarType[scalarType.name] };`, ]; } return [`export type ${this.options.typePrefix}${scalarType.name} = any;`]; } private isStringEnumSupported(): boolean { const [major, minor] = TSVersion.split('.').map((v) => +v); return (major === 2 && minor >= 5) || major > 2; } private generateEnumType(enumType: IntrospectionEnumType): string[] { // if using old typescript, which doesn't support string enum: convert enum to string union if (!this.isStringEnumSupported() || this.options.noStringEnum) { return this.createUnionType( enumType.name, enumType.enumValues.map((v) => `'${v.name}'`) ); } let enumBody = enumType.enumValues.reduce( (prevTypescriptDefs, enumValue, index) => { let typescriptDefs: string[] = []; const enumValueJsDoc = descriptionToJSDoc(enumValue); const isLastEnum = index === enumType.enumValues.length - 1; const graphQlEnumValueName = enumValue.name; const typescriptEnumValueName = this.generateEnumValueName( graphQlEnumValueName ); if (!isLastEnum) { typescriptDefs = [ ...enumValueJsDoc, `${typescriptEnumValueName} = '${graphQlEnumValueName}',`, ]; } else { typescriptDefs = [ ...enumValueJsDoc, `${typescriptEnumValueName} = '${graphQlEnumValueName}'`, ]; } if (enumValueJsDoc.length > 0) { typescriptDefs = ['', ...typescriptDefs]; } return prevTypescriptDefs.concat(typescriptDefs); }, [] ); // if code is generated as type declaration, better use export const enum instead // of just export enum const isGeneratingDeclaration = this.options.global || !!this.options.namespace || this.outputPath.endsWith('.d.ts'); const enumModifier = isGeneratingDeclaration ? ' const ' : ' '; return [ `export${enumModifier}enum ${this.options.typePrefix}${enumType.name} {`, ...enumBody, '}', ]; } private generateEnumValueName(graphQlName: string): string { if (this.options.enumsAsPascalCase) { return pascalCase(graphQlName); } else { return graphQlName; } } private generateObjectType( objectType: | IntrospectionObjectType | IntrospectionInputObjectType | IntrospectionInterfaceType, allGQLTypes: IntrospectionType[] ): string[] { const fields: readonly (IntrospectionInputValue | IntrospectionField)[] = objectType.kind === 'INPUT_OBJECT' ? objectType.inputFields : objectType.fields; const extendTypes: string[] = objectType.kind === 'OBJECT' ? objectType.interfaces.map((i) => i.name) : []; const extendGqlTypes = allGQLTypes.filter( (t) => extendTypes.indexOf(t.name) !== -1 ) as IntrospectionInterfaceType[]; const extendFields = extendGqlTypes.reduce( (prevFieldNames, gqlType) => { return prevFieldNames.concat(gqlType.fields.map((f) => f.name)); }, [] ); const objectFields = fields.reduce( (prevTypescriptDefs, field, index) => { if ( extendFields.indexOf(field.name) !== -1 && this.options.minimizeInterfaceImplementation ) { return prevTypescriptDefs; } const fieldJsDoc = descriptionToJSDoc(field); const { fieldName, fieldType } = createFieldRef( field, this.options.typePrefix, this.options.strictNulls ); const fieldNameAndType = `${fieldName}: ${fieldType};`; let typescriptDefs = [...fieldJsDoc, fieldNameAndType]; if (fieldJsDoc.length > 0) { typescriptDefs = ['', ...typescriptDefs]; } return prevTypescriptDefs.concat(typescriptDefs); }, [] ); const possibleTypeNames: string[] = []; const possibleTypeNamesMap: string[] = []; if (objectType.kind === 'INTERFACE') { possibleTypeNames.push( ...[ '', `/** Use this to resolve interface type ${objectType.name} */`, ...this.createUnionType( `Possible${objectType.name}TypeNames`, objectType.possibleTypes.map((pt) => `'${pt.name}'`) ), ] ); possibleTypeNamesMap.push( ...[ '', `export interface ${this.options.typePrefix}${objectType.name}NameMap {`, `${objectType.name}: ${this.options.typePrefix}${objectType.name};`, ...objectType.possibleTypes.map((pt) => { return `${pt.name}: ${this.options.typePrefix}${pt.name};`; }), '}', ] ); } const extendStr = extendTypes.length === 0 ? '' : `extends ${extendTypes .map((t) => this.options.typePrefix + t) .join(', ')} `; return [ `export interface ${this.options.typePrefix}${objectType.name} ${extendStr}{`, ...objectFields, '}', ...possibleTypeNames, ...possibleTypeNamesMap, ]; } private generateUnionType(unionType: IntrospectionUnionType): string[] { const { typePrefix } = this.options; const possibleTypesNames = [ '', `/** Use this to resolve union type ${unionType.name} */`, ...this.createUnionType( `Possible${unionType.name}TypeNames`, unionType.possibleTypes.map((pt) => `'${pt.name}'`) ), ]; const possibleTypeNamesMap = [ '', `export interface ${this.options.typePrefix}${unionType.name}NameMap {`, `${unionType.name}: ${this.options.typePrefix}${unionType.name};`, ...unionType.possibleTypes.map((pt) => { return `${pt.name}: ${this.options.typePrefix}${pt.name};`; }), '}', ]; const unionTypeTSDefs = this.createUnionType( unionType.name, unionType.possibleTypes.map((type) => { if (isBuiltinType(type)) { return type.name; } else { return typePrefix + type.name; } }) ); return [...unionTypeTSDefs, ...possibleTypesNames, ...possibleTypeNamesMap]; } /** * Create a union type e.g: type Color = 'Red' | 'Green' | 'Blue' | ... * Also, if the type is too long to fit in one line, split them info multiple lines * => type Color = 'Red' * | 'Green' * | 'Blue' * | ... */ private createUnionType(typeName: string, possibleTypes: string[]): string[] { const result = `export type ${ this.options.typePrefix }${typeName} = ${possibleTypes.join(' | ')};`; if (result.length <= 80) { return [result]; } const [firstLine, rest] = result.split('='); return [ firstLine + '=', ...rest .replace(/ \| /g, ' |\n') .split('\n') .map((line) => line.trim()), ]; } }