From 29fdfa724ec199835a6493b5f9cccb6bec58f074 Mon Sep 17 00:00:00 2001 From: doug-martin Date: Tue, 25 Aug 2020 22:07:55 -0500 Subject: [PATCH] feat(sequelize): Add additional filter options to QueryService --- .../__tests__/__fixtures__/seeds.ts | 6 +- .../services/sequelize-query.service.spec.ts | 307 +++++++++++++++++- .../src/query/filter-query.builder.ts | 17 + .../src/services/relation-query.service.ts | 117 +++++-- .../src/services/sequelize-query.service.ts | 28 +- 5 files changed, 421 insertions(+), 54 deletions(-) diff --git a/packages/query-sequelize/__tests__/__fixtures__/seeds.ts b/packages/query-sequelize/__tests__/__fixtures__/seeds.ts index 7cb7291a4..02bdb71ae 100644 --- a/packages/query-sequelize/__tests__/__fixtures__/seeds.ts +++ b/packages/query-sequelize/__tests__/__fixtures__/seeds.ts @@ -26,19 +26,19 @@ export const PLAIN_TEST_RELATIONS: Pick< ...relations, { testRelationPk: `test-relations-${te.testEntityPk}-1`, - relationName: `${te.stringType}-test-relation`, + relationName: `${te.stringType}-test-relation-one`, testEntityId: te.testEntityPk, oneTestEntityId: te.testEntityPk, }, { testRelationPk: `test-relations-${te.testEntityPk}-2`, - relationName: `${te.stringType}-test-relation`, + relationName: `${te.stringType}-test-relation-two`, testEntityId: te.testEntityPk, oneTestEntityId: null, }, { testRelationPk: `test-relations-${te.testEntityPk}-3`, - relationName: `${te.stringType}-test-relation`, + relationName: `${te.stringType}-test-relation-three`, testEntityId: te.testEntityPk, oneTestEntityId: null, }, diff --git a/packages/query-sequelize/__tests__/services/sequelize-query.service.spec.ts b/packages/query-sequelize/__tests__/services/sequelize-query.service.spec.ts index 2a6b68a71..f514ca3df 100644 --- a/packages/query-sequelize/__tests__/services/sequelize-query.service.spec.ts +++ b/packages/query-sequelize/__tests__/services/sequelize-query.service.spec.ts @@ -330,12 +330,12 @@ describe('SequelizeQueryService', (): 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', }, @@ -350,12 +350,12 @@ describe('SequelizeQueryService', (): 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', }, @@ -370,12 +370,12 @@ describe('SequelizeQueryService', (): 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', }, @@ -414,12 +414,12 @@ describe('SequelizeQueryService', (): 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', }, @@ -492,6 +492,20 @@ describe('SequelizeQueryService', (): void => { expect(queryResult!.get({ plain: true })).toEqual(PLAIN_TEST_RELATIONS[0]); }); + it('apply the filter option', async () => { + const entity = TestEntity.build(PLAIN_TEST_ENTITIES[0]); + const queryService = moduleRef.get(TestEntityService); + const queryResult1 = await queryService.findRelation(TestRelation, 'oneTestRelation', entity, { + filter: { relationName: { eq: PLAIN_TEST_RELATIONS[0].relationName } }, + }); + expect(queryResult1!.get({ plain: true })).toEqual(PLAIN_TEST_RELATIONS[0]); + + const queryResult2 = await queryService.findRelation(TestRelation, 'oneTestRelation', entity, { + filter: { relationName: { eq: PLAIN_TEST_RELATIONS[1].relationName } }, + }); + expect(queryResult2).toBeUndefined(); + }); + it('should return undefined select if no results are found.', async () => { const entity = { ...PLAIN_TEST_ENTITIES[0], testEntityPk: 'not-real' } as TestEntity; const queryService = moduleRef.get(TestEntityService); @@ -524,6 +538,22 @@ describe('SequelizeQueryService', (): void => { ); }); + it('should apply the filter option', async () => { + const entities = PLAIN_TEST_ENTITIES.slice(0, 3).map((pe) => TestEntity.build(pe)); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.findRelation(TestRelation, 'oneTestRelation', entities, { + filter: { + testRelationPk: { in: [PLAIN_TEST_RELATIONS[0].testRelationPk, PLAIN_TEST_RELATIONS[6].testRelationPk] }, + }, + }); + expect(queryResult).toEqual( + new Map([ + [entities[0], expect.objectContaining(PLAIN_TEST_RELATIONS[0])], + [entities[2], expect.objectContaining(PLAIN_TEST_RELATIONS[6])], + ]), + ); + }); + it('should return undefined select if no results are found.', async () => { const entities: TestEntity[] = [ PLAIN_TEST_ENTITIES[0] as TestEntity, @@ -551,6 +581,38 @@ describe('SequelizeQueryService', (): 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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.addRelations( + 'testRelations', + entity.testEntityPk, + PLAIN_TEST_RELATIONS.slice(3, 6).map((r) => r.testRelationPk), + { + filter: { stringType: { eq: PLAIN_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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.addRelations( + 'testRelations', + entity.testEntityPk, + PLAIN_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', () => { @@ -567,6 +629,33 @@ describe('SequelizeQueryService', (): void => { const relation = await queryService.findRelation(TestRelation, 'oneTestRelation', entity); expect(relation!.testRelationPk).toBe(PLAIN_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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.setRelation('oneTestRelation', entity.testEntityPk, PLAIN_TEST_RELATIONS[1].testRelationPk, { + filter: { stringType: { eq: PLAIN_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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.setRelation( + 'oneTestRelation', + entity.testEntityPk, + PLAIN_TEST_RELATIONS[1].testRelationPk, + { + relationFilter: { relationName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find oneTestRelation to set on TestEntity'); + }); + }); }); describe('#removeRelations', () => { @@ -583,6 +672,38 @@ describe('SequelizeQueryService', (): 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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelations( + 'testRelations', + entity.testEntityPk, + PLAIN_TEST_RELATIONS.slice(3, 6).map((r) => r.testRelationPk), + { + filter: { stringType: { eq: PLAIN_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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelations( + 'testRelations', + entity.testEntityPk, + PLAIN_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', () => { @@ -600,6 +721,38 @@ describe('SequelizeQueryService', (): 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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelation( + 'oneTestRelation', + entity.testEntityPk, + PLAIN_TEST_RELATIONS[1].testRelationPk, + { + filter: { stringType: { eq: PLAIN_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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelation( + 'oneTestRelation', + entity.testEntityPk, + PLAIN_TEST_RELATIONS[1].testRelationPk, + { + relationFilter: { relationName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find oneTestRelation to remove from TestEntity'); + }); + }); }); describe('manyToOne', () => { @@ -616,6 +769,28 @@ describe('SequelizeQueryService', (): void => { const entity = await queryService.findRelation(TestEntity, 'testEntity', queryResult); 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 = PLAIN_TEST_RELATIONS[0]; + const queryService = moduleRef.get(TestRelationService); + return expect( + queryService.removeRelation('testEntity', relation.testRelationPk, PLAIN_TEST_ENTITIES[1].testEntityPk, { + filter: { relationName: { eq: PLAIN_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 = PLAIN_TEST_RELATIONS[0]; + const queryService = moduleRef.get(TestRelationService); + return expect( + queryService.removeRelation('testEntity', relation.testRelationPk, PLAIN_TEST_ENTITIES[0].testEntityPk, { + relationFilter: { stringType: { eq: PLAIN_TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow('Unable to find testEntity to remove from TestRelation'); + }); + }); }); describe('oneToMany', () => { @@ -632,6 +807,33 @@ describe('SequelizeQueryService', (): 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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelation('testRelations', entity.testEntityPk, PLAIN_TEST_RELATIONS[4].testRelationPk, { + filter: { stringType: { eq: PLAIN_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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.removeRelation( + 'testRelations', + entity.testEntityPk, + PLAIN_TEST_RELATIONS[4].testRelationPk, + { + relationFilter: { relationName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find testRelations to remove from TestEntity'); + }); + }); }); }); @@ -648,6 +850,26 @@ describe('SequelizeQueryService', (): 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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.findById(entity.testEntityPk, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(found).toEqual(expect.objectContaining(entity)); + }); + + it('should return an undefined if an entitity with the pk and filter is not found', async () => { + const entity = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.findById(entity.testEntityPk, { + filter: { stringType: { eq: PLAIN_TEST_ENTITIES[1].stringType } }, + }); + expect(found).toBeUndefined(); + }); + }); }); describe('#getById', () => { @@ -658,10 +880,31 @@ describe('SequelizeQueryService', (): void => { expect(found).toEqual(expect.objectContaining(entity)); }); - it('throw an error if the record is not found', () => { + it('should throw an error if the record is 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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.getById(entity.testEntityPk, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(found).toEqual(expect.objectContaining(entity)); + }); + + it('should return an undefined if an entitity with the pk and filter is not found', async () => { + const entity = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.getById(entity.testEntityPk, { + filter: { stringType: { eq: PLAIN_TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.testEntityPk}`); + }); + }); }); describe('#createMany', () => { @@ -733,6 +976,27 @@ describe('SequelizeQueryService', (): void => { const queryService = moduleRef.get(TestEntityService); 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 = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const deleted = await queryService.deleteOne(entity.testEntityPk, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(deleted).toEqual(expect.objectContaining(PLAIN_TEST_ENTITIES[0])); + }); + + it('should return throw an error if unable to find ', async () => { + const entity = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.deleteOne(entity.testEntityPk, { + filter: { stringType: { eq: PLAIN_TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.testEntityPk}`); + }); + }); }); describe('#updateMany', () => { @@ -775,5 +1039,30 @@ describe('SequelizeQueryService', (): void => { 'Unable to find TestEntity with id: bad-id', ); }); + + describe('with filter', () => { + it('should update the entity if all filters match', async () => { + const entity = PLAIN_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(expect.objectContaining({ ...PLAIN_TEST_ENTITIES[0], stringType: 'updated' })); + }); + + it('should throw an error if unable to find the entity', async () => { + const entity = PLAIN_TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.updateOne( + entity.testEntityPk, + { stringType: 'updated' }, + { filter: { stringType: { eq: PLAIN_TEST_ENTITIES[1].stringType } } }, + ), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.testEntityPk}`); + }); + }); }); }); diff --git a/packages/query-sequelize/src/query/filter-query.builder.ts b/packages/query-sequelize/src/query/filter-query.builder.ts index 82e1b953a..68ac50df5 100644 --- a/packages/query-sequelize/src/query/filter-query.builder.ts +++ b/packages/query-sequelize/src/query/filter-query.builder.ts @@ -58,6 +58,23 @@ export class FilterQueryBuilder> { return opts; } + /** + * Create a `sequelize` SelectQueryBuilder with `WHERE`, `ORDER BY` and `LIMIT/OFFSET` clauses. + * + * @param pk - The primary key(s) of records to find. + * @param query - the query to apply. + */ + findByIdOptions(pk: string | number | (string | number)[], query: Query): FindOptions { + let opts: FindOptions = this.applyAssociationIncludes({ subQuery: false }, query.filter); + opts = this.applyFilter(opts, { + ...query.filter, + [this.model.primaryKeyAttribute]: { [Array.isArray(pk) ? 'in' : 'eq']: pk }, + }); + opts = this.applySorting(opts, query.sorting); + opts = this.applyPaging(opts, query.paging); + return opts; + } + /** * Create a `sequelize` SelectQueryBuilder with `WHERE`, `ORDER BY` and `LIMIT/OFFSET` clauses. * diff --git a/packages/query-sequelize/src/services/relation-query.service.ts b/packages/query-sequelize/src/services/relation-query.service.ts index 403383d51..1d2b0857f 100644 --- a/packages/query-sequelize/src/services/relation-query.service.ts +++ b/packages/query-sequelize/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, + GetByIdOptions, + FindRelationOptions, +} from '@nestjs-query/core'; import { Model, ModelCtor } from 'sequelize-typescript'; import { ModelCtor as SequelizeModelCtor } from 'sequelize'; import { AggregateBuilder, FilterQueryBuilder } from '../query'; @@ -18,7 +28,7 @@ export abstract class RelationQueryService { abstract model: ModelCtor; - abstract getById(id: string | number): Promise; + 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. @@ -80,7 +90,8 @@ export abstract class RelationQueryService { * @param RelationClass - The class to serialize the relations into. * @param dto - The dto to query relations for. * @param relationName - The name of relation to query for. - * @param query - A query to filter, page and sort relations. + * @param filter - Filter for relations to aggregate on. + * @param aggregate - Aggregate query */ async aggregateRelations( RelationClass: Class, @@ -152,11 +163,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>; /** @@ -164,23 +177,31 @@ 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 relation = await this.ensureIsEntity(dto).$get(relationName as keyof Entity); + const relationEntity = this.getRelationEntity(relationName); + const assembler = AssemblerFactory.getAssembler(RelationClass, relationEntity); + const relationQueryBuilder = this.getRelationQueryBuilder(relationEntity); + const relation = await this.ensureIsEntity(dto).$get( + relationName as keyof Entity, + relationQueryBuilder.findOptions(opts ?? {}), + ); if (!relation) { return undefined; } @@ -192,13 +213,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 - Additional options */ async addRelations( relationName: string, id: string | number, relationIds: string[] | number[], + opts?: ModifyRelationOptions, ): Promise { - const entity = await this.getById(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.model.name}`); + } await entity.$add(relationName, relationIds); return entity; } @@ -209,9 +236,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.getById(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.model.name}`); + } await entity.$set(relationName as keyof Entity, relationId); return entity; } @@ -221,13 +258,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.getById(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.model.name}`); + } await entity.$remove(relationName, relationIds); return entity; } @@ -238,14 +281,20 @@ 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 removeRelation( relationName: string, id: string | number, relationId: string | number, + opts?: ModifyRelationOptions, ): Promise { - const entity = await this.getById(id); + const entity = await this.getById(id, opts); const association = this.getAssociation(relationName); + const relation = (await this.getRelations(relationName, [relationId], opts?.relationFilter))[0]; + if (!relation) { + throw new Error(`Unable to find ${relationName} to remove from ${this.model.name}`); + } if (association.isSingleAssociation) { // todo update that this line to remove the casting once https://github.com/RobinBuschmann/sequelize-typescript/issues/803 is addressed. await entity.$set(relationName as keyof Entity, (null as unknown) as string); @@ -284,13 +333,6 @@ export abstract class RelationQueryService { }, Promise.resolve(new Map())); } - /** - * Query for an array of relations for multiple dtos. - * @param RelationClass - The class to serialize the relations into. - * @param entities - The entities to query relations for. - * @param relationName - The name of relation to query for. - * @param query - A query to filter, page or sort relations. - */ private async batchAggregateRelations( RelationClass: Class, relationName: string, @@ -317,13 +359,6 @@ export abstract class RelationQueryService { }, Promise.resolve(new Map>())); } - /** - * Query for an array of relations for multiple dtos. - * @param RelationClass - The class to serialize the relations into. - * @param entities - The entities to query relations for. - * @param relationName - The name of relation to query for. - * @param query - A query to filter, page or sort relations. - */ private async batchCountRelations( RelationClass: Class, relationName: string, @@ -342,22 +377,21 @@ export abstract class RelationQueryService { }, Promise.resolve(new Map())); } - /** - * Query for a relation for multiple dtos. - * @param RelationClass - The class to serialize the relations into. - * @param dtos - The dto to query relations for. - * @param relationName - The name of relation to query for. - * @param query - A query to filter, page or sort relations. - */ private async batchFindRelations( RelationClass: Class, relationName: string, dtos: Entity[], + opts?: FindRelationOptions, ): Promise> { - const assembler = AssemblerFactory.getAssembler(RelationClass, this.getRelationEntity(relationName)); + const relationEntity = this.getRelationEntity(relationName); + const assembler = AssemblerFactory.getAssembler(RelationClass, relationEntity); + const relationQueryBuilder = this.getRelationQueryBuilder(relationEntity); return dtos.reduce(async (mapPromise, e) => { const map = await mapPromise; - const relation = await this.ensureIsEntity(e).$get(relationName as keyof Entity); + const relation = await this.ensureIsEntity(e).$get( + relationName as keyof Entity, + relationQueryBuilder.findOptions(opts ?? {}), + ); if (relation) { map.set(e, assembler.convertToDTO((relation as unknown) as Model)); } @@ -383,4 +417,19 @@ export abstract class RelationQueryService { private getRelationEntity(relationName: string): ModelCtor { return this.getAssociation(relationName).target as ModelCtor; } + + private getRelations( + relationName: string, + ids: (string | number)[], + filter?: Filter, + ): Promise { + const relationEntity = this.getRelationEntity(relationName); + const relationQueryBuilder = this.getRelationQueryBuilder(relationEntity); + const findOptions = relationQueryBuilder.findByIdOptions(ids, { filter }); + return relationEntity.findAll({ ...findOptions, attributes: relationEntity.primaryKeyAttributes }); + } + + private foundAllRelations(relationIds: (string | number)[], relations: Model[]): boolean { + return new Set([...relationIds]).size === relations.length; + } } diff --git a/packages/query-sequelize/src/services/sequelize-query.service.ts b/packages/query-sequelize/src/services/sequelize-query.service.ts index b7abae34a..585d77760 100644 --- a/packages/query-sequelize/src/services/sequelize-query.service.ts +++ b/packages/query-sequelize/src/services/sequelize-query.service.ts @@ -7,6 +7,10 @@ import { Filter, AggregateQuery, AggregateResponse, + FindByIdOptions, + GetByIdOptions, + UpdateOneOptions, + DeleteOneOptions, } from '@nestjs-query/core'; import lodashPick from 'lodash.pick'; import { Model, ModelCtor } from 'sequelize-typescript'; @@ -77,9 +81,10 @@ export class SequelizeQueryService> extends Relatio * const todoItem = await this.service.findById(1); * ``` * @param id - The id of the record to find. + * @param opts - Additional options */ - async findById(id: string | number): Promise { - const model = await this.model.findByPk(id); + async findById(id: string | number, opts?: FindByIdOptions): Promise { + const model = await this.model.findOne(this.filterQueryBuilder.findByIdOptions(id, opts ?? {})); if (!model) { return undefined; } @@ -98,9 +103,10 @@ export class SequelizeQueryService> extends Relatio * } * ``` * @param id - The id of the record to find. + * @param opts - Additional options */ - async getById(id: string | number): Promise { - const entity = await this.model.findByPk(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.model.name} with id: ${id}`); } @@ -147,10 +153,15 @@ export class SequelizeQueryService> extends Relatio * ``` * @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.getById(id); + const entity = await this.getById(id, opts); return entity.update(this.getChangedValues(update)); } @@ -186,9 +197,10 @@ export class SequelizeQueryService> extends Relatio * ``` * * @param id - The `id` of the entity to delete. + * @param opts - Additional options. */ - async deleteOne(id: string | number): Promise { - const entity = await this.getById(id); + async deleteOne(id: string | number, opts?: DeleteOneOptions): Promise { + const entity = await this.getById(id, opts); await entity.destroy(); return entity; }