Skip to content

Commit

Permalink
feat(graphql): Add relation/connection decorators
Browse files Browse the repository at this point in the history
* Allow specifying relations and connections on DTOs
  • Loading branch information
doug-martin committed May 9, 2020
1 parent cd09abe commit a75cf96
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ObjectType } from '@nestjs/graphql';
import { Relation, Connection } from '../../src';
import { getMetadataStorage } from '../../src/metadata';

@ObjectType()
class TestRelation {}

describe('@Relation', () => {
it('should add the relation metadata to the metadata storage', () => {
const relationFn = () => TestRelation;
const relationOpts = { disableRead: true };
@ObjectType()
@Relation('test', relationFn, relationOpts)
class TestDTO {}

const relations = getMetadataStorage().getRelations(TestDTO);
expect(relations).toHaveLength(1);
const relation = relations![0];
expect(relation.name).toBe('test');
expect(relation.relationTypeFunc).toBe(relationFn);
expect(relation.isConnection).toBe(false);
expect(relation.relationOpts).toBe(relationOpts);
});
});

describe('@Connection', () => {
it('should add the relation metadata to the metadata storage', () => {
const relationFn = () => TestRelation;
const relationOpts = { disableRead: true };
@ObjectType()
@Connection('test', relationFn, relationOpts)
class TestDTO {}

const relations = getMetadataStorage().getRelations(TestDTO);
expect(relations).toHaveLength(1);
const relation = relations![0];
expect(relation.name).toBe('test');
expect(relation.relationTypeFunc).toBe(relationFn);
expect(relation.isConnection).toBe(true);
expect(relation.relationOpts).toBe(relationOpts);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ObjectType } from '@nestjs/graphql';
import { Connection, Relation } from '../../../src/decorators';
import { FilterableField } from '../../../src/decorators/filterable-field.decorator';
import * as readRelations from '../../../src/resolvers/relations/read-relations.resolver';
import * as updateRelations from '../../../src/resolvers/relations/update-relations.resolver';
import * as removeRelations from '../../../src/resolvers/relations/remove-relations.resolver';
import { Relatable } from '../../../src';
import { BaseServiceResolver } from '../../../src/resolvers/resolver.interface';

describe('Relatable', () => {
const readMixinSpy = jest.spyOn(readRelations, 'ReadRelationsMixin');
const updateMixinSpy = jest.spyOn(updateRelations, 'UpdateRelationsMixin');
const removeMixinSpy = jest.spyOn(removeRelations, 'RemoveRelationsMixin');

@ObjectType()
class TestRelation {
@FilterableField()
id!: number;
}

afterEach(() => jest.clearAllMocks());

it('should call the mixins with the relations derived from decorators', () => {
@ObjectType()
@Relation('testRelation', () => TestRelation)
@Connection('testConnection', () => TestRelation)
class Test {}

Relatable(Test, {}, {})(BaseServiceResolver);

const relations = {
one: { testRelation: { DTO: TestRelation } },
many: { testConnection: { DTO: TestRelation } },
};
expect(readMixinSpy).toBeCalledWith(Test, relations);
expect(updateMixinSpy).toBeCalledWith(Test, relations);
expect(removeMixinSpy).toBeCalledWith(Test, relations);
});

it('should call the mixins with the relations that are passed in', () => {
@ObjectType()
class Test {}

Relatable(
Test,
{
one: { testRelation: { DTO: TestRelation } },
many: { testConnection: { DTO: TestRelation } },
},
{},
)(BaseServiceResolver);

const relations = {
one: { testRelation: { DTO: TestRelation } },
many: { testConnection: { DTO: TestRelation } },
};
expect(readMixinSpy).toBeCalledWith(Test, relations);
expect(updateMixinSpy).toBeCalledWith(Test, relations);
expect(removeMixinSpy).toBeCalledWith(Test, relations);
});
});
1 change: 1 addition & 0 deletions packages/query-graphql/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { FilterableField } from './filterable-field.decorator';
export { ResolverMethodOpts } from './resolver-method.decorator';
export { Connection, Relation, RelationDecoratorOpts, RelationTypeFunc } from './relation.decorator';
export * from './resolver-mutation.decorator';
export * from './resolver-query.decorator';
export * from './resolver-field.decorator';
38 changes: 38 additions & 0 deletions packages/query-graphql/src/decorators/relation.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Class } from '@nestjs-query/core';
import { getMetadataStorage } from '../metadata';
import { ResolverRelation } from '../resolvers/relations/relations.interface';

export type RelationDecoratorOpts<Relation> = Omit<ResolverRelation<Relation>, 'DTO'>;
export type RelationTypeFunc<Relation> = () => Class<Relation>;

export function Relation<DTO, Relation>(
name: string,
relationTypeFunction: RelationTypeFunc<Relation>,
options?: RelationDecoratorOpts<Relation>,
) {
return <Cls extends Class<DTO>>(DTOClass: Cls): Cls | void => {
getMetadataStorage().addRelation(DTOClass, name, {
name,
isConnection: false,
relationOpts: options,
relationTypeFunc: relationTypeFunction,
});
return DTOClass;
};
}

export function Connection<DTO, Relation>(
name: string,
relationTypeFunction: RelationTypeFunc<Relation>,
options?: RelationDecoratorOpts<Relation>,
) {
return <Cls extends Class<DTO>>(DTOClass: Cls): Cls | void => {
getMetadataStorage().addRelation(DTOClass, name, {
name,
isConnection: true,
relationOpts: options,
relationTypeFunc: relationTypeFunction,
});
return DTOClass;
};
}
9 changes: 8 additions & 1 deletion packages/query-graphql/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export * from './types';
export { FilterableField, ResolverMethodOpts } from './decorators';
export {
FilterableField,
ResolverMethodOpts,
Relation,
Connection,
RelationTypeFunc,
RelationDecoratorOpts,
} from './decorators';
export * from './resolvers';
export * from './federation';
export { DTONamesOpts } from './common';
Expand Down
24 changes: 24 additions & 0 deletions packages/query-graphql/src/metadata/metadata-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TypeMetadataStorage } from '@nestjs/graphql/dist/schema-builder/storage
import { Class, Filter, SortField } from '@nestjs-query/core';
import { ObjectTypeMetadata } from '@nestjs/graphql/dist/schema-builder/metadata/object-type.metadata';
import { ReturnTypeFunc, FieldOptions } from '@nestjs/graphql';
import { ResolverRelation } from '../resolvers/relations';
import { EdgeType, StaticConnectionType } from '../types/connection';

/**
Expand All @@ -14,6 +15,12 @@ interface FilterableFieldDescriptor<T> {
advancedOptions?: FieldOptions;
}

interface RelationDescriptor<Relation> {
name: string;
relationTypeFunc: () => Class<Relation>;
isConnection: boolean;
relationOpts?: Omit<ResolverRelation<Relation>, 'DTO'>;
}
/**
* @internal
*/
Expand All @@ -28,12 +35,15 @@ export class GraphQLQueryMetadataStorage {

private readonly edgeTypeStorage: Map<Class<unknown>, Class<EdgeType<unknown>>>;

private readonly relationStorage: Map<Class<unknown>, RelationDescriptor<unknown>[]>;

constructor() {
this.filterableObjectStorage = new Map();
this.filterTypeStorage = new Map();
this.sortTypeStorage = new Map();
this.connectionTypeStorage = new Map();
this.edgeTypeStorage = new Map();
this.relationStorage = new Map();
}

addFilterableObjectField<T>(type: Class<T>, field: FilterableFieldDescriptor<unknown>): void {
Expand Down Expand Up @@ -93,6 +103,19 @@ export class GraphQLQueryMetadataStorage {
return this.getValue(this.edgeTypeStorage, type);
}

addRelation<T>(type: Class<T>, name: string, relation: RelationDescriptor<unknown>): void {
let relations: RelationDescriptor<unknown>[] | undefined = this.relationStorage.get(type);
if (!relations) {
relations = [];
this.relationStorage.set(type, relations);
}
relations.push(relation);
}

getRelations<T>(type: Class<T>): RelationDescriptor<unknown>[] | undefined {
return this.relationStorage.get(type);
}

getGraphqlObjectMetadata<T>(objType: Class<T>): ObjectTypeMetadata | undefined {
return TypeMetadataStorage.getObjectTypesMetadata().find((o) => o.target === objType);
}
Expand All @@ -104,6 +127,7 @@ export class GraphQLQueryMetadataStorage {
this.sortTypeStorage.clear();
this.connectionTypeStorage.clear();
this.edgeTypeStorage.clear();
this.relationStorage.clear();
}

private getValue<V>(map: Map<Class<unknown>, Class<unknown>>, key: Class<unknown>): V | undefined {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
import { Class } from '@nestjs-query/core';
import { getMetadataStorage } from '../../metadata';
import { ServiceResolver } from '../resolver.interface';
import { ReadRelationsMixin } from './read-relations.resolver';
import { ReferencesRelationMixin } from './references-relation.resolver';
import { ReferencesOpts, RelationsOpts } from './relations.interface';
import { RemoveRelationsMixin } from './remove-relations.resolver';
import { UpdateRelationsMixin } from './update-relations.resolver';

const getRelationsFromMetadata = <DTO>(DTOClass: Class<DTO>): RelationsOpts => {
const relations: RelationsOpts = {};
const metaRelations = getMetadataStorage().getRelations(DTOClass) ?? [];
metaRelations.forEach((r) => {
const opts = { ...r.relationOpts, DTO: r.relationTypeFunc() };
if (r.isConnection) {
relations.many = { ...relations.many, [r.name]: opts };
} else {
relations.one = { ...relations.one, [r.name]: opts };
}
});
return relations;
};

export const Relatable = <DTO>(DTOClass: Class<DTO>, relations: RelationsOpts, referencesOpts: ReferencesOpts<DTO>) => <
B extends Class<ServiceResolver<DTO>>
>(
Base: B,
): B => {
const metaRelations = getRelationsFromMetadata(DTOClass);
const oneRelations = { ...relations.one, ...(metaRelations.one ?? {}) };
const manyRelations = { ...relations.many, ...(metaRelations.many ?? {}) };
const mergedRelations = { one: oneRelations, many: manyRelations };
return ReferencesRelationMixin(
DTOClass,
referencesOpts,
)(
ReadRelationsMixin(
DTOClass,
relations,
)(UpdateRelationsMixin(DTOClass, relations)(RemoveRelationsMixin(DTOClass, relations)(Base))),
mergedRelations,
)(UpdateRelationsMixin(DTOClass, mergedRelations)(RemoveRelationsMixin(DTOClass, mergedRelations)(Base))),
);
};

0 comments on commit a75cf96

Please sign in to comment.