Skip to content

Commit

Permalink
feat(schema-generator): refactors schema generator (#405)
Browse files Browse the repository at this point in the history
* feat(schema-generator): refactors schema generator
  • Loading branch information
goldcaddy77 authored Sep 3, 2020
1 parent 3db4652 commit 51f75f6
Show file tree
Hide file tree
Showing 19 changed files with 792 additions and 745 deletions.
2 changes: 2 additions & 0 deletions examples/02-complex-example/generated/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import {
DateTimeString
} from "../../../src";
import { StringEnum } from "../src/modules/user/user.model";

// @ts-ignore

import { User } from "../src/modules/user/user.model";

export enum UserOrderByEnum {
Expand Down
21 changes: 7 additions & 14 deletions src/cli/commands/codegen.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import { Container } from 'typedi';

import { logger } from '../../core';
import { CodeGenerator } from '../../core/code-generator';
import { CodeGenerator } from '../../core';

import { WarthogGluegunToolbox } from '../types';
Container.import([CodeGenerator]);

// BLOG: needed to switch from module.exports because it didn't compile correctly
export default {
// module.exports = {
name: 'codegen',
run: async (toolbox: WarthogGluegunToolbox) => {
const {
config: { load }
} = toolbox;

const config = load();

run: async () => {
try {
await new CodeGenerator(config.get('GENERATED_FOLDER'), config.get('DB_ENTITIES'), {
resolversPath: config.get('RESOLVERS_PATH'),
validateResolvers: config.get('VALIDATE_RESOLVERS') === 'true',
warthogImportPath: config.get('MODULE_IMPORT_PATH')
}).generate();
const generator = Container.get('CodeGenerator') as CodeGenerator;
await generator.generate();
} catch (error) {
logger.error(error);
if (error.name.indexOf('Cannot determine GraphQL input type') > -1) {
Expand Down
93 changes: 27 additions & 66 deletions src/core/code-generator.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,42 @@
// TODO-MVP: Add custom scalars such as graphql-iso-date
// import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date';

import { writeFile } from 'fs';
import { GraphQLID, GraphQLSchema, printSchema } from 'graphql';
import { GraphQLSchema, printSchema } from 'graphql';
import * as mkdirp from 'mkdirp';
import * as path from 'path';
import { buildSchema } from 'type-graphql';
import * as util from 'util';
import { Container, Inject, Service } from 'typedi';

import { Config, logger } from '../core';
import { debug } from '../decorators';
import { generateBindingFile } from '../gql';
import { SchemaGenerator } from '../schema';
import { SchemaBuilder, SchemaGenerator } from '../schema';
import { authChecker, loadFromGlobArray } from '../tgql';
import { logger } from '../core';
// Load all model files so that decorators will gather metadata for code generation
import { writeFilePromise } from '../utils';

import * as Debug from 'debug';

const debug = Debug('warthog:code-generators');

const writeFilePromise = util.promisify(writeFile);

interface CodeGeneratorOptions {
resolversPath: string[];
validateResolvers?: boolean;
warthogImportPath?: string;
}
Container.import([SchemaGenerator]);

@Service('CodeGenerator')
export class CodeGenerator {
generatedFolder: string;
schema?: GraphQLSchema;

constructor(
private generatedFolder: string,
// @ts-ignore
private modelsArray: string[],
private options: CodeGeneratorOptions
@Inject('Config') readonly config: Config,
@Inject('SchemaGenerator') readonly schemaGenerator: SchemaGenerator,
@Inject('SchemaBuilder') readonly schemaBuilder: SchemaBuilder
) {
this.generatedFolder = this.config.get('GENERATED_FOLDER');
this.createGeneratedFolder();
loadFromGlobArray(modelsArray);

loadFromGlobArray(this.config.get('DB_ENTITIES'));
}

createGeneratedFolder() {
return mkdirp.sync(this.generatedFolder);
}

@debug('warthog:code-generator')
async generate() {
debug('generate:start');
try {
await this.writeGeneratedIndexFile();
await this.writeGeneratedTSTypes();
Expand All @@ -55,70 +47,39 @@ export class CodeGenerator {
logger.error(error);
debug(error); // this is required to log when run in a separate project
}
debug('generate:end');
}

private async generateBinding() {
debug('generateBinding:start');
const schemaFilePath = path.join(this.generatedFolder, 'schema.graphql');
const outputBindingPath = path.join(this.generatedFolder, 'binding.ts');

const x = generateBindingFile(schemaFilePath, outputBindingPath);

debug('generateBinding:end');
return x;
return generateBindingFile(schemaFilePath, outputBindingPath);
}

// @debug
private async buildGraphQLSchema(): Promise<GraphQLSchema> {
if (!this.schema) {
debug('code-generator:buildGraphQLSchema:start');
debug(this.options.resolversPath);
this.schema = await buildSchema({
// TODO: we should replace this with an empty authChecker
// Note: using the base authChecker here just to generated the .graphql file
// it's not actually being utilized here
authChecker,
scalarsMap: [
{
type: 'ID' as any,
scalar: GraphQLID
}
],
resolvers: this.options.resolversPath,
validate: this.options.validateResolvers
this.schema = await this.schemaBuilder.build({
authChecker
});
debug('code-generator:buildGraphQLSchema:end');
}

return this.schema;
}

private async writeGeneratedTSTypes() {
debug('writeGeneratedTSTypes:start');
const generatedTSTypes = await this.getGeneratedTypes();
const x = await this.writeToGeneratedFolder('classes.ts', generatedTSTypes);
debug('writeGeneratedTSTypes:end');
return x;
const generatedTSTypes = this.getGeneratedTypes();
return this.writeToGeneratedFolder('classes.ts', generatedTSTypes);
}

private async getGeneratedTypes() {
debug('getGeneratedTypes:start');
const x = SchemaGenerator.generate(this.options.warthogImportPath);
debug('getGeneratedTypes:end');
return x;
// @debug
private getGeneratedTypes() {
return this.schemaGenerator.generate(this.config.get('MODULE_IMPORT_PATH'));
}

private async writeSchemaFile() {
debug('writeSchemaFile:start');
await this.buildGraphQLSchema();

const x = this.writeToGeneratedFolder(
'schema.graphql',
printSchema(this.schema as GraphQLSchema)
);

debug('writeSchemaFile:end');
return x;
return this.writeToGeneratedFolder('schema.graphql', printSchema(this.schema as GraphQLSchema));
}

// Write an index file that loads `classes` so that you can just import `../../generated`
Expand All @@ -128,7 +89,7 @@ export class CodeGenerator {
}

private async writeOrmConfig() {
const contents = `import { getBaseConfig } from '${this.options.warthogImportPath}';
const contents = `import { getBaseConfig } from '${this.config.get('MODULE_IMPORT_PATH')}';
module.exports = getBaseConfig();`;

Expand Down
3 changes: 2 additions & 1 deletion src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ export class Config {
// Prevent 502s from happening in AWS and GCP (and probably other Production ENVs)
// See https://shuheikagawa.com/blog/2019/04/25/keep-alive-timeout/
WARTHOG_KEEP_ALIVE_TIMEOUT_MS: 30000,
WARTHOG_HEADERS_TIMEOUT_MS: 60000
WARTHOG_HEADERS_TIMEOUT_MS: 60000,
WARTHOG_EXPLICIT_ENDPOINT_GENERATION: false
};

this.devDefaults = {
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './server';
export * from './BaseModel';
export * from './config';
export * from './code-generator';
export * from './Context';
export * from './logger';
export * from './types';
Expand Down
66 changes: 17 additions & 49 deletions src/core/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,26 @@
import { ApolloServer, OptionsJson, ApolloServerExpressConfig } from 'apollo-server-express';
import { Request } from 'express';
import express = require('express');
import { GraphQLID, GraphQLSchema } from 'graphql';
import { GraphQLSchema } from 'graphql';
import { Binding } from 'graphql-binding';
import { DateResolver } from 'graphql-scalars';
import { Server as HttpServer } from 'http';
import { Server as HttpsServer } from 'https';
const open = require('open'); // eslint-disable-line @typescript-eslint/no-var-requires
import { AuthChecker, buildSchema } from 'type-graphql'; // formatArgumentValidationError
import { AuthChecker } from 'type-graphql'; // formatArgumentValidationError
import { Container } from 'typedi';
import { Connection, ConnectionOptions, useContainer as TypeORMUseContainer } from 'typeorm';

import { logger, Logger } from '../core/logger';
import { debug } from '../decorators';
import { getRemoteBinding } from '../gql';
import { DataLoaderMiddleware, healthCheckMiddleware } from '../middleware';
import { healthCheckMiddleware } from '../middleware';
import { SchemaBuilder } from '../schema';
import { createDBConnection } from '../torm';

import { CodeGenerator } from './code-generator';
import { Config } from './config';

import { BaseContext } from './Context';

import * as Debug from 'debug';

const debug = Debug('warthog:server');

export interface ServerOptions<T> {
container?: Container;
apolloConfig?: ApolloServerExpressConfig;
Expand Down Expand Up @@ -61,6 +57,7 @@ export class Server<C extends BaseContext> {
logger: Logger;
schema?: GraphQLSchema;
bodyParserConfig?: OptionsJson;
schemaBuilder: SchemaBuilder;

constructor(
private appOptions: ServerOptions<C>,
Expand Down Expand Up @@ -103,7 +100,8 @@ export class Server<C extends BaseContext> {

// NOTE: this should be after we hard-code the WARTHOG_ env vars above because we want the config
// module to think they were set by the user
this.config = new Config({ container: this.container, logger: this.logger });
this.config = Container.get('Config') as Config;
this.schemaBuilder = Container.get('SchemaBuilder') as SchemaBuilder;

this.expressApp = this.appOptions.expressApp || express();

Expand All @@ -121,11 +119,10 @@ export class Server<C extends BaseContext> {
return logger;
}

@debug('warthog:server')
async establishDBConnection(): Promise<Connection> {
if (!this.connection) {
debug('establishDBConnection:start');
this.connection = await createDBConnection(this.dbOptions);
debug('establishDBConnection:end');
}

return this.connection;
Expand Down Expand Up @@ -158,44 +155,22 @@ export class Server<C extends BaseContext> {
}
}

@debug('warthog:server')
async buildGraphQLSchema(): Promise<GraphQLSchema> {
if (!this.schema) {
debug('server:buildGraphQLSchema:start');
this.schema = await buildSchema({
this.schema = await this.schemaBuilder.build({
authChecker: this.authChecker,
scalarsMap: [
{
type: 'ID' as any,
scalar: GraphQLID
},
// Note: DateTime already included in type-graphql
{
type: 'DateOnlyString' as any,
scalar: DateResolver
}
],
container: this.container as any,
// TODO: ErrorLoggerMiddleware
globalMiddlewares: [DataLoaderMiddleware, ...(this.appOptions.middlewares || [])],
resolvers: this.config.get('RESOLVERS_PATH'),
// TODO: scalarsMap: [{ type: GraphQLDate, scalar: GraphQLDate }]
validate: this.config.get('VALIDATE_RESOLVERS') === 'true'
middlewares: this.appOptions.middlewares
});
debug('server:buildGraphQLSchema:end');
}

return this.schema;
}

// @debug
async generateFiles(): Promise<void> {
debug('start:generateFiles:start');
await new CodeGenerator(this.config.get('GENERATED_FOLDER'), this.config.get('DB_ENTITIES'), {
resolversPath: this.config.get('RESOLVERS_PATH'),
validateResolvers: this.config.get('VALIDATE_RESOLVERS') === 'true',
warthogImportPath: this.config.get('MODULE_IMPORT_PATH')
}).generate();

debug('start:generateFiles:end');
const generator = Container.get('CodeGenerator') as CodeGenerator;
return generator.generate();
}

private startHttpServer(url: string): void {
Expand All @@ -210,8 +185,8 @@ export class Server<C extends BaseContext> {
this.httpServer.headersTimeout = headersTimeout;
}

@debug('warthog:server')
async start() {
debug('start:start');
await this.establishDBConnection();
if (this.config.get('AUTO_GENERATE_FILES') === 'true') {
await this.generateFiles();
Expand All @@ -224,7 +199,6 @@ export class Server<C extends BaseContext> {
return {};
});

debug('start:ApolloServerAllocation:start');
// See all options here: https://github.com/apollographql/apollo-server/blob/9ffb4a847e1503ea2ab1f3fcd47837daacf40870/packages/apollo-server-core/src/types.ts#L69
const playgroundOption = this.config.get('PLAYGROUND') === 'true' ? { playground: true } : {};
const introspectionOption =
Expand All @@ -251,21 +225,17 @@ export class Server<C extends BaseContext> {
...this.apolloConfig
});

debug('start:ApolloServerAllocation:end');

this.expressApp.use('/health', healthCheckMiddleware);

if (this.appOptions.onBeforeGraphQLMiddleware) {
this.appOptions.onBeforeGraphQLMiddleware(this.expressApp);
}

debug('start:applyMiddleware:start');
this.graphQLServer.applyMiddleware({
app: this.expressApp,
bodyParserConfig: this.bodyParserConfig,
path: '/graphql'
});
debug('start:applyMiddleware:end');

if (this.appOptions.onAfterGraphQLMiddleware) {
this.appOptions.onAfterGraphQLMiddleware(this.expressApp);
Expand All @@ -282,11 +252,9 @@ export class Server<C extends BaseContext> {
// Open playground in the browser
if (this.config.get('AUTO_OPEN_PLAYGROUND') === 'true') {
// Assigning to variable and logging to appease linter
const process = open(url, { wait: false });
debug('process', process);
open(url, { wait: false });
}

debug('start:end');
return this;
}

Expand Down
6 changes: 6 additions & 0 deletions src/decorators/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Helpful resources

### Creating new decorators

- Good examples: https://github.com/NetanelBasal/helpful-decorators
- Templates: https://github.com/arolson101/typescript-decorators
Loading

0 comments on commit 51f75f6

Please sign in to comment.