Skip to content

Commit

Permalink
feat: Added support for withDeleted in Relation decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
TriPSs committed May 27, 2022
1 parent 27c4222 commit 923d972
Show file tree
Hide file tree
Showing 22 changed files with 293 additions and 39 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"files": ["*.spec.ts", "*/__fixtures__/*.ts"],
"extends": ["plugin:@nrwl/nx/typescript"],
"rules": {
"max-classes-per-file": ["off"],
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Filterable } from './filterable.interface';

export interface FindByIdOptions<DTO> extends Filterable<DTO> {
/**
* Allow also deleted records to be get
* Allow also deleted records to be retrieved
*/
withDeleted?: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { Filterable } from './filterable.interface';

export type FindRelationOptions<Relation> = Filterable<Relation>;
export interface FindRelationOptions<Relation> extends Filterable<Relation> {
/**
* Allow also deleted records to be retrieved
*/
withDeleted?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ describe('FederationResolver', () => {
testResolverId: dto.id
};
when(
mockService.findRelation(TestRelationDTO, 'relation', deepEqual([dto]), deepEqual({ filter: undefined }))
mockService.findRelation(
TestRelationDTO,
'relation',
deepEqual([dto]),
deepEqual({ filter: undefined, withDeleted: undefined })
)
).thenResolve(new Map([[dto, output]]));
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
Expand All @@ -95,7 +100,12 @@ describe('FederationResolver', () => {
testResolverId: dto.id
};
when(
mockService.findRelation(TestRelationDTO, 'other', deepEqual([dto]), deepEqual({ filter: undefined }))
mockService.findRelation(
TestRelationDTO,
'other',
deepEqual([dto]),
deepEqual({ filter: undefined, withDeleted: undefined })
)
).thenResolve(new Map([[dto, output]]));
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('ReadRelationsResolver', () => {
super(service);
}
}

it('should use the object type name', () => expectResolverSDL({ one: { relation: { DTO: TestRelationDTO } } }));

it('should use the dtoName if provided', () =>
Expand All @@ -61,7 +62,15 @@ describe('ReadRelationsResolver', () => {
testResolverId: dto.id
};
when(
mockService.findRelation(TestRelationDTO, 'relation', deepEqual([dto]), deepEqual({ filter: undefined }))
mockService.findRelation(
TestRelationDTO,
'relation',
deepEqual([dto]),
deepEqual({
filter: undefined,
withDeleted: undefined
})
)
).thenResolve(new Map([[dto, output]]));
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
Expand All @@ -80,7 +89,15 @@ describe('ReadRelationsResolver', () => {
testResolverId: dto.id
};
when(
mockService.findRelation(TestRelationDTO, 'other', deepEqual([dto]), deepEqual({ filter: undefined }))
mockService.findRelation(
TestRelationDTO,
'other',
deepEqual([dto]),
deepEqual({
filter: undefined,
withDeleted: undefined
})
)
).thenResolve(new Map([[dto, output]]));
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
Expand All @@ -89,6 +106,44 @@ describe('ReadRelationsResolver', () => {
});
});

describe('one (withDeleted)', () => {
@Resolver(() => TestResolverDTO)
class TestDeletedResolver extends ReadRelationsResolver(TestResolverDTO, {
one: { relation: { DTO: TestRelationDTO, withDeleted: true } }
}) {
constructor(service: TestService) {
super(service);
}
}

it('should call the service findRelation with the provided dto', async () => {
const { resolver, mockService } = await createResolverFromNest(TestDeletedResolver);
const dto: TestResolverDTO = {
id: 'id-1',
stringField: 'foo'
};
const output: TestRelationDTO = {
id: 'id-2',
testResolverId: dto.id
};
when(
mockService.findRelation(
TestRelationDTO,
'relation',
deepEqual([dto]),
deepEqual({
filter: undefined,
withDeleted: true
})
)
).thenResolve(new Map([[dto, output]]));
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const result = await resolver.findRelation(dto, {});
return expect(result).toEqual(output);
});
});

describe('many', () => {
it('should use the object type name', () => expectResolverSDL({ many: { relations: { DTO: TestRelationDTO } } }));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { mergeBaseResolverOpts } from '../common';

export const reflector = new ArrayReflector(RELATION_KEY);

export type RelationDecoratorOpts<Relation> = Omit<ResolverRelation<Relation>, 'DTO'>;
export type RelationDecoratorOpts<Relation> = Omit<ResolverRelation<Relation>, 'DTO' | 'allowFiltering'>;
export type RelationTypeFunc<Relation> = () => Class<Relation>;
export type RelationClassDecorator<DTO> = <Cls extends Class<DTO>>(DTOClass: Cls) => Cls | void;

Expand Down
21 changes: 13 additions & 8 deletions packages/query-graphql/src/loader/find-relations.loader.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Class, Filter, QueryService } from '@ptc-org/nestjs-query-core';
import { Class, FindRelationOptions, QueryService } from '@ptc-org/nestjs-query-core';
import { NestjsQueryDataloader } from './relations.loader';

export type FindRelationsArgs<DTO, Relation> = { dto: DTO; filter?: Filter<Relation> };
export type FindRelationsArgs<DTO, Relation> = { dto: DTO } & FindRelationOptions<Relation>;
type FindRelationsOpts<Relation> = Omit<FindRelationOptions<Relation>, 'filter'>;
type FindRelationsMap<DTO, Relation> = Map<string, (FindRelationsArgs<DTO, Relation> & { index: number })[]>;

export class FindRelationsLoader<DTO, Relation>
implements NestjsQueryDataloader<DTO, FindRelationsArgs<DTO, Relation>, Relation | undefined | Error>
{
constructor(readonly RelationDTO: Class<Relation>, readonly relationName: string) {}

createLoader(service: QueryService<DTO, unknown, unknown>) {
createLoader(service: QueryService<DTO, unknown, unknown>, opts?: FindRelationsOpts<Relation>) {
return async (args: ReadonlyArray<FindRelationsArgs<DTO, Relation>>): Promise<(Relation | undefined | Error)[]> => {
const grouped = this.groupFinds(args);
const grouped = this.groupFinds(args, opts);
return this.loadResults(service, grouped);
};
}
Expand All @@ -23,9 +24,10 @@ export class FindRelationsLoader<DTO, Relation>
const results: (Relation | undefined)[] = [];
await Promise.all(
[...findRelationsMap.values()].map(async (args) => {
const { filter } = args[0];
const { filter, withDeleted } = args[0];
const dtos = args.map((a) => a.dto);
const relationResults = await service.findRelation(this.RelationDTO, this.relationName, dtos, { filter });
const opts = { filter, withDeleted };
const relationResults = await service.findRelation(this.RelationDTO, this.relationName, dtos, opts);
const dtoRelations: (Relation | undefined)[] = dtos.map((dto) => relationResults.get(dto));
dtoRelations.forEach((relation, index) => {
results[args[index].index] = relation;
Expand All @@ -35,15 +37,18 @@ export class FindRelationsLoader<DTO, Relation>
return results;
}

private groupFinds(queryArgs: ReadonlyArray<FindRelationsArgs<DTO, Relation>>): FindRelationsMap<DTO, Relation> {
private groupFinds(
queryArgs: ReadonlyArray<FindRelationsArgs<DTO, Relation>>,
opts?: FindRelationsOpts<Relation>
): FindRelationsMap<DTO, Relation> {
// group
return queryArgs.reduce((map, args, index) => {
const filterJson = JSON.stringify(args.filter);
if (!map.has(filterJson)) {
map.set(filterJson, []);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
map.get(filterJson)!.push({ ...args, index });
map.get(filterJson)!.push({ ...args, ...opts, index });
return map;
}, new Map<string, (FindRelationsArgs<DTO, Relation> & { index: number })[]>());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ const ReadOneRelationMixin =
})
authFilter?: Filter<Relation>
): Promise<Relation | undefined> {
return DataLoaderFactory.getOrCreateLoader(context, loaderName, findLoader.createLoader(this.service)).load({
return DataLoaderFactory.getOrCreateLoader(
context,
loaderName,
findLoader.createLoader(this.service, { withDeleted: relation.withDeleted })
).load({
dto,
filter: authFilter
});
Expand Down Expand Up @@ -100,7 +104,7 @@ const ReadManyRelationMixin =
})
relationFilter?: Filter<Relation>
): Promise<InstanceType<typeof CT>> {
const qa = await transformAndValidate(RelationQA, q);
const relationQuery = await transformAndValidate(RelationQA, q);
const relationLoader = DataLoaderFactory.getOrCreateLoader(
context,
relationLoaderName,
Expand All @@ -113,7 +117,7 @@ const ReadManyRelationMixin =
);
return CT.createFromPromise(
(query) => relationLoader.load({ dto, query }),
mergeQuery(qa, { filter: relationFilter }),
mergeQuery(relationQuery, { filter: relationFilter }),
(filter) => relationCountLoader.load({ dto, filter })
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ export type ResolverRelation<Relation> = {
* Enable aggregation queries.
*/
enableAggregate?: boolean;

/**
* Indicates if soft-deleted rows should be included in relation result.
*/
withDeleted?: boolean;
/**
* Set to true if you should be able to filter on this relation.
*
Expand Down
4 changes: 2 additions & 2 deletions packages/query-typegoose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"peerDependencies": {
"@nestjs/common": "^8.0.4",
"@typegoose/typegoose": "^8.0.0-beta.2",
"nestjs-typegoose": "^7.1.38",
"mongoose": "^6.3.3"
"mongoose": "^6.3.3",
"nestjs-typegoose": "^7.1.38"
},
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ import { TestSoftDeleteEntity } from './test-soft-delete.entity';
import { TestEntity } from './test.entity';
import { seed } from './seeds';
import { RelationOfTestRelationEntity } from './relation-of-test-relation.entity';
import { TestSoftDeleteRelation } from './test-soft-delete.relation';

export const CONNECTION_OPTIONS: ConnectionOptions = {
type: 'sqlite',
database: ':memory:',
dropSchema: true,
entities: [TestEntity, TestSoftDeleteEntity, TestRelation, TestEntityRelationEntity, RelationOfTestRelationEntity],
entities: [
TestEntity,
TestSoftDeleteEntity,
TestRelation,
TestEntityRelationEntity,
RelationOfTestRelationEntity,
TestSoftDeleteRelation
],
synchronize: true,
logging: false
};
Expand All @@ -34,12 +42,14 @@ const tables = [
'test_relation',
'test_entity_relation_entity',
'test_soft_delete_entity',
'test_soft_delete_relation',
'test_entity_many_test_relations_test_relation'
];
export const truncate = async (connection: Connection): Promise<void> => {
await tables.reduce(async (prev, table) => {
await prev;
await connection.query(`DELETE FROM ${table}`);
await connection.query(`DELETE
FROM ${table}`);
}, Promise.resolve());
};

Expand Down
20 changes: 19 additions & 1 deletion packages/query-typeorm/__tests__/__fixtures__/seeds.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Connection, getConnection } from 'typeorm';
import { Connection, getConnection, In } from 'typeorm';
import { TestRelation } from './test-relation.entity';
import { TestSoftDeleteEntity } from './test-soft-delete.entity';
import { TestEntity } from './test.entity';
import { RelationOfTestRelationEntity } from './relation-of-test-relation.entity';
import { TestSoftDeleteRelation } from './test-soft-delete.relation';

export const TEST_ENTITIES: TestEntity[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => {
const testEntityPk = `test-entity-${i}`;
Expand All @@ -24,6 +25,14 @@ export const TEST_SOFT_DELETE_ENTITIES: TestSoftDeleteEntity[] = [1, 2, 3, 4, 5,
};
});

export const TEST_SOFT_DELETE_RELATION_ENTITIES: TestSoftDeleteRelation[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => {
const testEntityPk = `test-deleted-entity-${i}`;
return {
testEntityPk,
stringType: `foo${i}`
};
});

export const TEST_RELATIONS: TestRelation[] = TEST_ENTITIES.reduce(
(relations, te) => [
...relations,
Expand Down Expand Up @@ -60,10 +69,14 @@ export const seed = async (connection: Connection = getConnection()): Promise<vo
const testRelationRepo = connection.getRepository(TestRelation);
const relationOfTestRelationRepo = connection.getRepository(RelationOfTestRelationEntity);
const testSoftDeleteRepo = connection.getRepository(TestSoftDeleteEntity);
const testSoftDeleteRelationRepo = connection.getRepository(TestSoftDeleteRelation);

const testEntities = await testEntityRepo.save(TEST_ENTITIES.map((e: TestEntity) => ({ ...e })));

const testRelations = await testRelationRepo.save(TEST_RELATIONS.map((r: TestRelation) => ({ ...r })));
const testSoftDeleteRelations = await testSoftDeleteRelationRepo.save(
TEST_SOFT_DELETE_RELATION_ENTITIES.map((r: TestSoftDeleteRelation) => ({ ...r }))
);

await relationOfTestRelationRepo.save(
TEST_RELATIONS_OF_RELATION.map((r: RelationOfTestRelationEntity) => ({ ...r }))
Expand All @@ -73,6 +86,7 @@ export const seed = async (connection: Connection = getConnection()): Promise<vo
testEntities.map((te) => {
// eslint-disable-next-line no-param-reassign
te.oneTestRelation = testRelations.find((tr) => tr.testRelationPk === `test-relations-${te.testEntityPk}-1`);
te.oneSoftDeleteTestRelation = testSoftDeleteRelations[0];
if (te.numberType % 2 === 0) {
// eslint-disable-next-line no-param-reassign
te.manyTestRelations = testRelations.filter((tr) => tr.relationName.endsWith('two'));
Expand All @@ -97,4 +111,8 @@ export const seed = async (connection: Connection = getConnection()): Promise<vo
);

await testSoftDeleteRepo.save(TEST_SOFT_DELETE_ENTITIES.map((e: TestSoftDeleteEntity) => ({ ...e })));

await testSoftDeleteRelationRepo.softDelete({
testEntityPk: In(TEST_SOFT_DELETE_RELATION_ENTITIES.map(({ testEntityPk }) => testEntityPk))
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Entity, DeleteDateColumn, PrimaryColumn, Column } from 'typeorm';

@Entity()
export class TestSoftDeleteRelation {
@PrimaryColumn({ name: 'test_entity_pk' })
testEntityPk!: string;

@Column({ name: 'string_type' })
stringType!: string;

@DeleteDateColumn({ name: 'deleted_at' })
deletedAt?: Date;
}
7 changes: 7 additions & 0 deletions packages/query-typeorm/__tests__/__fixtures__/test.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from 'typeorm';
import { TestEntityRelationEntity } from './test-entity-relation.entity';
import { TestRelation } from './test-relation.entity';
import { TestSoftDeleteRelation } from './test-soft-delete.relation';

@Entity()
export class TestEntity {
Expand Down Expand Up @@ -38,6 +39,12 @@ export class TestEntity {
@JoinColumn({ name: 'many_to_one_relation_id' })
manyToOneRelation?: TestRelation;

@ManyToOne(() => TestSoftDeleteRelation, {
nullable: true
})
@JoinColumn({ name: 'many_to_one_soft_delete_relation_id' })
oneSoftDeleteTestRelation?: TestSoftDeleteRelation;

@ManyToMany(() => TestRelation, (tr) => tr.manyTestEntities, { onDelete: 'CASCADE', nullable: false })
@JoinTable()
manyTestRelations?: TestRelation[];
Expand Down
Loading

0 comments on commit 923d972

Please sign in to comment.