diff --git a/packages/query-typeorm/__tests__/__fixtures__/seeds.ts b/packages/query-typeorm/__tests__/__fixtures__/seeds.ts index ce7895783..198539a12 100644 --- a/packages/query-typeorm/__tests__/__fixtures__/seeds.ts +++ b/packages/query-typeorm/__tests__/__fixtures__/seeds.ts @@ -27,17 +27,17 @@ export const TEST_RELATIONS: TestRelation[] = TEST_ENTITIES.reduce((relations, t ...relations, { testRelationPk: `test-relations-${te.testEntityPk}-1`, - relationName: `${te.stringType}-test-relation`, + relationName: `${te.stringType}-test-relation-one`, testEntityId: te.testEntityPk, }, { testRelationPk: `test-relations-${te.testEntityPk}-2`, - relationName: `${te.stringType}-test-relation`, + relationName: `${te.stringType}-test-relation-two`, testEntityId: te.testEntityPk, }, { testRelationPk: `test-relations-${te.testEntityPk}-3`, - relationName: `${te.stringType}-test-relation`, + relationName: `${te.stringType}-test-relation-three`, testEntityId: te.testEntityPk, }, ]; diff --git a/packages/query-typeorm/__tests__/services/typeorm-query.service.spec.ts b/packages/query-typeorm/__tests__/services/typeorm-query.service.spec.ts index 4eeb7cf28..8879d7689 100644 --- a/packages/query-typeorm/__tests__/services/typeorm-query.service.spec.ts +++ b/packages/query-typeorm/__tests__/services/typeorm-query.service.spec.ts @@ -392,12 +392,12 @@ describe('TypeOrmQueryService', (): void => { testRelationPk: 3, }, max: { - relationName: 'foo1-test-relation', + relationName: 'foo1-test-relation-two', testEntityId: 'test-entity-1', testRelationPk: 'test-relations-test-entity-1-3', }, min: { - relationName: 'foo1-test-relation', + relationName: 'foo1-test-relation-one', testEntityId: 'test-entity-1', testRelationPk: 'test-relations-test-entity-1-1', }, @@ -412,12 +412,12 @@ describe('TypeOrmQueryService', (): void => { testRelationPk: 3, }, max: { - relationName: 'foo2-test-relation', + relationName: 'foo2-test-relation-two', testEntityId: 'test-entity-2', testRelationPk: 'test-relations-test-entity-2-3', }, min: { - relationName: 'foo2-test-relation', + relationName: 'foo2-test-relation-one', testEntityId: 'test-entity-2', testRelationPk: 'test-relations-test-entity-2-1', }, @@ -432,12 +432,12 @@ describe('TypeOrmQueryService', (): void => { testRelationPk: 3, }, max: { - relationName: 'foo3-test-relation', + relationName: 'foo3-test-relation-two', testEntityId: 'test-entity-3', testRelationPk: 'test-relations-test-entity-3-3', }, min: { - relationName: 'foo3-test-relation', + relationName: 'foo3-test-relation-one', testEntityId: 'test-entity-3', testRelationPk: 'test-relations-test-entity-3-1', }, @@ -474,12 +474,12 @@ describe('TypeOrmQueryService', (): void => { testRelationPk: 2, }, max: { - relationName: 'foo1-test-relation', + relationName: 'foo1-test-relation-two', testEntityId: 'test-entity-1', testRelationPk: 'test-relations-test-entity-1-3', }, min: { - relationName: 'foo1-test-relation', + relationName: 'foo1-test-relation-three', testEntityId: 'test-entity-1', testRelationPk: 'test-relations-test-entity-1-2', }, @@ -494,12 +494,12 @@ describe('TypeOrmQueryService', (): void => { testRelationPk: 2, }, max: { - relationName: 'foo2-test-relation', + relationName: 'foo2-test-relation-two', testEntityId: 'test-entity-2', testRelationPk: 'test-relations-test-entity-2-3', }, min: { - relationName: 'foo2-test-relation', + relationName: 'foo2-test-relation-three', testEntityId: 'test-entity-2', testRelationPk: 'test-relations-test-entity-2-2', }, @@ -514,12 +514,12 @@ describe('TypeOrmQueryService', (): void => { testRelationPk: 2, }, max: { - relationName: 'foo3-test-relation', + relationName: 'foo3-test-relation-two', testEntityId: 'test-entity-3', testRelationPk: 'test-relations-test-entity-3-3', }, min: { - relationName: 'foo3-test-relation', + relationName: 'foo3-test-relation-three', testEntityId: 'test-entity-3', testRelationPk: 'test-relations-test-entity-3-2', }, @@ -555,12 +555,12 @@ describe('TypeOrmQueryService', (): void => { testRelationPk: 3, }, max: { - relationName: 'foo1-test-relation', + relationName: 'foo1-test-relation-two', testEntityId: 'test-entity-1', testRelationPk: 'test-relations-test-entity-1-3', }, min: { - relationName: 'foo1-test-relation', + relationName: 'foo1-test-relation-one', testEntityId: 'test-entity-1', testRelationPk: 'test-relations-test-entity-1-1', }, @@ -632,6 +632,20 @@ describe('TypeOrmQueryService', (): void => { expect(queryResult).toEqual(TEST_RELATIONS[0]); }); + it('apply the filter option', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const queryResult1 = await queryService.findRelation(TestRelation, 'oneTestRelation', entity, { + filter: { relationName: { eq: TEST_RELATIONS[0].relationName } }, + }); + expect(queryResult1).toEqual(TEST_RELATIONS[0]); + + const queryResult2 = await queryService.findRelation(TestRelation, 'oneTestRelation', entity, { + filter: { relationName: { eq: TEST_RELATIONS[1].relationName } }, + }); + expect(queryResult2).toBeUndefined(); + }); + it('should return undefined select if no results are found.', async () => { const entity = { ...TEST_ENTITIES[0], testEntityPk: 'not-real' }; const queryService = moduleRef.get(TestEntityService); @@ -663,6 +677,20 @@ describe('TypeOrmQueryService', (): void => { ); }); + it('should apply the filter option', async () => { + const entities = TEST_ENTITIES.slice(0, 3); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.findRelation(TestRelation, 'oneTestRelation', entities, { + filter: { testRelationPk: { in: [TEST_RELATIONS[0].testRelationPk, TEST_RELATIONS[6].testRelationPk] } }, + }); + expect(queryResult).toEqual( + new Map([ + [entities[0], TEST_RELATIONS[0]], + [entities[2], TEST_RELATIONS[6]], + ]), + ); + }); + it('should return undefined select if no results are found.', async () => { const entities: TestEntity[] = [TEST_ENTITIES[0], { testEntityPk: 'does-not-exist' } as TestEntity]; const queryService = moduleRef.get(TestEntityService); @@ -687,6 +715,38 @@ describe('TypeOrmQueryService', (): void => { const relations = await queryService.queryRelations(TestRelation, 'testRelations', entity, {}); expect(relations).toHaveLength(6); }); + + describe('with modify options', () => { + it('should throw an error if the entity is not found with the id and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.addRelations( + 'testRelations', + entity.testEntityPk, + TEST_RELATIONS.slice(3, 6).map((r) => r.testRelationPk), + { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }, + ), + ).rejects.toThrow('Unable to find TestEntity with id: test-entity-1'); + }); + + it('should throw an error if the relations are not found with the relationIds and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.addRelations( + 'testRelations', + entity.testEntityPk, + TEST_RELATIONS.slice(3, 6).map((r) => r.testRelationPk), + { + relationFilter: { relationName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find all testRelations to add to TestEntity'); + }); + }); }); describe('#setRelation', () => { @@ -703,6 +763,33 @@ describe('TypeOrmQueryService', (): void => { const relation = await queryService.findRelation(TestRelation, 'oneTestRelation', entity); expect(relation?.testRelationPk).toBe(TEST_RELATIONS[1].testRelationPk); }); + + describe('with modify options', () => { + it('should throw an error if the entity is not found with the id and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.setRelation('oneTestRelation', entity.testEntityPk, TEST_RELATIONS[1].testRelationPk, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow('Unable to find TestEntity with id: test-entity-1'); + }); + + it('should throw an error if the relations are not found with the relationIds and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.setRelation( + 'oneTestRelation', + entity.testEntityPk, + TEST_RELATIONS[1].testRelationPk, + { + relationFilter: { relationName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find oneTestRelation to set on TestEntity'); + }); + }); }); describe('#removeRelations', () => { @@ -719,6 +806,38 @@ describe('TypeOrmQueryService', (): void => { const relations = await queryService.queryRelations(TestRelation, 'testRelations', entity, {}); expect(relations).toHaveLength(0); }); + + describe('with modify options', () => { + it('should throw an error if the entity is not found with the id and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelations( + 'testRelations', + entity.testEntityPk, + TEST_RELATIONS.slice(3, 6).map((r) => r.testRelationPk), + { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }, + ), + ).rejects.toThrow('Unable to find TestEntity with id: test-entity-1'); + }); + + it('should throw an error if the relations are not found with the relationIds and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelations( + 'testRelations', + entity.testEntityPk, + TEST_RELATIONS.slice(3, 6).map((r) => r.testRelationPk), + { + relationFilter: { relationName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find all testRelations to remove from TestEntity'); + }); + }); }); describe('#removeRelation', () => { @@ -736,6 +855,33 @@ describe('TypeOrmQueryService', (): void => { const relation = await queryService.findRelation(TestRelation, 'oneTestRelation', entity); expect(relation).toBeUndefined(); }); + + describe('with modify options', () => { + it('should throw an error if the entity is not found with the id and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelation('oneTestRelation', entity.testEntityPk, TEST_RELATIONS[1].testRelationPk, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow('Unable to find TestEntity with id: test-entity-1'); + }); + + it('should throw an error if the relations are not found with the relationIds and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelation( + 'oneTestRelation', + entity.testEntityPk, + TEST_RELATIONS[1].testRelationPk, + { + relationFilter: { relationName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find oneTestRelation to remove from TestEntity'); + }); + }); }); describe('manyToOne', () => { @@ -752,6 +898,28 @@ describe('TypeOrmQueryService', (): void => { const entity = await queryService.findRelation(TestEntity, 'testEntity', relation); expect(entity).toBeUndefined(); }); + + describe('with modify options', () => { + it('should throw an error if the entity is not found with the id and provided filter', async () => { + const relation = TEST_RELATIONS[0]; + const queryService = moduleRef.get(TestRelationService); + return expect( + queryService.removeRelation('testEntity', relation.testRelationPk, TEST_ENTITIES[1].testEntityPk, { + filter: { relationName: { eq: TEST_RELATIONS[1].relationName } }, + }), + ).rejects.toThrow('Unable to find TestRelation with id: test-relations-test-entity-1-1'); + }); + + it('should throw an error if the relations are not found with the relationIds and provided filter', async () => { + const relation = TEST_RELATIONS[0]; + const queryService = moduleRef.get(TestRelationService); + return expect( + queryService.removeRelation('testEntity', relation.testRelationPk, TEST_ENTITIES[0].testEntityPk, { + relationFilter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow('Unable to find testEntity to remove from TestRelation'); + }); + }); }); describe('oneToMany', () => { @@ -768,6 +936,33 @@ describe('TypeOrmQueryService', (): void => { const relations = await queryService.queryRelations(TestRelation, 'testRelations', entity, {}); expect(relations).toHaveLength(2); }); + + describe('with modify options', () => { + it('should throw an error if the entity is not found with the id and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelation('testRelations', entity.testEntityPk, TEST_RELATIONS[4].testRelationPk, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow('Unable to find TestEntity with id: test-entity-1'); + }); + + it('should throw an error if the relations are not found with the relationIds and provided filter', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelation( + 'testRelations', + entity.testEntityPk, + TEST_RELATIONS[4].testRelationPk, + { + relationFilter: { relationName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find testRelations to remove from TestEntity'); + }); + }); }); }); @@ -784,6 +979,26 @@ describe('TypeOrmQueryService', (): void => { const found = await queryService.findById('bad-id'); expect(found).toBeUndefined(); }); + + describe('with filter', () => { + it('should return an entity if all filters match', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.findById(entity.testEntityPk, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(found).toEqual(entity); + }); + + it('should return an undefined if an entitity with the pk and filter is not found', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.findById(entity.testEntityPk, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }); + expect(found).toBeUndefined(); + }); + }); }); describe('#getById', () => { @@ -794,10 +1009,31 @@ describe('TypeOrmQueryService', (): void => { expect(found).toEqual(entity); }); - it('return undefined if not found', () => { + it('should throw an error if not found', () => { const queryService = moduleRef.get(TestEntityService); return expect(queryService.getById('bad-id')).rejects.toThrow('Unable to find TestEntity with id: bad-id'); }); + + describe('with filter', () => { + it('should return an entity if all filters match', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.getById(entity.testEntityPk, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(found).toEqual(entity); + }); + + it('should return an undefined if an entitity with the pk and filter is not found', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.getById(entity.testEntityPk, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.testEntityPk}`); + }); + }); }); describe('#createMany', () => { @@ -867,9 +1103,28 @@ describe('TypeOrmQueryService', (): void => { it('call fail if the entity is not found', async () => { const queryService = moduleRef.get(TestEntityService); - return expect(queryService.deleteOne('bad-id')).rejects.toThrow( - 'Could not find any entity of type "TestEntity" matching: "bad-id"', - ); + return expect(queryService.deleteOne('bad-id')).rejects.toThrow('Unable to find TestEntity with id: bad-id'); + }); + + describe('with filter', () => { + it('should delete the entity if all filters match', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const deleted = await queryService.deleteOne(entity.testEntityPk, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(deleted).toEqual({ ...TEST_ENTITIES[0], testEntityPk: undefined }); + }); + + it('should return throw an error if unable to find ', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.deleteOne(entity.testEntityPk, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.testEntityPk}`); + }); }); }); @@ -910,9 +1165,34 @@ describe('TypeOrmQueryService', (): void => { it('call fail if the entity is not found', async () => { const queryService = moduleRef.get(TestEntityService); return expect(queryService.updateOne('bad-id', { stringType: 'updated' })).rejects.toThrow( - 'Could not find any entity of type "TestEntity" matching: "bad-id"', + 'Unable to find TestEntity with id: bad-id', ); }); + + describe('with filter', () => { + it('should update the entity if all filters match', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const updated = await queryService.updateOne( + entity.testEntityPk, + { stringType: 'updated' }, + { filter: { stringType: { eq: entity.stringType } } }, + ); + expect(updated).toEqual({ ...entity, stringType: 'updated' }); + }); + + it('should throw an error if unable to find the entity', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.updateOne( + entity.testEntityPk, + { stringType: 'updated' }, + { filter: { stringType: { eq: TEST_ENTITIES[1].stringType } } }, + ), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.testEntityPk}`); + }); + }); }); describe('#isSoftDelete', () => { @@ -944,7 +1224,7 @@ describe('TypeOrmQueryService', (): void => { it('should fail if the entity is not found', async () => { const queryService = moduleRef.get(TestSoftDeleteEntityService); return expect(queryService.deleteOne('bad-id')).rejects.toThrow( - 'Could not find any entity of type "TestSoftDeleteEntity" matching: "bad-id"', + 'Unable to find TestSoftDeleteEntity with id: bad-id', ); }); }); @@ -973,6 +1253,31 @@ describe('TypeOrmQueryService', (): void => { 'Restore not allowed for non soft deleted entity TestEntity.', ); }); + + describe('with filter', () => { + it('should restore the entity if all filters match', async () => { + const queryService = moduleRef.get(TestSoftDeleteEntityService); + const entity = TEST_SOFT_DELETE_ENTITIES[0]; + await queryService.deleteOne(entity.testEntityPk); + const restored = await queryService.restoreOne(entity.testEntityPk, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(restored).toEqual({ ...entity, deletedAt: null }); + const foundEntity = await queryService.findById(entity.testEntityPk); + expect(foundEntity).toEqual({ ...entity, deletedAt: null }); + }); + + it('should return throw an error if unable to find ', async () => { + const queryService = moduleRef.get(TestSoftDeleteEntityService); + const entity = TEST_SOFT_DELETE_ENTITIES[0]; + await queryService.deleteOne(entity.testEntityPk); + return expect( + queryService.restoreOne(entity.testEntityPk, { + filter: { stringType: { eq: TEST_SOFT_DELETE_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow(`Unable to find TestSoftDeleteEntity with id: ${entity.testEntityPk}`); + }); + }); }); describe('#restoreMany', () => { diff --git a/packages/query-typeorm/src/query/filter-query.builder.ts b/packages/query-typeorm/src/query/filter-query.builder.ts index 8481500fa..6eb58eb40 100644 --- a/packages/query-typeorm/src/query/filter-query.builder.ts +++ b/packages/query-typeorm/src/query/filter-query.builder.ts @@ -56,6 +56,16 @@ export class FilterQueryBuilder { return qb; } + selectById(id: string | number | (string | number)[], query: Query): SelectQueryBuilder { + let qb = this.createQueryBuilder(); + qb = this.applyRelationJoins(qb, query.filter); + qb = qb.andWhereInIds(id); + qb = this.applyFilter(qb, query.filter, qb.alias); + qb = this.applySorting(qb, query.sorting, qb.alias); + qb = this.applyPaging(qb, query.paging); + return qb; + } + aggregate(query: Query, aggregate: AggregateQuery): SelectQueryBuilder { let qb = this.createQueryBuilder(); qb = this.applyAggregate(qb, aggregate, qb.alias); diff --git a/packages/query-typeorm/src/services/relation-query.service.ts b/packages/query-typeorm/src/services/relation-query.service.ts index d70ee0dbb..f8386e659 100644 --- a/packages/query-typeorm/src/services/relation-query.service.ts +++ b/packages/query-typeorm/src/services/relation-query.service.ts @@ -1,4 +1,14 @@ -import { Query, Class, AssemblerFactory, Filter, AggregateQuery, AggregateResponse } from '@nestjs-query/core'; +import { + Query, + Class, + AssemblerFactory, + Filter, + AggregateQuery, + AggregateResponse, + ModifyRelationOptions, + FindRelationOptions, + GetByIdOptions, +} from '@nestjs-query/core'; import { Repository, RelationQueryBuilder as TypeOrmRelationQueryBuilder, ObjectLiteral } from 'typeorm'; import lodashFilter from 'lodash.filter'; import lodashOmit from 'lodash.omit'; @@ -22,6 +32,8 @@ export abstract class RelationQueryService { abstract repo: Repository; + abstract getById(id: string | number, opts?: GetByIdOptions): Promise; + /** * Query for relations for an array of Entities. This method will return a map with the Entity as the key and the relations as the value. * @param RelationClass - The class of the relation. @@ -134,11 +146,13 @@ export abstract class RelationQueryService { * @param RelationClass - the class of the relation * @param relationName - the name of the relation to load. * @param dtos - the dtos to find the relation for. + * @param opts - Additional options */ async findRelation( RelationClass: Class, relationName: string, dtos: Entity[], + opts?: FindRelationOptions, ): Promise>; /** @@ -146,23 +160,28 @@ export abstract class RelationQueryService { * @param RelationClass - The class to serialize the relation into. * @param dto - The dto to find the relation for. * @param relationName - The name of the relation to query for. + * @param opts - Additional options */ async findRelation( RelationClass: Class, relationName: string, dto: Entity, + opts?: FindRelationOptions, ): Promise; async findRelation( RelationClass: Class, relationName: string, dto: Entity | Entity[], + opts?: FindRelationOptions, ): Promise<(Relation | undefined) | Map> { if (Array.isArray(dto)) { - return this.batchFindRelations(RelationClass, relationName, dto); + return this.batchFindRelations(RelationClass, relationName, dto, opts); } const assembler = AssemblerFactory.getAssembler(RelationClass, this.getRelationEntity(relationName)); - const relationEntity = await this.createRelationQueryBuilder(dto, relationName).loadOne(); + const relationEntity = await this.getRelationQueryBuilder(relationName) + .select(dto, { filter: opts?.filter, paging: { limit: 1 } }) + .getOne(); return relationEntity ? assembler.convertToDTO(relationEntity) : undefined; } @@ -171,13 +190,19 @@ export abstract class RelationQueryService { * @param id - The id of the entity to add the relation to. * @param relationName - The name of the relation to query for. * @param relationIds - The ids of relations to add. + * @param opts - Addition options */ async addRelations( relationName: string, id: string | number, relationIds: (string | number)[], + opts?: ModifyRelationOptions, ): Promise { - const entity = await this.repo.findOneOrFail(id); + const entity = await this.getById(id, opts); + const relations = await this.getRelations(relationName, relationIds, opts?.relationFilter); + if (!this.foundAllRelations(relationIds, relations)) { + throw new Error(`Unable to find all ${relationName} to add to ${this.EntityClass.name}`); + } await this.createRelationQueryBuilder(entity, relationName).add(relationIds); return entity; } @@ -188,9 +213,19 @@ export abstract class RelationQueryService { * @param id - The id of the entity to set the relation on. * @param relationName - The name of the relation to query for. * @param relationId - The id of the relation to set on the entity. + * @param opts - Additional options */ - async setRelation(relationName: string, id: string | number, relationId: string | number): Promise { - const entity = await this.repo.findOneOrFail(id); + async setRelation( + relationName: string, + id: string | number, + relationId: string | number, + opts?: ModifyRelationOptions, + ): Promise { + const entity = await this.getById(id, opts); + const relation = (await this.getRelations(relationName, [relationId], opts?.relationFilter))[0]; + if (!relation) { + throw new Error(`Unable to find ${relationName} to set on ${this.EntityClass.name}`); + } await this.createRelationQueryBuilder(entity, relationName).set(relationId); return entity; } @@ -200,13 +235,19 @@ export abstract class RelationQueryService { * @param id - The id of the entity to add the relation to. * @param relationName - The name of the relation to query for. * @param relationIds - The ids of the relations to add. + * @param opts - Additional options */ async removeRelations( relationName: string, id: string | number, relationIds: (string | number)[], + opts?: ModifyRelationOptions, ): Promise { - const entity = await this.repo.findOneOrFail(id); + const entity = await this.getById(id, opts); + const relations = await this.getRelations(relationName, relationIds, opts?.relationFilter); + if (!this.foundAllRelations(relationIds, relations)) { + throw new Error(`Unable to find all ${relationName} to remove from ${this.EntityClass.name}`); + } await this.createRelationQueryBuilder(entity, relationName).remove(relationIds); return entity; } @@ -222,8 +263,13 @@ export abstract class RelationQueryService { relationName: string, id: string | number, relationId: string | number, + opts?: ModifyRelationOptions, ): Promise { - const entity = await this.repo.findOneOrFail(id); + const entity = await this.getById(id, opts); + const relation = (await this.getRelations(relationName, [relationId], opts?.relationFilter))[0]; + if (!relation) { + throw new Error(`Unable to find ${relationName} to remove from ${this.EntityClass.name}`); + } const meta = this.getRelationMeta(relationName); if (meta.isOneToOne || meta.isManyToOne) { await this.createRelationQueryBuilder(entity, relationName).set(null); @@ -338,8 +384,12 @@ export abstract class RelationQueryService { RelationClass: Class, relationName: string, dtos: Entity[], + opts?: FindRelationOptions, ): Promise> { - const batchResults = await this.batchQueryRelations(RelationClass, relationName, dtos, { paging: { limit: 1 } }); + const batchResults = await this.batchQueryRelations(RelationClass, relationName, dtos, { + paging: { limit: 1 }, + filter: opts?.filter, + }); const results = new Map(); batchResults.forEach((relation, dto) => { // get just the first one. @@ -380,4 +430,17 @@ export abstract class RelationQueryService { }, {} as Partial); return lodashFilter(relations, filter) as Relation[]; } + + private getRelations( + relationName: string, + ids: (string | number)[], + filter?: Filter, + ): Promise { + const relationQueryBuilder = this.getRelationQueryBuilder(relationName).filterQueryBuilder; + return relationQueryBuilder.selectById(ids, { filter }).getMany(); + } + + private foundAllRelations(relationIds: (string | number)[], relations: Relation[]): boolean { + return new Set([...relationIds]).size === relations.length; + } } diff --git a/packages/query-typeorm/src/services/typeorm-query.service.ts b/packages/query-typeorm/src/services/typeorm-query.service.ts index 135cfa9cb..7504804e3 100644 --- a/packages/query-typeorm/src/services/typeorm-query.service.ts +++ b/packages/query-typeorm/src/services/typeorm-query.service.ts @@ -8,6 +8,11 @@ import { Filter, AggregateQuery, AggregateResponse, + FindByIdOptions, + GetByIdOptions, + UpdateOneOptions, + DeleteOneOptions, + Filterable, } from '@nestjs-query/core'; import { Repository, DeleteResult } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; @@ -88,8 +93,8 @@ export class TypeOrmQueryService extends RelationQueryService im * ``` * @param id - The id of the record to find. */ - async findById(id: string | number): Promise { - return this.repo.findOne(id); + async findById(id: string | number, opts?: FindByIdOptions): Promise { + return this.filterQueryBuilder.selectById(id, opts ?? {}).getOne(); } /** @@ -105,8 +110,8 @@ export class TypeOrmQueryService extends RelationQueryService im * ``` * @param id - The id of the record to find. */ - async getById(id: string | number): Promise { - const entity = await this.findById(id); + async getById(id: string | number, opts?: GetByIdOptions): Promise { + const entity = await this.findById(id, opts); if (!entity) { throw new NotFoundException(`Unable to find ${this.EntityClass.name} with id: ${id}`); } @@ -153,10 +158,15 @@ export class TypeOrmQueryService extends RelationQueryService im * ``` * @param id - The `id` of the record. * @param update - A `Partial` of the entity with fields to update. + * @param opts - Additional options. */ - async updateOne>(id: number | string, update: U): Promise { + async updateOne>( + id: number | string, + update: U, + opts?: UpdateOneOptions, + ): Promise { this.ensureIdIsNotPresent(update); - const entity = await this.repo.findOneOrFail(id); + const entity = await this.getById(id, opts); return this.repo.save(this.repo.merge(entity, update)); } @@ -192,9 +202,10 @@ export class TypeOrmQueryService extends RelationQueryService im * ``` * * @param id - The `id` of the entity to delete. + * @param filter Additional filter to use when finding the entity to delete. */ - async deleteOne(id: string | number): Promise { - const entity = await this.repo.findOneOrFail(id); + async deleteOne(id: string | number, opts?: DeleteOneOptions): Promise { + const entity = await this.getById(id, opts); if (this.useSoftDelete) { return this.repo.softRemove(entity); } @@ -234,11 +245,12 @@ export class TypeOrmQueryService extends RelationQueryService im * ``` * * @param id - The `id` of the entity to restore. + * @param opts Additional filter to use when finding the entity to restore. */ - async restoreOne(id: string | number): Promise { + async restoreOne(id: string | number, opts?: Filterable): Promise { this.ensureSoftDeleteEnabled(); await this.repo.restore(id); - return this.getById(id); + return this.getById(id, opts); } /**