Skip to content

Commit

Permalink
feat(graphql): Add graphql module
Browse files Browse the repository at this point in the history
* Automatically create resolvers
  • Loading branch information
doug-martin committed May 9, 2020
1 parent 34ff217 commit 282c421
Show file tree
Hide file tree
Showing 22 changed files with 139 additions and 36 deletions.
8 changes: 7 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/* eslint-disable import/export */
export * from './interfaces';
export * from './common';
export { QueryService, AssemblerQueryService, RelationQueryService, QueryServiceRelation } from './services';
export {
QueryService,
AssemblerQueryService,
RelationQueryService,
QueryServiceRelation,
getQueryServiceToken,
} from './services';
export { transformFilter, transformQuery, transformSort, QueryFieldMap } from './helpers';
export {
ClassTransformerAssembler,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { QueryService } from './query.service';
export { QueryService, getQueryServiceToken } from './query.service';
export { AssemblerQueryService } from './assembler-query.service';
export { RelationQueryService, QueryServiceRelation } from './relation-query.service';
4 changes: 4 additions & 0 deletions packages/core/src/services/query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,7 @@ export function QueryService<DTO>(DTOClass: Class<DTO>) {
return Injectable()(cls);
};
}

export function getQueryServiceToken<DTO>(DTOClass: Class<DTO>): string {
return `${DTOClass.name}QueryService`;
}
27 changes: 27 additions & 0 deletions packages/query-graphql/__tests__/module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ObjectType } from '@nestjs/graphql';
import { NestjsQueryGraphQLModule } from '../src';
import { FilterableField } from '../src/decorators/filterable-field.decorator';

describe('NestjsQueryTypeOrmModule', () => {
@ObjectType()
class TestDTO {
@FilterableField()
name!: string;
}

it('should create a module', () => {
const typeOrmModule = NestjsQueryGraphQLModule.forFeature({
imports: [],
resolvers: [
{
DTOClass: TestDTO,
EntityClass: TestDTO,
},
],
});
expect(typeOrmModule.imports).toHaveLength(0);
expect(typeOrmModule.module).toBe(NestjsQueryGraphQLModule);
expect(typeOrmModule.providers).toHaveLength(1);
expect(typeOrmModule.exports).toHaveLength(1);
});
});
22 changes: 22 additions & 0 deletions packages/query-graphql/__tests__/providers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Class } from '@nestjs-query/core';
import { NoOpQueryService } from '@nestjs-query/core/src/services/noop-query.service';
import { ObjectType } from '@nestjs/graphql';
import { FilterableField } from '../src/decorators';
import { createResolvers } from '../src/providers';
import { CRUDResolver } from '../src/resolvers';

describe('createTypeOrmQueryServiceProviders', () => {
@ObjectType()
class TestDTO {
@FilterableField()
name!: string;
}

it('should create a provider for the entity', () => {
const providers = createResolvers([{ DTOClass: TestDTO, EntityClass: TestDTO }]);
expect(providers).toHaveLength(1);
const Provider = providers[0] as Class<CRUDResolver<TestDTO, TestDTO, TestDTO>>;
expect(Provider.name).toBe('TestDTOAutoResolver');
expect(new Provider(NoOpQueryService.getInstance())).toBeInstanceOf(Provider);
});
});
2 changes: 2 additions & 0 deletions packages/query-graphql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export { FilterableField, ResolverMethodOpts } from './decorators';
export * from './resolvers';
export * from './federation';
export { DTONamesOpts } from './common';
export { NestjsQueryGraphQLModule } from './module';
export { AutoResolverOpts } from './providers';
19 changes: 19 additions & 0 deletions packages/query-graphql/src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DynamicModule } from '@nestjs/common';
import { AutoResolverOpts, createResolvers } from './providers';

export interface NestjsQueryGraphqlModuleOpts {
imports: DynamicModule[];
resolvers: AutoResolverOpts<unknown, unknown>[];
}

export class NestjsQueryGraphQLModule {
static forFeature(opts: NestjsQueryGraphqlModuleOpts): DynamicModule {
const resolverProviders = createResolvers(opts.resolvers);
return {
module: NestjsQueryGraphQLModule,
imports: [...opts.imports],
providers: [...resolverProviders],
exports: [...resolverProviders, ...opts.imports],
};
}
}
37 changes: 37 additions & 0 deletions packages/query-graphql/src/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Class, QueryService, getQueryServiceToken, AssemblerQueryService, AssemblerFactory } from '@nestjs-query/core';
import { Provider, Inject } from '@nestjs/common';
import { Resolver } from '@nestjs/graphql';
import { CRUDResolver, CRUDResolverOpts } from './resolvers';

export interface AutoResolverOpts<DTO, Entity> extends CRUDResolverOpts<DTO> {
DTOClass: Class<DTO>;
EntityClass: Class<Entity>;
}

const getResolverToken = <DTO>(DTOClass: Class<DTO>): string => `${DTOClass.name}AutoResolver`;

function createResolver<DTO, Entity>(resolverOpts: AutoResolverOpts<DTO, Entity>): Provider {
const { DTOClass, EntityClass } = resolverOpts;

class Service extends AssemblerQueryService<DTO, Entity> {
constructor(service: QueryService<Entity>) {
const assembler = AssemblerFactory.getAssembler(DTOClass, EntityClass);
super(assembler, service);
}
}

@Resolver(() => DTOClass)
class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) {
constructor(@Inject(getQueryServiceToken(resolverOpts.EntityClass)) service: QueryService<Entity>) {
super(new Service(service));
}
}
// need to set class name so DI works properly
Object.defineProperty(AutoResolver, 'name', { value: getResolverToken(DTOClass), writable: false });

return AutoResolver;
}

export const createResolvers = (opts: AutoResolverOpts<unknown, unknown>[]): Provider[] => {
return opts.map((opt) => createResolver(opt));
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { QueryService } from '@nestjs-query/core';
import { getQueryServiceToken, QueryService } from '@nestjs-query/core';
import * as nestjsCommon from '@nestjs/common';
import { Model } from 'sequelize-typescript';
import { getSequelizeQueryServiceKey, InjectSequelizeQueryService } from '../../src/decorators';
import { InjectSequelizeQueryService } from '../../src';

describe('@InjectSequelizeQueryService', () => {
const injectSpy = jest.spyOn(nestjsCommon, 'Inject');
Expand All @@ -15,6 +15,6 @@ describe('@InjectSequelizeQueryService', () => {
constructor(@InjectSequelizeQueryService(TestEntity) readonly service: QueryService<TestEntity>) {}
}
expect(injectSpy).toBeCalledTimes(1);
expect(injectSpy).toBeCalledWith(getSequelizeQueryServiceKey(TestEntity));
expect(injectSpy).toBeCalledWith(getQueryServiceToken(TestEntity));
});
});
4 changes: 2 additions & 2 deletions packages/query-sequelize/__tests__/providers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getQueryServiceToken } from '@nestjs-query/core';
import { getModelToken } from '@nestjs/sequelize';
import { Model, Sequelize, Table, Column } from 'sequelize-typescript';
import * as core from '@nestjs-query/core';
import { getSequelizeQueryServiceKey } from '../src/decorators';
import { createSequelizeQueryServiceProviders } from '../src/providers';
import { SequelizeQueryService } from '../src/services';

Expand All @@ -22,7 +22,7 @@ describe('createSequelizeQueryServiceProviders', () => {
});
const providers = createSequelizeQueryServiceProviders([TestEntity]);
expect(providers).toHaveLength(1);
expect(providers[0].provide).toBe(getSequelizeQueryServiceKey(TestEntity));
expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity));
expect(providers[0].inject).toEqual([getModelToken(TestEntity)]);
expect(providers[0].useFactory(TestEntity)).toBeInstanceOf(SequelizeQueryService);
expect(assemblerDeserializerSpy).toBeCalledTimes(1);
Expand Down
5 changes: 0 additions & 5 deletions packages/query-sequelize/src/decorators/decorators.utils.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/query-sequelize/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { InjectSequelizeQueryService } from './inject-sequelize-query-service.decorator';
export { getSequelizeQueryServiceKey } from './decorators.utils';
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getQueryServiceToken } from '@nestjs-query/core';
import { Inject } from '@nestjs/common';
import { Model, ModelCtor } from 'sequelize-typescript';
import { getSequelizeQueryServiceKey } from './decorators.utils';

export const InjectSequelizeQueryService = <Entity extends Model<Entity>>(
entity: ModelCtor<Entity>,
): ParameterDecorator => Inject(getSequelizeQueryServiceKey(entity));
): ParameterDecorator => Inject(getQueryServiceToken(entity));
2 changes: 1 addition & 1 deletion packages/query-sequelize/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { SequelizeQueryService } from './services';
export { InjectSequelizeQueryService, getSequelizeQueryServiceKey } from './decorators';
export { InjectSequelizeQueryService } from './decorators';
export { NestjsQuerySequelizeModule } from './module';
5 changes: 2 additions & 3 deletions packages/query-sequelize/src/providers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { AssemblerSerializer, AssemblerDeserializer } from '@nestjs-query/core';
import { AssemblerSerializer, AssemblerDeserializer, getQueryServiceToken } from '@nestjs-query/core';
import { FactoryProvider } from '@nestjs/common';
import { ModelCtor, Model, SequelizeOptions } from 'sequelize-typescript';
import { getModelToken } from '@nestjs/sequelize';
import { getSequelizeQueryServiceKey } from './decorators';
import { SequelizeQueryService } from './services';

function createSequelizeQueryServiceProvider<Entity extends Model>(
EntityClass: ModelCtor<Entity>,
connection?: SequelizeOptions | string,
): FactoryProvider {
return {
provide: getSequelizeQueryServiceKey(EntityClass),
provide: getQueryServiceToken(EntityClass),
useFactory(entity: ModelCtor<Entity>) {
AssemblerSerializer<Entity>((instance) => instance.get({ plain: true }))(entity);
AssemblerDeserializer<Entity>((obj: object) => entity.build(obj) as Entity)(entity);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { QueryService } from '@nestjs-query/core';
import { getQueryServiceToken } from '@nestjs-query/core/src';
import * as nestjsCommon from '@nestjs/common';
import { getTypeOrmQueryServiceKey, InjectTypeOrmQueryService } from '../../src/decorators';
import { InjectTypeOrmQueryService } from '../../src/decorators';

describe('@InjectTypeOrmQueryService', () => {
const injectSpy = jest.spyOn(nestjsCommon, 'Inject');
Expand All @@ -14,6 +15,6 @@ describe('@InjectTypeOrmQueryService', () => {
constructor(@InjectTypeOrmQueryService(TestEntity) readonly service: QueryService<TestEntity>) {}
}
expect(injectSpy).toBeCalledTimes(1);
expect(injectSpy).toBeCalledWith(getTypeOrmQueryServiceKey(TestEntity));
expect(injectSpy).toBeCalledWith(getQueryServiceToken(TestEntity));
});
});
4 changes: 2 additions & 2 deletions packages/query-typeorm/__tests__/providers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getQueryServiceToken } from '@nestjs-query/core';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { mock, instance } from 'ts-mockito';
import { createTypeOrmQueryServiceProviders } from '../src/providers';
import { getTypeOrmQueryServiceKey } from '../src/decorators';
import { TypeOrmQueryService } from '../src/services';

describe('createTypeOrmQueryServiceProviders', () => {
Expand All @@ -11,7 +11,7 @@ describe('createTypeOrmQueryServiceProviders', () => {
const mockRepo = mock<Repository<TestEntity>>(Repository);
const providers = createTypeOrmQueryServiceProviders([TestEntity]);
expect(providers).toHaveLength(1);
expect(providers[0].provide).toBe(getTypeOrmQueryServiceKey(TestEntity));
expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity));
expect(providers[0].inject).toEqual([getRepositoryToken(TestEntity)]);
expect(providers[0].useFactory(instance(mockRepo))).toBeInstanceOf(TypeOrmQueryService);
});
Expand Down
5 changes: 0 additions & 5 deletions packages/query-typeorm/src/decorators/decorators.utils.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/query-typeorm/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { InjectTypeOrmQueryService } from './inject-typeorm-query-service.decorator';
export { getTypeOrmQueryServiceKey } from './decorators.utils';
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Class } from '@nestjs-query/core';
import { Class, getQueryServiceToken } from '@nestjs-query/core';
import { Inject } from '@nestjs/common';
import { getTypeOrmQueryServiceKey } from './decorators.utils';

export const InjectTypeOrmQueryService = <Entity>(entity: Class<Entity>): ParameterDecorator =>
Inject(getTypeOrmQueryServiceKey(entity));
Inject(getQueryServiceToken(entity));
2 changes: 1 addition & 1 deletion packages/query-typeorm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { TypeOrmQueryService, TypeOrmQueryServiceOpts } from './services';
export { InjectTypeOrmQueryService, getTypeOrmQueryServiceKey } from './decorators';
export { InjectTypeOrmQueryService } from './decorators';
export { NestjsQueryTypeOrmModule } from './module';
5 changes: 2 additions & 3 deletions packages/query-typeorm/src/providers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Class } from '@nestjs-query/core';
import { Class, getQueryServiceToken } from '@nestjs-query/core';
import { FactoryProvider } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, Connection, ConnectionOptions } from 'typeorm';
import { getTypeOrmQueryServiceKey } from './decorators';
import { TypeOrmQueryService } from './services';

function createTypeOrmQueryServiceProvider<Entity>(
EntityClass: Class<Entity>,
connection?: Connection | ConnectionOptions | string,
): FactoryProvider {
return {
provide: getTypeOrmQueryServiceKey(EntityClass),
provide: getQueryServiceToken(EntityClass),
useFactory(repo: Repository<Entity>) {
return new TypeOrmQueryService(repo);
},
Expand Down

0 comments on commit 282c421

Please sign in to comment.