From 5cdfa39b7d91cf0f8438ef3387a89aac850f4452 Mon Sep 17 00:00:00 2001 From: doug-martin Date: Mon, 28 Sep 2020 09:07:14 -0500 Subject: [PATCH] feat(mongoose): Switch to native mongoose support --- jest.e2e.config.js | 1 + jest.unit.config.js | 1 + packages/core/package.json | 2 +- packages/core/src/common/deep-partial.type.ts | 6 +- packages/core/src/decorators/helpers.ts | 2 +- .../.eslintrc.js | 3 + .../README.md | 0 .../__fixtures__/connection.fixture.ts | 0 .../__tests__/__fixtures__/index.ts | 4 + .../__tests__/__fixtures__/seeds.ts | 55 + .../__fixtures__/test-reference.entity.ts | 18 + .../__tests__/__fixtures__/test.entity.ts | 33 + .../__tests__/module.spec.ts | 6 +- .../__tests__/providers.spec.ts | 4 +- .../query/comparison.builder.spec.ts | 200 +++ .../__tests__/query/where.builder.spec.ts | 188 +++ .../services/mongoose-query.service.spec.ts | 1073 +++++++++++++++++ .../package.json | 32 +- packages/query-mongoose/src/index.ts | 2 + packages/query-mongoose/src/module.ts | 16 + .../src/mongoose-types.helper.ts | 58 + packages/query-mongoose/src/providers.ts | 18 + .../src/query/aggregate.builder.ts | 85 ++ .../src/query/comparison.builder.ts | 107 ++ .../src/query/filter-query.builder.ts | 96 ++ packages/query-mongoose/src/query/helpers.ts | 3 + packages/query-mongoose/src/query/index.ts | 4 + .../query-mongoose/src/query/where.builder.ts | 80 ++ packages/query-mongoose/src/services/index.ts | 1 + .../src/services/mongoose-query.service.ts | 253 ++++ .../src/services/reference-query.service.ts | 338 ++++++ .../tsconfig.build.json | 0 .../tsconfig.json | 0 packages/query-typegoose/CHANGELOG.md | 277 ----- .../__tests__/__fixtures__/seeds.ts | 45 - .../__fixtures__/test-reference.entity.ts | 18 - .../__tests__/__fixtures__/test.entity.ts | 43 - .../services/typegoose-query.service.spec.ts | 453 ------- packages/query-typegoose/src/index.ts | 2 - packages/query-typegoose/src/module.ts | 17 - packages/query-typegoose/src/providers.ts | 18 - .../query-typegoose/src/services/index.ts | 1 - .../src/services/typegoose-query.service.ts | 447 ------- tsconfig.json | 4 +- 44 files changed, 2666 insertions(+), 1348 deletions(-) rename packages/{query-typegoose => query-mongoose}/.eslintrc.js (70%) rename packages/{query-typegoose => query-mongoose}/README.md (100%) rename packages/{query-typegoose => query-mongoose}/__tests__/__fixtures__/connection.fixture.ts (100%) create mode 100644 packages/query-mongoose/__tests__/__fixtures__/index.ts create mode 100644 packages/query-mongoose/__tests__/__fixtures__/seeds.ts create mode 100644 packages/query-mongoose/__tests__/__fixtures__/test-reference.entity.ts create mode 100644 packages/query-mongoose/__tests__/__fixtures__/test.entity.ts rename packages/{query-typegoose => query-mongoose}/__tests__/module.spec.ts (58%) rename packages/{query-typegoose => query-mongoose}/__tests__/providers.spec.ts (87%) create mode 100644 packages/query-mongoose/__tests__/query/comparison.builder.spec.ts create mode 100644 packages/query-mongoose/__tests__/query/where.builder.spec.ts create mode 100644 packages/query-mongoose/__tests__/services/mongoose-query.service.spec.ts rename packages/{query-typegoose => query-mongoose}/package.json (65%) create mode 100644 packages/query-mongoose/src/index.ts create mode 100644 packages/query-mongoose/src/module.ts create mode 100644 packages/query-mongoose/src/mongoose-types.helper.ts create mode 100644 packages/query-mongoose/src/providers.ts create mode 100644 packages/query-mongoose/src/query/aggregate.builder.ts create mode 100644 packages/query-mongoose/src/query/comparison.builder.ts create mode 100644 packages/query-mongoose/src/query/filter-query.builder.ts create mode 100644 packages/query-mongoose/src/query/helpers.ts create mode 100644 packages/query-mongoose/src/query/index.ts create mode 100644 packages/query-mongoose/src/query/where.builder.ts create mode 100644 packages/query-mongoose/src/services/index.ts create mode 100644 packages/query-mongoose/src/services/mongoose-query.service.ts create mode 100644 packages/query-mongoose/src/services/reference-query.service.ts rename packages/{query-typegoose => query-mongoose}/tsconfig.build.json (100%) rename packages/{query-typegoose => query-mongoose}/tsconfig.json (100%) delete mode 100644 packages/query-typegoose/CHANGELOG.md delete mode 100644 packages/query-typegoose/__tests__/__fixtures__/seeds.ts delete mode 100644 packages/query-typegoose/__tests__/__fixtures__/test-reference.entity.ts delete mode 100644 packages/query-typegoose/__tests__/__fixtures__/test.entity.ts delete mode 100644 packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts delete mode 100644 packages/query-typegoose/src/index.ts delete mode 100644 packages/query-typegoose/src/module.ts delete mode 100644 packages/query-typegoose/src/providers.ts delete mode 100644 packages/query-typegoose/src/services/index.ts delete mode 100644 packages/query-typegoose/src/services/typegoose-query.service.ts diff --git a/jest.e2e.config.js b/jest.e2e.config.js index 030643b84..832fddd48 100644 --- a/jest.e2e.config.js +++ b/jest.e2e.config.js @@ -4,6 +4,7 @@ module.exports = { preset: 'ts-jest', testMatch: ['**/e2e/**/*.spec.ts'], collectCoverageFrom: ['packages/**/*.ts', '!**/__tests__/**', '!**/dist/**', '!**/node_modules/**'], + testEnvironment: 'node', moduleNameMapper: { '^@nestjs-query/(.*)$': '/packages/$1/src', }, diff --git a/jest.unit.config.js b/jest.unit.config.js index de7629217..a4228569c 100644 --- a/jest.unit.config.js +++ b/jest.unit.config.js @@ -4,6 +4,7 @@ module.exports = { preset: 'ts-jest', collectCoverageFrom: ['packages/**/*.ts', '!**/__tests__/**', '!**/dist/**', '!**/node_modules/**'], testMatch: ['**/__tests__/**/*.spec.ts'], + testEnvironment: 'node', moduleNameMapper: { '^@nestjs-query/(.*)$': '/packages/$1/src', }, diff --git a/packages/core/package.json b/packages/core/package.json index 6b9cbf026..3ee416e38 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,7 +31,7 @@ "url": "https://github.com/doug-martin/nestjs-query/issues" }, "dependencies": { - "lodash.merge": "4.6.2" + "lodash.merge": "^4.6.2" }, "peerDependencies": { "@nestjs/common": "^7.0.0", diff --git a/packages/core/src/common/deep-partial.type.ts b/packages/core/src/common/deep-partial.type.ts index 380c20905..16d86c082 100644 --- a/packages/core/src/common/deep-partial.type.ts +++ b/packages/core/src/common/deep-partial.type.ts @@ -4,7 +4,7 @@ export declare type DeepPartial = { [P in keyof T]?: T[P] extends Array ? Array> - : T[P] extends ReadonlyArray - ? ReadonlyArray> - : DeepPartial; + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : DeepPartial | T[P]; }; diff --git a/packages/core/src/decorators/helpers.ts b/packages/core/src/decorators/helpers.ts index c7b9b16e0..a7f5bca31 100644 --- a/packages/core/src/decorators/helpers.ts +++ b/packages/core/src/decorators/helpers.ts @@ -1,7 +1,7 @@ import { Assembler } from '../assemblers'; import { Class } from '../common'; -export function getQueryServiceToken(DTOClass: Class): string { +export function getQueryServiceToken(DTOClass: { name: string }): string { return `${DTOClass.name}QueryService`; } diff --git a/packages/query-typegoose/.eslintrc.js b/packages/query-mongoose/.eslintrc.js similarity index 70% rename from packages/query-typegoose/.eslintrc.js rename to packages/query-mongoose/.eslintrc.js index ae15c4d67..60e9eccb1 100644 --- a/packages/query-typegoose/.eslintrc.js +++ b/packages/query-mongoose/.eslintrc.js @@ -8,6 +8,9 @@ module.exports = { { assertFunctionNames: [ 'expect', + 'assertFilterQuery', + 'expectEqualEntities', + 'expectEqualCreate' ], }, ], diff --git a/packages/query-typegoose/README.md b/packages/query-mongoose/README.md similarity index 100% rename from packages/query-typegoose/README.md rename to packages/query-mongoose/README.md diff --git a/packages/query-typegoose/__tests__/__fixtures__/connection.fixture.ts b/packages/query-mongoose/__tests__/__fixtures__/connection.fixture.ts similarity index 100% rename from packages/query-typegoose/__tests__/__fixtures__/connection.fixture.ts rename to packages/query-mongoose/__tests__/__fixtures__/connection.fixture.ts diff --git a/packages/query-mongoose/__tests__/__fixtures__/index.ts b/packages/query-mongoose/__tests__/__fixtures__/index.ts new file mode 100644 index 000000000..57d9ba8d5 --- /dev/null +++ b/packages/query-mongoose/__tests__/__fixtures__/index.ts @@ -0,0 +1,4 @@ +export * from './seeds'; +export * from './test.entity'; +export * from './test-reference.entity'; +export * from './connection.fixture'; diff --git a/packages/query-mongoose/__tests__/__fixtures__/seeds.ts b/packages/query-mongoose/__tests__/__fixtures__/seeds.ts new file mode 100644 index 000000000..2452c4a13 --- /dev/null +++ b/packages/query-mongoose/__tests__/__fixtures__/seeds.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-underscore-dangle */ +import { Connection } from 'mongoose'; +import { TestEntity } from './test.entity'; +import { TestReference } from './test-reference.entity'; + +export const TEST_ENTITIES: TestEntity[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + boolType: i % 2 === 0, + dateType: new Date(`2020-02-${i}`), + numberType: i, + stringType: `foo${i}`, + } as TestEntity; +}); + +export const TEST_REFERENCES: TestReference[] = TEST_ENTITIES.reduce((relations, te) => { + return [ + ...relations, + { + referenceName: `${te.stringType}-test-reference-1-one`, + } as TestReference, + { + referenceName: `${te.stringType}-test-reference-2-two`, + } as TestReference, + { + referenceName: `${te.stringType}-test-reference-3-three`, + } as TestReference, + ]; +}, [] as TestReference[]); + +export const seed = async (connection: Connection): Promise => { + const TestEntityModel = connection.model('TestEntity'); + const TestReferencesModel = connection.model('TestReference'); + + const testEntities = await TestEntityModel.create(TEST_ENTITIES); + const testReferences = await TestReferencesModel.create(TEST_REFERENCES); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + testEntities.forEach((te, index) => Object.assign(TEST_ENTITIES[index], te.toObject())); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + testReferences.forEach((tr, index) => Object.assign(TEST_REFERENCES[index], tr.toObject())); + await Promise.all( + testEntities.map(async (te, index) => { + const references = testReferences.filter((tr) => tr.referenceName.includes(`${te.stringType}-`)); + TEST_ENTITIES[index].testReference = references[0]._id; + TEST_ENTITIES[index].testReferences = references.map((r) => r._id); + await te.update({ $set: { testReferences: references.map((r) => r._id), testReference: references[0]._id } }); + await Promise.all( + references.map((r) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + TEST_REFERENCES.find((tr) => tr._id.toString() === r._id.toString())!.testEntity = te._id; + return r.update({ $set: { testEntity: te._id } }); + }), + ); + }), + ); +}; diff --git a/packages/query-mongoose/__tests__/__fixtures__/test-reference.entity.ts b/packages/query-mongoose/__tests__/__fixtures__/test-reference.entity.ts new file mode 100644 index 000000000..b4ea28b7c --- /dev/null +++ b/packages/query-mongoose/__tests__/__fixtures__/test-reference.entity.ts @@ -0,0 +1,18 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { SchemaTypes, Document, Types } from 'mongoose'; + +@Schema() +export class TestReference extends Document { + @Prop({ required: true }) + referenceName!: string; + + @Prop({ type: SchemaTypes.ObjectId, ref: 'TestEntity' }) + testEntity?: Types.ObjectId; +} +export const TestReferenceSchema = SchemaFactory.createForClass(TestReference); +TestReferenceSchema.virtual('virtualTestEntity', { + ref: 'TestEntity', + localField: 'testEntity', + foreignField: '_id', + justOne: true, +}); diff --git a/packages/query-mongoose/__tests__/__fixtures__/test.entity.ts b/packages/query-mongoose/__tests__/__fixtures__/test.entity.ts new file mode 100644 index 000000000..f6d20e155 --- /dev/null +++ b/packages/query-mongoose/__tests__/__fixtures__/test.entity.ts @@ -0,0 +1,33 @@ +import { Document, Types, SchemaTypes } from 'mongoose'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { TestReference } from 'packages/query-mongoose/__tests__/__fixtures__/test-reference.entity'; + +@Schema() +export class TestEntity extends Document { + @Prop({ required: true }) + stringType!: string; + + @Prop({ required: true }) + boolType!: boolean; + + @Prop({ required: true }) + numberType!: number; + + @Prop({ required: true }) + dateType!: Date; + + @Prop({ type: SchemaTypes.ObjectId, ref: 'TestReference' }) + testReference?: Types.ObjectId; + + @Prop([{ type: SchemaTypes.ObjectId, ref: 'TestReference' }]) + testReferences?: Types.ObjectId[]; + + virtualTestReference?: TestReference; +} + +export const TestEntitySchema = SchemaFactory.createForClass(TestEntity); +TestEntitySchema.virtual('virtualTestReferences', { + ref: 'TestReference', + localField: '_id', + foreignField: 'testEntity', +}); diff --git a/packages/query-typegoose/__tests__/module.spec.ts b/packages/query-mongoose/__tests__/module.spec.ts similarity index 58% rename from packages/query-typegoose/__tests__/module.spec.ts rename to packages/query-mongoose/__tests__/module.spec.ts index 70811cd6e..f061bfe1e 100644 --- a/packages/query-typegoose/__tests__/module.spec.ts +++ b/packages/query-mongoose/__tests__/module.spec.ts @@ -1,11 +1,11 @@ -import { NestjsQueryTypegooseModule } from '../src'; +import { NestjsQueryMongooseModule } from '../src'; describe('NestjsQueryTypegooseModule', () => { it('should create a module', () => { class TestEntity {} - const typeOrmModule = NestjsQueryTypegooseModule.forFeature([TestEntity]); + const typeOrmModule = NestjsQueryMongooseModule.forFeature([TestEntity]); expect(typeOrmModule.imports).toHaveLength(1); - expect(typeOrmModule.module).toBe(NestjsQueryTypegooseModule); + expect(typeOrmModule.module).toBe(NestjsQueryMongooseModule); expect(typeOrmModule.providers).toHaveLength(1); expect(typeOrmModule.exports).toHaveLength(2); }); diff --git a/packages/query-typegoose/__tests__/providers.spec.ts b/packages/query-mongoose/__tests__/providers.spec.ts similarity index 87% rename from packages/query-typegoose/__tests__/providers.spec.ts rename to packages/query-mongoose/__tests__/providers.spec.ts index e2efc5412..1fe903857 100644 --- a/packages/query-typegoose/__tests__/providers.spec.ts +++ b/packages/query-mongoose/__tests__/providers.spec.ts @@ -1,7 +1,7 @@ import { getQueryServiceToken } from '@nestjs-query/core'; import { instance } from 'ts-mockito'; import { createTypegooseQueryServiceProviders } from '../src/providers'; -import { TypegooseQueryService } from '../src/services'; +import { MongooseQueryService } from '../src/services'; describe('createTypegooseQueryServiceProviders', () => { it('should create a provider for the entity', () => { @@ -10,6 +10,6 @@ describe('createTypegooseQueryServiceProviders', () => { expect(providers).toHaveLength(1); expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity)); expect(providers[0].inject).toEqual([`${TestEntity.name}Model`]); - expect(providers[0].useFactory(instance(() => {}))).toBeInstanceOf(TypegooseQueryService); + expect(providers[0].useFactory(instance(() => {}))).toBeInstanceOf(MongooseQueryService); }); }); diff --git a/packages/query-mongoose/__tests__/query/comparison.builder.spec.ts b/packages/query-mongoose/__tests__/query/comparison.builder.spec.ts new file mode 100644 index 000000000..2c5dfc8ec --- /dev/null +++ b/packages/query-mongoose/__tests__/query/comparison.builder.spec.ts @@ -0,0 +1,200 @@ +import { CommonFieldComparisonBetweenType } from '@nestjs-query/core'; +import { TestEntity } from '../__fixtures__/test.entity'; +import { ComparisonBuilder } from '../../src/query'; + +describe('ComparisonBuilder', (): void => { + const createComparisonBuilder = () => new ComparisonBuilder(); + + it('should throw an error for an invalid comparison type', () => { + // @ts-ignore + expect(() => createComparisonBuilder().build('stringType', 'bad', 'foo')).toThrow('unknown operator "bad"'); + }); + + describe('eq comparisons', () => { + it('should build an unqualified eq sql fragment', (): void => { + expect(createComparisonBuilder().build('stringType', 'eq', 'foo')).toEqual({ + stringType: { + $eq: 'foo', + }, + }); + }); + }); + + it('should build neq sql fragment', (): void => { + expect(createComparisonBuilder().build('numberType', 'neq', 1)).toEqual({ + numberType: { + $ne: 1, + }, + }); + }); + + it('should build gt sql fragment', (): void => { + expect(createComparisonBuilder().build('numberType', 'gt', 1)).toEqual({ + numberType: { + $gt: 1, + }, + }); + }); + + it('should build gte sql fragment', (): void => { + expect(createComparisonBuilder().build('numberType', 'gte', 1)).toEqual({ + numberType: { + $gte: 1, + }, + }); + }); + + it('should build lt sql fragment', (): void => { + expect(createComparisonBuilder().build('numberType', 'lt', 1)).toEqual({ + numberType: { + $lt: 1, + }, + }); + }); + + it('should build lte sql fragment', (): void => { + expect(createComparisonBuilder().build('numberType', 'lte', 1)).toEqual({ + numberType: { + $lte: 1, + }, + }); + }); + + it('should build like sql fragment', (): void => { + expect(createComparisonBuilder().build('stringType', 'like', '%hello%')).toEqual({ + stringType: { + $regex: /.*hello.*/, + }, + }); + }); + + it('should build notLike sql fragment', (): void => { + expect(createComparisonBuilder().build('stringType', 'notLike', '%hello%')).toEqual({ + stringType: { + $not: { $regex: /.*hello.*/ }, + }, + }); + }); + + it('should build iLike sql fragment', (): void => { + expect(createComparisonBuilder().build('stringType', 'iLike', '%hello%')).toEqual({ + stringType: { + $regex: /.*hello.*/i, + }, + }); + }); + + it('should build notILike sql fragment', (): void => { + expect(createComparisonBuilder().build('stringType', 'notILike', '%hello%')).toEqual({ + stringType: { + $not: { $regex: /.*hello.*/i }, + }, + }); + }); + + describe('is comparisons', () => { + it('should build is true', (): void => { + expect(createComparisonBuilder().build('boolType', 'is', true)).toEqual({ + boolType: { + $eq: true, + }, + }); + }); + + it('should build is false', (): void => { + expect(createComparisonBuilder().build('boolType', 'is', false)).toEqual({ + boolType: { + $eq: false, + }, + }); + }); + + it('should build is null', (): void => { + expect(createComparisonBuilder().build('boolType', 'is', null)).toEqual({ + boolType: { + $eq: null, + }, + }); + }); + }); + + describe('isNot comparisons', () => { + it('should build is true', (): void => { + expect(createComparisonBuilder().build('boolType', 'isNot', true)).toEqual({ + boolType: { + $ne: true, + }, + }); + }); + + it('should build is false', (): void => { + expect(createComparisonBuilder().build('boolType', 'isNot', false)).toEqual({ + boolType: { + $ne: false, + }, + }); + }); + + it('should build is null', (): void => { + expect(createComparisonBuilder().build('boolType', 'isNot', null)).toEqual({ + boolType: { + $ne: null, + }, + }); + }); + }); + + describe('in comparisons', () => { + it('should build in comparisons', (): void => { + const arr = [1, 2, 3]; + expect(createComparisonBuilder().build('numberType', 'in', arr)).toEqual({ + numberType: { + $in: arr, + }, + }); + }); + }); + + describe('notIn comparisons', () => { + it('should build notIn comparisons', (): void => { + const arr = ['a', 'b', 'c']; + expect(createComparisonBuilder().build('stringType', 'notIn', arr)).toEqual({ + stringType: { + $nin: arr, + }, + }); + }); + }); + + describe('between comparisons', () => { + it('should build between comparisons', (): void => { + const between: CommonFieldComparisonBetweenType = { lower: 1, upper: 10 }; + expect(createComparisonBuilder().build('numberType', 'between', between)).toEqual({ + numberType: { $gte: between.lower, $lte: between.upper }, + }); + }); + + it('should throw an error if the comparison is not a between comparison', (): void => { + const between = [1, 10]; + expect(() => createComparisonBuilder().build('numberType', 'between', between)).toThrow( + 'Invalid value for between expected {lower: val, upper: val} got [1,10]', + ); + }); + }); + + describe('notBetween comparisons', () => { + it('should build not between comparisons', (): void => { + const between: CommonFieldComparisonBetweenType = { lower: 1, upper: 10 }; + expect(createComparisonBuilder().build('numberType', 'notBetween', between)).toEqual({ + numberType: { $lt: between.lower, $gt: between.upper }, + }); + }); + + it('should throw an error if the comparison is not a between comparison', (): void => { + const between = [1, 10]; + expect(() => createComparisonBuilder().build('numberType', 'notBetween', between)).toThrow( + 'Invalid value for not between expected {lower: val, upper: val} got [1,10]', + ); + }); + }); +}); diff --git a/packages/query-mongoose/__tests__/query/where.builder.spec.ts b/packages/query-mongoose/__tests__/query/where.builder.spec.ts new file mode 100644 index 000000000..e125adf1f --- /dev/null +++ b/packages/query-mongoose/__tests__/query/where.builder.spec.ts @@ -0,0 +1,188 @@ +import { Filter } from '@nestjs-query/core'; +import { FilterQuery } from 'mongoose'; +import { TestEntity } from '../__fixtures__/test.entity'; +import { WhereBuilder } from '../../src/query'; + +describe('WhereBuilder', (): void => { + const createWhereBuilder = () => new WhereBuilder(); + + const assertFilterQuery = (filter: Filter, expectedFilterQuery: FilterQuery): void => { + const actual = createWhereBuilder().build(filter); + expect(actual).toEqual(expectedFilterQuery); + }; + + it('should accept a empty filter', (): void => { + assertFilterQuery({}, {}); + }); + + it('or multiple operators for a single field together', (): void => { + assertFilterQuery( + { + numberType: { gt: 10, lt: 20, gte: 21, lte: 31 }, + }, + { + $and: [ + { + $or: [ + { numberType: { $gt: 10 } }, + { numberType: { $lt: 20 } }, + { numberType: { $gte: 21 } }, + { numberType: { $lte: 31 } }, + ], + }, + ], + }, + ); + }); + + it('and multiple field comparisons together', (): void => { + assertFilterQuery( + { + numberType: { eq: 1 }, + stringType: { like: 'foo%' }, + boolType: { is: true }, + }, + { + $and: [ + { + $and: [{ numberType: { $eq: 1 } }, { stringType: { $regex: /foo.*/ } }, { boolType: { $eq: true } }], + }, + ], + }, + ); + }); + + describe('and', (): void => { + it('and multiple expressions together', (): void => { + assertFilterQuery( + { + and: [ + { numberType: { gt: 10 } }, + { numberType: { lt: 20 } }, + { numberType: { gte: 30 } }, + { numberType: { lte: 40 } }, + ], + }, + { + $and: [ + { $and: [{ numberType: { $gt: 10 } }] }, + { $and: [{ numberType: { $lt: 20 } }] }, + { $and: [{ numberType: { $gte: 30 } }] }, + { $and: [{ numberType: { $lte: 40 } }] }, + ], + }, + ); + }); + + it('and multiple filters together with multiple fields', (): void => { + assertFilterQuery( + { + and: [ + { numberType: { gt: 10 }, stringType: { like: 'foo%' } }, + { numberType: { lt: 20 }, stringType: { like: '%bar' } }, + ], + }, + { + $and: [ + { $and: [{ $and: [{ numberType: { $gt: 10 } }, { stringType: { $regex: /foo.*/ } }] }] }, + { $and: [{ $and: [{ numberType: { $lt: 20 } }, { stringType: { $regex: /.*bar/ } }] }] }, + ], + }, + ); + }); + + it('should support nested ors', (): void => { + assertFilterQuery( + { + and: [ + { or: [{ numberType: { gt: 10 } }, { numberType: { lt: 20 } }] }, + { or: [{ numberType: { gte: 30 } }, { numberType: { lte: 40 } }] }, + ], + }, + { + $and: [ + { + $or: [{ $and: [{ numberType: { $gt: 10 } }] }, { $and: [{ numberType: { $lt: 20 } }] }], + }, + { + $or: [{ $and: [{ numberType: { $gte: 30 } }] }, { $and: [{ numberType: { $lte: 40 } }] }], + }, + ], + }, + ); + }); + }); + + describe('or', (): void => { + it('or multiple expressions together', (): void => { + assertFilterQuery( + { + or: [ + { numberType: { gt: 10 } }, + { numberType: { lt: 20 } }, + { numberType: { gte: 30 } }, + { numberType: { lte: 40 } }, + ], + }, + { + $or: [ + { $and: [{ numberType: { $gt: 10 } }] }, + { $and: [{ numberType: { $lt: 20 } }] }, + { $and: [{ numberType: { $gte: 30 } }] }, + { $and: [{ numberType: { $lte: 40 } }] }, + ], + }, + ); + }); + + it('and multiple and filters together', (): void => { + assertFilterQuery( + { + or: [ + { numberType: { gt: 10 }, stringType: { like: 'foo%' } }, + { numberType: { lt: 20 }, stringType: { like: '%bar' } }, + ], + }, + { + $or: [ + { + $and: [ + { + $and: [{ numberType: { $gt: 10 } }, { stringType: { $regex: /foo.*/ } }], + }, + ], + }, + { + $and: [ + { + $and: [{ numberType: { $lt: 20 } }, { stringType: { $regex: /.*bar/ } }], + }, + ], + }, + ], + }, + ); + }); + + it('should support nested ands', (): void => { + assertFilterQuery( + { + or: [ + { and: [{ numberType: { gt: 10 } }, { numberType: { lt: 20 } }] }, + { and: [{ numberType: { gte: 30 } }, { numberType: { lte: 40 } }] }, + ], + }, + { + $or: [ + { + $and: [{ $and: [{ numberType: { $gt: 10 } }] }, { $and: [{ numberType: { $lt: 20 } }] }], + }, + { + $and: [{ $and: [{ numberType: { $gte: 30 } }] }, { $and: [{ numberType: { $lte: 40 } }] }], + }, + ], + }, + ); + }); + }); +}); diff --git a/packages/query-mongoose/__tests__/services/mongoose-query.service.spec.ts b/packages/query-mongoose/__tests__/services/mongoose-query.service.spec.ts new file mode 100644 index 000000000..dbb984741 --- /dev/null +++ b/packages/query-mongoose/__tests__/services/mongoose-query.service.spec.ts @@ -0,0 +1,1073 @@ +/* eslint-disable no-underscore-dangle,@typescript-eslint/no-unsafe-return */ +import { Test, TestingModule } from '@nestjs/testing'; +import { InjectModel, MongooseModule } from '@nestjs/mongoose'; +import { ObjectId } from 'mongodb'; +import { Model } from 'mongoose'; +import { SortDirection } from '@nestjs-query/core'; +import { MongooseQueryService } from '../../src/services'; +import { + TestReference, + TestEntity, + TEST_ENTITIES, + TEST_REFERENCES, + getConnectionUri, + prepareDb, + closeDbConnection, + dropDatabase, + TestEntitySchema, + TestReferenceSchema, +} from '../__fixtures__'; + +describe('MongooseQueryService', () => { + let moduleRef: TestingModule; + let TestEntityModel: Model; + let TestReferenceModel: Model; + + class TestEntityService extends MongooseQueryService { + constructor(@InjectModel(TestEntity.name) readonly model: Model) { + super(model, { documentToObjectOptions: { virtuals: true } }); + TestEntityModel = model; + } + } + + class TestReferenceService extends MongooseQueryService { + constructor(@InjectModel(TestReference.name) readonly model: Model) { + super(model, { documentToObjectOptions: { virtuals: true } }); + TestReferenceModel = model; + } + } + + beforeAll(async () => { + moduleRef = await Test.createTestingModule({ + imports: [ + MongooseModule.forRoot(await getConnectionUri(), { + useFindAndModify: false, + useNewUrlParser: true, + useUnifiedTopology: true, + }), + MongooseModule.forFeature([ + { name: TestReference.name, schema: TestReferenceSchema }, + { name: TestEntity.name, schema: TestEntitySchema }, + ]), + ], + providers: [TestReferenceService, TestEntityService], + }).compile(); + }); + + function testEntityToObject(te: TestEntity): Partial { + return { + _id: te._id, + stringType: te.stringType, + boolType: te.boolType, + numberType: te.numberType, + dateType: te.dateType, + }; + } + + function expectEqualEntities(result: TestEntity[], expected: TestEntity[]) { + const cleansedResults = result.map(testEntityToObject); + const cleansedExpected = expected.map(testEntityToObject); + expect(cleansedResults).toEqual(expect.arrayContaining(cleansedExpected)); + } + + function testEntityToCreate(te: TestEntity): Partial { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { _id, ...insert } = testEntityToObject(te); + return insert; + } + + function expectEqualCreate(result: TestEntity[], expected: TestEntity[]) { + const cleansedResults = result.map(testEntityToCreate); + const cleansedExpected = expected.map(testEntityToCreate); + expect(cleansedResults).toEqual(cleansedExpected); + } + + afterAll(async () => closeDbConnection()); + + beforeEach(() => prepareDb()); + + afterEach(() => dropDatabase()); + + describe('#query', () => { + it('call find and return the result', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({}); + expectEqualEntities(queryResult, TEST_ENTITIES); + }); + + it('should support eq operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { stringType: { eq: 'foo1' } } }); + expectEqualEntities(queryResult, [TEST_ENTITIES[0]]); + }); + + it('should support neq operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { stringType: { neq: 'foo1' } } }); + expectEqualEntities(queryResult, TEST_ENTITIES.slice(1)); + }); + + it('should support gt operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { numberType: { gt: 5 } } }); + expectEqualEntities(queryResult, TEST_ENTITIES.slice(5)); + }); + + it('should support gte operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { numberType: { gte: 5 } } }); + expectEqualEntities(queryResult, TEST_ENTITIES.slice(4)); + }); + + it('should support lt operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { numberType: { lt: 5 } } }); + expectEqualEntities(queryResult, TEST_ENTITIES.slice(0, 4)); + }); + + it('should support lte operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { numberType: { lte: 5 } } }); + expectEqualEntities(queryResult, TEST_ENTITIES.slice(0, 5)); + }); + + it('should support in operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { numberType: { in: [1, 2, 3] } } }); + expectEqualEntities(queryResult, TEST_ENTITIES.slice(0, 3)); + }); + + it('should support notIn operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { numberType: { notIn: [1, 2, 3] } } }); + expectEqualEntities(queryResult, TEST_ENTITIES.slice(4)); + }); + + it('should support is operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { boolType: { is: true } } }); + expectEqualEntities( + queryResult, + TEST_ENTITIES.filter((e) => e.boolType), + ); + }); + + it('should support isNot operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { boolType: { isNot: true } } }); + expectEqualEntities( + queryResult, + TEST_ENTITIES.filter((e) => !e.boolType), + ); + }); + + it('should support like operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { stringType: { like: 'foo%' } } }); + expectEqualEntities(queryResult, TEST_ENTITIES); + }); + + it('should support notLike operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { stringType: { notLike: 'foo%' } } }); + expectEqualEntities(queryResult, []); + }); + + it('should support iLike operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { stringType: { iLike: 'FOO%' } } }); + expectEqualEntities(queryResult, TEST_ENTITIES); + }); + + it('should support notILike operator', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.query({ filter: { stringType: { notILike: 'FOO%' } } }); + expectEqualEntities(queryResult, []); + }); + }); + + describe('#aggregate', () => { + it('call select with the aggregate columns and return the result', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.aggregate( + {}, + { + count: ['id'], + avg: ['numberType'], + sum: ['numberType'], + max: ['id', 'dateType', 'numberType', 'stringType'], + min: ['id', 'dateType', 'numberType', 'stringType'], + }, + ); + return expect(queryResult).toEqual({ + avg: { + numberType: 5.5, + }, + count: { + id: 10, + }, + max: { + dateType: TEST_ENTITIES[9].dateType, + numberType: 10, + stringType: 'foo9', + id: expect.any(ObjectId), + }, + min: { + dateType: TEST_ENTITIES[0].dateType, + numberType: 1, + stringType: 'foo1', + id: expect.any(ObjectId), + }, + sum: { + numberType: 55, + }, + }); + }); + + it('call select with the aggregate columns and return the result with a filter', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.aggregate( + { stringType: { in: ['foo1', 'foo2', 'foo3'] } }, + { + count: ['id'], + avg: ['numberType'], + sum: ['numberType'], + max: ['id', 'dateType', 'numberType', 'stringType'], + min: ['id', 'dateType', 'numberType', 'stringType'], + }, + ); + return expect(queryResult).toEqual({ + avg: { + numberType: 2, + }, + count: { + id: 3, + }, + max: { + dateType: TEST_ENTITIES[2].dateType, + numberType: 3, + stringType: 'foo3', + id: expect.any(ObjectId), + }, + min: { + dateType: TEST_ENTITIES[0].dateType, + numberType: 1, + stringType: 'foo1', + id: expect.any(ObjectId), + }, + sum: { + numberType: 6, + }, + }); + }); + }); + + describe('#count', () => { + it('should return number of elements matching a query', async () => { + const queryService = moduleRef.get(TestEntityService); + const expectedEntities = TEST_ENTITIES.slice(0, 2); + const count = await queryService.count({ stringType: { in: expectedEntities.map((e) => e.stringType) } }); + expect(count).toEqual(2); + }); + }); + + describe('#findById', () => { + it('return the entity if found', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.findById(entity._id); + expectEqualEntities([found!], [entity]); + }); + + it('return undefined if not found', async () => { + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.findById(new ObjectId().toHexString()); + 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 } }, + }); + expectEqualEntities([found!], [entity]); + }); + + 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', () => { + it('return the entity if found', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const found = await queryService.getById(entity._id); + expectEqualEntities([found], [entity]); + }); + + it('return undefined if not found', () => { + const badId = new ObjectId().toHexString(); + 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 } }, + }); + expectEqualEntities([found], [entity]); + }); + + 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); + return expect( + queryService.getById(entity._id, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }), + ).rejects.toThrow(`Unable to find TestEntity with id: ${String(entity._id)}`); + }); + }); + }); + + describe('#createMany', () => { + it('call save on the repo with instances of entities when passed plain objects', async () => { + const queryService = moduleRef.get(TestEntityService); + const created = await queryService.createMany(TEST_ENTITIES.map(testEntityToCreate)); + expectEqualCreate(created, TEST_ENTITIES); + }); + + it('call save on the repo with instances of entities when passed instances', async () => { + const instances = TEST_ENTITIES.map((e) => new TestEntityModel(testEntityToCreate(e))); + const queryService = moduleRef.get(TestEntityService); + const created = await queryService.createMany(instances); + expectEqualCreate(created, TEST_ENTITIES); + }); + + it('should reject if the entities already exist', async () => { + const queryService = moduleRef.get(TestEntityService); + return expect(queryService.createMany(TEST_ENTITIES)).rejects.toThrow( + 'Id cannot be specified when updating or creating', + ); + }); + }); + + describe('#createOne', () => { + it('call save on the repo with an instance of the entity when passed a plain object', async () => { + const entity = testEntityToCreate(TEST_ENTITIES[0]); + const queryService = moduleRef.get(TestEntityService); + const created = await queryService.createOne(entity); + expect(created).toEqual(expect.objectContaining(entity)); + }); + + it('call save on the repo with an instance of the entity when passed an instance', async () => { + const entity = new TestEntityModel(testEntityToCreate(TEST_ENTITIES[0])); + const queryService = moduleRef.get(TestEntityService); + const created = await queryService.createOne(entity); + expect(created).toEqual(expect.objectContaining(entity)); + // expectEqualCreate([created], [TEST_ENTITIES[0]]); + }); + + it('should reject if the entity contains an id', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect(queryService.createOne({ ...entity })).rejects.toThrow( + 'Id cannot be specified when updating or creating', + ); + }); + }); + + describe('#deleteMany', () => { + it('delete all records that match the query', async () => { + const queryService = moduleRef.get(TestEntityService); + const entities = TEST_ENTITIES.slice(0, 5); + const { deletedCount } = await queryService.deleteMany({ + stringType: { in: entities.map((e) => e.stringType) }, + }); + expect(deletedCount).toEqual(5); + const allCount = await queryService.count({}); + expect(allCount).toBe(5); + }); + }); + + describe('#deleteOne', () => { + it('remove the entity', async () => { + const queryService = moduleRef.get(TestEntityService); + const deleted = await queryService.deleteOne(TEST_ENTITIES[0]._id); + expectEqualEntities([deleted], [TEST_ENTITIES[0]]); + }); + + it('call fail if the entity is not found', async () => { + const badId = new ObjectId().toHexString(); + 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 } }, + }); + expectEqualEntities([deleted], [TEST_ENTITIES[0]]); + }); + + 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: ${String(entity._id)}`); + }); + }); + }); + + describe('#updateMany', () => { + it('update all entities in the filter', async () => { + const queryService = moduleRef.get(TestEntityService); + const filter = { + stringType: { in: TEST_ENTITIES.slice(0, 5).map((e) => e.stringType) }, + }; + await queryService.updateMany({ stringType: 'updated' }, filter); + const entities = await queryService.query({ filter: { stringType: { eq: 'updated' } } }); + expect(entities).toHaveLength(5); + }); + + it('should reject if the update contains the ID', () => { + const queryService = moduleRef.get(TestEntityService); + return expect(queryService.updateMany({ id: new ObjectId().toHexString() }, {})).rejects.toThrow( + 'Id cannot be specified when updating', + ); + }); + }); + + describe('#updateOne', () => { + it('update the entity', async () => { + const queryService = moduleRef.get(TestEntityService); + const entity = TEST_ENTITIES[0]; + const update = { stringType: 'updated' }; + const updated = await queryService.updateOne(entity._id, update); + expect(updated).toEqual( + expect.objectContaining({ + _id: entity._id, + ...update, + }), + ); + }); + + it('should reject if the update contains the ID', async () => { + const queryService = moduleRef.get(TestEntityService); + return expect(queryService.updateOne(TEST_ENTITIES[0]._id, { id: new ObjectId().toHexString() })).rejects.toThrow( + 'Id cannot be specified when updating', + ); + }); + + it('call fail if the entity is not found', async () => { + const badId = new ObjectId().toHexString(); + const queryService = moduleRef.get(TestEntityService); + return expect(queryService.updateOne(badId, { stringType: 'updated' })).rejects.toThrow( + `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 update = { stringType: 'updated' }; + const updated = await queryService.updateOne(entity._id, update, { + filter: { stringType: { eq: entity.stringType } }, + }); + expect(updated).toEqual(expect.objectContaining({ _id: entity._id, ...update })); + }); + + 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: ${String(entity._id)}`); + }); + }); + }); + + describe('#findRelation', () => { + describe('with one entity', () => { + it('call select and return the result', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.findRelation(TestReference, 'testReference', entity); + + expect(queryResult?.toObject()).toEqual(TEST_REFERENCES[0]); + }); + + it('apply the filter option', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const queryResult1 = await queryService.findRelation(TestReference, 'testReference', entity, { + filter: { referenceName: { eq: TEST_REFERENCES[0].referenceName } }, + }); + expect(queryResult1?.toObject()).toEqual(TEST_REFERENCES[0]); + + const queryResult2 = await queryService.findRelation(TestReference, 'testReference', entity, { + filter: { referenceName: { eq: TEST_REFERENCES[1].referenceName } }, + }); + expect(queryResult2?.toObject()).toBeUndefined(); + }); + + it('should return undefined select if no results are found.', async () => { + const entity = TEST_ENTITIES[0]; + await TestEntityModel.updateOne({ _id: entity._id }, { $set: { testReference: undefined } }); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.findRelation(TestReference, 'testReference', entity); + expect(queryResult?.toObject()).toBeUndefined(); + }); + + it('throw an error if a relation with that name is not found.', async () => { + const queryService = moduleRef.get(TestEntityService); + const entity = TEST_ENTITIES[0]; + return expect(queryService.findRelation(TestReference, 'badReference', entity)).rejects.toThrow( + 'Unable to find reference badReference on TestEntity', + ); + }); + + describe('virtual reference', () => { + it('call select and return the result', async () => { + const entity = TEST_REFERENCES[0]; + const queryService = moduleRef.get(TestReferenceService); + const queryResult = await queryService.findRelation(TestEntity, 'virtualTestEntity', entity); + + expect(queryResult?.toObject()).toEqual(TEST_ENTITIES[0]); + }); + + it('apply the filter option', async () => { + const entity = TEST_REFERENCES[0]; + const queryService = moduleRef.get(TestReferenceService); + const queryResult1 = await queryService.findRelation(TestEntity, 'virtualTestEntity', entity, { + filter: { stringType: { eq: TEST_ENTITIES[0].stringType } }, + }); + expect(queryResult1?.toObject()).toEqual(TEST_ENTITIES[0]); + + const queryResult2 = await queryService.findRelation(TestEntity, 'virtualTestEntity', entity, { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }); + expect(queryResult2?.toObject()).toBeUndefined(); + }); + + it('should return undefined select if no results are found.', async () => { + const entity = TEST_REFERENCES[0]; + await TestReferenceModel.updateOne({ _id: entity._id }, { $set: { testEntity: undefined } }); + const queryService = moduleRef.get(TestReferenceService); + const queryResult = await queryService.findRelation(TestEntity, 'virtualTestEntity', entity); + expect(queryResult?.toObject()).toBeUndefined(); + }); + + it('throw an error if a relation with that name is not found.', async () => { + const entity = TEST_REFERENCES[0]; + const queryService = moduleRef.get(TestReferenceService); + return expect(queryService.findRelation(TestEntity, 'badReference', entity)).rejects.toThrow( + 'Unable to find reference badReference on TestReference', + ); + }); + }); + }); + + describe('with multiple entities', () => { + it('call select and return the result', async () => { + const entities = TEST_ENTITIES.slice(0, 3); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.findRelation(TestReference, 'testReference', entities); + + expect(queryResult).toEqual( + new Map([ + [entities[0], expect.objectContaining(TEST_REFERENCES[0])], + [entities[1], expect.objectContaining(TEST_REFERENCES[3])], + [entities[2], expect.objectContaining(TEST_REFERENCES[6])], + ]), + ); + }); + + it('should apply the filter option', async () => { + const entities = TEST_ENTITIES.slice(0, 3); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.findRelation(TestReference, 'testReference', entities, { + filter: { + id: { in: [TEST_REFERENCES[0]._id, TEST_REFERENCES[6]._id] }, + }, + }); + expect(queryResult).toEqual( + new Map([ + [entities[0], expect.objectContaining(TEST_REFERENCES[0])], + [entities[1], undefined], + [entities[2], expect.objectContaining(TEST_REFERENCES[6])], + ]), + ); + }); + + it('should return undefined select if no results are found.', async () => { + const entities: TestEntity[] = [TEST_ENTITIES[0], { _id: new ObjectId() } as TestEntity]; + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.findRelation(TestReference, 'testReference', entities); + + expect(queryResult).toEqual( + new Map([ + [entities[0], expect.objectContaining(TEST_REFERENCES[0])], + [entities[1], undefined], + ]), + ); + }); + }); + }); + + describe('#queryRelations', () => { + const referenceContaining = (refs: TestReference[]): unknown[] => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return refs.map((r) => expect.objectContaining(r)); + }; + + describe('with one entity', () => { + it('call select and return the result', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations(TestReference, 'testReferences', TEST_ENTITIES[0], { + filter: { referenceName: { isNot: null } }, + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return expect(queryResult.map((r) => r.toObject())).toEqual(referenceContaining(TEST_REFERENCES.slice(0, 3))); + }); + + it('should apply a filter', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations(TestReference, 'testReferences', TEST_ENTITIES[0], { + filter: { referenceName: { eq: TEST_REFERENCES[1].referenceName } }, + }); + expect(queryResult.map((r) => r.toObject())).toEqual(referenceContaining([TEST_REFERENCES[1]])); + }); + + it('should apply paging', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations(TestReference, 'testReferences', TEST_ENTITIES[0], { + paging: { limit: 2, offset: 1 }, + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + expect(queryResult.map((r) => r.toObject())).toEqual(referenceContaining(TEST_REFERENCES.slice(1, 3))); + }); + }); + + describe('with virtual entity', () => { + it('call select and return the result', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations( + TestReference, + 'virtualTestReferences', + TEST_ENTITIES[0], + { + filter: { referenceName: { isNot: null } }, + }, + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return expect(queryResult.map((r) => r.toObject())).toEqual( + expect.arrayContaining(referenceContaining(TEST_REFERENCES.slice(0, 3))), + ); + }); + + it('should apply a filter', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations( + TestReference, + 'virtualTestReferences', + TEST_ENTITIES[0], + { + filter: { referenceName: { eq: TEST_REFERENCES[1].referenceName } }, + }, + ); + expect(queryResult.map((r) => r.toObject())).toEqual(referenceContaining([TEST_REFERENCES[1]])); + }); + + it('should apply paging', async () => { + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations( + TestReference, + 'virtualTestReferences', + TEST_ENTITIES[0], + { + paging: { limit: 2, offset: 1 }, + sorting: [{ field: 'referenceName', direction: SortDirection.ASC }], + }, + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + expect(queryResult.map((r) => r.toObject())).toEqual(referenceContaining(TEST_REFERENCES.slice(1, 3))); + }); + }); + + describe('with multiple entities', () => { + it('call return a map of results for each entity', async () => { + const entities = TEST_ENTITIES.slice(0, 3); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations(TestReference, 'testReferences', entities, { + filter: { referenceName: { isNot: null } }, + }); + expect(queryResult.size).toBe(3); + expect(queryResult.get(entities[0])?.map((r) => r.toObject())).toEqual( + referenceContaining(TEST_REFERENCES.slice(0, 3)), + ); + expect(queryResult.get(entities[1])?.map((r) => r.toObject())).toEqual( + referenceContaining(TEST_REFERENCES.slice(3, 6)), + ); + expect(queryResult.get(entities[2])?.map((r) => r.toObject())).toEqual( + referenceContaining(TEST_REFERENCES.slice(6, 9)), + ); + }); + + it('should apply a filter per entity', async () => { + const entities = TEST_ENTITIES.slice(0, 3); + const references = [TEST_REFERENCES[1], TEST_REFERENCES[4], TEST_REFERENCES[7]]; + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations(TestReference, 'testReferences', entities, { + filter: { referenceName: { in: references.map((r) => r.referenceName) } }, + }); + expect(queryResult.size).toBe(3); + expect(queryResult.get(entities[0])?.map((r) => r.toObject())).toEqual(referenceContaining([references[0]])); + expect(queryResult.get(entities[1])?.map((r) => r.toObject())).toEqual(referenceContaining([references[1]])); + expect(queryResult.get(entities[2])?.map((r) => r.toObject())).toEqual(referenceContaining([references[2]])); + }); + + it('should apply paging per entity', async () => { + const entities = TEST_ENTITIES.slice(0, 3); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations(TestReference, 'testReferences', entities, { + paging: { limit: 2, offset: 1 }, + }); + expect(queryResult.size).toBe(3); + expect(queryResult.get(entities[0])?.map((r) => r.toObject())).toEqual( + referenceContaining(TEST_REFERENCES.slice(1, 3)), + ); + expect(queryResult.get(entities[1])?.map((r) => r.toObject())).toEqual( + referenceContaining(TEST_REFERENCES.slice(4, 6)), + ); + expect(queryResult.get(entities[2])?.map((r) => r.toObject())).toEqual( + referenceContaining(TEST_REFERENCES.slice(7, 9)), + ); + }); + + it('should return an empty array if no results are found.', async () => { + const entities: TestEntity[] = [TEST_ENTITIES[0], { id: new ObjectId() } as TestEntity]; + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.queryRelations(TestReference, 'testReferences', entities, { + filter: { referenceName: { isNot: null } }, + }); + expect(queryResult.size).toBe(2); + expect(queryResult.get(entities[0])?.map((r) => r.toObject())).toEqual( + referenceContaining(TEST_REFERENCES.slice(0, 3)), + ); + expect(queryResult.get(entities[1])).toEqual([]); + }); + }); + }); + + describe('#aggregateRelations', () => { + describe('with one entity', () => { + it('call select and return the result', async () => { + const queryService = moduleRef.get(TestEntityService); + const aggResult = await queryService.aggregateRelations( + TestReference, + 'testReferences', + TEST_ENTITIES[0], + { referenceName: { isNot: null } }, + { count: ['id'] }, + ); + return expect(aggResult).toEqual({ + count: { + id: 3, + }, + }); + }); + }); + + describe('with virtual relation', () => { + it('call select and return the result', async () => { + const queryService = moduleRef.get(TestEntityService); + const aggResult = await queryService.aggregateRelations( + TestReference, + 'virtualTestReferences', + TEST_ENTITIES[0], + { referenceName: { isNot: null } }, + { count: ['id'] }, + ); + return expect(aggResult).toEqual({ + count: { + id: 3, + }, + }); + }); + }); + + describe('with multiple entities', () => { + it('call select and return the result', async () => { + const entities = TEST_ENTITIES.slice(0, 3); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.aggregateRelations( + TestReference, + 'testReferences', + entities, + { referenceName: { isNot: null } }, + { + count: ['id', 'referenceName', 'testEntity'], + min: ['id', 'referenceName', 'testEntity'], + max: ['id', 'referenceName', 'testEntity'], + }, + ); + + expect(queryResult.size).toBe(3); + expect(queryResult).toEqual( + new Map([ + [ + entities[0], + { + count: { + referenceName: 3, + testEntity: 3, + id: 3, + }, + max: { + referenceName: TEST_REFERENCES[2].referenceName, + testEntity: entities[0]._id, + id: expect.any(ObjectId), + }, + min: { + referenceName: TEST_REFERENCES[0].referenceName, + testEntity: entities[0]._id, + id: expect.any(ObjectId), + }, + }, + ], + [ + entities[1], + { + count: { + referenceName: 3, + testEntity: 3, + id: 3, + }, + max: { + referenceName: TEST_REFERENCES[5].referenceName, + testEntity: entities[1]._id, + id: expect.any(ObjectId), + }, + min: { + referenceName: TEST_REFERENCES[3].referenceName, + testEntity: entities[1]._id, + id: expect.any(ObjectId), + }, + }, + ], + [ + entities[2], + { + count: { + referenceName: 3, + testEntity: 3, + id: 3, + }, + max: { + referenceName: TEST_REFERENCES[8].referenceName, + testEntity: entities[2]._id, + id: expect.any(ObjectId), + }, + min: { + referenceName: TEST_REFERENCES[6].referenceName, + testEntity: entities[2]._id, + id: expect.any(ObjectId), + }, + }, + ], + ]), + ); + }); + + it('should return an empty array if no results are found.', async () => { + const entities: TestEntity[] = [TEST_ENTITIES[0], { _id: new ObjectId() } as TestEntity]; + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.aggregateRelations( + TestReference, + 'testReferences', + entities, + { referenceName: { isNot: null } }, + { + count: ['id', 'referenceName', 'testEntity'], + min: ['id', 'referenceName', 'testEntity'], + max: ['id', 'referenceName', 'testEntity'], + }, + ); + + expect(queryResult).toEqual( + new Map([ + [ + entities[0], + { + count: { + referenceName: 3, + testEntity: 3, + id: 3, + }, + max: { + referenceName: TEST_REFERENCES[2].referenceName, + testEntity: entities[0]._id, + id: expect.any(ObjectId), + }, + min: { + referenceName: TEST_REFERENCES[0].referenceName, + testEntity: entities[0]._id, + id: expect.any(ObjectId), + }, + }, + ], + [entities[1], {}], + ]), + ); + }); + }); + }); + + describe('#countRelations', () => { + describe('with one entity', () => { + it('count the references', async () => { + const queryService = moduleRef.get(TestEntityService); + const entity = TEST_ENTITIES[0]; + const countResult = await queryService.countRelations(TestReference, 'testReferences', entity, { + referenceName: { in: [TEST_REFERENCES[1].referenceName, TEST_REFERENCES[2].referenceName] }, + }); + return expect(countResult).toEqual(2); + }); + }); + + describe('with virtual entity', () => { + it('count references', async () => { + const queryService = moduleRef.get(TestEntityService); + const entity = TEST_ENTITIES[0]; + const countResult = await queryService.countRelations(TestReference, 'virtualTestReferences', entity, {}); + return expect(countResult).toEqual(3); + }); + it('count and return the result', async () => { + const queryService = moduleRef.get(TestEntityService); + const entity = TEST_ENTITIES[0]; + const countResult = await queryService.countRelations(TestReference, 'virtualTestReferences', entity, { + referenceName: { in: [TEST_REFERENCES[1].referenceName, TEST_REFERENCES[2].referenceName] }, + }); + return expect(countResult).toEqual(2); + }); + }); + + describe('with multiple entities', () => { + it('call count and return the result', async () => { + const entities = TEST_ENTITIES.slice(0, 3); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.countRelations(TestReference, 'testReferences', entities, { + referenceName: { + in: [ + TEST_REFERENCES[1].referenceName, + TEST_REFERENCES[2].referenceName, + TEST_REFERENCES[4].referenceName, + TEST_REFERENCES[5].referenceName, + TEST_REFERENCES[7].referenceName, + TEST_REFERENCES[8].referenceName, + ], + }, + }); + + expect(queryResult).toEqual( + new Map([ + [entities[0], 2], + [entities[1], 2], + [entities[2], 2], + ]), + ); + }); + }); + }); + + describe('#addRelations', () => { + it('call select and return the result', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.addRelations( + 'testReferences', + entity._id, + TEST_REFERENCES.slice(3, 6).map((r) => r._id.toString()), + ); + expect(queryResult).toEqual( + expect.objectContaining({ + _id: entity._id, + testReferences: expect.arrayContaining(TEST_REFERENCES.slice(0, 6).map((r) => r._id)), + }), + ); + + const relations = await queryService.queryRelations(TestReference, 'testReferences', entity, {}); + expect(relations).toHaveLength(6); + }); + + describe('with virtual reference', () => { + it('should return a rejected promise', async () => { + const entity = TEST_ENTITIES[0]; + const queryService = moduleRef.get(TestEntityService); + return expect( + queryService.addRelations( + 'virtualTestReferences', + entity._id, + TEST_REFERENCES.slice(3, 6).map((r) => r._id.toString()), + ), + ).rejects.toThrow('Mutations on virtual relation virtualTestReferences not supported'); + }); + }); + + 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( + 'testReferences', + entity._id, + TEST_REFERENCES.slice(3, 6).map((r) => r._id.toString()), + { + filter: { stringType: { eq: TEST_ENTITIES[1].stringType } }, + }, + ), + ).rejects.toThrow(`Unable to find TestEntity with id: ${String(entity._id)}`); + }); + + 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( + 'testReferences', + entity._id, + TEST_REFERENCES.slice(3, 6).map((r) => r._id.toString()), + { + relationFilter: { referenceName: { like: '%-one' } }, + }, + ), + ).rejects.toThrow('Unable to find all testReferences to add to TestEntity'); + }); + }); + }); +}); diff --git a/packages/query-typegoose/package.json b/packages/query-mongoose/package.json similarity index 65% rename from packages/query-typegoose/package.json rename to packages/query-mongoose/package.json index 7ed5d076d..8b5719aaf 100644 --- a/packages/query-typegoose/package.json +++ b/packages/query-mongoose/package.json @@ -1,7 +1,7 @@ { - "name": "@nestjs-query/query-typegoose", + "name": "@nestjs-query/query-mongoose", "version": "0.19.1", - "description": "Typeorm adapter for @nestjs-query/core", + "description": "Mongoose adapter for @nestjs-query/core", "author": "doug-martin ", "homepage": "https://github.com/doug-martin/nestjs-query#readme", "license": "MIT", @@ -18,31 +18,31 @@ "access": "public" }, "dependencies": { - "@nestjs-query/core": "0.19.0", + "@nestjs-query/core": "0.20.0", "lodash.escaperegexp": "^4.1.2", "lodash.merge": "^4.6.2" }, "peerDependencies": { "@nestjs/common": "^7.0.0", - "@typegoose/typegoose": "^7.3.3", + "@nestjs/mongoose": "^7.0.0", "class-transformer": "^0.2.3 || ^0.3.0", "mongoose": "^5.10.0", - "nestjs-typegoose": "^7.1.32" + "mongodb": "^3.6.0" }, "devDependencies": { - "@nestjs/common": "7.4.2", - "@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", + "@nestjs/common": "7.4.4", + "@nestjs/mongoose": "7.0.2", + "@nestjs/testing": "7.4.4", + "@types/lodash.escaperegexp": "4.1.6", + "@types/lodash.merge": "4.6.6", + "@types/mongodb": "3.5.27", + "@types/mongoose": "5.7.36", "class-transformer": "0.3.1", - "mongodb-memory-server": "^6.6.5", - "mongoose": "^5.10.0", - "nestjs-typegoose": "^7.1.32", + "mongodb-memory-server": "6.7.6", + "mongoose": "5.10.6", + "mongodb": "3.6.2", "ts-mockito": "2.6.1", - "typescript": "4.0.2" + "typescript": "4.0.3" }, "repository": { "type": "git", diff --git a/packages/query-mongoose/src/index.ts b/packages/query-mongoose/src/index.ts new file mode 100644 index 000000000..5cb5c632e --- /dev/null +++ b/packages/query-mongoose/src/index.ts @@ -0,0 +1,2 @@ +export { MongooseQueryService, MongooseQueryServiceOpts } from './services'; +export { NestjsQueryMongooseModule } from './module'; diff --git a/packages/query-mongoose/src/module.ts b/packages/query-mongoose/src/module.ts new file mode 100644 index 000000000..c382eba03 --- /dev/null +++ b/packages/query-mongoose/src/module.ts @@ -0,0 +1,16 @@ +import { DynamicModule } from '@nestjs/common'; +import { ModelDefinition, MongooseModule } from '@nestjs/mongoose'; +import { createMongooseQueryServiceProviders } from './providers'; + +export class NestjsQueryMongooseModule { + static forFeature(models: ModelDefinition[], connectionName?: string): DynamicModule { + const queryServiceProviders = createMongooseQueryServiceProviders(models); + const mongooseModule = MongooseModule.forFeature(models, connectionName); + return { + imports: [mongooseModule], + module: NestjsQueryMongooseModule, + providers: [...queryServiceProviders], + exports: [...queryServiceProviders, mongooseModule], + }; + } +} diff --git a/packages/query-mongoose/src/mongoose-types.helper.ts b/packages/query-mongoose/src/mongoose-types.helper.ts new file mode 100644 index 000000000..0cece2603 --- /dev/null +++ b/packages/query-mongoose/src/mongoose-types.helper.ts @@ -0,0 +1,58 @@ +import { SchemaType } from 'mongoose'; + +export type ReferenceOptions = { + type: SchemaType; + ref: string; +}; + +export function isReferenceOptions(options: unknown): options is ReferenceOptions { + return options && typeof options === 'object' && 'type' in options && 'ref' in options; +} + +export type SchemaTypeWithReferenceOptions = { + options: ReferenceOptions; +}; + +export function isSchemaTypeWithReferenceOptions(type: unknown): type is SchemaTypeWithReferenceOptions { + if (type && typeof type === 'object' && 'options' in type) { + const { options } = type as { options: unknown }; + return isReferenceOptions(options); + } + return false; +} + +export type EmbeddedSchemaTypeOptions = { + $embeddedSchemaType: { options: ReferenceOptions }; +}; + +export function isEmbeddedSchemaTypeOptions(options: unknown): options is EmbeddedSchemaTypeOptions { + if (options && typeof options === 'object' && '$embeddedSchemaType' in options) { + const { $embeddedSchemaType } = options as { $embeddedSchemaType: { options: unknown } }; + return isReferenceOptions($embeddedSchemaType.options); + } + return false; +} + +export type VirtualReferenceOptions = { + ref: string; + localField: string; + foreignField: string; +}; + +export function isVirtualReferenceOptions(options: unknown): options is VirtualReferenceOptions { + return ( + options && typeof options === 'object' && 'ref' in options && 'localField' in options && 'foreignField' in options + ); +} + +export type VirtualTypeWithOptions = { + options: VirtualReferenceOptions; +}; + +export function isVirtualTypeWithReferenceOptions(virtualType: unknown): virtualType is VirtualTypeWithOptions { + if (virtualType && typeof virtualType === 'object' && 'options' in virtualType) { + const { options } = virtualType as { options: unknown }; + return isVirtualReferenceOptions(options); + } + return false; +} diff --git a/packages/query-mongoose/src/providers.ts b/packages/query-mongoose/src/providers.ts new file mode 100644 index 000000000..825597607 --- /dev/null +++ b/packages/query-mongoose/src/providers.ts @@ -0,0 +1,18 @@ +import { getQueryServiceToken } from '@nestjs-query/core'; +import { FactoryProvider } from '@nestjs/common'; +import { ModelDefinition } from '@nestjs/mongoose'; +import { Model, Document } from 'mongoose'; +import { MongooseQueryService } from './services/mongoose-query.service'; + +function createMongooseQueryServiceProvider(model: ModelDefinition): FactoryProvider { + return { + provide: getQueryServiceToken({ name: model.name }), + useFactory(modelClass: Model) { + return new MongooseQueryService(modelClass); + }, + inject: [`${model.name}Model`], + }; +} + +export const createMongooseQueryServiceProviders = (models: ModelDefinition[]): FactoryProvider[] => + models.map((model) => createMongooseQueryServiceProvider(model)); diff --git a/packages/query-mongoose/src/query/aggregate.builder.ts b/packages/query-mongoose/src/query/aggregate.builder.ts new file mode 100644 index 000000000..2944a3393 --- /dev/null +++ b/packages/query-mongoose/src/query/aggregate.builder.ts @@ -0,0 +1,85 @@ +import { AggregateQuery, AggregateResponse } from '@nestjs-query/core'; +import { Document } from 'mongoose'; +import { getSchemaKey } from './helpers'; + +enum AggregateFuncs { + AVG = 'avg', + SUM = 'sum', + COUNT = 'count', + MAX = 'max', + MIN = 'min', +} + +export type MongooseAggregate = { + [k: string]: { + [o: string]: unknown; + }; +}; + +const AGG_REGEXP = /(avg|sum|count|max|min)_(.*)/; + +/** + * @internal + * Builds a WHERE clause from a Filter. + */ +export class AggregateBuilder { + // eslint-disable-next-line @typescript-eslint/no-shadow + static convertToAggregateResponse({ _id, ...response }: Record): AggregateResponse { + return Object.keys(response).reduce((agg, resultField: string) => { + const matchResult = AGG_REGEXP.exec(resultField); + if (!matchResult) { + throw new Error('Unknown aggregate column encountered.'); + } + const [matchedFunc, matchedFieldName] = matchResult.slice(1); + const aggFunc = matchedFunc.toLowerCase() as keyof AggregateResponse; + const fieldName = matchedFieldName as keyof Entity; + const aggResult = agg[aggFunc] || {}; + return { + ...agg, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + [aggFunc]: { ...aggResult, [fieldName]: response[resultField] }, + }; + }, {} as AggregateResponse); + } + + constructor() {} + + /** + * Builds a aggregate SELECT clause from a aggregate. + * @param aggregate - the aggregates to select. + */ + build(aggregate: AggregateQuery): MongooseAggregate { + return { + ...this.createAggSelect(AggregateFuncs.COUNT, aggregate.count), + ...this.createAggSelect(AggregateFuncs.SUM, aggregate.sum), + ...this.createAggSelect(AggregateFuncs.AVG, aggregate.avg), + ...this.createAggSelect(AggregateFuncs.MAX, aggregate.max), + ...this.createAggSelect(AggregateFuncs.MIN, aggregate.min), + }; + } + + private createAggSelect(func: AggregateFuncs, fields?: (keyof Entity)[]): MongooseAggregate { + if (!fields) { + return {}; + } + return fields.reduce((agg: MongooseAggregate, field) => { + const aggAlias = `${func}_${field as string}`; + const fieldAlias = `$${getSchemaKey(String(field))}`; + if (func === 'count') { + return { + ...agg, + [aggAlias]: { + $sum: { + $cond: { + if: { $ne: [fieldAlias, null] }, + then: 1, + else: 0, + }, + }, + }, + }; + } + return { ...agg, [aggAlias]: { [`$${func}`]: fieldAlias } }; + }, {}); + } +} diff --git a/packages/query-mongoose/src/query/comparison.builder.ts b/packages/query-mongoose/src/query/comparison.builder.ts new file mode 100644 index 000000000..bec40a3e8 --- /dev/null +++ b/packages/query-mongoose/src/query/comparison.builder.ts @@ -0,0 +1,107 @@ +import { CommonFieldComparisonBetweenType, FilterComparisonOperators } from '@nestjs-query/core'; +import escapeRegExp from 'lodash.escaperegexp'; +import { FilterQuery, Document } from 'mongoose'; +import { QuerySelector } from 'mongodb'; +import { getSchemaKey } from './helpers'; + +/** + * @internal + */ +export type EntityComparisonField = + | Entity[F] + | Entity[F][] + | CommonFieldComparisonBetweenType + | true + | false + | null; + +/** + * @internal + * Builder to create SQL Comparisons. (=, !=, \>, etc...) + */ +export class ComparisonBuilder { + static DEFAULT_COMPARISON_MAP: Record = { + eq: '$eq', + neq: '$ne', + gt: '$gt', + gte: '$gte', + lt: '$lt', + lte: '$lte', + in: '$in', + notin: '$nin', + is: '$eq', + isnot: '$ne', + }; + + constructor(readonly comparisonMap: Record = ComparisonBuilder.DEFAULT_COMPARISON_MAP) {} + + /** + * Creates a valid SQL fragment with parameters. + * + * @param field - the property in Entity to create the comparison for. + * @param cmp - the FilterComparisonOperator (eq, neq, gt, etc...) + * @param val - the value to compare to. + */ + build( + field: F, + cmp: FilterComparisonOperators, + val: EntityComparisonField, + ): FilterQuery { + const schemaKey = getSchemaKey(`${String(field)}`); + const normalizedCmp = (cmp as string).toLowerCase(); + let querySelector: QuerySelector | undefined; + if (this.comparisonMap[normalizedCmp]) { + // comparison operator (e.b. =, !=, >, <) + querySelector = { [this.comparisonMap[normalizedCmp]]: val }; + } + if (normalizedCmp.includes('like')) { + querySelector = (this.likeComparison(normalizedCmp, val) as unknown) as QuerySelector; + } + if (normalizedCmp === 'between') { + // between comparison (field BETWEEN x AND y) + querySelector = this.betweenComparison(val); + } + if (normalizedCmp === 'notbetween') { + // notBetween comparison (field NOT BETWEEN x AND y) + querySelector = this.notBetweenComparison(val); + } + if (!querySelector) { + throw new Error(`unknown operator ${JSON.stringify(cmp)}`); + } + return { [schemaKey]: querySelector } as FilterQuery; + } + + private betweenComparison(val: EntityComparisonField): QuerySelector { + if (this.isBetweenVal(val)) { + return { $gte: val.lower, $lte: val.upper }; + } + throw new Error(`Invalid value for between expected {lower: val, upper: val} got ${JSON.stringify(val)}`); + } + + private notBetweenComparison( + val: EntityComparisonField, + ): QuerySelector { + if (this.isBetweenVal(val)) { + return { $lt: val.lower, $gt: val.upper }; + } + throw new Error(`Invalid value for not between expected {lower: val, upper: val} got ${JSON.stringify(val)}`); + } + + private isBetweenVal( + val: EntityComparisonField, + ): val is CommonFieldComparisonBetweenType { + return val !== null && typeof val === 'object' && 'lower' in val && 'upper' in val; + } + + private likeComparison( + cmp: string, + val: EntityComparisonField, + ): QuerySelector { + const regExpStr = escapeRegExp(`${String(val)}`).replace(/%/g, '.*'); + const regExp = new RegExp(regExpStr, cmp.includes('ilike') ? 'i' : undefined); + if (cmp.startsWith('not')) { + return { $not: { $regex: regExp } }; + } + return { $regex: regExp }; + } +} diff --git a/packages/query-mongoose/src/query/filter-query.builder.ts b/packages/query-mongoose/src/query/filter-query.builder.ts new file mode 100644 index 000000000..17b2ab793 --- /dev/null +++ b/packages/query-mongoose/src/query/filter-query.builder.ts @@ -0,0 +1,96 @@ +import { AggregateQuery, Filter, Paging, Query, SortDirection, SortField } from '@nestjs-query/core'; +import { FilterQuery, Document } from 'mongoose'; +import { AggregateBuilder, MongooseAggregate } from './aggregate.builder'; +import { getSchemaKey } from './helpers'; +import { WhereBuilder } from './where.builder'; + +type MongooseSort = Record; + +type MongooseQuery = { + filterQuery: FilterQuery; + paging?: Paging; + sorting?: MongooseSort[]; +}; + +type MongooseAggregateQuery = { + filterQuery: FilterQuery; + aggregate: MongooseAggregate; +}; +/** + * @internal + * + * Class that will convert a Query into a `typeorm` Query Builder. + */ +export class FilterQueryBuilder { + constructor( + readonly whereBuilder: WhereBuilder = new WhereBuilder(), + readonly aggregateBuilder: AggregateBuilder = new AggregateBuilder(), + ) {} + + buildQuery(query: Query): MongooseQuery { + return { + filterQuery: this.buildFilterQuery(query.filter), + paging: query.paging, + sorting: this.buildSorting(query.sorting), + }; + } + + buildIdQuery(id: unknown | unknown[], query: Query): MongooseQuery { + return { + filterQuery: this.buildIdFilterQuery(id, query.filter), + paging: query.paging, + sorting: this.buildSorting(query.sorting), + }; + } + + buildAggregateQuery(aggregate: AggregateQuery, filter?: Filter): MongooseAggregateQuery { + return { + filterQuery: this.buildFilterQuery(filter), + aggregate: this.aggregateBuilder.build(aggregate), + }; + } + + buildIdAggregateQuery( + id: unknown | unknown[], + filter: Filter, + aggregate: AggregateQuery, + ): MongooseAggregateQuery { + return { + filterQuery: this.buildIdFilterQuery(id, filter), + aggregate: this.aggregateBuilder.build(aggregate), + }; + } + + buildIdFilterQuery(id: unknown | unknown[], filter?: Filter): FilterQuery { + return { + ...this.buildFilterQuery(filter), + _id: Array.isArray(id) ? { $in: id } : id, + }; + } + + /** + * Applies the filter from a Query to a `typeorm` QueryBuilder. + * + * @param qb - the `typeorm` QueryBuilder. + * @param filter - the filter. + * @param alias - optional alias to use to qualify an identifier + */ + buildFilterQuery(filter?: Filter): FilterQuery { + if (!filter) { + return {}; + } + return this.whereBuilder.build(filter); + } + + /** + * Applies the ORDER BY clause to a `typeorm` QueryBuilder. + * @param qb - the `typeorm` QueryBuilder. + * @param sorts - an array of SortFields to create the ORDER BY clause. + * @param alias - optional alias to use to qualify an identifier + */ + buildSorting(sorts?: SortField[], alias?: string): MongooseSort[] { + return (sorts || []).map((sort) => ({ + [getSchemaKey(sort.field.toString())]: sort.direction === SortDirection.ASC ? 'asc' : 'desc', + })); + } +} diff --git a/packages/query-mongoose/src/query/helpers.ts b/packages/query-mongoose/src/query/helpers.ts new file mode 100644 index 000000000..9256cf5ba --- /dev/null +++ b/packages/query-mongoose/src/query/helpers.ts @@ -0,0 +1,3 @@ +export function getSchemaKey(key: string): string { + return key === 'id' ? '_id' : key; +} diff --git a/packages/query-mongoose/src/query/index.ts b/packages/query-mongoose/src/query/index.ts new file mode 100644 index 000000000..8c3630ebc --- /dev/null +++ b/packages/query-mongoose/src/query/index.ts @@ -0,0 +1,4 @@ +export * from './filter-query.builder'; +export * from './where.builder'; +export * from './comparison.builder'; +export * from './aggregate.builder'; diff --git a/packages/query-mongoose/src/query/where.builder.ts b/packages/query-mongoose/src/query/where.builder.ts new file mode 100644 index 000000000..81a84ef3c --- /dev/null +++ b/packages/query-mongoose/src/query/where.builder.ts @@ -0,0 +1,80 @@ +import { Filter, FilterComparisons, FilterFieldComparison } from '@nestjs-query/core'; +import { FilterQuery, Document } from 'mongoose'; +import { EntityComparisonField, ComparisonBuilder } from './comparison.builder'; + +/** + * @internal + * Builds a WHERE clause from a Filter. + */ +export class WhereBuilder { + constructor(readonly comparisonBuilder: ComparisonBuilder = new ComparisonBuilder()) {} + + /** + * Builds a WHERE clause from a Filter. + * @param filter - the filter to build the WHERE clause from. + * @param associations - map of associations that are included in the query. + */ + build(filter: Filter, alias?: string): FilterQuery { + const { and, or } = filter; + let ands: FilterQuery[] = []; + let ors: FilterQuery[] = []; + let filterQuery: FilterQuery = {}; + if (and && and.length) { + ands = and.map((f) => this.build(f)); + } + if (or && or.length) { + ors = or.map((f) => this.build(f)); + } + const filterAnds = this.filterFields(filter); + if (filterAnds) { + ands = [...ands, filterAnds]; + } + if (ands.length) { + filterQuery = { ...filterQuery, $and: ands } as FilterQuery; + } + if (ors.length) { + filterQuery = { ...filterQuery, $or: ors } as FilterQuery; + } + return filterQuery; + } + + /** + * Creates field comparisons from a filter. This method will ignore and/or properties. + * @param filter - the filter with fields to create comparisons for. + */ + private filterFields(filter: Filter): FilterQuery | undefined { + const ands = Object.keys(filter) + .filter((f) => f !== 'and' && f !== 'or') + .map((field) => this.withFilterComparison(field as keyof Entity, this.getField(filter, field as keyof Entity))); + if (ands.length === 1) { + return ands[0]; + } + if (ands.length) { + return { $and: ands } as FilterQuery; + } + return undefined; + } + + private getField>( + obj: FilterComparisons, + field: K, + ): FilterFieldComparison { + return obj[field] as FilterFieldComparison; + } + + private withFilterComparison( + field: T, + cmp: FilterFieldComparison, + ): FilterQuery { + const opts = Object.keys(cmp) as (keyof FilterFieldComparison)[]; + if (opts.length === 1) { + const cmpType = opts[0]; + return this.comparisonBuilder.build(field, cmpType, cmp[cmpType] as EntityComparisonField); + } + return { + $or: opts.map((cmpType) => + this.comparisonBuilder.build(field, cmpType, cmp[cmpType] as EntityComparisonField), + ), + } as FilterQuery; + } +} diff --git a/packages/query-mongoose/src/services/index.ts b/packages/query-mongoose/src/services/index.ts new file mode 100644 index 000000000..c301cdc9d --- /dev/null +++ b/packages/query-mongoose/src/services/index.ts @@ -0,0 +1 @@ +export * from './mongoose-query.service'; diff --git a/packages/query-mongoose/src/services/mongoose-query.service.ts b/packages/query-mongoose/src/services/mongoose-query.service.ts new file mode 100644 index 000000000..ab4b905f8 --- /dev/null +++ b/packages/query-mongoose/src/services/mongoose-query.service.ts @@ -0,0 +1,253 @@ +/* eslint-disable no-underscore-dangle */ +import { + AggregateQuery, + AggregateResponse, + DeepPartial, + DeleteManyResponse, + DeleteOneOptions, + Filter, + FindByIdOptions, + GetByIdOptions, + Query, + QueryService, + UpdateManyResponse, + UpdateOneOptions, +} from '@nestjs-query/core'; +import { NotFoundException } from '@nestjs/common'; +import { CreateQuery, Document, DocumentToObjectOptions, Model as MongooseModel, UpdateQuery } from 'mongoose'; +import { AggregateBuilder, FilterQueryBuilder } from '../query'; +import { ReferenceQueryService } from './reference-query.service'; + +type MongoDBUpdatedOutput = { + nModified: number; +}; + +export interface MongooseQueryServiceOpts { + documentToObjectOptions?: DocumentToObjectOptions; +} + +/** + * Base class for all query services that use Typegoose. + * + * @example + * + * ```ts + * @QueryService(TodoItemEntity) + * export class TodoItemService extends TypegooseQueryService { + * constructor( + * @InjectModel(TodoItemEntity) model: ReturnModelType, + * ) { + * super(model); + * } + * } + * ``` + */ +export class MongooseQueryService + extends ReferenceQueryService + implements QueryService, DeepPartial> { + protected readonly documentToObjectOptions: DocumentToObjectOptions; + + readonly filterQueryBuilder: FilterQueryBuilder; + + constructor(readonly Model: MongooseModel, opts?: MongooseQueryServiceOpts) { + super(); + this.documentToObjectOptions = opts?.documentToObjectOptions || { virtuals: true }; + this.filterQueryBuilder = new FilterQueryBuilder(); + } + + /** + * Query for multiple entities, using a Query from `@nestjs-query/core`. + * + * @example + * ```ts + * const todoItems = await this.service.query({ + * filter: { title: { eq: 'Foo' } }, + * paging: { limit: 10 }, + * sorting: [{ field: "create", direction: SortDirection.DESC }], + * }); + * ``` + * @param query - The Query used to filter, page, and sort rows. + */ + async query(query: Query): Promise { + const { filterQuery, sorting, paging } = this.filterQueryBuilder.buildQuery(query); + return this.Model.find(filterQuery, {}, { limit: paging?.limit, skip: paging?.offset, sort: sorting }).exec(); + } + + async aggregate(filter: Filter, aggregateQuery: AggregateQuery): Promise> { + const { aggregate, filterQuery } = this.filterQueryBuilder.buildAggregateQuery(aggregateQuery, filter); + const aggResult = (await this.Model.aggregate>([ + { $match: filterQuery }, + { $group: { _id: null, ...aggregate } }, + ]).exec()) as Record[]; + return AggregateBuilder.convertToAggregateResponse(aggResult[0]); + } + + count(filter: Filter): Promise { + const filterQuery = this.filterQueryBuilder.buildFilterQuery(filter); + return this.Model.count(filterQuery).exec(); + } + + /** + * Find an entity by it's `id`. + * + * @example + * ```ts + * 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, opts?: FindByIdOptions): Promise { + const filterQuery = this.filterQueryBuilder.buildIdFilterQuery(id, opts?.filter); + const doc = await this.Model.findOne(filterQuery); + if (!doc) { + return undefined; + } + return doc; + } + + /** + * Gets an entity by it's `id`. If the entity is not found a rejected promise is returned. + * + * @example + * ```ts + * try { + * const todoItem = await this.service.getById(1); + * } catch(e) { + * console.error('Unable to find entity with id = 1'); + * } + * ``` + * @param id - The id of the record to find. + * @param opts - Additional options + */ + 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}`); + } + return entity; + } + + /** + * Creates a single entity. + * + * @example + * ```ts + * const todoItem = await this.service.createOne({title: 'Todo Item', completed: false }); + * ``` + * @param record - The entity to create. + */ + async createOne(record: DeepPartial): Promise { + this.ensureIdIsNotPresent(record); + return this.Model.create(record as CreateQuery); + } + + /** + * Create multiple entities. + * + * @example + * ```ts + * const todoItem = await this.service.createMany([ + * {title: 'Todo Item 1', completed: false }, + * {title: 'Todo Item 2', completed: true }, + * ]); + * ``` + * @param records - The entities to create. + */ + async createMany(records: DeepPartial[]): Promise { + records.forEach((r) => this.ensureIdIsNotPresent(r)); + return this.Model.create(records as CreateQuery[]); + } + + /** + * Update an entity. + * + * @example + * ```ts + * const updatedEntity = await this.service.updateOne(1, { completed: true }); + * ``` + * @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: DeepPartial, opts?: UpdateOneOptions): Promise { + this.ensureIdIsNotPresent(update); + const filterQuery = this.filterQueryBuilder.buildIdFilterQuery(id, opts?.filter); + const doc = await this.Model.findOneAndUpdate(filterQuery, update as UpdateQuery, { + new: true, + }); + if (!doc) { + throw new NotFoundException(`Unable to find ${this.Model.modelName} with id: ${id}`); + } + return doc; + } + + /** + * Update multiple entities with a `@nestjs-query/core` Filter. + * + * @example + * ```ts + * const { updatedCount } = await this.service.updateMany( + * { completed: true }, // the update to apply + * { title: { eq: 'Foo Title' } } // Filter to find records to update + * ); + * ``` + * @param update - A `Partial` of entity with the fields to update + * @param filter - A Filter used to find the records to update + */ + async updateMany(update: DeepPartial, filter: Filter): Promise { + this.ensureIdIsNotPresent(update); + const filterQuery = this.filterQueryBuilder.buildFilterQuery(filter); + const res = (await this.Model.updateMany( + filterQuery, + update as UpdateQuery, + ).exec()) as MongoDBUpdatedOutput; + return { updatedCount: res.nModified || 0 }; + } + + /** + * Delete an entity by `id`. + * + * @example + * + * ```ts + * const deletedTodo = await this.service.deleteOne(1); + * ``` + * + * @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, opts?: DeleteOneOptions): Promise { + const filterQuery = this.filterQueryBuilder.buildIdFilterQuery(id, opts?.filter); + const doc = await this.Model.findOneAndDelete(filterQuery); + if (!doc) { + throw new NotFoundException(`Unable to find ${this.Model.modelName} with id: ${id}`); + } + return doc; + } + + /** + * Delete multiple records with a `@nestjs-query/core` `Filter`. + * + * @example + * + * ```ts + * const { deletedCount } = this.service.deleteMany({ + * created: { lte: new Date('2020-1-1') } + * }); + * ``` + * + * @param filter - A `Filter` to find records to delete. + */ + async deleteMany(filter: Filter): Promise { + const filterQuery = this.filterQueryBuilder.buildFilterQuery(filter); + const res = await this.Model.deleteMany(filterQuery).exec(); + return { deletedCount: res.deletedCount || 0 }; + } + + private ensureIdIsNotPresent(e: DeepPartial): void { + if (Object.keys(e).find((f) => f === 'id' || f === '_id')) { + throw new Error('Id cannot be specified when updating or creating'); + } + } +} diff --git a/packages/query-mongoose/src/services/reference-query.service.ts b/packages/query-mongoose/src/services/reference-query.service.ts new file mode 100644 index 000000000..3f6237d23 --- /dev/null +++ b/packages/query-mongoose/src/services/reference-query.service.ts @@ -0,0 +1,338 @@ +/* eslint-disable no-underscore-dangle */ +import { + AggregateQuery, + AggregateResponse, + Class, + Filter, + FindRelationOptions, + GetByIdOptions, + mergeFilter, + ModifyRelationOptions, + Query, +} from '@nestjs-query/core'; +import { Document, Model as MongooseModel } from 'mongoose'; +import { AggregateBuilder, FilterQueryBuilder } from '../query'; +import { + isEmbeddedSchemaTypeOptions, + isSchemaTypeWithReferenceOptions, + isVirtualTypeWithReferenceOptions, + VirtualTypeWithOptions, +} from '../mongoose-types.helper'; + +export abstract class ReferenceQueryService { + abstract readonly Model: MongooseModel; + + abstract getById(id: string | number, opts?: GetByIdOptions): Promise; + + abstract findById(id: string | number, opts?: GetByIdOptions): Promise; + + async addRelations( + relationName: string, + id: string, + relationIds: (string | number)[], + opts?: ModifyRelationOptions, + ): Promise { + this.checkForReference(relationName); + const referenceModel = this.getReferenceModel(relationName); + const entity = await this.getById(id, opts); + const referenceQueryBuilder = this.getReferenceQueryBuilder(); + const refCount = await referenceModel.count( + referenceQueryBuilder.buildIdFilterQuery(relationIds, opts?.relationFilter), + ); + if (relationIds.length !== refCount) { + throw new Error(`Unable to find all ${relationName} to add to ${this.Model.modelName}`); + } + if (this.isVirtualPath(relationName)) { + throw new Error(`AddRelations not supported for virtual relation ${relationName}`); + } + await entity + .updateOne({ + $push: { [relationName]: { $each: relationIds } }, + }) + .exec(); + // reload the document + return this.getById(id); + } + + aggregateRelations( + RelationClass: Class, + relationName: string, + entities: Entity[], + filter: Filter, + aggregate: AggregateQuery, + ): Promise>>; + + aggregateRelations( + RelationClass: Class, + relationName: string, + dto: Entity, + filter: Filter, + aggregate: AggregateQuery, + ): Promise>; + + async aggregateRelations( + RelationClass: Class, + relationName: string, + dto: Entity | Entity[], + filter: Filter, + aggregateQuery: AggregateQuery, + ): Promise | Map>> { + this.checkForReference(relationName); + const relationModel = this.getReferenceModel(relationName); + const referenceQueryBuilder = this.getReferenceQueryBuilder(); + if (Array.isArray(dto)) { + return dto.reduce(async (mapPromise, entity) => { + const map = await mapPromise; + const refs = await this.aggregateRelations(RelationClass, relationName, entity, filter, aggregateQuery); + return map.set(entity, refs); + }, Promise.resolve(new Map>())); + } + const refFilter = this.getReferenceFilter(relationName, dto, filter); + if (!refFilter) { + return {}; + } + const { filterQuery, aggregate } = referenceQueryBuilder.buildAggregateQuery(aggregateQuery, refFilter); + const [aggResult] = (await relationModel + .aggregate>([{ $match: filterQuery }, { $group: { _id: null, ...aggregate } }]) + .exec()) as Record[]; + return aggResult ? AggregateBuilder.convertToAggregateResponse(aggResult) : {}; + } + + countRelations( + RelationClass: Class, + relationName: string, + entities: Entity[], + filter: Filter, + ): Promise>; + + countRelations( + RelationClass: Class, + relationName: string, + dto: Entity, + filter: Filter, + ): Promise; + + async countRelations( + RelationClass: Class, + relationName: string, + dto: Entity | Entity[], + filter: Filter, + ): Promise> { + this.checkForReference(relationName); + if (Array.isArray(dto)) { + return dto.reduce(async (mapPromise, entity) => { + const map = await mapPromise; + const refs = await this.countRelations(RelationClass, relationName, entity, filter); + return map.set(entity, refs); + }, Promise.resolve(new Map())); + } + const relationModel = this.getReferenceModel(relationName); + const referenceQueryBuilder = this.getReferenceQueryBuilder(); + const refFilter = this.getReferenceFilter(relationName, dto, filter); + if (!refFilter) { + return 0; + } + return relationModel.count(referenceQueryBuilder.buildFilterQuery(refFilter)).exec(); + } + + findRelation( + RelationClass: Class, + relationName: string, + dtos: Entity[], + opts?: FindRelationOptions, + ): Promise>; + 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> { + this.checkForReference(relationName); + const referenceQueryBuilder = this.getReferenceQueryBuilder(); + if (Array.isArray(dto)) { + return dto.reduce(async (prev, curr) => { + const map = await prev; + const ref = await this.findRelation(RelationClass, relationName, curr, opts); + return map.set(curr, ref); + }, Promise.resolve(new Map())); + } + const foundEntity = await this.findById(dto._id ?? dto.id); + if (!foundEntity) { + return undefined; + } + const filterQuery = referenceQueryBuilder.buildFilterQuery(opts?.filter); + const populated = await foundEntity + .populate({ + path: relationName, + match: filterQuery, + }) + .execPopulate(); + const populatedRef: unknown = populated.get(relationName); + return populatedRef ? (populatedRef as Relation) : undefined; + } + + queryRelations( + RelationClass: Class, + relationName: string, + entities: Entity[], + query: Query, + ): Promise>; + queryRelations( + RelationClass: Class, + relationName: string, + dto: Entity, + query: Query, + ): Promise; + async queryRelations( + RelationClass: Class, + relationName: string, + dto: Entity | Entity[], + query: Query, + ): Promise> { + this.checkForReference(relationName); + const referenceQueryBuilder = this.getReferenceQueryBuilder(); + if (Array.isArray(dto)) { + return dto.reduce(async (mapPromise, entity) => { + const map = await mapPromise; + const refs = await this.queryRelations(RelationClass, relationName, entity, query); + return map.set(entity, refs); + }, Promise.resolve(new Map())); + } + const foundEntity = await this.findById(dto._id ?? dto.id); + if (!foundEntity) { + return []; + } + const { filterQuery, paging, sorting } = referenceQueryBuilder.buildQuery(query); + const populated = await foundEntity + .populate({ + path: relationName, + match: filterQuery, + options: { limit: paging?.limit, skip: paging?.offset, sort: sorting }, + }) + .execPopulate(); + return populated.get(relationName) as Relation[]; + } + + removeRelation(): Promise { + throw new Error('Not implemented yet'); + } + + removeRelations(): Promise { + throw new Error('Not implemented yet'); + } + + async setRelation( + relationName: string, + id: string | number, + relationId: string | number, + opts?: ModifyRelationOptions, + ): Promise { + this.checkForReference(relationName); + const referenceModel = this.getReferenceModel(relationName); + const entity = await this.getById(id, opts); + const referenceQueryBuilder = this.getReferenceQueryBuilder(); + const refCount = await referenceModel.count( + referenceQueryBuilder.buildIdFilterQuery([relationId], opts?.relationFilter), + ); + if (refCount !== 1) { + throw new Error(`Unable to find ${relationName} to set on ${this.Model.modelName}`); + } + if (this.isVirtualPath(relationName)) { + throw new Error(`SetRelation not supported for virtual relation ${relationName}`); + } + await entity + .updateOne({ + [relationName]: relationId, + }) + .exec(); + // reload the document + return this.getById(id); + } + + private checkForReference(refName: string): void { + const found = this.isReferencePath(refName) || this.isVirtualPath(refName); + if (!found) { + throw new Error(`Unable to find reference ${refName} on ${this.Model.modelName}`); + } + } + + private isReferencePath(refName: string): boolean { + return !!this.Model.schema.path(refName); + } + + private isVirtualPath(refName: string): boolean { + return !!this.Model.schema.virtualpath(refName); + } + + private getReferenceQueryBuilder(): FilterQueryBuilder { + return new FilterQueryBuilder(); + } + + private getReferenceModel(refName: string): MongooseModel { + if (this.isReferencePath(refName)) { + const schemaType = this.Model.schema.path(refName); + if (isEmbeddedSchemaTypeOptions(schemaType)) { + return this.Model.model(schemaType.$embeddedSchemaType.options.ref); + } + if (isSchemaTypeWithReferenceOptions(schemaType)) { + return this.Model.model(schemaType.options.ref); + } + } else if (this.isVirtualPath(refName)) { + const schemaType = this.Model.schema.virtualpath(refName); + if (isVirtualTypeWithReferenceOptions(schemaType)) { + return this.Model.model(schemaType.options.ref); + } + } + throw new Error(`Unable to lookup reference type for ${refName}`); + } + + private getReferenceFilter( + refName: string, + entity: Entity, + filter?: Filter, + ): Filter | undefined { + if (this.isReferencePath(refName)) { + return this.getObjectIdReferenceFilter(refName, entity, filter); + } + if (this.isVirtualPath(refName)) { + const virtualType = this.Model.schema.virtualpath(refName); + if (isVirtualTypeWithReferenceOptions(virtualType)) { + return this.getVirtualReferenceFilter(virtualType, entity, filter); + } + throw new Error(`Unable to lookup reference type for ${refName}`); + } + return undefined; + } + + private getObjectIdReferenceFilter( + refName: string, + entity: Entity, + filter?: Filter, + ): Filter { + const referenceIds = entity[refName as keyof Entity]; + const refFilter = ({ + _id: { [Array.isArray(referenceIds) ? 'in' : 'eq']: referenceIds }, + } as unknown) as Filter; + return mergeFilter(filter ?? ({} as Filter), refFilter); + } + + private getVirtualReferenceFilter( + virtualType: VirtualTypeWithOptions, + entity: Entity, + filter?: Filter, + ): Filter { + const { foreignField, localField } = virtualType.options; + const refVal = entity[localField as keyof Entity]; + const isArray = Array.isArray(refVal); + const lookupFilter = ({ + [foreignField as keyof Ref]: { [isArray ? 'in' : 'eq']: refVal }, + } as unknown) as Filter; + return mergeFilter(filter ?? ({} as Filter), lookupFilter); + } +} diff --git a/packages/query-typegoose/tsconfig.build.json b/packages/query-mongoose/tsconfig.build.json similarity index 100% rename from packages/query-typegoose/tsconfig.build.json rename to packages/query-mongoose/tsconfig.build.json diff --git a/packages/query-typegoose/tsconfig.json b/packages/query-mongoose/tsconfig.json similarity index 100% rename from packages/query-typegoose/tsconfig.json rename to packages/query-mongoose/tsconfig.json diff --git a/packages/query-typegoose/CHANGELOG.md b/packages/query-typegoose/CHANGELOG.md deleted file mode 100644 index 1fe86455a..000000000 --- a/packages/query-typegoose/CHANGELOG.md +++ /dev/null @@ -1,277 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.18.1](https://github.com/doug-martin/nestjs-query/compare/v0.18.0...v0.18.1) (2020-08-14) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -# [0.18.0](https://github.com/doug-martin/nestjs-query/compare/v0.17.10...v0.18.0) (2020-08-11) - - -### Bug Fixes - -* **type:** Pin dev dependencies ([442db4c](https://github.com/doug-martin/nestjs-query/commit/442db4cd9b9d48d0c6a20209f0b44c4a314660ac)) - - -### Features - -* **typeorm:** Switch to use unioned queries for relations ([327c676](https://github.com/doug-martin/nestjs-query/commit/327c6760e3e1a7db6bb0f872928d0502345c925f)) - - - - - -## [0.17.10](https://github.com/doug-martin/nestjs-query/compare/v0.17.9...v0.17.10) (2020-08-01) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.17.8](https://github.com/doug-martin/nestjs-query/compare/v0.17.7...v0.17.8) (2020-07-28) - - -### Features - -* **graphql:** Allow specifying allowed comparisons on filterable fields ([ced2792](https://github.com/doug-martin/nestjs-query/commit/ced27920e5c2278c2a04c027a692e25b3306f6cb)) - - - - - -## [0.17.7](https://github.com/doug-martin/nestjs-query/compare/v0.17.6...v0.17.7) (2020-07-27) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.17.2](https://github.com/doug-martin/nestjs-query/compare/v0.17.1...v0.17.2) (2020-07-17) - - -### Bug Fixes - -* **typeorm:** Ensure record is entity instance when saving ([3cdbbaf](https://github.com/doug-martin/nestjs-query/commit/3cdbbaff11b18bcc5e6fd29fd182e2bd66b14f17)), closes [#380](https://github.com/doug-martin/nestjs-query/issues/380) - - - - - -# [0.17.0](https://github.com/doug-martin/nestjs-query/compare/v0.16.2...v0.17.0) (2020-07-16) - - -### Features - -* **aggregations:** Add aggregations to graphql ([af075d2](https://github.com/doug-martin/nestjs-query/commit/af075d2e93b6abbbfbe32afcc917350f803fadaa)) -* **aggregations,typeorm:** Add relation aggregation to typeorm ([2bf35a9](https://github.com/doug-martin/nestjs-query/commit/2bf35a92ce80b1f3026fd87cb62cad17eb6eff03)) -* **aggretations:** Add aggregations support to typeorm ([7233c23](https://github.com/doug-martin/nestjs-query/commit/7233c2397d0ac332e5209ab87ae62f5f555609d6)) - - - - - -## [0.16.2](https://github.com/doug-martin/nestjs-query/compare/v0.16.1...v0.16.2) (2020-07-09) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.16.1](https://github.com/doug-martin/nestjs-query/compare/v0.16.0...v0.16.1) (2020-07-07) - - -### Bug Fixes - -* **typeorm:** Fix import path in relation service [#363](https://github.com/doug-martin/nestjs-query/issues/363) ([0e6d484](https://github.com/doug-martin/nestjs-query/commit/0e6d484920960ed1966360a89af979230667b5f7)) - - - - - -# [0.16.0](https://github.com/doug-martin/nestjs-query/compare/v0.15.1...v0.16.0) (2020-07-05) - - -### Features - -* **typeorm:** Add support for filtering on relations ([aa8788c](https://github.com/doug-martin/nestjs-query/commit/aa8788cbbc0c95465e1633b57ca48c91b160038a)) - - - - - -## [0.15.1](https://github.com/doug-martin/nestjs-query/compare/v0.15.0...v0.15.1) (2020-06-27) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -# [0.15.0](https://github.com/doug-martin/nestjs-query/compare/v0.14.3...v0.15.0) (2020-06-23) - - -### Features - -* **graphql,connection:** Add totalCount to connections ([ed1e84a](https://github.com/doug-martin/nestjs-query/commit/ed1e84a2feb6f89c3b270fcbc1d0eaf6aec5e575)) - - - - - -## [0.14.2](https://github.com/doug-martin/nestjs-query/compare/v0.14.1...v0.14.2) (2020-06-19) - - -### Bug Fixes - -* **typeorm:** Allow using string based typeorm relations ([55c157d](https://github.com/doug-martin/nestjs-query/commit/55c157dbea9ce8c1186a2c2ea17f847857fd2226)) - - - - - -# [0.14.0](https://github.com/doug-martin/nestjs-query/compare/v0.13.2...v0.14.0) (2020-06-18) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -# [0.13.0](https://github.com/doug-martin/nestjs-query/compare/v0.12.0...v0.13.0) (2020-06-12) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -# [0.12.0](https://github.com/doug-martin/nestjs-query/compare/v0.11.8...v0.12.0) (2020-06-07) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.11.8](https://github.com/doug-martin/nestjs-query/compare/v0.11.7...v0.11.8) (2020-05-30) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.11.7](https://github.com/doug-martin/nestjs-query/compare/v0.11.6...v0.11.7) (2020-05-29) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.11.6](https://github.com/doug-martin/nestjs-query/compare/v0.11.5...v0.11.6) (2020-05-26) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.11.5](https://github.com/doug-martin/nestjs-query/compare/v0.11.4...v0.11.5) (2020-05-21) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.11.4](https://github.com/doug-martin/nestjs-query/compare/v0.11.3...v0.11.4) (2020-05-19) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.11.3](https://github.com/doug-martin/nestjs-query/compare/v0.11.2...v0.11.3) (2020-05-16) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.11.2](https://github.com/doug-martin/nestjs-query/compare/v0.11.1...v0.11.2) (2020-05-14) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.11.1](https://github.com/doug-martin/nestjs-query/compare/v0.11.0...v0.11.1) (2020-05-11) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -# [0.11.0](https://github.com/doug-martin/nestjs-query/compare/v0.10.2...v0.11.0) (2020-05-09) - - -### Features - -* **graphql:** Add graphql module ([282c421](https://github.com/doug-martin/nestjs-query/commit/282c421d0e6f67fe750fa6005f6cb7d960c8fbd0)) - - - - - -## [0.10.2](https://github.com/doug-martin/nestjs-query/compare/v0.10.1...v0.10.2) (2020-05-04) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -# [0.10.0](https://github.com/doug-martin/nestjs-query/compare/v0.9.0...v0.10.0) (2020-04-29) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -# [0.9.0](https://github.com/doug-martin/nestjs-query/compare/v0.8.9...v0.9.0) (2020-04-26) - - -### Features - -* **typeorm:** Add support for soft deletes ([2ab42fa](https://github.com/doug-martin/nestjs-query/commit/2ab42faee2802abae4d8496e2529b8eb23860ed4)) - - - - - -## [0.8.9](https://github.com/doug-martin/nestjs-query/compare/v0.8.8...v0.8.9) (2020-04-24) - -**Note:** Version bump only for package @nestjs-query/query-typeorm - - - - - -## [0.8.7](https://github.com/doug-martin/nestjs-query/compare/v0.8.6...v0.8.7) (2020-04-23) - -**Note:** Version bump only for package @nestjs-query/query-typeorm diff --git a/packages/query-typegoose/__tests__/__fixtures__/seeds.ts b/packages/query-typegoose/__tests__/__fixtures__/seeds.ts deleted file mode 100644 index bb2991387..000000000 --- a/packages/query-typegoose/__tests__/__fixtures__/seeds.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Connection } from 'mongoose'; -import { plainToClass } from 'class-transformer'; -import { TestEntity } from './test.entity'; -import { TestReference } from './test-reference.entity'; - -export const TEST_ENTITIES: TestEntity[] = new Array(15) - .fill(0) - .map((e, i) => i + 1) - .map((i) => { - return plainToClass(TestEntity, { - boolType: i % 2 === 0, - dateType: new Date(`2020-02-${i}`), - numberType: i, - stringType: `foo${i}`, - testReferences: [], - }); - }); - -export const TEST_REFERENCES: TestReference[] = [1, 2, 3, 4, 5].map((i) => { - return plainToClass(TestReference, { - name: `name${i}`, - }); -}); - -export const seed = async (connection: Connection): Promise => { - const testEntitiesCollection = connection.collection('testentities'); - const testReferencesCollection = connection.collection('testreferences'); - - await testEntitiesCollection.insertMany(TEST_ENTITIES); - await testReferencesCollection.insertMany(TEST_REFERENCES); - - await Promise.all( - TEST_REFERENCES.map((testReference, i) => { - return testEntitiesCollection.updateOne( - { stringType: TEST_ENTITIES[i + 10].stringType }, - { $set: { testReference: testReference.id } }, - ); - }), - ); - - await testEntitiesCollection.updateOne( - { stringType: TEST_ENTITIES[TEST_ENTITIES.length - 1].stringType }, - { $set: { testReferences: TEST_REFERENCES.map((ref) => ref.id) } }, - ); -}; diff --git a/packages/query-typegoose/__tests__/__fixtures__/test-reference.entity.ts b/packages/query-typegoose/__tests__/__fixtures__/test-reference.entity.ts deleted file mode 100644 index 0cbcd9f5a..000000000 --- a/packages/query-typegoose/__tests__/__fixtures__/test-reference.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { mongoose, prop } from '@typegoose/typegoose'; - -export class TestReference { - get id(): string { - const idKey = '_id'; - return ((this as unknown) as Record)[idKey]?.toString(); - } - - @prop({ required: true }) - name!: string; - - getOutputData(): TestReference { - return { - ...this, - id: this.id, - }; - } -} diff --git a/packages/query-typegoose/__tests__/__fixtures__/test.entity.ts b/packages/query-typegoose/__tests__/__fixtures__/test.entity.ts deleted file mode 100644 index 26df54f97..000000000 --- a/packages/query-typegoose/__tests__/__fixtures__/test.entity.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { mongoose, prop, Ref } from '@typegoose/typegoose'; -import { TestReference } from './test-reference.entity'; - -export class TestEntity { - get id(): string { - const idKey = '_id'; - return ((this as unknown) as Record)[idKey]?.toString(); - } - - @prop({ required: true }) - stringType!: string; - - @prop({ required: true }) - boolType!: boolean; - - @prop({ required: true }) - numberType!: number; - - @prop({ required: true }) - dateType!: Date; - - @prop({ ref: TestReference }) - testReference?: Ref; - - @prop({ ref: TestReference }) - testReferences?: Ref[]; - - getInputData(): Partial { - return { - stringType: this.stringType, - boolType: this.boolType, - numberType: this.numberType, - dateType: this.dateType, - }; - } - - getOutputData(): TestEntity { - return { - ...this, - id: this.id, - }; - } -} diff --git a/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts b/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts deleted file mode 100644 index bd99d2c49..000000000 --- a/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { plainToClass } from 'class-transformer'; -import { InjectModel, TypegooseModule } from 'nestjs-typegoose'; -import { ReturnModelType, mongoose } from '@typegoose/typegoose'; -import { TypegooseQueryService } from '../../src/services'; -import { TestEntity } from '../__fixtures__/test.entity'; -import { getConnectionUri, prepareDb, closeDbConnection, dropDatabase } from '../__fixtures__/connection.fixture'; -import { TEST_ENTITIES, TEST_REFERENCES } from '../__fixtures__/seeds'; -import { TestReference } from '../__fixtures__/test-reference.entity'; - -describe('TypegooseQueryService', () => { - let moduleRef: TestingModule; - - class TestEntityService extends TypegooseQueryService { - constructor(@InjectModel(TestEntity) readonly model: ReturnModelType) { - super(model, { documentToObjectOptions: { virtuals: true } }); - } - } - - beforeAll(async () => { - moduleRef = await Test.createTestingModule({ - imports: [ - TypegooseModule.forRoot(await getConnectionUri(), { useFindAndModify: false }), - TypegooseModule.forFeature([TestEntity, TestReference]), - ], - providers: [TestEntityService], - }).compile(); - }); - - afterAll(async () => closeDbConnection()); - - beforeEach(() => prepareDb()); - - afterEach(() => dropDatabase()); - - describe('#query', () => { - it('call find and return the result', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({}); - return expect(queryResult).toHaveLength(TEST_ENTITIES.length); - }); - - it('should support eq operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { stringType: { eq: 'foo1' } } }); - return expect(queryResult).toEqual([TEST_ENTITIES[0].getOutputData()]); - }); - - it('should support neq operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { stringType: { neq: 'foo1' } } }); - return expect(queryResult).toHaveLength(TEST_ENTITIES.length - 1); - }); - - it('should support gt operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { numberType: { gt: 5 } } }); - return expect(queryResult).toHaveLength(TEST_ENTITIES.length - 5); - }); - - it('should support gte operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { numberType: { gte: 5 } } }); - return expect(queryResult).toHaveLength(TEST_ENTITIES.length - 4); - }); - - it('should support lt operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { numberType: { lt: 10 } } }); - return expect(queryResult).toHaveLength(9); - }); - - it('should support lte operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { numberType: { lte: 10 } } }); - return expect(queryResult).toHaveLength(10); - }); - - it('should support in operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { numberType: { in: [1, 2, 3] } } }); - return expect(queryResult).toEqual(TEST_ENTITIES.slice(0, 3).map((e) => e.getOutputData())); - }); - - it('should support notIn operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { numberType: { notIn: [1, 2, 3] } } }); - return expect(queryResult).toHaveLength(12); - }); - - it('should support is operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { boolType: { is: true } } }); - return expect(queryResult).toHaveLength(7); - }); - - it('should support isNot operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { boolType: { isNot: true } } }); - return expect(queryResult).toHaveLength(8); - }); - - it('should support like operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { stringType: { like: 'foo%' } } }); - return expect(queryResult).toHaveLength(15); - }); - - it('should support notLike operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { stringType: { notLike: 'foo%' } } }); - return expect(queryResult).toHaveLength(0); - }); - - it('should support iLike operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { stringType: { iLike: 'FOO%' } } }); - return expect(queryResult).toHaveLength(15); - }); - - it('should support notILike operator', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.query({ filter: { stringType: { notILike: 'FOO%' } } }); - return expect(queryResult).toHaveLength(0); - }); - }); - - describe('#count', () => { - it('should return number of elements matching a query', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.count({ stringType: { gt: 'foo' } }); - return expect(queryResult).toBe(15); - }); - }); - - describe('#findById', () => { - it('return the entity if found', async () => { - const entity = TEST_ENTITIES[0]; - const queryService = moduleRef.get(TestEntityService); - const found = await queryService.findById(entity.id.toString()); - expect(found).toEqual(entity.getOutputData()); - }); - - it('return undefined if not found', async () => { - const queryService = moduleRef.get(TestEntityService); - 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', () => { - it('return the entity if found', async () => { - const entity = TEST_ENTITIES[0]; - const queryService = moduleRef.get(TestEntityService); - const found = await queryService.getById(entity.id.toString()); - expect(found).toEqual(entity.getOutputData()); - }); - - it('return undefined if not found', () => { - const badId = new mongoose.Types.ObjectId().toString(); - 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', () => { - it('call save on the repo with instances of entities when passed plain objects', async () => { - const queryService = moduleRef.get(TestEntityService); - const created = await queryService.createMany(TEST_ENTITIES.map((e) => e.getInputData())); - created.forEach((createdEntity, i) => { - expect(createdEntity).toEqual(expect.objectContaining(TEST_ENTITIES[i].getInputData())); - }); - }); - - it('call save on the repo with instances of entities when passed instances', async () => { - const instances = TEST_ENTITIES.map((e) => plainToClass(TestEntity, e.getInputData())); - const queryService = moduleRef.get(TestEntityService); - const created = await queryService.createMany(instances); - created.forEach((createdEntity, i) => { - expect(createdEntity).toEqual(expect.objectContaining(instances[i].getInputData())); - }); - }); - - it('should reject if the entities already exist', async () => { - const queryService = moduleRef.get(TestEntityService); - return expect(queryService.createMany(TEST_ENTITIES)).rejects.toThrow(/duplicate key error dup key/); - }); - }); - - describe('#createOne', () => { - it('call save on the repo with an instance of the entity when passed a plain object', async () => { - const entity = TEST_ENTITIES[0].getInputData(); - const queryService = moduleRef.get(TestEntityService); - const created = await queryService.createOne(entity); - expect(created).toEqual(expect.objectContaining(entity)); - }); - - it('call save on the repo with an instance of the entity when passed an instance', async () => { - const entity = plainToClass(TestEntity, TEST_ENTITIES[0].getInputData()); - const queryService = moduleRef.get(TestEntityService); - const created = await queryService.createOne(entity); - expect(created).toEqual(expect.objectContaining(entity)); - }); - - it('should reject if the entity contains an id', async () => { - const entity = TEST_ENTITIES[0]; - const queryService = moduleRef.get(TestEntityService); - return expect(queryService.createOne({ ...entity })).rejects.toThrow(/duplicate key error dup key/); - }); - }); - - describe('#deleteMany', () => { - it('delete all records that match the query', async () => { - const queryService = moduleRef.get(TestEntityService); - const { deletedCount } = await queryService.deleteMany({ - stringType: { in: TEST_ENTITIES.slice(0, 5).map((e) => e.stringType) }, - }); - expect(deletedCount).toEqual(expect.any(Number)); - const allCount = await queryService.count({}); - expect(allCount).toBe(TEST_ENTITIES.length - 5); - }); - }); - - describe('#deleteOne', () => { - it('remove the entity', async () => { - const queryService = moduleRef.get(TestEntityService); - const deleted = await queryService.deleteOne(TEST_ENTITIES[0].id.toString()); - expect(deleted).toEqual(TEST_ENTITIES[0].getOutputData()); - }); - - it('call fail if the entity is not found', async () => { - const badId = new mongoose.Types.ObjectId().toString(); - 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', () => { - it('update all entities in the filter', async () => { - const queryService = moduleRef.get(TestEntityService); - const filter = { - stringType: { in: TEST_ENTITIES.slice(0, 5).map((e) => e.stringType) }, - }; - await queryService.updateMany({ stringType: 'updated' }, filter); - const entities = await queryService.query({ filter: { stringType: { eq: 'updated' } } }); - expect(entities).toHaveLength(5); - }); - - it('should reject if the update contains the ID', () => { - const queryService = moduleRef.get(TestEntityService); - return expect(queryService.updateMany({ id: new mongoose.Types.ObjectId().toString() }, {})).rejects.toThrow( - 'Id cannot be specified when updating', - ); - }); - }); - - describe('#updateOne', () => { - it('update the entity', async () => { - const queryService = moduleRef.get(TestEntityService); - const updated = await queryService.updateOne(TEST_ENTITIES[0].id.toString(), { stringType: 'updated' }); - expect(updated).toEqual({ - ...TEST_ENTITIES[0].getOutputData(), - stringType: 'updated', - }); - }); - - it('should reject if the update contains the ID', async () => { - const queryService = moduleRef.get(TestEntityService); - return expect( - queryService.updateOne(TEST_ENTITIES[0].id.toString(), { id: new mongoose.Types.ObjectId().toString() }), - ).rejects.toThrow('Id cannot be specified when updating'); - }); - - it('call fail if the entity is not found', async () => { - const badId = new mongoose.Types.ObjectId().toString(); - const queryService = moduleRef.get(TestEntityService); - return expect(queryService.updateOne(badId, { stringType: 'updated' })).rejects.toThrow( - `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', () => { - it('call select and return the result', async () => { - const entity = TEST_ENTITIES[10]; - entity.testReference = new mongoose.Types.ObjectId(TEST_REFERENCES[0].id); - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.findRelation(TestReference, 'testReference', [entity]); - - expect(queryResult.values().next().value).toEqual(TEST_REFERENCES[0].getOutputData()); - }); - - it('should return undefined select if no results are found.', async () => { - const entity = TEST_ENTITIES[0]; - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.findRelation(TestReference, 'testReference', [entity]); - - expect(queryResult.values().next().value).toBeUndefined(); - }); - - it('should return undefined select if relation entity does not exist.', async () => { - const entity = TEST_ENTITIES[10]; - entity.testReference = new mongoose.Types.ObjectId(TEST_REFERENCES[0].id); - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.findRelation(TestReference, 'badRelation', [entity]); - - 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', () => { - let testEntity: TestEntity; - - beforeEach(() => { - testEntity = plainToClass(TestEntity, { - ...TEST_ENTITIES[0], - testReferences: TEST_REFERENCES.map((ref) => ref.id), - }); - }); - - it('should return a map containing a list of references', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.queryRelations(TestReference, 'testReferences', testEntity, {}); - expect(queryResult.values().next().value).toEqual(TEST_REFERENCES.map((ref) => ref.getOutputData())); - }); - - it('should apply a filter', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.queryRelations(TestReference, 'testReferences', testEntity, { - filter: { name: { eq: 'name2' } }, - }); - expect(queryResult.values().next().value).toEqual(TEST_REFERENCES.slice(1, 2).map((ref) => ref.getOutputData())); - }); - - it('should apply paging', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.queryRelations(TestReference, 'testReferences', testEntity, { - paging: { limit: 2, offset: 1 }, - }); - expect(queryResult.values().next().value).toEqual(TEST_REFERENCES.slice(1, 3).map((ref) => ref.getOutputData())); - }); - - it('should return an empty array if no results are found.', async () => { - const queryService = moduleRef.get(TestEntityService); - const queryResult = await queryService.queryRelations(TestReference, 'testReferences', testEntity, { - filter: { name: { eq: 'does-not-exist' } }, - }); - expect(queryResult.values().next().value).toEqual([]); - }); - }); -}); diff --git a/packages/query-typegoose/src/index.ts b/packages/query-typegoose/src/index.ts deleted file mode 100644 index b0c427a67..000000000 --- a/packages/query-typegoose/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { TypegooseQueryService, TypegooseQueryServiceOpts } from './services'; -export { NestjsQueryTypegooseModule } from './module'; diff --git a/packages/query-typegoose/src/module.ts b/packages/query-typegoose/src/module.ts deleted file mode 100644 index 770afadc3..000000000 --- a/packages/query-typegoose/src/module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DynamicModule } from '@nestjs/common'; -import { TypegooseModule } from 'nestjs-typegoose'; -import { TypegooseClass } from 'nestjs-typegoose/dist/typegoose-class.interface'; -import { createTypegooseQueryServiceProviders } from './providers'; - -export class NestjsQueryTypegooseModule { - static forFeature(models: TypegooseClass[], connectionName?: string): DynamicModule { - const queryServiceProviders = createTypegooseQueryServiceProviders(models); - const typegooseModule = TypegooseModule.forFeature(models, connectionName); - return { - imports: [typegooseModule], - module: NestjsQueryTypegooseModule, - providers: [...queryServiceProviders], - exports: [...queryServiceProviders, typegooseModule], - }; - } -} diff --git a/packages/query-typegoose/src/providers.ts b/packages/query-typegoose/src/providers.ts deleted file mode 100644 index d064bbb03..000000000 --- a/packages/query-typegoose/src/providers.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getQueryServiceToken } from '@nestjs-query/core'; -import { FactoryProvider } from '@nestjs/common'; -import { ReturnModelType } from '@typegoose/typegoose'; -import { TypegooseClass } from 'nestjs-typegoose/dist/typegoose-class.interface'; -import { TypegooseQueryService } from './services'; - -function createTypegooseQueryServiceProvider(model: TypegooseClass): FactoryProvider { - return { - provide: getQueryServiceToken(model), - useFactory(modelClass: ReturnModelType Entity>) { - return new TypegooseQueryService(modelClass); - }, - inject: [`${model.name}Model`], - }; -} - -export const createTypegooseQueryServiceProviders = (models: TypegooseClass[]): FactoryProvider[] => - models.map((model) => createTypegooseQueryServiceProvider(model)); diff --git a/packages/query-typegoose/src/services/index.ts b/packages/query-typegoose/src/services/index.ts deleted file mode 100644 index daae21469..000000000 --- a/packages/query-typegoose/src/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './typegoose-query.service'; diff --git a/packages/query-typegoose/src/services/typegoose-query.service.ts b/packages/query-typegoose/src/services/typegoose-query.service.ts deleted file mode 100644 index a97180a05..000000000 --- a/packages/query-typegoose/src/services/typegoose-query.service.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { - AggregateQuery, - AggregateResponse, - 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; -} - -/** - * Base class for all query services that use Typegoose. - * - * @example - * - * ```ts - * @QueryService(TodoItemEntity) - * export class TodoItemService extends TypegooseQueryService { - * constructor( - * @InjectModel(TodoItemEntity) model: ReturnModelType, - * ) { - * super(model); - * } - * } - * ``` - */ -export class TypegooseQueryService implements QueryService { - protected readonly documentToObjectOptions: DocumentToObjectOptions; - - protected mongoOperatorMapper: Record = { - eq: '$eq', - neq: '$ne', - gt: '$gt', - gte: '$gte', - lt: '$lt', - lte: '$lte', - in: '$in', - notIn: '$nin', - is: '$eq', - isNot: '$ne', - }; - - constructor(readonly Model: ReturnModelType Entity>, opts?: TypegooseQueryServiceOpts) { - this.documentToObjectOptions = opts?.documentToObjectOptions || { virtuals: true }; - } - - protected buildExpression(filter: Filter): FilterQuery T> { - return Object.entries(filter).reduce((prev: FilterQuery T>, [key, value]) => { - if (!value) { - return prev; - } - if (Array.isArray(value)) { - return { - ...prev, - [`$${key}`]: value.map((subFilter) => this.buildExpression(subFilter)), - }; - } - const findConditions = Object.entries(value).reduce( - (prevCondition: FilterQuery Entity>, [fieldKey, fieldValue]) => { - if (this.mongoOperatorMapper[fieldKey]) { - return { - ...prevCondition, - [this.mongoOperatorMapper[fieldKey]]: fieldValue, - }; - } - if (['like', 'notLike', 'iLike', 'notILike'].includes(fieldKey)) { - const regExpStr = (escapeRegExp as (str: string) => string)(fieldValue as string).replace('%', '.*'); - const regExp = new RegExp(regExpStr, fieldKey.toLowerCase().includes('ilike') ? 'i' : undefined); - if (fieldKey.startsWith('not')) { - return { - ...prevCondition, - $not: { $regex: regExp }, - }; - } - return { - ...prevCondition, - $regex: regExp, - }; - } - return prevCondition; - }, - {}, - ); - return { - ...prev, - [this.getSchemaKey(key)]: findConditions, - }; - }, {}); - } - - private getSchemaKey(key: string): string { - 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`. - * - * @example - * ```ts - * const todoItems = await this.service.query({ - * filter: { title: { eq: 'Foo' } }, - * paging: { limit: 10 }, - * sorting: [{ field: "create", direction: SortDirection.DESC }], - * }); - * ``` - * @param query - The Query used to filter, page, and sort rows. - */ - async query(query: Query): Promise { - const entities = await this.Model.find( - this.buildExpression(query.filter || {}), - {}, - { - limit: query.paging?.limit, - skip: query.paging?.offset, - sort: (query.sorting || []).map((sort) => ({ - [this.getSchemaKey(sort.field.toString())]: sort.direction.toLowerCase(), - })), - }, - ).exec(); - return entities.map((doc) => doc.toObject(this.documentToObjectOptions) as Entity); - } - - aggregate(): Promise> { - throw new Error('Not implemented yet'); - } - - count(filter: Filter): Promise { - return this.Model.count(this.buildExpression(filter)).exec(); - } - - /** - * Find an entity by it's `id`. - * - * @example - * ```ts - * const todoItem = await this.service.findById(1); - * ``` - * @param id - The id of the record to find. - * @param opts - Additional options - */ - 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; - } - - /** - * Gets an entity by it's `id`. If the entity is not found a rejected promise is returned. - * - * @example - * ```ts - * try { - * const todoItem = await this.service.getById(1); - * } catch(e) { - * console.error('Unable to find entity with id = 1'); - * } - * ``` - * @param id - The id of the record to find. - * @param opts - Additional options - */ - 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}`); - } - return entity; - } - - /** - * Creates a single entity. - * - * @example - * ```ts - * const todoItem = await this.service.createOne({title: 'Todo Item', completed: false }); - * ``` - * @param record - The entity to create. - */ - async createOne>(record: C): Promise { - const doc = new this.Model(record); - await doc.save(record); - return doc.toObject(this.documentToObjectOptions) as Entity; - } - - /** - * Create multiple entities. - * - * @example - * ```ts - * const todoItem = await this.service.createMany([ - * {title: 'Todo Item 1', completed: false }, - * {title: 'Todo Item 2', completed: true }, - * ]); - * ``` - * @param records - The entities to create. - */ - createMany>(records: C[]): Promise { - return Promise.all(records.map((r) => this.createOne(r))); - } - - /** - * Update an entity. - * - * @example - * ```ts - * const updatedEntity = await this.service.updateOne(1, { completed: true }); - * ``` - * @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, - opts?: UpdateOneOptions, - ): Promise { - this.ensureIdIsNotPresent(update); - 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; - } - throw new NotFoundException(`Unable to find ${this.Model.modelName} with id: ${id}`); - } - - /** - * Update multiple entities with a `@nestjs-query/core` Filter. - * - * @example - * ```ts - * const { updatedCount } = await this.service.updateMany( - * { completed: true }, // the update to apply - * { title: { eq: 'Foo Title' } } // Filter to find records to update - * ); - * ``` - * @param update - A `Partial` of entity with the fields to update - * @param filter - A Filter used to find the records to update - */ - async updateMany>(update: U, filter: Filter): Promise { - this.ensureIdIsNotPresent(update); - const res = (await this.Model.updateMany( - this.buildExpression(filter), - update as UpdateQuery Entity>, - ).exec()) as { nModified: number }; - return { updatedCount: res.nModified || 0 }; - } - - /** - * Delete an entity by `id`. - * - * @example - * - * ```ts - * const deletedTodo = await this.service.deleteOne(1); - * ``` - * - * @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, opts?: DeleteOneOptions): Promise { - const doc = await this.Model.findOneAndDelete(this.mergeFilterWithId(id, opts?.filter)); - if (doc) { - return doc.toObject(this.documentToObjectOptions) as Entity; - } - throw new NotFoundException(`Unable to find ${this.Model.modelName} with id: ${id}`); - } - - /** - * Delete multiple records with a `@nestjs-query/core` `Filter`. - * - * @example - * - * ```ts - * const { deletedCount } = this.service.deleteMany({ - * created: { lte: new Date('2020-1-1') } - * }); - * ``` - * - * @param filter - A `Filter` to find records to delete. - */ - async deleteMany(filter: Filter): Promise { - const res = await this.Model.deleteMany(this.buildExpression(filter)).exec(); - return { deletedCount: res.deletedCount || 0 }; - } - - private ensureIdIsNotPresent(e: DeepPartial): void { - if (Object.keys(e).includes('id')) { - throw new Error('Id cannot be specified when updating'); - } - } - - addRelations(): Promise { - throw new Error('Not implemented yet'); - } - - aggregateRelations( - RelationClass: Class, - relationName: string, - entities: Entity[], - filter: Filter, - aggregate: AggregateQuery, - ): Promise>>; - - aggregateRelations( - RelationClass: Class, - relationName: string, - dto: Entity, - filter: Filter, - aggregate: AggregateQuery, - ): Promise>; - - aggregateRelations(): Promise | Map>> { - throw new Error('Not implemented yet'); - } - - countRelations( - RelationClass: Class, - relationName: string, - entities: Entity[], - filter: Filter, - ): Promise>; - - countRelations( - RelationClass: Class, - relationName: string, - dto: Entity, - filter: Filter, - ): Promise; - - countRelations(): Promise> { - throw new Error('Not implemented yet'); - } - - findRelation( - 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]; - return dtos.reduce(async (prev, curr) => { - const map = await prev; - const referenceId = curr[relationName as keyof Entity]; - if (referenceId) { - const relationDoc = await relationModel.findOne(this.mergeFilterWithId(referenceId, opts?.filter)); - map.set(curr, relationDoc?.toObject(this.documentToObjectOptions)); - } - return map; - }, Promise.resolve(new Map())); - } - - queryRelations( - RelationClass: Class, - relationName: string, - entities: Entity[], - query: Query, - ): Promise>; - queryRelations( - RelationClass: Class, - relationName: string, - dto: Entity, - query: Query, - ): Promise; - queryRelations( - RelationClass: Class, - relationName: string, - dto: Entity | Entity[], - query: Query, - ): Promise> { - const relationModel = this.Model.model(RelationClass.name) as ReturnModelType Relation>; - const relationQS = new TypegooseQueryService(relationModel, { - documentToObjectOptions: this.documentToObjectOptions, - }); - const dtos: Entity[] = Array.isArray(dto) ? dto : [dto]; - return dtos.reduce(async (prev, curr) => { - const map = await prev; - const ids = curr[relationName as keyof Entity]; - if (Array.isArray(ids)) { - const relQuery = { - ...query, - filter: { - ...query.filter, - id: { in: ids }, - }, - } as Query; - const entities = await relationQS.query(relQuery); - const sortedEntities = ids - .map((id: string) => { - const mongoDBKey = '_id' as keyof Relation; // eslint complains about keys starting with underscore - return entities.find((e) => ((e[mongoDBKey] as unknown) as string).toString() === id.toString()); - }) - .filter((e) => !!e); - map.set(curr, sortedEntities as Relation[]); - } - return map; - }, Promise.resolve(new Map())); - } - - removeRelation(): Promise { - throw new Error('Not implemented yet'); - } - - removeRelations(): Promise { - throw new Error('Not implemented yet'); - } - - setRelation(): Promise { - throw new Error('Not implemented yet'); - } -} diff --git a/tsconfig.json b/tsconfig.json index 04c6cdf92..0d34391fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "@nestjs-query/core": ["packages/core/src"], "@nestjs-query/query-graphql": ["packages/query-graphql/src"], "@nestjs-query/query-typeorm": ["packages/query-typeorm/src"], - "@nestjs-query/query-sequelize": ["packages/query-sequelize/src"] + "@nestjs-query/query-sequelize": ["packages/query-sequelize/src"], + "@nestjs-query/query-mongoose": ["packages/query-mongoose/src"] } }, "references": [ @@ -14,6 +15,7 @@ {"path": "./packages/query-graphql"}, {"path": "./packages/query-typeorm"}, {"path": "./packages/query-sequelize"}, + {"path": "./packages/query-mongoose-"}, {"path": "./examples"} ], "files": []