From 11098c441de41462fe6c45742bc317f52ea09711 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Mon, 5 Apr 2021 23:39:17 -0600 Subject: [PATCH] fix(mongoose,typegoose,#881): Allow string objectId filters --- .../filter-field-comparison.interface.ts | 2 - .../__tests__/__fixtures__/test.entity.ts | 2 +- .../query/comparison.builder.spec.ts | 59 ++++++++++++++++- .../__tests__/query/where.builder.spec.ts | 6 +- .../src/query/comparison.builder.ts | 64 +++++++++++------- .../src/query/filter-query.builder.ts | 5 +- .../query-mongoose/src/query/where.builder.ts | 7 +- .../src/services/mongoose-query.service.ts | 7 +- .../src/services/reference-query.service.ts | 14 ++-- .../__tests__/__fixtures__/test.entity.ts | 2 +- .../query/comparison.builder.spec.ts | 58 ++++++++++++++++- .../__tests__/query/where.builder.spec.ts | 3 +- .../src/query/comparison.builder.ts | 65 ++++++++++++------- .../src/query/filter-query.builder.ts | 5 +- .../src/query/where.builder.ts | 6 +- .../src/services/reference-query.service.ts | 44 ++++++------- .../src/services/typegoose-query-service.ts | 9 +-- 17 files changed, 257 insertions(+), 101 deletions(-) diff --git a/packages/core/src/interfaces/filter-field-comparison.interface.ts b/packages/core/src/interfaces/filter-field-comparison.interface.ts index c6195e261..fcb46ad89 100644 --- a/packages/core/src/interfaces/filter-field-comparison.interface.ts +++ b/packages/core/src/interfaces/filter-field-comparison.interface.ts @@ -207,8 +207,6 @@ type FilterFieldComparisonType = FieldTy ? StringFieldComparisons // eslint-disable-next-line @typescript-eslint/ban-types : FieldType extends boolean | Boolean ? BooleanFieldComparisons - : FieldType extends null | undefined | never - ? BooleanFieldComparisons // eslint-disable-next-line @typescript-eslint/no-explicit-any : FieldType extends number | Date | RegExp | bigint | BuiltInTypes[] | symbol ? CommonFieldComparisonType : FieldType extends Array diff --git a/packages/query-mongoose/__tests__/__fixtures__/test.entity.ts b/packages/query-mongoose/__tests__/__fixtures__/test.entity.ts index 575ed6736..a363de56e 100644 --- a/packages/query-mongoose/__tests__/__fixtures__/test.entity.ts +++ b/packages/query-mongoose/__tests__/__fixtures__/test.entity.ts @@ -16,7 +16,7 @@ export class TestEntity extends Document { dateType!: Date; @Prop({ type: SchemaTypes.ObjectId, ref: 'TestReference' }) - testReference?: Types.ObjectId; + testReference?: Types.ObjectId | string; @Prop([{ type: SchemaTypes.ObjectId, ref: 'TestReference' }]) testReferences?: Types.ObjectId[]; diff --git a/packages/query-mongoose/__tests__/query/comparison.builder.spec.ts b/packages/query-mongoose/__tests__/query/comparison.builder.spec.ts index 2c5dfc8ec..1591e82fc 100644 --- a/packages/query-mongoose/__tests__/query/comparison.builder.spec.ts +++ b/packages/query-mongoose/__tests__/query/comparison.builder.spec.ts @@ -1,9 +1,10 @@ import { CommonFieldComparisonBetweenType } from '@nestjs-query/core'; -import { TestEntity } from '../__fixtures__/test.entity'; +import { model, Types } from 'mongoose'; +import { TestEntity, TestEntitySchema } from '../__fixtures__/test.entity'; import { ComparisonBuilder } from '../../src/query'; describe('ComparisonBuilder', (): void => { - const createComparisonBuilder = () => new ComparisonBuilder(); + const createComparisonBuilder = () => new ComparisonBuilder(model('TestEntity', TestEntitySchema)); it('should throw an error for an invalid comparison type', () => { // @ts-ignore @@ -18,6 +19,14 @@ describe('ComparisonBuilder', (): void => { }, }); }); + + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect(createComparisonBuilder().build('testReference', 'eq', '5f74af112fae2b251510e3ad')).toEqual({ + testReference: { + $eq: Types.ObjectId('5f74af112fae2b251510e3ad'), + }, + }); + }); }); it('should build neq sql fragment', (): void => { @@ -153,6 +162,14 @@ describe('ComparisonBuilder', (): void => { }, }); }); + + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect(createComparisonBuilder().build('testReference', 'in', ['5f74af112fae2b251510e3ad'])).toEqual({ + testReference: { + $in: [Types.ObjectId('5f74af112fae2b251510e3ad')], + }, + }); + }); }); describe('notIn comparisons', () => { @@ -164,6 +181,14 @@ describe('ComparisonBuilder', (): void => { }, }); }); + + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect(createComparisonBuilder().build('testReference', 'notIn', ['5f74af112fae2b251510e3ad'])).toEqual({ + testReference: { + $nin: [Types.ObjectId('5f74af112fae2b251510e3ad')], + }, + }); + }); }); describe('between comparisons', () => { @@ -174,6 +199,20 @@ describe('ComparisonBuilder', (): void => { }); }); + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect( + createComparisonBuilder().build('testReference', 'between', { + lower: '5f74af112fae2b251510e3ad', + upper: '5f74af112fae2b251510e3ad', + }), + ).toEqual({ + testReference: { + $gte: Types.ObjectId('5f74af112fae2b251510e3ad'), + $lte: Types.ObjectId('5f74af112fae2b251510e3ad'), + }, + }); + }); + 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( @@ -190,10 +229,24 @@ describe('ComparisonBuilder', (): void => { }); }); + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect( + createComparisonBuilder().build('testReference', 'notBetween', { + lower: '5f74af112fae2b251510e3ad', + upper: '5f74af112fae2b251510e3ad', + }), + ).toEqual({ + testReference: { + $lt: Types.ObjectId('5f74af112fae2b251510e3ad'), + $gt: Types.ObjectId('5f74af112fae2b251510e3ad'), + }, + }); + }); + 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]', + 'Invalid value for notbetween 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 index e125adf1f..031702e41 100644 --- a/packages/query-mongoose/__tests__/query/where.builder.spec.ts +++ b/packages/query-mongoose/__tests__/query/where.builder.spec.ts @@ -1,10 +1,10 @@ import { Filter } from '@nestjs-query/core'; -import { FilterQuery } from 'mongoose'; -import { TestEntity } from '../__fixtures__/test.entity'; +import { FilterQuery, model } from 'mongoose'; +import { TestEntity, TestEntitySchema } from '../__fixtures__/test.entity'; import { WhereBuilder } from '../../src/query'; describe('WhereBuilder', (): void => { - const createWhereBuilder = () => new WhereBuilder(); + const createWhereBuilder = () => new WhereBuilder(model('TestEntity', TestEntitySchema)); const assertFilterQuery = (filter: Filter, expectedFilterQuery: FilterQuery): void => { const actual = createWhereBuilder().build(filter); diff --git a/packages/query-mongoose/src/query/comparison.builder.ts b/packages/query-mongoose/src/query/comparison.builder.ts index bec40a3e8..532796861 100644 --- a/packages/query-mongoose/src/query/comparison.builder.ts +++ b/packages/query-mongoose/src/query/comparison.builder.ts @@ -1,7 +1,8 @@ import { CommonFieldComparisonBetweenType, FilterComparisonOperators } from '@nestjs-query/core'; import escapeRegExp from 'lodash.escaperegexp'; -import { FilterQuery, Document } from 'mongoose'; +import { Model as MongooseModel, FilterQuery, Document, Types, Schema } from 'mongoose'; import { QuerySelector } from 'mongodb'; +import { BadRequestException } from '@nestjs/common'; import { getSchemaKey } from './helpers'; /** @@ -33,7 +34,10 @@ export class ComparisonBuilder { isnot: '$ne', }; - constructor(readonly comparisonMap: Record = ComparisonBuilder.DEFAULT_COMPARISON_MAP) {} + constructor( + readonly Model: MongooseModel, + readonly comparisonMap: Record = ComparisonBuilder.DEFAULT_COMPARISON_MAP, + ) {} /** * Creates a valid SQL fragment with parameters. @@ -52,39 +56,32 @@ export class ComparisonBuilder { let querySelector: QuerySelector | undefined; if (this.comparisonMap[normalizedCmp]) { // comparison operator (e.b. =, !=, >, <) - querySelector = { [this.comparisonMap[normalizedCmp]]: val }; + querySelector = { [this.comparisonMap[normalizedCmp]]: this.convertQueryValue(field, val as Entity[F]) }; } 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 (normalizedCmp.includes('between')) { + querySelector = this.betweenComparison(normalizedCmp, field, val); } if (!querySelector) { - throw new Error(`unknown operator ${JSON.stringify(cmp)}`); + throw new BadRequestException(`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( + private betweenComparison( + cmp: string, + field: F, val: EntityComparisonField, ): QuerySelector { - if (this.isBetweenVal(val)) { - return { $lt: val.lower, $gt: val.upper }; + if (!this.isBetweenVal(val)) { + throw new Error(`Invalid value for ${cmp} expected {lower: val, upper: val} got ${JSON.stringify(val)}`); } - throw new Error(`Invalid value for not between expected {lower: val, upper: val} got ${JSON.stringify(val)}`); + if (cmp === 'notbetween') { + return { $lt: this.convertQueryValue(field, val.lower), $gt: this.convertQueryValue(field, val.upper) }; + } + return { $gte: this.convertQueryValue(field, val.lower), $lte: this.convertQueryValue(field, val.upper) }; } private isBetweenVal( @@ -104,4 +101,27 @@ export class ComparisonBuilder { } return { $regex: regExp }; } + + private convertQueryValue(field: F, val: Entity[F]): Entity[F] { + const schemaType = this.Model.schema.path(getSchemaKey(field as string)); + if (!schemaType) { + throw new BadRequestException(`unknown comparison field ${String(field)}`); + } + if (schemaType instanceof Schema.Types.ObjectId) { + return this.convertToObjectId(val) as Entity[F]; + } + return val; + } + + private convertToObjectId(val: unknown): unknown { + if (Array.isArray(val)) { + return val.map((v) => this.convertToObjectId(v)); + } + if (typeof val === 'string' || typeof val === 'number') { + if (Types.ObjectId.isValid(val)) { + return Types.ObjectId(val); + } + } + return val; + } } diff --git a/packages/query-mongoose/src/query/filter-query.builder.ts b/packages/query-mongoose/src/query/filter-query.builder.ts index 456cb865e..9fe57574c 100644 --- a/packages/query-mongoose/src/query/filter-query.builder.ts +++ b/packages/query-mongoose/src/query/filter-query.builder.ts @@ -1,5 +1,5 @@ import { AggregateQuery, Filter, Query, SortDirection, SortField } from '@nestjs-query/core'; -import { FilterQuery, Document } from 'mongoose'; +import { FilterQuery, Document, Model as MongooseModel } from 'mongoose'; import { AggregateBuilder, MongooseGroupAndAggregate } from './aggregate.builder'; import { getSchemaKey } from './helpers'; import { WhereBuilder } from './where.builder'; @@ -25,7 +25,8 @@ type MongooseAggregateQuery = MongooseQuery & { */ export class FilterQueryBuilder { constructor( - readonly whereBuilder: WhereBuilder = new WhereBuilder(), + readonly Model: MongooseModel, + readonly whereBuilder: WhereBuilder = new WhereBuilder(Model), readonly aggregateBuilder: AggregateBuilder = new AggregateBuilder(), ) {} diff --git a/packages/query-mongoose/src/query/where.builder.ts b/packages/query-mongoose/src/query/where.builder.ts index b19b0c219..df784d5f9 100644 --- a/packages/query-mongoose/src/query/where.builder.ts +++ b/packages/query-mongoose/src/query/where.builder.ts @@ -1,5 +1,5 @@ import { Filter, FilterComparisons, FilterFieldComparison } from '@nestjs-query/core'; -import { FilterQuery, Document } from 'mongoose'; +import { FilterQuery, Document, Model as MongooseModel } from 'mongoose'; import { EntityComparisonField, ComparisonBuilder } from './comparison.builder'; /** @@ -7,7 +7,10 @@ import { EntityComparisonField, ComparisonBuilder } from './comparison.builder'; * Builds a WHERE clause from a Filter. */ export class WhereBuilder { - constructor(readonly comparisonBuilder: ComparisonBuilder = new ComparisonBuilder()) {} + constructor( + readonly Model: MongooseModel, + readonly comparisonBuilder: ComparisonBuilder = new ComparisonBuilder(Model), + ) {} /** * Builds a WHERE clause from a Filter. diff --git a/packages/query-mongoose/src/services/mongoose-query.service.ts b/packages/query-mongoose/src/services/mongoose-query.service.ts index e6045e227..49feca230 100644 --- a/packages/query-mongoose/src/services/mongoose-query.service.ts +++ b/packages/query-mongoose/src/services/mongoose-query.service.ts @@ -44,9 +44,10 @@ type MongoDBDeletedOutput = { export class MongooseQueryService extends ReferenceQueryService implements QueryService, DeepPartial> { - readonly filterQueryBuilder: FilterQueryBuilder = new FilterQueryBuilder(); - - constructor(readonly Model: MongooseModel) { + constructor( + readonly Model: MongooseModel, + readonly filterQueryBuilder: FilterQueryBuilder = new FilterQueryBuilder(Model), + ) { super(); } diff --git a/packages/query-mongoose/src/services/reference-query.service.ts b/packages/query-mongoose/src/services/reference-query.service.ts index a5c4fa38d..41af583a4 100644 --- a/packages/query-mongoose/src/services/reference-query.service.ts +++ b/packages/query-mongoose/src/services/reference-query.service.ts @@ -50,7 +50,7 @@ export abstract class ReferenceQueryService { ): Promise[] | Map[]>> { this.checkForReference('AggregateRelations', relationName); const relationModel = this.getReferenceModel(relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); if (Array.isArray(dto)) { return dto.reduce(async (mapPromise, entity) => { const map = await mapPromise; @@ -108,7 +108,7 @@ export abstract class ReferenceQueryService { } const assembler = AssemblerFactory.getAssembler(RelationClass, Document); const relationModel = this.getReferenceModel(relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); const refFilter = this.getReferenceFilter(relationName, dto, assembler.convertQuery({ filter }).filter); if (!refFilter) { return 0; @@ -135,7 +135,7 @@ export abstract class ReferenceQueryService { opts?: FindRelationOptions, ): Promise<(Relation | undefined) | Map> { this.checkForReference('FindRelation', relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); if (Array.isArray(dto)) { return dto.reduce(async (prev, curr) => { const map = await prev; @@ -173,7 +173,7 @@ export abstract class ReferenceQueryService { query: Query, ): Promise> { this.checkForReference('QueryRelations', relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); if (Array.isArray(dto)) { return dto.reduce(async (mapPromise, entity) => { const map = await mapPromise; @@ -291,8 +291,8 @@ export abstract class ReferenceQueryService { return !!this.Model.schema.virtualpath(refName); } - static getReferenceQueryBuilder(): FilterQueryBuilder { - return new FilterQueryBuilder(); + private getReferenceQueryBuilder(refName: string): FilterQueryBuilder { + return new FilterQueryBuilder(this.getReferenceModel(refName)); } private getReferenceModel(refName: string): MongooseModel { @@ -364,7 +364,7 @@ export abstract class ReferenceQueryService { filter?: Filter, ): Promise { const referenceModel = this.getReferenceModel(relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); return referenceModel.count(referenceQueryBuilder.buildIdFilterQuery(relationIds, filter)).exec(); } } diff --git a/packages/query-typegoose/__tests__/__fixtures__/test.entity.ts b/packages/query-typegoose/__tests__/__fixtures__/test.entity.ts index c28fa554d..57e06bafd 100644 --- a/packages/query-typegoose/__tests__/__fixtures__/test.entity.ts +++ b/packages/query-typegoose/__tests__/__fixtures__/test.entity.ts @@ -20,7 +20,7 @@ export class TestEntity { dateType!: Date; @prop({ ref: TestReference, required: false }) - testReference?: Ref; + testReference?: Ref | string; @prop({ ref: TestReference, required: false }) testReferences?: Ref[]; diff --git a/packages/query-typegoose/__tests__/query/comparison.builder.spec.ts b/packages/query-typegoose/__tests__/query/comparison.builder.spec.ts index 2c5dfc8ec..066301412 100644 --- a/packages/query-typegoose/__tests__/query/comparison.builder.spec.ts +++ b/packages/query-typegoose/__tests__/query/comparison.builder.spec.ts @@ -1,9 +1,11 @@ import { CommonFieldComparisonBetweenType } from '@nestjs-query/core'; +import { getModelForClass } from '@typegoose/typegoose'; +import { Types } from 'mongoose'; import { TestEntity } from '../__fixtures__/test.entity'; import { ComparisonBuilder } from '../../src/query'; describe('ComparisonBuilder', (): void => { - const createComparisonBuilder = () => new ComparisonBuilder(); + const createComparisonBuilder = () => new ComparisonBuilder(getModelForClass(TestEntity)); it('should throw an error for an invalid comparison type', () => { // @ts-ignore @@ -18,6 +20,14 @@ describe('ComparisonBuilder', (): void => { }, }); }); + + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect(createComparisonBuilder().build('testReference', 'eq', '5f74af112fae2b251510e3ad')).toEqual({ + testReference: { + $eq: Types.ObjectId('5f74af112fae2b251510e3ad'), + }, + }); + }); }); it('should build neq sql fragment', (): void => { @@ -153,6 +163,14 @@ describe('ComparisonBuilder', (): void => { }, }); }); + + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect(createComparisonBuilder().build('testReference', 'in', ['5f74af112fae2b251510e3ad'])).toEqual({ + testReference: { + $in: [Types.ObjectId('5f74af112fae2b251510e3ad')], + }, + }); + }); }); describe('notIn comparisons', () => { @@ -164,6 +182,14 @@ describe('ComparisonBuilder', (): void => { }, }); }); + + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect(createComparisonBuilder().build('testReference', 'notIn', ['5f74af112fae2b251510e3ad'])).toEqual({ + testReference: { + $nin: [Types.ObjectId('5f74af112fae2b251510e3ad')], + }, + }); + }); }); describe('between comparisons', () => { @@ -174,6 +200,20 @@ describe('ComparisonBuilder', (): void => { }); }); + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect( + createComparisonBuilder().build('testReference', 'between', { + lower: '5f74af112fae2b251510e3ad', + upper: '5f74af112fae2b251510e3ad', + }), + ).toEqual({ + testReference: { + $gte: Types.ObjectId('5f74af112fae2b251510e3ad'), + $lte: Types.ObjectId('5f74af112fae2b251510e3ad'), + }, + }); + }); + 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( @@ -190,10 +230,24 @@ describe('ComparisonBuilder', (): void => { }); }); + it('should convert query fields to objectIds if the field is an objectId', (): void => { + expect( + createComparisonBuilder().build('testReference', 'notBetween', { + lower: '5f74af112fae2b251510e3ad', + upper: '5f74af112fae2b251510e3ad', + }), + ).toEqual({ + testReference: { + $lt: Types.ObjectId('5f74af112fae2b251510e3ad'), + $gt: Types.ObjectId('5f74af112fae2b251510e3ad'), + }, + }); + }); + 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]', + 'Invalid value for notbetween expected {lower: val, upper: val} got [1,10]', ); }); }); diff --git a/packages/query-typegoose/__tests__/query/where.builder.spec.ts b/packages/query-typegoose/__tests__/query/where.builder.spec.ts index e125adf1f..f367ef660 100644 --- a/packages/query-typegoose/__tests__/query/where.builder.spec.ts +++ b/packages/query-typegoose/__tests__/query/where.builder.spec.ts @@ -1,10 +1,11 @@ import { Filter } from '@nestjs-query/core'; import { FilterQuery } from 'mongoose'; +import { getModelForClass } from '@typegoose/typegoose'; import { TestEntity } from '../__fixtures__/test.entity'; import { WhereBuilder } from '../../src/query'; describe('WhereBuilder', (): void => { - const createWhereBuilder = () => new WhereBuilder(); + const createWhereBuilder = () => new WhereBuilder(getModelForClass(TestEntity)); const assertFilterQuery = (filter: Filter, expectedFilterQuery: FilterQuery): void => { const actual = createWhereBuilder().build(filter); diff --git a/packages/query-typegoose/src/query/comparison.builder.ts b/packages/query-typegoose/src/query/comparison.builder.ts index 23fc88c10..0b94ddca5 100644 --- a/packages/query-typegoose/src/query/comparison.builder.ts +++ b/packages/query-typegoose/src/query/comparison.builder.ts @@ -1,7 +1,9 @@ import { CommonFieldComparisonBetweenType, FilterComparisonOperators } from '@nestjs-query/core'; import escapeRegExp from 'lodash.escaperegexp'; -import { FilterQuery } from 'mongoose'; +import { FilterQuery, Types, Schema } from 'mongoose'; import { QuerySelector } from 'mongodb'; +import { BadRequestException } from '@nestjs/common'; +import { ReturnModelType } from '@typegoose/typegoose'; import { getSchemaKey } from './helpers'; /** @@ -33,7 +35,10 @@ export class ComparisonBuilder { isnot: '$ne', }; - constructor(readonly comparisonMap: Record = ComparisonBuilder.DEFAULT_COMPARISON_MAP) {} + constructor( + readonly Model: ReturnModelType Entity>, + readonly comparisonMap: Record = ComparisonBuilder.DEFAULT_COMPARISON_MAP, + ) {} /** * Creates a valid SQL fragment with parameters. @@ -52,39 +57,32 @@ export class ComparisonBuilder { let querySelector: QuerySelector | undefined; if (this.comparisonMap[normalizedCmp]) { // comparison operator (e.b. =, !=, >, <) - querySelector = { [this.comparisonMap[normalizedCmp]]: val }; + querySelector = { [this.comparisonMap[normalizedCmp]]: this.convertQueryValue(field, val as Entity[F]) }; } 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 (normalizedCmp.includes('between')) { + querySelector = this.betweenComparison(normalizedCmp, field, val); } if (!querySelector) { - throw new Error(`unknown operator ${JSON.stringify(cmp)}`); + throw new BadRequestException(`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( + private betweenComparison( + cmp: string, + field: F, val: EntityComparisonField, ): QuerySelector { - if (this.isBetweenVal(val)) { - return { $lt: val.lower, $gt: val.upper }; + if (!this.isBetweenVal(val)) { + throw new Error(`Invalid value for ${cmp} expected {lower: val, upper: val} got ${JSON.stringify(val)}`); } - throw new Error(`Invalid value for not between expected {lower: val, upper: val} got ${JSON.stringify(val)}`); + if (cmp === 'notbetween') { + return { $lt: this.convertQueryValue(field, val.lower), $gt: this.convertQueryValue(field, val.upper) }; + } + return { $gte: this.convertQueryValue(field, val.lower), $lte: this.convertQueryValue(field, val.upper) }; } private isBetweenVal( @@ -104,4 +102,27 @@ export class ComparisonBuilder { } return { $regex: regExp }; } + + private convertQueryValue(field: F, val: Entity[F]): Entity[F] { + const schemaType = this.Model.schema.path(getSchemaKey(field as string)); + if (!schemaType) { + throw new BadRequestException(`unknown comparison field ${String(field)}`); + } + if (schemaType instanceof Schema.Types.ObjectId) { + return this.convertToObjectId(val) as Entity[F]; + } + return val; + } + + private convertToObjectId(val: unknown): unknown { + if (Array.isArray(val)) { + return val.map((v) => this.convertToObjectId(v)); + } + if (typeof val === 'string' || typeof val === 'number') { + if (Types.ObjectId.isValid(val)) { + return Types.ObjectId(val); + } + } + return val; + } } diff --git a/packages/query-typegoose/src/query/filter-query.builder.ts b/packages/query-typegoose/src/query/filter-query.builder.ts index 895fc8fbd..0c594516e 100644 --- a/packages/query-typegoose/src/query/filter-query.builder.ts +++ b/packages/query-typegoose/src/query/filter-query.builder.ts @@ -1,6 +1,6 @@ import { AggregateQuery, Filter, Query, SortDirection, SortField } from '@nestjs-query/core'; import { FilterQuery } from 'mongoose'; -import { DocumentType } from '@typegoose/typegoose'; +import { DocumentType, ReturnModelType } from '@typegoose/typegoose'; import { AggregateBuilder, TypegooseGroupAndAggregate } from './aggregate.builder'; import { getSchemaKey } from './helpers'; import { WhereBuilder } from './where.builder'; @@ -26,7 +26,8 @@ type TypegooseAggregateQuery = TypegooseQuery & { */ export class FilterQueryBuilder { constructor( - readonly whereBuilder: WhereBuilder = new WhereBuilder(), + readonly Model: ReturnModelType Entity>, + readonly whereBuilder: WhereBuilder = new WhereBuilder(Model), readonly aggregateBuilder: AggregateBuilder = new AggregateBuilder(), ) {} diff --git a/packages/query-typegoose/src/query/where.builder.ts b/packages/query-typegoose/src/query/where.builder.ts index fc82b7940..24578f84e 100644 --- a/packages/query-typegoose/src/query/where.builder.ts +++ b/packages/query-typegoose/src/query/where.builder.ts @@ -1,4 +1,5 @@ import { Filter, FilterComparisons, FilterFieldComparison } from '@nestjs-query/core'; +import { ReturnModelType } from '@typegoose/typegoose'; import { FilterQuery } from 'mongoose'; import { EntityComparisonField, ComparisonBuilder } from './comparison.builder'; @@ -7,7 +8,10 @@ import { EntityComparisonField, ComparisonBuilder } from './comparison.builder'; * Builds a WHERE clause from a Filter. */ export class WhereBuilder { - constructor(readonly comparisonBuilder: ComparisonBuilder = new ComparisonBuilder()) {} + constructor( + readonly Model: ReturnModelType Entity>, + readonly comparisonBuilder: ComparisonBuilder = new ComparisonBuilder(Model), + ) {} /** * Builds a WHERE clause from a Filter. diff --git a/packages/query-typegoose/src/services/reference-query.service.ts b/packages/query-typegoose/src/services/reference-query.service.ts index f1a8bef50..ff15b79f6 100644 --- a/packages/query-typegoose/src/services/reference-query.service.ts +++ b/packages/query-typegoose/src/services/reference-query.service.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { Document, Model as MongooseModel, ToObjectOptions, UpdateQuery } from 'mongoose'; +import { Document, UpdateQuery } from 'mongoose'; import { AggregateQuery, AggregateResponse, @@ -16,7 +16,6 @@ import { Base } from '@typegoose/typegoose/lib/defaultClasses'; import { ReturnModelType, DocumentType, getModelWithString, getClass } from '@typegoose/typegoose'; import { NotFoundException } from '@nestjs/common'; import { AggregateBuilder, FilterQueryBuilder } from '../query'; -import { TypegooseQueryServiceOpts } from './typegoose-query-service'; import { isEmbeddedSchemaTypeOptions, isSchemaTypeWithReferenceOptions, @@ -27,11 +26,7 @@ import { export abstract class ReferenceQueryService { abstract readonly filterQueryBuilder: FilterQueryBuilder; - public readonly toObjectOptions: ToObjectOptions; - - protected constructor(readonly Model: ReturnModelType Entity>, opts?: TypegooseQueryServiceOpts) { - this.toObjectOptions = opts?.toObjectOptions || { virtuals: true }; - } + protected constructor(readonly Model: ReturnModelType Entity>) {} abstract getById(id: string | number, opts?: GetByIdOptions): Promise>; @@ -62,7 +57,7 @@ export abstract class ReferenceQueryService { > { this.checkForReference('AggregateRelations', relationName); const relationModel = this.getReferenceModel(relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); if (Array.isArray(dto)) { return dto.reduce(async (mapPromise, entity) => { const map = await mapPromise; @@ -120,7 +115,7 @@ export abstract class ReferenceQueryService { } const assembler = AssemblerFactory.getAssembler(RelationClass, this.getReferenceEntity(relationName)); const relationModel = this.getReferenceModel(relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); const refFilter = this.getReferenceFilter(relationName, dto, assembler.convertQuery({ filter }).filter); if (!refFilter) { return 0; @@ -147,7 +142,7 @@ export abstract class ReferenceQueryService { opts?: FindRelationOptions, ): Promise<(Relation | undefined) | Map> { this.checkForReference('FindRelation', relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); if (Array.isArray(dto)) { return dto.reduce(async (prev, curr) => { const map = await prev; @@ -187,7 +182,7 @@ export abstract class ReferenceQueryService { query: Query, ): Promise> { this.checkForReference('QueryRelations', relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); if (Array.isArray(dto)) { return dto.reduce(async (mapPromise, entity) => { const map = await mapPromise; @@ -199,10 +194,10 @@ export abstract class ReferenceQueryService { if (!foundEntity) { return []; } - const assembler = AssemblerFactory.getAssembler(RelationClass, this.getReferenceModel(relationName)); + const assembler = AssemblerFactory.getAssembler(RelationClass, this.getReferenceEntity(relationName)); const { filterQuery, options } = referenceQueryBuilder.buildQuery(assembler.convertQuery(query)); const populated = await foundEntity.populate({ path: relationName, match: filterQuery, options }).execPopulate(); - return assembler.convertToDTOs(populated.get(relationName) as Document[]); + return assembler.convertToDTOs(populated.get(relationName)); } async addRelations( @@ -346,22 +341,25 @@ export abstract class ReferenceQueryService { return mergeFilter(filter ?? ({} as Filter), lookupFilter); } - private getReferenceModel(refName: string): MongooseModel { + private getReferenceModel(refName: string): ReturnModelType> { + let refModel: ReturnModelType> | undefined; if (this.isReferencePath(refName)) { const schemaType = this.Model.schema.path(refName); if (isEmbeddedSchemaTypeOptions(schemaType)) { - return getModelWithString(schemaType.$embeddedSchemaType.options.ref) as MongooseModel; - } - if (isSchemaTypeWithReferenceOptions(schemaType)) { - return getModelWithString(schemaType.options.ref) as MongooseModel; + refModel = getModelWithString(schemaType.$embeddedSchemaType.options.ref); + } else if (isSchemaTypeWithReferenceOptions(schemaType)) { + refModel = getModelWithString(schemaType.options.ref); } } else if (this.isVirtualPath(refName)) { const schemaType = this.Model.schema.virtualpath(refName); if (isVirtualTypeWithReferenceOptions(schemaType)) { - return getModelWithString(schemaType.options.ref) as MongooseModel; + refModel = getModelWithString(schemaType.options.ref); } } - throw new Error(`Unable to lookup reference type for ${refName}`); + if (!refModel) { + throw new Error(`Unable to lookup reference type for ${refName}`); + } + return refModel; } private getRefCount( @@ -370,12 +368,12 @@ export abstract class ReferenceQueryService { filter?: Filter, ): Promise { const referenceModel = this.getReferenceModel(relationName); - const referenceQueryBuilder = ReferenceQueryService.getReferenceQueryBuilder(); + const referenceQueryBuilder = this.getReferenceQueryBuilder(relationName); return referenceModel.countDocuments(referenceQueryBuilder.buildIdFilterQuery(relationIds, filter)).exec(); } - static getReferenceQueryBuilder(): FilterQueryBuilder { - return new FilterQueryBuilder(); + private getReferenceQueryBuilder(refName: string): FilterQueryBuilder { + return new FilterQueryBuilder(this.getReferenceModel(refName)); } private checkForReference(operation: string, refName: string, allowVirtual = true): void { diff --git a/packages/query-typegoose/src/services/typegoose-query-service.ts b/packages/query-typegoose/src/services/typegoose-query-service.ts index 3ed179a61..ef9c4de1b 100644 --- a/packages/query-typegoose/src/services/typegoose-query-service.ts +++ b/packages/query-typegoose/src/services/typegoose-query-service.ts @@ -27,10 +27,11 @@ export interface TypegooseQueryServiceOpts { export class TypegooseQueryService extends ReferenceQueryService implements QueryService { - readonly filterQueryBuilder: FilterQueryBuilder = new FilterQueryBuilder(); - - constructor(readonly Model: ReturnModelType Entity>, opts?: TypegooseQueryServiceOpts) { - super(Model, opts); + constructor( + readonly Model: ReturnModelType Entity>, + readonly filterQueryBuilder: FilterQueryBuilder = new FilterQueryBuilder(Model), + ) { + super(Model); } /**