Skip to content

Commit

Permalink
Merge pull request #785 from nestjs/feat/def-generator-options
Browse files Browse the repository at this point in the history
feat(): add additional definitions generator options
  • Loading branch information
kamilmysliwiec committed Apr 16, 2020
2 parents 7d58f5e + 6e41314 commit 839630c
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 26 deletions.
61 changes: 49 additions & 12 deletions lib/graphql-ast.explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ import { DEFINITIONS_FILE_HEADER } from './graphql.constants';

let tsMorphLib: typeof import('ts-morph') | undefined;

export interface DefinitionsGeneratorOptions {
/**
* If true, the additional "__typename" field is generated for every object type.
* @default false
*/
emitTypenameField?: boolean;

/**
* If true, resolvers (query/mutation/etc) are generated as plain fields without arguments.
* @default false
*/
skipResolverArgs?: boolean;
}

@Injectable()
export class GraphQLAstExplorer {
private readonly root = ['Query', 'Mutation', 'Subscription'];
Expand All @@ -35,6 +49,7 @@ export class GraphQLAstExplorer {
documentNode: DocumentNode,
outputPath: string,
mode: 'class' | 'interface',
options: DefinitionsGeneratorOptions = {},
): Promise<SourceFile> {
if (!documentNode) {
return;
Expand All @@ -60,6 +75,7 @@ export class GraphQLAstExplorer {
item as Readonly<TypeSystemDefinitionNode>,
tsFile,
mode,
options,
),
);

Expand All @@ -71,6 +87,7 @@ export class GraphQLAstExplorer {
item: Readonly<TypeSystemDefinitionNode>,
tsFile: SourceFile,
mode: 'class' | 'interface',
options: DefinitionsGeneratorOptions,
) {
switch (item.kind) {
case 'SchemaDefinition':
Expand All @@ -81,9 +98,9 @@ export class GraphQLAstExplorer {
);
case 'ObjectTypeDefinition':
case 'InputObjectTypeDefinition':
return this.addObjectTypeDefinition(item, tsFile, mode);
return this.addObjectTypeDefinition(item, tsFile, mode, options);
case 'InterfaceTypeDefinition':
return this.addObjectTypeDefinition(item, tsFile, 'interface');
return this.addObjectTypeDefinition(item, tsFile, 'interface', options);
case 'ScalarTypeDefinition':
return this.addScalarDefinition(item, tsFile);
case 'EnumTypeDefinition':
Expand Down Expand Up @@ -133,6 +150,7 @@ export class GraphQLAstExplorer {
| InterfaceTypeDefinitionNode,
tsFile: SourceFile,
mode: 'class' | 'interface',
options: DefinitionsGeneratorOptions,
) {
const parentName = get(item, 'name.value');
if (!parentName) {
Expand Down Expand Up @@ -164,27 +182,38 @@ export class GraphQLAstExplorer {
this.addExtendInterfaces(interfaces, parentRef as InterfaceDeclaration);
}
}

const isObjectType = item.kind === 'ObjectTypeDefinition';
if (isObjectType && options.emitTypenameField) {
parentRef.addProperty({
name: '__typename',
type: `'${parentRef.getName()}'`,
hasQuestionToken: true,
});
}
((item.fields || []) as any).forEach((element) => {
this.lookupFieldDefiniton(element, parentRef, mode);
this.lookupFieldDefiniton(element, parentRef, mode, options);
});
}

lookupFieldDefiniton(
item: FieldDefinitionNode | InputValueDefinitionNode,
parentRef: InterfaceDeclaration | ClassDeclaration,
mode: 'class' | 'interface',
options: DefinitionsGeneratorOptions,
) {
switch (item.kind) {
case 'FieldDefinition':
case 'InputValueDefinition':
return this.lookupField(item, parentRef, mode);
return this.lookupField(item, parentRef, mode, options);
}
}

lookupField(
item: FieldDefinitionNode | InputValueDefinitionNode,
parentRef: InterfaceDeclaration | ClassDeclaration,
mode: 'class' | 'interface',
options: DefinitionsGeneratorOptions,
) {
const propertyName = get(item, 'name.value');
if (!propertyName) {
Expand All @@ -204,14 +233,22 @@ export class GraphQLAstExplorer {
});
return;
}
(parentRef as ClassDeclaration).addMethod({
isAbstract: mode === 'class',
name: propertyName,
returnType: `${type} | Promise<${type}>`,
parameters: this.getFunctionParameters(
(item as FieldDefinitionNode).arguments,
),
});
if (options.skipResolverArgs) {
(parentRef as ClassDeclaration).addProperty({
name: propertyName,
type,
hasQuestionToken: !required,
});
} else {
(parentRef as ClassDeclaration).addMethod({
isAbstract: mode === 'class',
name: propertyName,
returnType: `${type} | Promise<${type}>`,
parameters: this.getFunctionParameters(
(item as FieldDefinitionNode).arguments,
),
});
}
}

getFieldTypeDefinition(
Expand Down
42 changes: 31 additions & 11 deletions lib/graphql-definitions.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,46 @@ import { gql } from 'apollo-server-core';
import * as chokidar from 'chokidar';
import { printSchema } from 'graphql';
import { makeExecutableSchema } from 'graphql-tools';
import { GraphQLAstExplorer } from './graphql-ast.explorer';
import {
DefinitionsGeneratorOptions,
GraphQLAstExplorer,
} from './graphql-ast.explorer';
import { GraphQLTypesLoader } from './graphql-types.loader';
import { removeTempField } from './utils';

export class GraphQLDefinitionsFactory {
private readonly gqlAstExplorer = new GraphQLAstExplorer();
private readonly gqlTypesLoader = new GraphQLTypesLoader();

async generate(options: {
typePaths: string[];
path: string;
outputAs?: 'class' | 'interface';
watch?: boolean;
debug?: boolean;
federation?: boolean;
}) {
async generate(
options: {
typePaths: string[];
path: string;
outputAs?: 'class' | 'interface';
watch?: boolean;
debug?: boolean;
federation?: boolean;
} & DefinitionsGeneratorOptions,
) {
const isDebugEnabled = !(options && options.debug === false);
const typePathsExists = options.typePaths && !isEmpty(options.typePaths);
const isFederation = options && options.federation;
if (!typePathsExists) {
throw new Error(`"typePaths" property cannot be empty.`);
}

const isFederation = options && options.federation;
const definitionsGeneratorOptions: DefinitionsGeneratorOptions = {
emitTypenameField: options.emitTypenameField,
skipResolverArgs: options.skipResolverArgs,
};

if (options.watch) {
this.printMessage(
'GraphQL factory is watching your files...',
isDebugEnabled,
);
const watcher = chokidar.watch(options.typePaths);
watcher.on('change', async file => {
watcher.on('change', async (file) => {
this.printMessage(
`[${new Date().toLocaleTimeString()}] "${file}" has been changed.`,
isDebugEnabled,
Expand All @@ -43,6 +54,7 @@ export class GraphQLDefinitionsFactory {
options.outputAs,
isFederation,
isDebugEnabled,
definitionsGeneratorOptions,
);
});
}
Expand All @@ -52,6 +64,7 @@ export class GraphQLDefinitionsFactory {
options.outputAs,
isFederation,
isDebugEnabled,
definitionsGeneratorOptions,
);
}

Expand All @@ -61,20 +74,23 @@ export class GraphQLDefinitionsFactory {
outputAs: 'class' | 'interface',
isFederation: boolean,
isDebugEnabled: boolean,
definitionsGeneratorOptions: DefinitionsGeneratorOptions = {},
) {
if (isFederation) {
return this.exploreAndEmitFederation(
typePaths,
path,
outputAs,
isDebugEnabled,
definitionsGeneratorOptions,
);
}
return this.exploreAndEmitRegular(
typePaths,
path,
outputAs,
isDebugEnabled,
definitionsGeneratorOptions,
);
}

Expand All @@ -83,6 +99,7 @@ export class GraphQLDefinitionsFactory {
path: string,
outputAs: 'class' | 'interface',
isDebugEnabled: boolean,
definitionsGeneratorOptions: DefinitionsGeneratorOptions,
) {
const typeDefs = await this.gqlTypesLoader.mergeTypesByPaths(typePaths);

Expand All @@ -107,6 +124,7 @@ export class GraphQLDefinitionsFactory {
`,
path,
outputAs,
definitionsGeneratorOptions,
);
await tsFile.save();
this.printMessage(
Expand All @@ -120,6 +138,7 @@ export class GraphQLDefinitionsFactory {
path: string,
outputAs: 'class' | 'interface',
isDebugEnabled: boolean,
definitionsGeneratorOptions: DefinitionsGeneratorOptions,
) {
const typeDefs = await this.gqlTypesLoader.mergeTypesByPaths(
typePaths || [],
Expand All @@ -138,6 +157,7 @@ export class GraphQLDefinitionsFactory {
`,
path,
outputAs,
definitionsGeneratorOptions,
);
await tsFile.save();
this.printMessage(
Expand Down
10 changes: 9 additions & 1 deletion lib/graphql.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
SchemaDirectiveVisitor,
} from 'graphql-tools';
import { forEach, isEmpty } from 'lodash';
import { GraphQLAstExplorer } from './graphql-ast.explorer';
import {
DefinitionsGeneratorOptions,
GraphQLAstExplorer,
} from './graphql-ast.explorer';
import { GraphQLSchemaBuilder } from './graphql-schema.builder';
import { GraphQLSchemaHost } from './graphql-schema.host';
import { GqlModuleOptions } from './interfaces';
Expand Down Expand Up @@ -192,12 +195,17 @@ export class GraphQLFactory {
if (isEmpty(typeDefs) || !options.definitions) {
return;
}
const definitionsGeneratorOptions: DefinitionsGeneratorOptions = {
emitTypenameField: options.definitions.emitTypenameField,
skipResolverArgs: options.definitions.skipResolverArgs,
};
const tsFile = await this.graphqlAstExplorer.explore(
gql`
${typeDefs}
`,
options.definitions.path,
options.definitions.outputAs,
definitionsGeneratorOptions,
);
if (
!existsSync(options.definitions.path) ||
Expand Down
3 changes: 2 additions & 1 deletion lib/interfaces/gql-module-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Type } from '@nestjs/common';
import { ModuleMetadata } from '@nestjs/common/interfaces';
import { Config } from 'apollo-server-core';
import { GraphQLSchema } from 'graphql';
import { DefinitionsGeneratorOptions } from '../graphql-ast.explorer';
import { BuildSchemaOptions } from './build-schema-options.interface';

export interface ServerRegistration {
Expand Down Expand Up @@ -48,7 +49,7 @@ export interface GqlModuleOptions
definitions?: {
path?: string;
outputAs?: 'class' | 'interface';
};
} & DefinitionsGeneratorOptions;
autoSchemaFile?: string | boolean;
buildSchemaOptions?: BuildSchemaOptions;
/**
Expand Down
45 changes: 44 additions & 1 deletion tests/e2e/generated-definitions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ApplicationModule } from '../code-first/app.module';

const readFile = util.promisify(fs.readFile);

const generatedDefinitions = fileName =>
const generatedDefinitions = (fileName) =>
path.join(__dirname, '..', 'generated-definitions', fileName);

describe('Generated Definitions', () => {
Expand Down Expand Up @@ -105,6 +105,30 @@ describe('Generated Definitions', () => {
).toBe(await readFile(outputFile, 'utf8'));
});

it('should generate queries with methods as fields (skipResolverArgs: true)', async () => {
const typeDefs = await readFile(
generatedDefinitions('query.graphql'),
'utf8',
);

const outputFile = generatedDefinitions(
'query-skip-args.test-definitions.ts',
);
await graphqlFactory.generateDefinitions(typeDefs, {
definitions: {
path: outputFile,
skipResolverArgs: true,
},
});

expect(
await readFile(
generatedDefinitions('query-skip-args.fixture.ts'),
'utf8',
),
).toBe(await readFile(outputFile, 'utf8'));
});

it('should generate mutations', async () => {
const typeDefs = await readFile(
generatedDefinitions('mutation.graphql'),
Expand Down Expand Up @@ -176,6 +200,25 @@ describe('Generated Definitions', () => {
).toBe(await readFile(outputFile, 'utf8'));
});

it('should generate with __typename field for each object type', async () => {
const typeDefs = await readFile(
generatedDefinitions('typename.graphql'),
'utf8',
);

const outputFile = generatedDefinitions('typename.test-definitions.ts');
await graphqlFactory.generateDefinitions(typeDefs, {
definitions: {
path: outputFile,
emitTypenameField: true,
},
});

expect(
await readFile(generatedDefinitions('typename.fixture.ts'), 'utf8'),
).toBe(await readFile(outputFile, 'utf8'));
});

afterEach(async () => {
await app.close();
});
Expand Down
15 changes: 15 additions & 0 deletions tests/generated-definitions/query-skip-args.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

/** ------------------------------------------------------
* THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
* -------------------------------------------------------
*/

/* tslint:disable */
/* eslint-disable */
export interface Cat {
id: number;
}

export interface IQuery {
cat?: Cat;
}

0 comments on commit 839630c

Please sign in to comment.