diff --git a/packages/core/src/helpers/filter.builder.ts b/packages/core/src/helpers/filter.builder.ts index 3cf2b1df9..a3f4ca2d9 100644 --- a/packages/core/src/helpers/filter.builder.ts +++ b/packages/core/src/helpers/filter.builder.ts @@ -1,4 +1,4 @@ -import { Filter, FilterComparisonOperators, FilterComparisons, FilterFieldComparison } from '../interfaces'; +import { Filter, FilterComparisons, FilterFieldComparison } from '../interfaces'; import { ComparisonBuilder } from './comparison.builder'; import { ComparisonField, FilterFn } from './types'; @@ -48,7 +48,7 @@ export class FilterBuilder { field: T, cmp: FilterFieldComparison, ): FilterFn { - const operators = Object.keys(cmp) as FilterComparisonOperators[]; + const operators = Object.keys(cmp) as (keyof FilterFieldComparison)[]; return this.orFilterFn( ...operators.map((operator) => ComparisonBuilder.build(field, operator, cmp[operator] as ComparisonField), diff --git a/packages/core/src/interfaces/filter-field-comparison.interface.ts b/packages/core/src/interfaces/filter-field-comparison.interface.ts index 0fcde3757..c6195e261 100644 --- a/packages/core/src/interfaces/filter-field-comparison.interface.ts +++ b/packages/core/src/interfaces/filter-field-comparison.interface.ts @@ -203,7 +203,7 @@ type BuiltInTypes = * * all other types use [[CommonFieldComparisonType]] */ // eslint-disable-next-line @typescript-eslint/ban-types -export type FilterFieldComparison = FieldType extends string | String +type FilterFieldComparisonType = FieldType extends string | String ? StringFieldComparisons // eslint-disable-next-line @typescript-eslint/ban-types : FieldType extends boolean | Boolean ? BooleanFieldComparisons @@ -212,12 +212,24 @@ export type FilterFieldComparison = FieldType extends string | String : FieldType extends number | Date | RegExp | bigint | BuiltInTypes[] | symbol ? CommonFieldComparisonType : FieldType extends Array - ? CommonFieldComparisonType | Filter + ? CommonFieldComparisonType | Filter // eslint-disable-next-line @typescript-eslint/ban-types + : IsKeys extends true + ? CommonFieldComparisonType & StringFieldComparisons & Filter : CommonFieldComparisonType | Filter; +/** + * Type for field comparisons. + * + * * `string` - [[StringFieldComparisons]] + * * `boolean|null|undefined|never` - [[BooleanFieldComparisons]] + * * all other types use [[CommonFieldComparisonType]] + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export type FilterFieldComparison = FilterFieldComparisonType; + /** * Type for all comparison operators for a field type. * * @typeparam FieldType - The TS type of the field. */ -export type FilterComparisonOperators = keyof FilterFieldComparison; +export type FilterComparisonOperators = keyof FilterFieldComparisonType; diff --git a/packages/query-graphql/__tests__/__fixtures__/filter-allowed-comparisons-input-type.graphql b/packages/query-graphql/__tests__/__fixtures__/filter-allowed-comparisons-input-type.graphql new file mode 100644 index 000000000..5b37f191f --- /dev/null +++ b/packages/query-graphql/__tests__/__fixtures__/filter-allowed-comparisons-input-type.graphql @@ -0,0 +1,85 @@ +type Query { + test(input: TestComparisonDtoFilter!): Int! +} + +input TestComparisonDtoFilter { + and: [TestAllowedComparisonFilter!] + or: [TestAllowedComparisonFilter!] + id: NumberFieldComparison + boolField: TestAllowedComparisonBoolFieldFilterComparison + dateField: TestAllowedComparisonDateFieldFilterComparison + floatField: TestAllowedComparisonFloatFieldFilterComparison + intField: TestAllowedComparisonIntFieldFilterComparison + numberField: TestAllowedComparisonNumberFieldFilterComparison + stringField: TestAllowedComparisonStringFieldFilterComparison +} + +input TestAllowedComparisonFilter { + and: [TestAllowedComparisonFilter!] + or: [TestAllowedComparisonFilter!] + id: NumberFieldComparison + boolField: TestAllowedComparisonBoolFieldFilterComparison + dateField: TestAllowedComparisonDateFieldFilterComparison + floatField: TestAllowedComparisonFloatFieldFilterComparison + intField: TestAllowedComparisonIntFieldFilterComparison + numberField: TestAllowedComparisonNumberFieldFilterComparison + stringField: TestAllowedComparisonStringFieldFilterComparison +} + +input NumberFieldComparison { + is: Boolean + isNot: Boolean + eq: Float + neq: Float + gt: Float + gte: Float + lt: Float + lte: Float + in: [Float!] + notIn: [Float!] + between: NumberFieldComparisonBetween + notBetween: NumberFieldComparisonBetween +} + +input NumberFieldComparisonBetween { + lower: Float! + upper: Float! +} + +input TestAllowedComparisonBoolFieldFilterComparison { + is: Boolean +} + +input TestAllowedComparisonDateFieldFilterComparison { + eq: DateTime + neq: DateTime +} + +""" +A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. +""" +scalar DateTime + +input TestAllowedComparisonFloatFieldFilterComparison { + gt: Float + gte: Float +} + +input TestAllowedComparisonIntFieldFilterComparison { + lt: Int + lte: Int +} + +input TestAllowedComparisonNumberFieldFilterComparison { + eq: Float + neq: Float + gt: Float + gte: Float + lt: Float + lte: Float +} + +input TestAllowedComparisonStringFieldFilterComparison { + like: String + notLike: String +} diff --git a/packages/query-graphql/__tests__/__fixtures__/index.ts b/packages/query-graphql/__tests__/__fixtures__/index.ts index 53660ff6a..c8cd8f1b3 100644 --- a/packages/query-graphql/__tests__/__fixtures__/index.ts +++ b/packages/query-graphql/__tests__/__fixtures__/index.ts @@ -39,6 +39,9 @@ export const mutationArgsTypeSDL = readGraphql(resolve(__dirname, './mutation-ar export const relationInputTypeSDL = readGraphql(resolve(__dirname, './relation-input-type.graphql')); export const relationsInputTypeSDL = readGraphql(resolve(__dirname, './relations-input-type.graphql')); export const filterInputTypeSDL = readGraphql(resolve(__dirname, './filter-input-type.graphql')); +export const filterAllowedComparisonsInputTypeSDL = readGraphql( + resolve(__dirname, './filter-allowed-comparisons-input-type.graphql'), +); export const updateFilterInputTypeSDL = readGraphql(resolve(__dirname, './update-filter-input-type.graphql')); export const deleteFilterInputTypeSDL = readGraphql(resolve(__dirname, './delete-filter-input-type.graphql')); export const subscriptionFilterInputTypeSDL = readGraphql( diff --git a/packages/query-graphql/__tests__/types/query/filter.type.spec.ts b/packages/query-graphql/__tests__/types/query/filter.type.spec.ts index 76b620792..7279e33d8 100644 --- a/packages/query-graphql/__tests__/types/query/filter.type.spec.ts +++ b/packages/query-graphql/__tests__/types/query/filter.type.spec.ts @@ -30,6 +30,7 @@ import { updateFilterInputTypeSDL, deleteFilterInputTypeSDL, subscriptionFilterInputTypeSDL, + filterAllowedComparisonsInputTypeSDL, } from '../../__fixtures__'; describe('filter types', (): void => { @@ -170,6 +171,45 @@ describe('filter types', (): void => { const filterInstance = plainToClass(TestDtoFilter, filterObject); expect(filterInstance.or![0]).toBeInstanceOf(TestGraphQLFilter); }); + + describe('allowedComparisons option', () => { + @ObjectType('TestAllowedComparison') + class TestAllowedComparisonsDto extends BaseType { + @FilterableField({ allowedComparisons: ['is'] }) + boolField!: boolean; + + @FilterableField({ allowedComparisons: ['eq', 'neq'] }) + dateField!: Date; + + @FilterableField(() => Float, { allowedComparisons: ['gt', 'gte'] }) + floatField!: number; + + @FilterableField(() => Int, { allowedComparisons: ['lt', 'lte'] }) + intField!: number; + + @FilterableField({ allowedComparisons: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'] }) + numberField!: number; + + @FilterableField({ allowedComparisons: ['like', 'notLike'] }) + stringField!: string; + } + + const TestGraphQLComparisonFilter: Class> = FilterType(TestAllowedComparisonsDto); + @InputType() + class TestComparisonDtoFilter extends TestGraphQLComparisonFilter {} + + it('should only expose allowed comparisons', () => { + @Resolver() + class FilterTypeSpec { + @Query(() => Int) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + test(@Args('input') input: TestComparisonDtoFilter): number { + return 1; + } + } + return expectSDL([FilterTypeSpec], filterAllowedComparisonsInputTypeSDL); + }); + }); }); describe('UpdateFilterType', () => { diff --git a/packages/query-graphql/src/decorators/filterable-field.decorator.ts b/packages/query-graphql/src/decorators/filterable-field.decorator.ts index e45b9b8b1..7ae6aa956 100644 --- a/packages/query-graphql/src/decorators/filterable-field.decorator.ts +++ b/packages/query-graphql/src/decorators/filterable-field.decorator.ts @@ -1,10 +1,10 @@ -import { Class } from '@nestjs-query/core'; +import { Class, FilterComparisonOperators } from '@nestjs-query/core'; import { Field, FieldOptions, ReturnTypeFunc } from '@nestjs/graphql'; import { getMetadataStorage } from '../metadata'; -/** @internal */ -export const filterFieldMetaDataKey = 'filter:field'; - +export type FilterableFieldOptions = { + allowedComparisons?: FilterComparisonOperators[]; +} & FieldOptions; /** * Decorator for Fields that should be filterable through a [[FilterType]] * @@ -36,17 +36,17 @@ export const filterFieldMetaDataKey = 'filter:field'; * ``` */ export function FilterableField(): PropertyDecorator & MethodDecorator; -export function FilterableField(options: FieldOptions): PropertyDecorator & MethodDecorator; +export function FilterableField(options: FilterableFieldOptions): PropertyDecorator & MethodDecorator; export function FilterableField( returnTypeFunction?: ReturnTypeFunc, - options?: FieldOptions, + options?: FilterableFieldOptions, ): PropertyDecorator & MethodDecorator; -export function FilterableField( - returnTypeFuncOrOptions?: ReturnTypeFunc | FieldOptions, - maybeOptions?: FieldOptions, +export function FilterableField( + returnTypeFuncOrOptions?: ReturnTypeFunc | FilterableFieldOptions, + maybeOptions?: FilterableFieldOptions, ): MethodDecorator | PropertyDecorator { let returnTypeFunc: ReturnTypeFunc | undefined; - let advancedOptions: FieldOptions | undefined; + let advancedOptions: FilterableFieldOptions | undefined; if (typeof returnTypeFuncOrOptions === 'function') { returnTypeFunc = returnTypeFuncOrOptions; advancedOptions = maybeOptions; diff --git a/packages/query-graphql/src/decorators/index.ts b/packages/query-graphql/src/decorators/index.ts index 750b26f1b..348dada4c 100644 --- a/packages/query-graphql/src/decorators/index.ts +++ b/packages/query-graphql/src/decorators/index.ts @@ -1,4 +1,4 @@ -export { FilterableField } from './filterable-field.decorator'; +export { FilterableField, FilterableFieldOptions } from './filterable-field.decorator'; export { ResolverMethodOpts } from './resolver-method.decorator'; export { Connection, diff --git a/packages/query-graphql/src/index.ts b/packages/query-graphql/src/index.ts index 7e06c40a5..a8b52f049 100644 --- a/packages/query-graphql/src/index.ts +++ b/packages/query-graphql/src/index.ts @@ -1,6 +1,7 @@ export * from './types'; export { FilterableField, + FilterableFieldOptions, ResolverMethodOpts, Relation, FilterableRelation, diff --git a/packages/query-graphql/src/metadata/metadata-storage.ts b/packages/query-graphql/src/metadata/metadata-storage.ts index 2c8e0eb12..1703b4311 100644 --- a/packages/query-graphql/src/metadata/metadata-storage.ts +++ b/packages/query-graphql/src/metadata/metadata-storage.ts @@ -2,19 +2,20 @@ import { LazyMetadataStorage } from '@nestjs/graphql/dist/schema-builder/storage import { ObjectTypeMetadata } from '@nestjs/graphql/dist/schema-builder/metadata/object-type.metadata'; import { EnumMetadata } from '@nestjs/graphql/dist/schema-builder/metadata'; import { AggregateResponse, Class, Filter, SortField } from '@nestjs-query/core'; -import { ReturnTypeFunc, FieldOptions, TypeMetadataStorage } from '@nestjs/graphql'; +import { ReturnTypeFunc, TypeMetadataStorage } from '@nestjs/graphql'; import { ReferencesOpts, RelationsOpts, ResolverRelation, ResolverRelationReference } from '../resolvers/relations'; import { ReferencesKeys } from '../resolvers/relations/relations.interface'; import { EdgeType, StaticConnectionType } from '../types/connection'; +import { FilterableFieldOptions } from '../decorators'; /** * @internal */ -export interface FilterableFieldDescriptor { +export interface FilterableFieldDescriptor { propertyName: string; - target: Class; + target: Class; returnTypeFunc?: ReturnTypeFunc; - advancedOptions?: FieldOptions; + advancedOptions?: FilterableFieldOptions; } interface RelationDescriptor { @@ -37,7 +38,7 @@ type ConnectionTypes = 'cursor' | 'array'; * @internal */ export class GraphQLQueryMetadataStorage { - private readonly filterableObjectStorage: Map, FilterableFieldDescriptor[]>; + private readonly filterableObjectStorage: Map, FilterableFieldDescriptor[]>; private readonly filterTypeStorage: Map>>; @@ -54,7 +55,7 @@ export class GraphQLQueryMetadataStorage { private readonly aggregateStorage: Map>>; constructor() { - this.filterableObjectStorage = new Map, FilterableFieldDescriptor[]>(); + this.filterableObjectStorage = new Map, FilterableFieldDescriptor[]>(); this.filterTypeStorage = new Map>>(); this.sortTypeStorage = new Map, Class>>(); this.connectionTypeStorage = new Map>(); @@ -64,7 +65,7 @@ export class GraphQLQueryMetadataStorage { this.aggregateStorage = new Map>>(); } - addFilterableObjectField(type: Class, field: FilterableFieldDescriptor): void { + addFilterableObjectField(type: Class, field: FilterableFieldDescriptor): void { let fields = this.filterableObjectStorage.get(type); if (!fields) { fields = []; @@ -73,7 +74,7 @@ export class GraphQLQueryMetadataStorage { fields.push(field); } - getFilterableObjectFields(type: Class): FilterableFieldDescriptor[] | undefined { + getFilterableObjectFields(type: Class): FilterableFieldDescriptor[] | undefined { const typeFields = this.filterableObjectStorage.get(type) ?? []; const fieldNames = typeFields.map((t) => t.propertyName); const baseClass = Object.getPrototypeOf(type) as Class; diff --git a/packages/query-graphql/src/types/aggregate/aggregate-response.type.ts b/packages/query-graphql/src/types/aggregate/aggregate-response.type.ts index 18410a1c1..016eaa2a6 100644 --- a/packages/query-graphql/src/types/aggregate/aggregate-response.type.ts +++ b/packages/query-graphql/src/types/aggregate/aggregate-response.type.ts @@ -7,7 +7,7 @@ import { UnregisteredObjectType } from '../type.errors'; function NumberAggregatedType( name: string, - fields: FilterableFieldDescriptor[], + fields: FilterableFieldDescriptor[], NumberType: GraphQLScalarType, ): Class> { const fieldNames = fields.map((f) => f.propertyName); @@ -20,7 +20,7 @@ function NumberAggregatedType( return Aggregated; } -function AggregatedType(name: string, fields: FilterableFieldDescriptor[]): Class> { +function AggregatedType(name: string, fields: FilterableFieldDescriptor[]): Class> { @ObjectType(name) class Aggregated {} fields.forEach(({ propertyName, target, returnTypeFunc }) => { diff --git a/packages/query-graphql/src/types/query/field-comparison/field-comparison.factory.ts b/packages/query-graphql/src/types/query/field-comparison/field-comparison.factory.ts index 9e21f3bbd..f01f38622 100644 --- a/packages/query-graphql/src/types/query/field-comparison/field-comparison.factory.ts +++ b/packages/query-graphql/src/types/query/field-comparison/field-comparison.factory.ts @@ -1,4 +1,4 @@ -import { Class, FilterFieldComparison } from '@nestjs-query/core'; +import { Class, FilterFieldComparison, FilterComparisonOperators } from '@nestjs-query/core'; import { IsBoolean, IsOptional } from 'class-validator'; import { upperCaseFirst } from 'upper-case-first'; import { @@ -22,6 +22,7 @@ import { getOrCreateBooleanFieldComparison } from './boolean-field-comparison.ty import { getOrCreateNumberFieldComparison } from './number-field-comparison.type'; import { getOrCreateDateFieldComparison } from './date-field-comparison.type'; import { getOrCreateTimestampFieldComparison } from './timestamp-field-comparison.type'; +import { SkipIf } from '../../../decorators'; /** @internal */ const filterComparisonMap = new Map Class>>(); @@ -67,88 +68,111 @@ const getTypeName = (SomeType: ReturnTypeFuncValue): string => { throw new Error(`Unable to create filter comparison for ${JSON.stringify(SomeType)}.`); }; +const isCustomFieldComparison = (options: FilterComparisonOptions): boolean => { + return !!options.allowedComparisons; +}; + +const getComparisonTypeName = (fieldType: ReturnTypeFuncValue, options: FilterComparisonOptions): string => { + if (isCustomFieldComparison(options)) { + return `${upperCaseFirst(options.fieldName)}FilterComparison`; + } + return `${getTypeName(fieldType)}FilterComparison`; +}; + +const isNotAllowedChecker = (options: FilterComparisonOptions) => { + const { allowedComparisons } = options; + return (cmp: FilterComparisonOperators) => () => { + return allowedComparisons ? !allowedComparisons.includes(cmp) : false; + }; +}; + +type FilterComparisonOptions = { + FieldType: Class; + fieldName: string; + allowedComparisons?: FilterComparisonOperators[]; + returnTypeFunc?: ReturnTypeFunc; +}; + /** @internal */ -export function createFilterComparisonType( - TClass: Class, - returnTypeFunc?: ReturnTypeFunc, -): Class> { - const fieldType = returnTypeFunc ? returnTypeFunc() : TClass; - const inputName = `${getTypeName(fieldType)}FilterComparison`; +export function createFilterComparisonType(options: FilterComparisonOptions): Class> { + const { FieldType, returnTypeFunc } = options; + const fieldType = returnTypeFunc ? returnTypeFunc() : FieldType; + const inputName = getComparisonTypeName(fieldType, options); const generator = filterComparisonMap.get(inputName); if (generator) { return generator() as Class>; } - + const isNotAllowed = isNotAllowedChecker(options as FilterComparisonOptions); @InputType(inputName) class Fc { - @Field(() => Boolean, { nullable: true }) + @SkipIf(isNotAllowed('is'), Field(() => Boolean, { nullable: true })) @IsBoolean() @IsOptional() is?: boolean | null; - @Field(() => Boolean, { nullable: true }) + @SkipIf(isNotAllowed('isNot'), Field(() => Boolean, { nullable: true })) @IsBoolean() @IsOptional() isNot?: boolean | null; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('eq'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) eq?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('neq'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) neq?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('gt'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) gt?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('gte'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) gte?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('lt'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) lt?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('lte'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) lte?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('like'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) like?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('notLike'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) notLike?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('iLike'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) iLike?: T; - @Field(() => fieldType, { nullable: true }) + @SkipIf(isNotAllowed('notILike'), Field(() => fieldType, { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) notILike?: T; - @Field(() => [fieldType], { nullable: true }) + @SkipIf(isNotAllowed('in'), Field(() => [fieldType], { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) in?: T[]; - @Field(() => [fieldType], { nullable: true }) + @SkipIf(isNotAllowed('notIn'), Field(() => [fieldType], { nullable: true })) @IsUndefined() - @Type(() => TClass) + @Type(() => FieldType) notIn?: T[]; } diff --git a/packages/query-graphql/src/types/query/filter.type.ts b/packages/query-graphql/src/types/query/filter.type.ts index 236b6ef30..e3c63a900 100644 --- a/packages/query-graphql/src/types/query/filter.type.ts +++ b/packages/query-graphql/src/types/query/filter.type.ts @@ -2,10 +2,12 @@ import { Class, Filter } from '@nestjs-query/core'; import { InputType, Field } from '@nestjs/graphql'; import { Type } from 'class-transformer'; import { ValidateNested } from 'class-validator'; +import { upperCaseFirst } from 'upper-case-first'; import { getMetadataStorage } from '../../metadata'; import { ResolverRelation } from '../../resolvers/relations'; import { createFilterComparisonType } from './field-comparison'; import { UnregisteredObjectType } from '../type.errors'; +import { getDTONames } from '../../common'; export type FilterableRelations = Record>; @@ -37,8 +39,14 @@ function getOrCreateFilterType( or?: Filter[]; } - fields.forEach(({ propertyName, target, returnTypeFunc }) => { - const FC = createFilterComparisonType(target, returnTypeFunc); + const { baseName } = getDTONames(TClass); + fields.forEach(({ propertyName, target, advancedOptions, returnTypeFunc }) => { + const FC = createFilterComparisonType({ + FieldType: target, + fieldName: `${baseName}${upperCaseFirst(propertyName)}`, + allowedComparisons: advancedOptions?.allowedComparisons, + returnTypeFunc, + }); ValidateNested()(GraphQLFilter.prototype, propertyName); Field(() => FC, { nullable: true })(GraphQLFilter.prototype, propertyName); Type(() => FC)(GraphQLFilter.prototype, propertyName); diff --git a/packages/query-sequelize/src/query/where.builder.ts b/packages/query-sequelize/src/query/where.builder.ts index 1193413ad..be45856a4 100644 --- a/packages/query-sequelize/src/query/where.builder.ts +++ b/packages/query-sequelize/src/query/where.builder.ts @@ -1,5 +1,5 @@ import { WhereOptions, Op, Association } from 'sequelize'; -import { Filter, FilterComparisonOperators, FilterComparisons, FilterFieldComparison } from '@nestjs-query/core'; +import { Filter, FilterComparisons, FilterFieldComparison } from '@nestjs-query/core'; import { EntityComparisonField, SQLComparisonBuilder } from './sql-comparison.builder'; /** @@ -87,7 +87,7 @@ export class WhereBuilder { if (alias && associations.has(alias)) { colName = (associations.get(alias)?.target.rawAttributes[colName as string]?.field ?? colName) as T; } - const opts = Object.keys(cmp) as FilterComparisonOperators[]; + const opts = Object.keys(cmp) as (keyof FilterFieldComparison)[]; if (opts.length === 1) { const cmpType = opts[0]; return this.sqlComparisonBuilder.build(colName, cmpType, cmp[cmpType] as EntityComparisonField, alias); diff --git a/packages/query-typeorm/src/query/where.builder.ts b/packages/query-typeorm/src/query/where.builder.ts index 6c8be116d..c394a74fb 100644 --- a/packages/query-typeorm/src/query/where.builder.ts +++ b/packages/query-typeorm/src/query/where.builder.ts @@ -1,5 +1,5 @@ import { Brackets, WhereExpression } from 'typeorm'; -import { Filter, FilterComparisonOperators, FilterComparisons, FilterFieldComparison } from '@nestjs-query/core'; +import { Filter, FilterComparisons, FilterFieldComparison } from '@nestjs-query/core'; import { EntityComparisonField, SQLComparisonBuilder } from './sql-comparison.builder'; /** @@ -121,7 +121,7 @@ export class WhereBuilder { } return where.andWhere( new Brackets((qb) => { - const opts = Object.keys(cmp) as FilterComparisonOperators[]; + const opts = Object.keys(cmp) as (keyof FilterFieldComparison)[]; const sqlComparisons = opts.map((cmpType) => this.sqlComparisonBuilder.build(field, cmpType, cmp[cmpType] as EntityComparisonField, alias), );