From de34e9240055b0f1cfbb360b66c37f216f115ddb Mon Sep 17 00:00:00 2001 From: marian2js Date: Thu, 3 Sep 2020 18:16:41 -0300 Subject: [PATCH] feat(mongodb): Use new filter on typegoose query service --- .../services/typegoose-query.service.spec.ts | 102 ++++++++++++++++++ packages/query-typegoose/package.json | 4 +- .../src/services/typegoose-query.service.ts | 60 ++++++++--- 3 files changed, 149 insertions(+), 17 deletions(-) diff --git a/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts b/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts index fb5bfb945..bd99d2c49 100644 --- a/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts +++ b/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts @@ -146,6 +146,26 @@ describe('TypegooseQueryService', () => { const found = await queryService.findById(new mongoose.Types.ObjectId().toString()); 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.id, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(found).toEqual(entity.getOutputData()); + }); + + it('should return an undefined if an entity 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.id, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }); + expect(found).toBeUndefined(); + }); + }); }); describe('#getById', () => { @@ -161,6 +181,27 @@ describe('TypegooseQueryService', () => { const queryService = moduleRef.get(TestEntityService); return expect(queryService.getById(badId)).rejects.toThrow(`Unable to find TestEntity with id: ${badId}`); }); + + 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.id, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(found).toEqual(entity.getOutputData()); + }); + + 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.id, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.id}`); + }); + }); }); describe('#createMany', () => { @@ -233,6 +274,27 @@ describe('TypegooseQueryService', () => { const queryService = moduleRef.get(TestEntityService); return expect(queryService.deleteOne(badId)).rejects.toThrow(`Unable to find TestEntity with id: ${badId}`); }); + + 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.id, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(deleted).toEqual(TEST_ENTITIES[0].getOutputData()); + }); + + 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.id, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.id}`); + }); + }); }); describe('#updateMany', () => { @@ -278,6 +340,31 @@ describe('TypegooseQueryService', () => { `Unable to find TestEntity with id: ${badId}`, ); }); + + 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.id, + { stringType: 'updated' }, + { filter: { stringType: { eq: entity.stringType } } }, + ); + expect(updated).toEqual({ ...entity.getOutputData(), 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.id, + { stringType: 'updated' }, + { filter: { stringType: { eq: TEST_ENTITIES[1].stringType } } }, + ), + ).rejects.toThrow(`Unable to find TestEntity with id: ${entity.id}`); + }); + }); }); describe('#findRelation', () => { @@ -306,6 +393,21 @@ describe('TypegooseQueryService', () => { expect(queryResult.values().next().value).toBeUndefined(); }); + + it('should apply the filter option', async () => { + const entity = TEST_ENTITIES[10]; + const queryService = moduleRef.get(TestEntityService); + + const queryResult1 = await queryService.findRelation(TestReference, 'testReference', [entity], { + filter: { name: { eq: TEST_REFERENCES[0].name } }, + }); + expect(queryResult1.values().next().value).toEqual(TEST_REFERENCES[0].getOutputData()); + + const queryResult2 = await queryService.findRelation(TestReference, 'testReference', [entity], { + filter: { name: { eq: TEST_REFERENCES[1].name } }, + }); + expect(queryResult2.values().next().value).toBeUndefined(); + }); }); describe('#queryRelations', () => { diff --git a/packages/query-typegoose/package.json b/packages/query-typegoose/package.json index 2d2e6d769..7ed5d076d 100644 --- a/packages/query-typegoose/package.json +++ b/packages/query-typegoose/package.json @@ -19,7 +19,8 @@ }, "dependencies": { "@nestjs-query/core": "0.19.0", - "lodash.escaperegexp": "^4.1.2" + "lodash.escaperegexp": "^4.1.2", + "lodash.merge": "^4.6.2" }, "peerDependencies": { "@nestjs/common": "^7.0.0", @@ -33,6 +34,7 @@ "@nestjs/testing": "7.4.2", "@typegoose/typegoose": "^7.3.3", "@types/lodash.escaperegexp": "^4.1.6", + "@types/lodash.merge": "^4.6.6", "@types/mongodb": "^3.5.26", "@types/mongoose": "^5.7.36", "class-transformer": "0.3.1", diff --git a/packages/query-typegoose/src/services/typegoose-query.service.ts b/packages/query-typegoose/src/services/typegoose-query.service.ts index 3f1227b49..a97180a05 100644 --- a/packages/query-typegoose/src/services/typegoose-query.service.ts +++ b/packages/query-typegoose/src/services/typegoose-query.service.ts @@ -4,15 +4,21 @@ import { Class, DeepPartial, DeleteManyResponse, + DeleteOneOptions, Filter, + FindByIdOptions, + FindRelationOptions, + GetByIdOptions, Query, QueryService, UpdateManyResponse, + UpdateOneOptions, } from '@nestjs-query/core'; import { NotFoundException } from '@nestjs/common'; import { DocumentToObjectOptions, FilterQuery, UpdateQuery } from 'mongoose'; import { ReturnModelType } from '@typegoose/typegoose'; import escapeRegExp from 'lodash.escaperegexp'; +import merge from 'lodash.merge'; export interface TypegooseQueryServiceOpts { documentToObjectOptions?: DocumentToObjectOptions; @@ -54,8 +60,8 @@ export class TypegooseQueryService implements QueryService { this.documentToObjectOptions = opts?.documentToObjectOptions || { virtuals: true }; } - protected buildExpression(filter: Filter): FilterQuery Entity> { - return Object.entries(filter).reduce((prev: FilterQuery Entity>, [key, value]) => { + protected buildExpression(filter: Filter): FilterQuery T> { + return Object.entries(filter).reduce((prev: FilterQuery T>, [key, value]) => { if (!value) { return prev; } @@ -102,6 +108,13 @@ export class TypegooseQueryService implements QueryService { return key === 'id' ? '_id' : key; } + private mergeFilterWithId(id: unknown, filter?: Filter): FilterQuery T> { + return merge({ + ...this.buildExpression(filter || {}), + [this.getSchemaKey('id')]: id, + }) as FilterQuery T>; + } + /** * Query for multiple entities, using a Query from `@nestjs-query/core`. * @@ -146,9 +159,10 @@ export class TypegooseQueryService implements QueryService { * const todoItem = await this.service.findById(1); * ``` * @param id - The id of the record to find. + * @param opts - Additional options */ - async findById(id: string): Promise { - const doc = await this.Model.findById(id); + async findById(id: string, opts?: FindByIdOptions): Promise { + const doc = await this.Model.findOne(this.mergeFilterWithId(id, opts?.filter)); return doc?.toObject(this.documentToObjectOptions) as Entity; } @@ -164,9 +178,10 @@ export class TypegooseQueryService implements QueryService { * } * ``` * @param id - The id of the record to find. + * @param opts - Additional options */ - async getById(id: string): Promise { - const entity = await this.findById(id); + async getById(id: string, opts?: GetByIdOptions): Promise { + const entity = await this.findById(id, opts); if (!entity) { throw new NotFoundException(`Unable to find ${this.Model.modelName} with id: ${id}`); } @@ -213,10 +228,19 @@ export class TypegooseQueryService implements QueryService { * ``` * @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: string, update: U): Promise { + async updateOne>( + id: string, + update: U, + opts?: UpdateOneOptions, + ): Promise { this.ensureIdIsNotPresent(update); - const doc = await this.Model.findByIdAndUpdate(id, update as UpdateQuery Entity>, { new: true }); + const doc = await this.Model.findOneAndUpdate( + this.mergeFilterWithId(id, opts?.filter), + update as UpdateQuery Entity>, + { new: true }, + ); if (doc) { return doc.toObject(this.documentToObjectOptions) as Entity; } @@ -255,9 +279,10 @@ export class TypegooseQueryService implements QueryService { * ``` * * @param id - The `id` of the entity to delete. + * @param opts - Additional filter to use when finding the entity to delete. */ - async deleteOne(id: string | number): Promise { - const doc = await this.Model.findByIdAndDelete(id); + async deleteOne(id: string, opts?: DeleteOneOptions): Promise { + const doc = await this.Model.findOneAndDelete(this.mergeFilterWithId(id, opts?.filter)); if (doc) { return doc.toObject(this.documentToObjectOptions) as Entity; } @@ -288,7 +313,7 @@ export class TypegooseQueryService implements QueryService { } } - addRelations(): Promise { + addRelations(): Promise { throw new Error('Not implemented yet'); } @@ -326,7 +351,7 @@ export class TypegooseQueryService implements QueryService { filter: Filter, ): Promise; - countRelations(): Promise> { + countRelations(): Promise> { throw new Error('Not implemented yet'); } @@ -334,16 +359,19 @@ export class TypegooseQueryService implements QueryService { RelationClass: Class, relationName: string, dtos: Entity[], + opts?: FindRelationOptions, ): Promise>; findRelation( RelationClass: Class, relationName: string, dto: Entity, + opts?: FindRelationOptions, ): Promise; findRelation( RelationClass: Class, relationName: string, dto: Entity | Entity[], + opts?: FindRelationOptions, ): Promise<(Relation | undefined) | Map> { const relationModel = this.Model.model(RelationClass.name); const dtos: Entity[] = Array.isArray(dto) ? dto : [dto]; @@ -351,7 +379,7 @@ export class TypegooseQueryService implements QueryService { const map = await prev; const referenceId = curr[relationName as keyof Entity]; if (referenceId) { - const relationDoc = await relationModel.findOne(referenceId); + const relationDoc = await relationModel.findOne(this.mergeFilterWithId(referenceId, opts?.filter)); map.set(curr, relationDoc?.toObject(this.documentToObjectOptions)); } return map; @@ -405,15 +433,15 @@ export class TypegooseQueryService implements QueryService { }, Promise.resolve(new Map())); } - removeRelation(): Promise { + removeRelation(): Promise { throw new Error('Not implemented yet'); } - removeRelations(): Promise { + removeRelations(): Promise { throw new Error('Not implemented yet'); } - setRelation(): Promise { + setRelation(): Promise { throw new Error('Not implemented yet'); } }