Skip to content

Commit

Permalink
feat(graphql,core): Add support for custom services and assemblers
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed May 14, 2020
1 parent d129487 commit 85e8658
Show file tree
Hide file tree
Showing 13 changed files with 230 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as nestjsCommon from '@nestjs/common';
import { QueryService, InjectAssemblerQueryService, DefaultAssembler } from '../../src';
import { getAssemblerQueryServiceToken } from '../../src/decorators/helpers';

describe('@InjectAssemblerQueryService', () => {
const injectSpy = jest.spyOn(nestjsCommon, 'Inject');

class Foo {
str!: string;
}

class Bar {
num!: string;
}

// eslint-disable-next-line @typescript-eslint/ban-types
class TestAssembler extends DefaultAssembler<Foo, Bar> {}

it('call inject with the correct key', () => {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class Test {
constructor(@InjectAssemblerQueryService(TestAssembler) readonly service: QueryService<Foo>) {}
}
expect(injectSpy).toBeCalledTimes(1);
expect(injectSpy).toBeCalledWith(getAssemblerQueryServiceToken(TestAssembler));
});
});
7 changes: 7 additions & 0 deletions packages/core/src/decorators/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Assembler } from '../assemblers';
import { Class } from '../common';

export function getQueryServiceToken<DTO>(DTOClass: Class<DTO>): string {
return `${DTOClass.name}QueryService`;
}

export function getAssemblerQueryServiceToken<DTO, Entity = unknown>(
AssemblerClass: Class<Assembler<DTO, Entity>>,
): string {
return `${AssemblerClass.name}QueryService`;
}
1 change: 1 addition & 0 deletions packages/core/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { getQueryServiceToken } from './helpers';
export { InjectQueryService } from './inject-query-service.decorator';
export { InjectAssemblerQueryService } from './inject-assembler-query-service.decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Inject } from '@nestjs/common';
import { Assembler } from '../assemblers';
import { Class } from '../common';
import { getAssemblerQueryServiceToken } from './helpers';

export const InjectAssemblerQueryService = <DTO, Entity>(
AssemblerClass: Class<Assembler<DTO, Entity>>,
): ParameterDecorator => Inject(getAssemblerQueryServiceToken(AssemblerClass));
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { Inject } from '@nestjs/common';
import { Class } from '../common';
import { getQueryServiceToken } from './helpers';

export const InjectQueryService = <DTO>(entity: Class<DTO>): ParameterDecorator => Inject(getQueryServiceToken(entity));
export const InjectQueryService = <DTO, Entity>(DTOClass: Class<DTO>): ParameterDecorator =>
Inject(getQueryServiceToken(DTOClass));
11 changes: 9 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
/* eslint-disable import/export */
export * from './interfaces';
export * from './common';
export { InjectQueryService, getQueryServiceToken } from './decorators';
export { QueryService, AssemblerQueryService, RelationQueryService, QueryServiceRelation } from './services';
export { InjectAssemblerQueryService, InjectQueryService, getQueryServiceToken } from './decorators';
export {
QueryService,
AssemblerQueryService,
RelationQueryService,
NoOpQueryService,
QueryServiceRelation,
} from './services';
export { transformFilter, transformQuery, transformSort, QueryFieldMap } from './helpers';
export {
ClassTransformerAssembler,
Expand All @@ -13,3 +19,4 @@ export {
AssemblerDeserializer,
AssemblerFactory,
} from './assemblers';
export { NestjsQueryCoreModule, NestjsQueryCoreModuleOpts } from './module';
22 changes: 22 additions & 0 deletions packages/core/src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DynamicModule, ForwardReference } from '@nestjs/common';
import { Assembler } from './assemblers';
import { Class } from './common';
import { createServices } from './providers';

export interface NestjsQueryCoreModuleOpts {
imports?: Array<Class<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
assemblers?: Class<Assembler<any, any>>[];
}

export class NestjsQueryCoreModule {
static forFeature(opts: NestjsQueryCoreModuleOpts): DynamicModule {
const { imports = [], assemblers = [] } = opts;
const assemblerServiceProviders = createServices(assemblers);
return {
module: NestjsQueryCoreModule,
imports: [...imports],
providers: [...assemblers, ...assemblerServiceProviders],
exports: [...imports, ...assemblers, ...assemblerServiceProviders],
};
}
}
28 changes: 28 additions & 0 deletions packages/core/src/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Provider } from '@nestjs/common';
import { Assembler } from './assemblers';
import { Class } from './common';
import { getQueryServiceToken } from './decorators';
import { getAssemblerQueryServiceToken } from './decorators/helpers';
import { getCoreMetadataStorage } from './metadata';
import { AssemblerQueryService, QueryService } from './services';

function createServiceProvider<DTO, Entity>(AssemblerClass: Class<Assembler<DTO, Entity>>): Provider {
const classes = getCoreMetadataStorage().getAssemblerClasses(AssemblerClass);
if (!classes) {
throw new Error(
`unable to determine DTO and Entity classes for ${AssemblerClass.name}. Did you decorate your class with @Assembler`,
);
}
const { EntityClass } = classes;
return {
provide: getAssemblerQueryServiceToken(AssemblerClass),
useFactory(assembler: Assembler<DTO, Entity>, entityService: QueryService<Entity>) {
return new AssemblerQueryService(assembler, entityService);
},
inject: [AssemblerClass, getQueryServiceToken(EntityClass)],
};
}

export const createServices = (opts: Class<Assembler<unknown, unknown>>[]): Provider[] => {
return opts.map((opt) => createServiceProvider(opt));
};
1 change: 1 addition & 0 deletions packages/core/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { QueryService } from './query.service';
export { AssemblerQueryService } from './assembler-query.service';
export { RelationQueryService, QueryServiceRelation } from './relation-query.service';
export { NoOpQueryService } from './noop-query.service';
10 changes: 5 additions & 5 deletions packages/query-graphql/__tests__/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('NestjsQueryTypeOrmModule', () => {
}

it('should create a module', () => {
const typeOrmModule = NestjsQueryGraphQLModule.forFeature({
const graphqlModule = NestjsQueryGraphQLModule.forFeature({
imports: [],
resolvers: [
{
Expand All @@ -19,9 +19,9 @@ describe('NestjsQueryTypeOrmModule', () => {
},
],
});
expect(typeOrmModule.imports).toHaveLength(0);
expect(typeOrmModule.module).toBe(NestjsQueryGraphQLModule);
expect(typeOrmModule.providers).toHaveLength(1);
expect(typeOrmModule.exports).toHaveLength(1);
expect(graphqlModule.imports).toHaveLength(1);
expect(graphqlModule.module).toBe(NestjsQueryGraphQLModule);
expect(graphqlModule.providers).toHaveLength(1);
expect(graphqlModule.exports).toHaveLength(2);
});
});
34 changes: 18 additions & 16 deletions packages/query-graphql/__tests__/providers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Class } from '@nestjs-query/core';
import { NoOpQueryService } from '@nestjs-query/core/src/services/noop-query.service';
import { Class, NoOpQueryService } from '@nestjs-query/core';
import { ObjectType } from '@nestjs/graphql';
import { FilterableField } from '../src/decorators';
import { createResolvers } from '../src/providers';
Expand All @@ -13,20 +12,23 @@ describe('createTypeOrmQueryServiceProviders', () => {
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);
});
describe('entity crud resolver', () => {
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);
});

it('should create a federated provider for the entity', () => {
class Service extends NoOpQueryService<TestDTO> {}

it('should create a federated provider for the entity', () => {
class Service extends NoOpQueryService<TestDTO> {}
const providers = createResolvers([{ type: 'federated', DTOClass: TestDTO, Service }]);
expect(providers).toHaveLength(1);
const Provider = providers[0] as Class<ServiceResolver<TestDTO>>;
expect(Provider.name).toBe('TestDTOFederatedAutoResolver');
expect(new Provider(NoOpQueryService.getInstance())).toBeInstanceOf(Provider);
const providers = createResolvers([{ type: 'federated', DTOClass: TestDTO, Service }]);
expect(providers).toHaveLength(1);
const Provider = providers[0] as Class<ServiceResolver<TestDTO>>;
expect(Provider.name).toBe('TestDTOFederatedAutoResolver');
expect(new Provider(NoOpQueryService.getInstance())).toBeInstanceOf(Provider);
});
});
});
14 changes: 10 additions & 4 deletions packages/query-graphql/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { Class } from '@nestjs-query/core';
import { Assembler, NestjsQueryCoreModule, Class } from '@nestjs-query/core';
import { DynamicModule, ForwardReference, Provider } from '@nestjs/common';
import { AutoResolverOpts, createResolvers } from './providers';

export interface NestjsQueryGraphqlModuleOpts {
imports: Array<Class<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
services?: Provider[];
assemblers?: Class<Assembler<any, any>>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolvers: AutoResolverOpts<any, any, unknown, unknown>[];
}

export class NestjsQueryGraphQLModule {
static forFeature(opts: NestjsQueryGraphqlModuleOpts): DynamicModule {
const coreModule = NestjsQueryCoreModule.forFeature({
assemblers: opts.assemblers,
imports: opts.imports,
});
const services = opts.services || [];
const resolverProviders = createResolvers(opts.resolvers);
return {
module: NestjsQueryGraphQLModule,
imports: [...opts.imports],
providers: [...(opts.services || []), ...resolverProviders],
exports: [...resolverProviders, ...opts.imports],
imports: [...opts.imports, coreModule],
providers: [...services, ...resolverProviders],
exports: [...resolverProviders, ...services, ...opts.imports, coreModule],
};
}
}
106 changes: 91 additions & 15 deletions packages/query-graphql/src/providers.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,62 @@
import { Class, QueryService, getQueryServiceToken, AssemblerQueryService, AssemblerFactory } from '@nestjs-query/core';
import {
Class,
QueryService,
InjectAssemblerQueryService,
InjectQueryService,
AssemblerFactory,
AssemblerQueryService,
} from '@nestjs-query/core';
import { Assembler } from '@nestjs-query/core/src';
import { Provider, Inject } from '@nestjs/common';
import { Resolver } from '@nestjs/graphql';
import { CRUDResolver, CRUDResolverOpts, FederationResolver, RelationsOpts } from './resolvers';

export type CRUDAutoResolverOpts<DTO, Entity, C, U> = CRUDResolverOpts<DTO, C, U> & {
type CRUDAutoResolverOpts<DTO, C, U> = CRUDResolverOpts<DTO, C, U> & {
DTOClass: Class<DTO>;
};

export type EntityCRUDAutoResolverOpts<DTO, Entity, C, U> = CRUDAutoResolverOpts<DTO, C, U> & {
EntityClass: Class<Entity>;
};

export type AssemblerCRUDAutoResolverOpts<DTO, Assembler, C, U> = CRUDAutoResolverOpts<DTO, C, U> & {
AssemblerClass: Class<Assembler>;
};

export type ServiceCRUDAutoResolverOpts<DTO, QueryService, C, U> = CRUDAutoResolverOpts<DTO, C, U> & {
ServiceClass: Class<QueryService>;
};

export type FederatedAutoResolverOpts<DTO, Service> = RelationsOpts & {
type: 'federated';
DTOClass: Class<DTO>;
Service: Class<Service>;
};

export type AutoResolverOpts<DTO, EntityOrService, C, U> =
| CRUDAutoResolverOpts<DTO, EntityOrService, C, U>
| FederatedAutoResolverOpts<DTO, EntityOrService>;
export type AutoResolverOpts<DTO, EntityServiceOrAssemler, C, U> =
| EntityCRUDAutoResolverOpts<DTO, EntityServiceOrAssemler, C, U>
| AssemblerCRUDAutoResolverOpts<DTO, EntityServiceOrAssemler, C, U>
| ServiceCRUDAutoResolverOpts<DTO, EntityServiceOrAssemler, C, U>
| FederatedAutoResolverOpts<DTO, EntityServiceOrAssemler>;

const isFederatedResolverOpts = <DTO, EntityOrService, C, U>(
opts: AutoResolverOpts<DTO, EntityOrService, C, U>,
): opts is FederatedAutoResolverOpts<DTO, EntityOrService> => {
export const isFederatedResolverOpts = <DTO, MaybeService, C, U>(
opts: AutoResolverOpts<DTO, MaybeService, C, U>,
): opts is FederatedAutoResolverOpts<DTO, MaybeService> => {
return 'type' in opts && opts.type === 'federated';
};

export const isAssemblerCRUDAutoResolverOpts = <DTO, MaybeAssembler, C, U, Assembler>(
opts: AutoResolverOpts<DTO, MaybeAssembler, C, U>,
): opts is AssemblerCRUDAutoResolverOpts<DTO, MaybeAssembler, C, U> => {
return 'DTOClass' in opts && 'AssemblerClass' in opts;
};

export const isServiceCRUDAutoResolverOpts = <DTO, MaybeService, C, U>(
opts: AutoResolverOpts<DTO, MaybeService, C, U>,
): opts is ServiceCRUDAutoResolverOpts<DTO, MaybeService, C, U> => {
return 'DTOClass' in opts && 'ServiceClass' in opts;
};

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

Expand All @@ -42,32 +75,75 @@ function createFederatedResolver<DTO, Service>(resolverOpts: FederatedAutoResolv
return AutoResolver;
}

function createResolver<DTO, Entity, C, U>(resolverOpts: AutoResolverOpts<DTO, Entity, C, U>): Provider {
if (isFederatedResolverOpts(resolverOpts)) {
return createFederatedResolver(resolverOpts);
}
function createEntityAutoResolver<DTO, Entity, C, U>(
resolverOpts: EntityCRUDAutoResolverOpts<DTO, Entity, C, U>,
): 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(EntityClass)) service: QueryService<Entity>) {
constructor(@InjectQueryService(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;
}

function createAssemblerAutoResolver<DTO, Asmblr, C, U>(
resolverOpts: AssemblerCRUDAutoResolverOpts<DTO, Asmblr, C, U>,
): Provider {
const { DTOClass, AssemblerClass } = resolverOpts;
@Resolver(() => DTOClass)
class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) {
constructor(
@InjectAssemblerQueryService((AssemblerClass as unknown) as Class<Assembler<DTO, unknown>>)
service: QueryService<DTO>,
) {
super(service);
}
}
// need to set class name so DI works properly
Object.defineProperty(AutoResolver, 'name', { value: getResolverToken(DTOClass), writable: false });
return AutoResolver;
}

function createServiceAutoResolver<DTO, Service, C, U>(
resolverOpts: ServiceCRUDAutoResolverOpts<DTO, Service, C, U>,
): Provider {
const { DTOClass, ServiceClass } = resolverOpts;
@Resolver(() => DTOClass)
class AutoResolver extends CRUDResolver(DTOClass, resolverOpts) {
constructor(@Inject(ServiceClass) service: QueryService<DTO>) {
super(service);
}
}
// need to set class name so DI works properly
Object.defineProperty(AutoResolver, 'name', { value: getResolverToken(DTOClass), writable: false });
return AutoResolver;
}

function createResolver<DTO, EntityServiceOrAssembler, C, U>(
resolverOpts: AutoResolverOpts<DTO, EntityServiceOrAssembler, C, U>,
): Provider {
if (isFederatedResolverOpts(resolverOpts)) {
return createFederatedResolver(resolverOpts);
}
if (isAssemblerCRUDAutoResolverOpts(resolverOpts)) {
return createAssemblerAutoResolver(resolverOpts);
}
if (isServiceCRUDAutoResolverOpts(resolverOpts)) {
return createServiceAutoResolver(resolverOpts);
}
return createEntityAutoResolver(resolverOpts);
}

export const createResolvers = (opts: AutoResolverOpts<unknown, unknown, unknown, unknown>[]): Provider[] => {
return opts.map((opt) => createResolver(opt));
};

0 comments on commit 85e8658

Please sign in to comment.