diff --git a/README.md b/README.md index 66cb441..829e9b5 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,10 @@ types to make it type-safed when developing GraphQL server (mainly resolvers) ## Features -### Generate Typescript from Schema Definition -### Support [Typescript string enum](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#typescript-25) with fallback to string union (fallback not tested yet) -### Convert GraphQL description into JSDoc -### Also add deprecated field and reason into JSDoc +### Generate Typescript from Schema Definition (1-1 mapping from GQL type to TypeScript) +### Convert GraphQL description into JSDoc, include deprecated directive ### [Generate TypeScripts to support writing resolvers](#type-resolvers) +### [VSCode extension](#https://github.com/liyikun/vscode-graphql-schema-typescript) (credit to [@liyikun](https://github.com/liyikun)) ## Usage @@ -52,7 +51,7 @@ The file generated will have some types that can make it type-safed when writing * Parent type and resolve result is default to `any`, but could be overwritten in your code For example, if you schema is like this: -``` +```gql schema { query: RootQuery } @@ -129,24 +128,15 @@ export interface RootQueryToUsersResolver`, +// due to an issue with VSCode that not showing auto completion when returns is a mix of `T | Promise` (see [#17](https://github.com/dangcuuson/graphql-schema-typescript/issues/17)) + +// smartTParent: true +// smartTResult: true +// asyncResult: 'always' +export interface RootQueryToUsersResolver { + (parent: TParent, args: RootQueryToUsersArgs, context: any, info: GraphQLResolveInfo): Promise; // the different is here + +``` \ No newline at end of file diff --git a/package.json b/package.json index d44d3e6..1bfe6cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-schema-typescript", - "version": "1.2.10", + "version": "1.3.1", "description": "Generate TypeScript from GraphQL's schema type definitions", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -55,4 +55,4 @@ "json" ] } -} \ No newline at end of file +} diff --git a/src/__tests__/__snapshots__/option.global_nameSpace.test.ts.snap b/src/__tests__/__snapshots__/option.global_nameSpace.test.ts.snap index 4ffb5ac..7946f02 100644 --- a/src/__tests__/__snapshots__/option.global_nameSpace.test.ts.snap +++ b/src/__tests__/__snapshots__/option.global_nameSpace.test.ts.snap @@ -61,8 +61,29 @@ declare global { * In the future, this will be changed by having User as interface * and implementing multiple User */ - export type GQLUserRole = 'sysAdmin' | 'manager' | 'clerk' | 'employee'; - // NOTE: enum UserRole is generate as string union instead of string enum because the types is generated under global scope + export const enum GQLUserRole { + + /** + * System Administrator + */ + sysAdmin = 'sysAdmin', + + /** + * Manager - Have access to manage functions + */ + manager = 'manager', + + /** + * General Staff + */ + clerk = 'clerk', + + /** + * + * @deprecated Use 'clerk' instead + */ + employee = 'employee' + } export interface GQLIProduct { id: string; @@ -577,7 +598,7 @@ declare global { }" `; -exports[`global + namespace should wrap types in global and use string union if global is configured 1`] = ` +exports[`global + namespace should wrap types in global and use const enum if global is configured 1`] = ` "/* tslint:disable */ /* eslint-disable */ import { GraphQLResolveInfo, GraphQLScalarType } from 'graphql'; @@ -637,8 +658,29 @@ declare global { * In the future, this will be changed by having User as interface * and implementing multiple User */ - export type GQLUserRole = 'sysAdmin' | 'manager' | 'clerk' | 'employee'; - // NOTE: enum UserRole is generate as string union instead of string enum because the types is generated under global scope + export const enum GQLUserRole { + + /** + * System Administrator + */ + sysAdmin = 'sysAdmin', + + /** + * Manager - Have access to manage functions + */ + manager = 'manager', + + /** + * General Staff + */ + clerk = 'clerk', + + /** + * + * @deprecated Use 'clerk' instead + */ + employee = 'employee' + } export interface GQLIProduct { id: string; @@ -1162,7 +1204,7 @@ import { GraphQLResolveInfo, GraphQLScalarType } from 'graphql'; */ -namespace MyNamespace { +declare namespace MyNamespace { /******************************* * * * TYPE DEFS * @@ -1210,7 +1252,7 @@ namespace MyNamespace { * In the future, this will be changed by having User as interface * and implementing multiple User */ - export enum GQLUserRole { + export const enum GQLUserRole { /** * System Administrator diff --git a/src/__tests__/option.global_nameSpace.test.ts b/src/__tests__/option.global_nameSpace.test.ts index 468bc3b..60b294e 100644 --- a/src/__tests__/option.global_nameSpace.test.ts +++ b/src/__tests__/option.global_nameSpace.test.ts @@ -1,15 +1,15 @@ import { executeApiTest } from './testUtils'; describe('global + namespace', () => { - it('should wrap types in global and use string union if global is configured', async () => { + it('should wrap types in global and use const enum if global is configured', async () => { const generated = await executeApiTest('global.ts', { global: true }); expect(generated).toContain('declare global {'); - expect(generated).toContain(`export type GQLUserRole = 'sysAdmin' | 'manager' | 'clerk' | 'employee';`); + expect(generated).toContain(`export const enum GQLUserRole {`); }); it('should wrap types in namespace if namespace is configured', async () => { const generated = await executeApiTest('global.ts', { namespace: 'MyNamespace' }); - expect(generated).toContain('namespace MyNamespace {'); + expect(generated).toContain('declare namespace MyNamespace {'); }); it('should have no conflict between global and namespace config', async () => { diff --git a/src/cli.ts b/src/cli.ts index be60623..abb9d2f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -72,7 +72,7 @@ yargs }) .option(asyncResult, { desc: 'Set return type of resolver to `TResult | Promise`', - boolean: true + choices: [true, 'always'] }) .option(requireResolverTypes, { desc: 'Set resolvers to be required. Useful to ensure no resolvers is missing', diff --git a/src/index.ts b/src/index.ts index a095f4e..b310278 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,11 @@ const typeResolversDecoration = [ ' *********************************/' ]; -export const generateTSTypesAsString = async (schema: GraphQLSchema | string, options: GenerateTypescriptOptions): Promise => { +export const generateTSTypesAsString = async ( + schema: GraphQLSchema | string, + outputPath: string, + options: GenerateTypescriptOptions +): Promise => { const mergedOptions = { ...defaultOptions, ...options }; let introspectResult: IntrospectionQuery; @@ -62,7 +66,7 @@ export const generateTSTypesAsString = async (schema: GraphQLSchema | string, op introspectResult = await introspectSchema(schema); } - const tsGenerator = new TypeScriptGenerator(mergedOptions, introspectResult); + const tsGenerator = new TypeScriptGenerator(mergedOptions, introspectResult, outputPath); const typeDefs = await tsGenerator.generate(); let typeResolvers: GenerateResolversResult = { @@ -83,7 +87,8 @@ export const generateTSTypesAsString = async (schema: GraphQLSchema | string, op if (mergedOptions.namespace) { body = [ - `namespace ${options.namespace} {`, + // if namespace is under global, it doesn't need to be declared again + `${mergedOptions.global ? '' : 'declare '}namespace ${options.namespace} {`, ...body, '}' ]; @@ -108,6 +113,6 @@ export async function generateTypeScriptTypes( outputPath: string, options: GenerateTypescriptOptions = defaultOptions ) { - const content = await generateTSTypesAsString(schema, options); + const content = await generateTSTypesAsString(schema, outputPath, options); fs.writeFileSync(outputPath, content, 'utf-8'); } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 203df17..5c85eae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -81,12 +81,13 @@ export interface GenerateTypescriptOptions { /** * This option is for resolvers * If true, set return type of resolver to `TResult | Promise` + * If 'awalys', set return type of resolver to `Promise` * * e.g: interface QueryToUsersResolver { * (parent: TParent, args: {}, context: any, info): TResult extends Promise ? TResult : TResult | Promise * } */ - asyncResult?: boolean; + asyncResult?: boolean | 'always'; /** * If true, field resolver of each type will be required, instead of optional diff --git a/src/typescriptGenerator.ts b/src/typescriptGenerator.ts index f235406..8bdf9ca 100644 --- a/src/typescriptGenerator.ts +++ b/src/typescriptGenerator.ts @@ -22,7 +22,8 @@ export class TypeScriptGenerator { constructor( protected options: GenerateTypescriptOptions, - protected introspectResult: IntrospectionQuery + protected introspectResult: IntrospectionQuery, + protected outputPath: string ) { } public async generate(): Promise { @@ -94,14 +95,6 @@ export class TypeScriptGenerator { return this.createUnionType(enumType.name, enumType.enumValues.map(v => `'${v.name}'`)); } - // if generate as global, don't generate string enum as it requires import - if (this.options.global) { - return [ - ...this.createUnionType(enumType.name, enumType.enumValues.map(v => `'${v.name}'`)), - `// NOTE: enum ${enumType.name} is generate as string union instead of string enum because the types is generated under global scope` - ]; - } - let enumBody = enumType.enumValues.reduce( (prevTypescriptDefs, enumValue, index) => { let typescriptDefs: string[] = []; @@ -125,8 +118,14 @@ export class TypeScriptGenerator { [] ); + // 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 enum ${this.options.typePrefix}${enumType.name} {`, + `export${enumModifier}enum ${this.options.typePrefix}${enumType.name} {`, ...enumBody, '}' ]; @@ -136,7 +135,7 @@ export class TypeScriptGenerator { objectType: IntrospectionObjectType | IntrospectionInputObjectType | IntrospectionInterfaceType, allGQLTypes: IntrospectionType[] ): string[] { - let fields: (IntrospectionInputValue | IntrospectionField)[] + const fields: (IntrospectionInputValue | IntrospectionField)[] = objectType.kind === 'INPUT_OBJECT' ? objectType.inputFields : objectType.fields; const extendTypes: string[] = objectType.kind === 'OBJECT' @@ -158,7 +157,7 @@ export class TypeScriptGenerator { return prevTypescriptDefs; } - let fieldJsDoc = descriptionToJSDoc(field); + const fieldJsDoc = descriptionToJSDoc(field); const { fieldName, fieldType } = createFieldRef(field, this.options.typePrefix, this.options.strictNulls); const fieldNameAndType = `${fieldName}: ${fieldType};`; let typescriptDefs = [...fieldJsDoc, fieldNameAndType]; @@ -252,12 +251,12 @@ export class TypeScriptGenerator { * | ... */ private createUnionType(typeName: string, possibleTypes: string[]): string[] { - let result = `export type ${this.options.typePrefix}${typeName} = ${possibleTypes.join(' | ')};`; + const result = `export type ${this.options.typePrefix}${typeName} = ${possibleTypes.join(' | ')};`; if (result.length <= 80) { return [result]; } - let [firstLine, rest] = result.split('='); + const [firstLine, rest] = result.split('='); return [ firstLine + '=', diff --git a/src/typescriptResolverGenerator.ts b/src/typescriptResolverGenerator.ts index 08a8ea7..8ea309d 100644 --- a/src/typescriptResolverGenerator.ts +++ b/src/typescriptResolverGenerator.ts @@ -116,7 +116,7 @@ export class TSResolverGenerator { private generateTypeResolver(type: IntrospectionUnionType | IntrospectionInterfaceType) { const possbileTypes = type.possibleTypes.map(pt => `'${pt.name}'`); const interfaceName = `${this.options.typePrefix}${type.name}TypeResolver`; - const infoModifier = this.options.optionalResolverInfo ? '?' : ''; + const infoModifier = this.options.optionalResolverInfo ? '?' : ''; this.resolverInterfaces.push(...[ `export interface ${interfaceName} {`, @@ -164,8 +164,13 @@ export class TSResolverGenerator { const TParent = this.guessTParent(objectType.name); const TResult = this.guessTResult(field); const infoModifier = this.options.optionalResolverInfo ? '?' : ''; - const returnType = this.options.asyncResult ? 'TResult | Promise' : 'TResult'; - const subscriptionReturnType = + const returnType = + this.options.asyncResult === 'always' + ? 'Promise' + : !!this.options.asyncResult + ? 'TResult | Promise' + : 'TResult'; + const subscriptionReturnType = this.options.asyncResult ? 'AsyncIterator | Promise>' : 'AsyncIterator'; const fieldResolverTypeDef = !isSubscription ? [ @@ -179,7 +184,7 @@ export class TSResolverGenerator { // tslint:disable-next-line:max-line-length `resolve${this.getModifier()}: (parent: TParent, args: ${argsType}, context: ${this.contextType}, info${infoModifier}: GraphQLResolveInfo) => ${returnType};`, // tslint:disable-next-line:max-line-length - `subscribe: (parent: TParent, args: ${argsType}, context: ${this.contextType}, info${infoModifier}: GraphQLResolveInfo) => ${subscriptionReturnType};`, + `subscribe: (parent: TParent, args: ${argsType}, context: ${this.contextType}, info${infoModifier}: GraphQLResolveInfo) => ${subscriptionReturnType};`, '}', '' ];