Skip to content
Merged

Dev #30

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 16 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -129,24 +128,15 @@ export interface RootQueryToUsersResolver<TParent = undefined, TResult = Array<G
}
```

## TODO
- [ ] More detailed API Documentation
- [ ] Integrate with Travis CI

## Change log
* v1.2.2:
* Strategy for guessing TParent & TResult in resolvers
* v1.2.1:
* Added strict nulls option for compatibility with apollo-codegen
* v1.2.0:
* Field resolvers under subscriptions are being generated with resolve and subscribe method
* v1.1.0:
* Add CLIs support
* v1.0.6:
* Generate TypeScript for resolvers. See [Type Resolvers](#type-resolvers)
* v1.0.4:
* If types is generated under global scope, use string union instead of string enum

* v1.0.2:
* Change default prefix from `GQL_` to `GQL`
* Add config options: allow to generate types under a global or namespace declaration
```javascript
// in v1.12.11, asyncResult also accept string value 'always',
// which will make returns value of resolve functions to be `Promise<TResult>`,
// due to an issue with VSCode that not showing auto completion when returns is a mix of `T | Promise<T>` (see [#17](https://github.com/dangcuuson/graphql-schema-typescript/issues/17))

// smartTParent: true
// smartTResult: true
// asyncResult: 'always'
export interface RootQueryToUsersResolver<TParent = undefined, TResult = Array<GQLUser> {
(parent: TParent, args: RootQueryToUsersArgs, context: any, info: GraphQLResolveInfo): Promise<TResult>; // the different is here

```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -55,4 +55,4 @@
"json"
]
}
}
}
56 changes: 49 additions & 7 deletions src/__tests__/__snapshots__/option.global_nameSpace.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1162,7 +1204,7 @@ import { GraphQLResolveInfo, GraphQLScalarType } from 'graphql';
*/


namespace MyNamespace {
declare namespace MyNamespace {
/*******************************
* *
* TYPE DEFS *
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/option.global_nameSpace.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ yargs
})
.option(asyncResult, {
desc: 'Set return type of resolver to `TResult | Promise<TResult>`',
boolean: true
choices: [true, 'always']
})
.option(requireResolverTypes, {
desc: 'Set resolvers to be required. Useful to ensure no resolvers is missing',
Expand Down
13 changes: 9 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ const typeResolversDecoration = [
' *********************************/'
];

export const generateTSTypesAsString = async (schema: GraphQLSchema | string, options: GenerateTypescriptOptions): Promise<string> => {
export const generateTSTypesAsString = async (
schema: GraphQLSchema | string,
outputPath: string,
options: GenerateTypescriptOptions
): Promise<string> => {
const mergedOptions = { ...defaultOptions, ...options };

let introspectResult: IntrospectionQuery;
Expand All @@ -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 = {
Expand All @@ -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,
'}'
];
Expand All @@ -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');
}
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,13 @@ export interface GenerateTypescriptOptions {
/**
* This option is for resolvers
* If true, set return type of resolver to `TResult | Promise<TResult>`
* If 'awalys', set return type of resolver to `Promise<TResult>`
*
* e.g: interface QueryToUsersResolver<TParent = any, TResult = any> {
* (parent: TParent, args: {}, context: any, info): TResult extends Promise ? TResult : TResult | Promise<TResult>
* }
*/
asyncResult?: boolean;
asyncResult?: boolean | 'always';

/**
* If true, field resolver of each type will be required, instead of optional
Expand Down
27 changes: 13 additions & 14 deletions src/typescriptGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export class TypeScriptGenerator {

constructor(
protected options: GenerateTypescriptOptions,
protected introspectResult: IntrospectionQuery
protected introspectResult: IntrospectionQuery,
protected outputPath: string
) { }

public async generate(): Promise<string[]> {
Expand Down Expand Up @@ -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<string[]>(
(prevTypescriptDefs, enumValue, index) => {
let typescriptDefs: string[] = [];
Expand All @@ -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,
'}'
];
Expand All @@ -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'
Expand All @@ -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];
Expand Down Expand Up @@ -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 + '=',
Expand Down
13 changes: 9 additions & 4 deletions src/typescriptResolverGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}<TParent = ${this.guessTParent(type.name)}> {`,
Expand Down Expand Up @@ -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>' : 'TResult';
const subscriptionReturnType =
const returnType =
this.options.asyncResult === 'always'
? 'Promise<TResult>'
: !!this.options.asyncResult
? 'TResult | Promise<TResult>'
: 'TResult';
const subscriptionReturnType =
this.options.asyncResult ? 'AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>' : 'AsyncIterator<TResult>';
const fieldResolverTypeDef = !isSubscription
? [
Expand All @@ -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};`,
'}',
''
];
Expand Down