Skip to content

Commit 6850047

Browse files
author
dangcuuson
authored
Merge pull request #30 from dangcuuson/dev
Close #20 & #29
2 parents 877ae13 + d00c9ca commit 6850047

File tree

9 files changed

+104
-62
lines changed

9 files changed

+104
-62
lines changed

README.md

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ types to make it type-safed when developing GraphQL server (mainly resolvers)
99

1010
## Features
1111

12-
### Generate Typescript from Schema Definition
13-
### 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)
14-
### Convert GraphQL description into JSDoc
15-
### Also add deprecated field and reason into JSDoc
12+
### Generate Typescript from Schema Definition (1-1 mapping from GQL type to TypeScript)
13+
### Convert GraphQL description into JSDoc, include deprecated directive
1614
### [Generate TypeScripts to support writing resolvers](#type-resolvers)
15+
### [VSCode extension](#https://github.com/liyikun/vscode-graphql-schema-typescript) (credit to [@liyikun](https://github.com/liyikun))
1716

1817
## Usage
1918

@@ -52,7 +51,7 @@ The file generated will have some types that can make it type-safed when writing
5251
* Parent type and resolve result is default to `any`, but could be overwritten in your code
5352

5453
For example, if you schema is like this:
55-
```
54+
```gql
5655
schema {
5756
query: RootQuery
5857
}
@@ -129,24 +128,15 @@ export interface RootQueryToUsersResolver<TParent = undefined, TResult = Array<G
129128
}
130129
```
131130

132-
## TODO
133-
- [ ] More detailed API Documentation
134-
- [ ] Integrate with Travis CI
135-
136-
## Change log
137-
* v1.2.2:
138-
* Strategy for guessing TParent & TResult in resolvers
139-
* v1.2.1:
140-
* Added strict nulls option for compatibility with apollo-codegen
141-
* v1.2.0:
142-
* Field resolvers under subscriptions are being generated with resolve and subscribe method
143-
* v1.1.0:
144-
* Add CLIs support
145-
* v1.0.6:
146-
* Generate TypeScript for resolvers. See [Type Resolvers](#type-resolvers)
147-
* v1.0.4:
148-
* If types is generated under global scope, use string union instead of string enum
149-
150-
* v1.0.2:
151-
* Change default prefix from `GQL_` to `GQL`
152-
* Add config options: allow to generate types under a global or namespace declaration
131+
```javascript
132+
// in v1.12.11, asyncResult also accept string value 'always',
133+
// which will make returns value of resolve functions to be `Promise<TResult>`,
134+
// 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))
135+
136+
// smartTParent: true
137+
// smartTResult: true
138+
// asyncResult: 'always'
139+
export interface RootQueryToUsersResolver<TParent = undefined, TResult = Array<GQLUser> {
140+
(parent: TParent, args: RootQueryToUsersArgs, context: any, info: GraphQLResolveInfo): Promise<TResult>; // the different is here
141+
142+
```

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-schema-typescript",
3-
"version": "1.2.10",
3+
"version": "1.3.1",
44
"description": "Generate TypeScript from GraphQL's schema type definitions",
55
"main": "./lib/index.js",
66
"types": "./lib/index.d.ts",
@@ -55,4 +55,4 @@
5555
"json"
5656
]
5757
}
58-
}
58+
}

src/__tests__/__snapshots__/option.global_nameSpace.test.ts.snap

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,29 @@ declare global {
6161
* In the future, this will be changed by having User as interface
6262
* and implementing multiple User
6363
*/
64-
export type GQLUserRole = 'sysAdmin' | 'manager' | 'clerk' | 'employee';
65-
// NOTE: enum UserRole is generate as string union instead of string enum because the types is generated under global scope
64+
export const enum GQLUserRole {
65+
66+
/**
67+
* System Administrator
68+
*/
69+
sysAdmin = 'sysAdmin',
70+
71+
/**
72+
* Manager - Have access to manage functions
73+
*/
74+
manager = 'manager',
75+
76+
/**
77+
* General Staff
78+
*/
79+
clerk = 'clerk',
80+
81+
/**
82+
*
83+
* @deprecated Use 'clerk' instead
84+
*/
85+
employee = 'employee'
86+
}
6687

6788
export interface GQLIProduct {
6889
id: string;
@@ -577,7 +598,7 @@ declare global {
577598
}"
578599
`;
579600

580-
exports[`global + namespace should wrap types in global and use string union if global is configured 1`] = `
601+
exports[`global + namespace should wrap types in global and use const enum if global is configured 1`] = `
581602
"/* tslint:disable */
582603
/* eslint-disable */
583604
import { GraphQLResolveInfo, GraphQLScalarType } from 'graphql';
@@ -637,8 +658,29 @@ declare global {
637658
* In the future, this will be changed by having User as interface
638659
* and implementing multiple User
639660
*/
640-
export type GQLUserRole = 'sysAdmin' | 'manager' | 'clerk' | 'employee';
641-
// NOTE: enum UserRole is generate as string union instead of string enum because the types is generated under global scope
661+
export const enum GQLUserRole {
662+
663+
/**
664+
* System Administrator
665+
*/
666+
sysAdmin = 'sysAdmin',
667+
668+
/**
669+
* Manager - Have access to manage functions
670+
*/
671+
manager = 'manager',
672+
673+
/**
674+
* General Staff
675+
*/
676+
clerk = 'clerk',
677+
678+
/**
679+
*
680+
* @deprecated Use 'clerk' instead
681+
*/
682+
employee = 'employee'
683+
}
642684

643685
export interface GQLIProduct {
644686
id: string;
@@ -1162,7 +1204,7 @@ import { GraphQLResolveInfo, GraphQLScalarType } from 'graphql';
11621204
*/
11631205

11641206

1165-
namespace MyNamespace {
1207+
declare namespace MyNamespace {
11661208
/*******************************
11671209
* *
11681210
* TYPE DEFS *
@@ -1210,7 +1252,7 @@ namespace MyNamespace {
12101252
* In the future, this will be changed by having User as interface
12111253
* and implementing multiple User
12121254
*/
1213-
export enum GQLUserRole {
1255+
export const enum GQLUserRole {
12141256

12151257
/**
12161258
* System Administrator

src/__tests__/option.global_nameSpace.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { executeApiTest } from './testUtils';
22

33
describe('global + namespace', () => {
4-
it('should wrap types in global and use string union if global is configured', async () => {
4+
it('should wrap types in global and use const enum if global is configured', async () => {
55
const generated = await executeApiTest('global.ts', { global: true });
66
expect(generated).toContain('declare global {');
7-
expect(generated).toContain(`export type GQLUserRole = 'sysAdmin' | 'manager' | 'clerk' | 'employee';`);
7+
expect(generated).toContain(`export const enum GQLUserRole {`);
88
});
99

1010
it('should wrap types in namespace if namespace is configured', async () => {
1111
const generated = await executeApiTest('global.ts', { namespace: 'MyNamespace' });
12-
expect(generated).toContain('namespace MyNamespace {');
12+
expect(generated).toContain('declare namespace MyNamespace {');
1313
});
1414

1515
it('should have no conflict between global and namespace config', async () => {

src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ yargs
7272
})
7373
.option(asyncResult, {
7474
desc: 'Set return type of resolver to `TResult | Promise<TResult>`',
75-
boolean: true
75+
choices: [true, 'always']
7676
})
7777
.option(requireResolverTypes, {
7878
desc: 'Set resolvers to be required. Useful to ensure no resolvers is missing',

src/index.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ const typeResolversDecoration = [
3636
' *********************************/'
3737
];
3838

39-
export const generateTSTypesAsString = async (schema: GraphQLSchema | string, options: GenerateTypescriptOptions): Promise<string> => {
39+
export const generateTSTypesAsString = async (
40+
schema: GraphQLSchema | string,
41+
outputPath: string,
42+
options: GenerateTypescriptOptions
43+
): Promise<string> => {
4044
const mergedOptions = { ...defaultOptions, ...options };
4145

4246
let introspectResult: IntrospectionQuery;
@@ -62,7 +66,7 @@ export const generateTSTypesAsString = async (schema: GraphQLSchema | string, op
6266
introspectResult = await introspectSchema(schema);
6367
}
6468

65-
const tsGenerator = new TypeScriptGenerator(mergedOptions, introspectResult);
69+
const tsGenerator = new TypeScriptGenerator(mergedOptions, introspectResult, outputPath);
6670
const typeDefs = await tsGenerator.generate();
6771

6872
let typeResolvers: GenerateResolversResult = {
@@ -83,7 +87,8 @@ export const generateTSTypesAsString = async (schema: GraphQLSchema | string, op
8387

8488
if (mergedOptions.namespace) {
8589
body = [
86-
`namespace ${options.namespace} {`,
90+
// if namespace is under global, it doesn't need to be declared again
91+
`${mergedOptions.global ? '' : 'declare '}namespace ${options.namespace} {`,
8792
...body,
8893
'}'
8994
];
@@ -108,6 +113,6 @@ export async function generateTypeScriptTypes(
108113
outputPath: string,
109114
options: GenerateTypescriptOptions = defaultOptions
110115
) {
111-
const content = await generateTSTypesAsString(schema, options);
116+
const content = await generateTSTypesAsString(schema, outputPath, options);
112117
fs.writeFileSync(outputPath, content, 'utf-8');
113118
}

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,13 @@ export interface GenerateTypescriptOptions {
8181
/**
8282
* This option is for resolvers
8383
* If true, set return type of resolver to `TResult | Promise<TResult>`
84+
* If 'awalys', set return type of resolver to `Promise<TResult>`
8485
*
8586
* e.g: interface QueryToUsersResolver<TParent = any, TResult = any> {
8687
* (parent: TParent, args: {}, context: any, info): TResult extends Promise ? TResult : TResult | Promise<TResult>
8788
* }
8889
*/
89-
asyncResult?: boolean;
90+
asyncResult?: boolean | 'always';
9091

9192
/**
9293
* If true, field resolver of each type will be required, instead of optional

src/typescriptGenerator.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export class TypeScriptGenerator {
2222

2323
constructor(
2424
protected options: GenerateTypescriptOptions,
25-
protected introspectResult: IntrospectionQuery
25+
protected introspectResult: IntrospectionQuery,
26+
protected outputPath: string
2627
) { }
2728

2829
public async generate(): Promise<string[]> {
@@ -94,14 +95,6 @@ export class TypeScriptGenerator {
9495
return this.createUnionType(enumType.name, enumType.enumValues.map(v => `'${v.name}'`));
9596
}
9697

97-
// if generate as global, don't generate string enum as it requires import
98-
if (this.options.global) {
99-
return [
100-
...this.createUnionType(enumType.name, enumType.enumValues.map(v => `'${v.name}'`)),
101-
`// NOTE: enum ${enumType.name} is generate as string union instead of string enum because the types is generated under global scope`
102-
];
103-
}
104-
10598
let enumBody = enumType.enumValues.reduce<string[]>(
10699
(prevTypescriptDefs, enumValue, index) => {
107100
let typescriptDefs: string[] = [];
@@ -125,8 +118,14 @@ export class TypeScriptGenerator {
125118
[]
126119
);
127120

121+
// if code is generated as type declaration, better use export const enum instead
122+
// of just export enum
123+
const isGeneratingDeclaration = this.options.global
124+
|| !!this.options.namespace
125+
|| this.outputPath.endsWith('.d.ts');
126+
const enumModifier = isGeneratingDeclaration ? ' const ' : ' ';
128127
return [
129-
`export enum ${this.options.typePrefix}${enumType.name} {`,
128+
`export${enumModifier}enum ${this.options.typePrefix}${enumType.name} {`,
130129
...enumBody,
131130
'}'
132131
];
@@ -136,7 +135,7 @@ export class TypeScriptGenerator {
136135
objectType: IntrospectionObjectType | IntrospectionInputObjectType | IntrospectionInterfaceType,
137136
allGQLTypes: IntrospectionType[]
138137
): string[] {
139-
let fields: (IntrospectionInputValue | IntrospectionField)[]
138+
const fields: (IntrospectionInputValue | IntrospectionField)[]
140139
= objectType.kind === 'INPUT_OBJECT' ? objectType.inputFields : objectType.fields;
141140

142141
const extendTypes: string[] = objectType.kind === 'OBJECT'
@@ -158,7 +157,7 @@ export class TypeScriptGenerator {
158157
return prevTypescriptDefs;
159158
}
160159

161-
let fieldJsDoc = descriptionToJSDoc(field);
160+
const fieldJsDoc = descriptionToJSDoc(field);
162161
const { fieldName, fieldType } = createFieldRef(field, this.options.typePrefix, this.options.strictNulls);
163162
const fieldNameAndType = `${fieldName}: ${fieldType};`;
164163
let typescriptDefs = [...fieldJsDoc, fieldNameAndType];
@@ -252,12 +251,12 @@ export class TypeScriptGenerator {
252251
* | ...
253252
*/
254253
private createUnionType(typeName: string, possibleTypes: string[]): string[] {
255-
let result = `export type ${this.options.typePrefix}${typeName} = ${possibleTypes.join(' | ')};`;
254+
const result = `export type ${this.options.typePrefix}${typeName} = ${possibleTypes.join(' | ')};`;
256255
if (result.length <= 80) {
257256
return [result];
258257
}
259258

260-
let [firstLine, rest] = result.split('=');
259+
const [firstLine, rest] = result.split('=');
261260

262261
return [
263262
firstLine + '=',

src/typescriptResolverGenerator.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class TSResolverGenerator {
116116
private generateTypeResolver(type: IntrospectionUnionType | IntrospectionInterfaceType) {
117117
const possbileTypes = type.possibleTypes.map(pt => `'${pt.name}'`);
118118
const interfaceName = `${this.options.typePrefix}${type.name}TypeResolver`;
119-
const infoModifier = this.options.optionalResolverInfo ? '?' : '';
119+
const infoModifier = this.options.optionalResolverInfo ? '?' : '';
120120

121121
this.resolverInterfaces.push(...[
122122
`export interface ${interfaceName}<TParent = ${this.guessTParent(type.name)}> {`,
@@ -164,8 +164,13 @@ export class TSResolverGenerator {
164164
const TParent = this.guessTParent(objectType.name);
165165
const TResult = this.guessTResult(field);
166166
const infoModifier = this.options.optionalResolverInfo ? '?' : '';
167-
const returnType = this.options.asyncResult ? 'TResult | Promise<TResult>' : 'TResult';
168-
const subscriptionReturnType =
167+
const returnType =
168+
this.options.asyncResult === 'always'
169+
? 'Promise<TResult>'
170+
: !!this.options.asyncResult
171+
? 'TResult | Promise<TResult>'
172+
: 'TResult';
173+
const subscriptionReturnType =
169174
this.options.asyncResult ? 'AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>' : 'AsyncIterator<TResult>';
170175
const fieldResolverTypeDef = !isSubscription
171176
? [
@@ -179,7 +184,7 @@ export class TSResolverGenerator {
179184
// tslint:disable-next-line:max-line-length
180185
`resolve${this.getModifier()}: (parent: TParent, args: ${argsType}, context: ${this.contextType}, info${infoModifier}: GraphQLResolveInfo) => ${returnType};`,
181186
// tslint:disable-next-line:max-line-length
182-
`subscribe: (parent: TParent, args: ${argsType}, context: ${this.contextType}, info${infoModifier}: GraphQLResolveInfo) => ${subscriptionReturnType};`,
187+
`subscribe: (parent: TParent, args: ${argsType}, context: ${this.contextType}, info${infoModifier}: GraphQLResolveInfo) => ${subscriptionReturnType};`,
183188
'}',
184189
''
185190
];

0 commit comments

Comments
 (0)