Skip to content

Commit a6a5ec3

Browse files
committed
WIP
1 parent bd5be16 commit a6a5ec3

File tree

10 files changed

+148
-66
lines changed

10 files changed

+148
-66
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { randomString } from '../../src/common';
3+
import { CustomFilter, CustomFilterResult } from '../../src/query/custom-filter.registry';
4+
5+
type IsMultipleOfOpType = 'isMultipleOf';
6+
7+
@Injectable()
8+
export class IsMultipleOfCustomFilter implements CustomFilter<IsMultipleOfOpType> {
9+
constructor() {}
10+
11+
readonly operations: IsMultipleOfOpType[] = ['isMultipleOf'];
12+
13+
apply(field: string, cmp: IsMultipleOfOpType, val: unknown, alias?: string): CustomFilterResult {
14+
alias = alias ? alias : '';
15+
const pname = `param${randomString()}`;
16+
return {
17+
sql: `("${alias}"."${field}" % :${pname}) == 0`,
18+
params: { [pname]: val },
19+
};
20+
}
21+
}
22+
23+
@Injectable()
24+
export class IsMultipleOfDateCustomFilter implements CustomFilter<IsMultipleOfOpType> {
25+
constructor() {}
26+
27+
readonly operations: IsMultipleOfOpType[] = ['isMultipleOf'];
28+
29+
apply(field: string, cmp: IsMultipleOfOpType, val: unknown, alias?: string): CustomFilterResult {
30+
alias = alias ? alias : '';
31+
const pname = `param${randomString()}`;
32+
return {
33+
sql: `(EXTRACT(EPOCH FROM "${alias}"."${field}") / 3600 / 24) % :${pname}) == 0`,
34+
params: { [pname]: val },
35+
};
36+
}
37+
}
38+
39+
type RadiusCustomFilterOp = 'distanceFrom';
40+
41+
@Injectable()
42+
export class RadiusCustomFilter implements CustomFilter<RadiusCustomFilterOp> {
43+
constructor() {}
44+
45+
readonly operations: RadiusCustomFilterOp[] = ['distanceFrom'];
46+
47+
apply(
48+
field: string,
49+
cmp: RadiusCustomFilterOp,
50+
val: { point: { lat: number; lng: number }; radius: number },
51+
alias?: string,
52+
): CustomFilterResult {
53+
alias = alias ? alias : '';
54+
const plat = `param${randomString()}`;
55+
const plng = `param${randomString()}`;
56+
const prad = `param${randomString()}`;
57+
return {
58+
sql: `ST_Distance("${alias}"."${field}", ST_MakePoint(:${plat},:${plng})) <= :${prad}`,
59+
params: { [plat]: val.point.lat, [plng]: val.point.lng, [prad]: val.radius },
60+
};
61+
}
62+
}

packages/query-typeorm/__tests__/__fixtures__/test.entity.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { Column, Entity, OneToMany, ManyToMany, JoinTable, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
2+
import { TypeormQueryFilter, TypeormQueryFilterOpts } from '../../src/decorators/typeorm-query-filter.decorator';
3+
import { IsMultipleOfCustomFilter } from './custom-filters.services';
24
import { TestEntityRelationEntity } from './test-entity-relation.entity';
35
import { TestRelation } from './test-relation.entity';
46

57
@Entity()
8+
@TypeormQueryFilter<TestEntity>({
9+
filter: IsMultipleOfCustomFilter,
10+
} as TypeormQueryFilterOpts<TestEntity>)
611
export class TestEntity {
712
@PrimaryColumn({ name: 'test_entity_pk' })
813
testEntityPk!: string;

packages/query-typeorm/__tests__/module.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { NestjsQueryTypeOrmModule } from '../src';
33
describe('NestjsQueryTypeOrmModule', () => {
44
it('should create a module', () => {
55
class TestEntity {}
6+
67
const typeOrmModule = NestjsQueryTypeOrmModule.forFeature([TestEntity]);
78
expect(typeOrmModule.imports).toHaveLength(1);
89
expect(typeOrmModule.module).toBe(NestjsQueryTypeOrmModule);
9-
expect(typeOrmModule.providers).toHaveLength(1);
10+
expect(typeOrmModule.providers).toHaveLength(2);
1011
expect(typeOrmModule.exports).toHaveLength(2);
1112
});
1213
});
Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,18 @@
1-
import { randomString } from '../src/common';
2-
import { CustomFilterRegistry, CustomFilterResult } from '../src/query';
1+
import { CustomFilterRegistry } from '../src/query';
2+
import {
3+
IsMultipleOfCustomFilter,
4+
IsMultipleOfDateCustomFilter,
5+
RadiusCustomFilter,
6+
} from './__fixtures__/custom-filters.services';
37
import { TestEntity } from './__fixtures__/test.entity';
48

59
export function getCustomFilterRegistry(): CustomFilterRegistry {
610
const customFilterRegistry = new CustomFilterRegistry();
711
// Test for (operation) filter registration (this is valid for all fields of all entities)
8-
customFilterRegistry.setFilter(
9-
{ operation: 'isMultipleOf' },
10-
{
11-
apply(field, cmp, val: number, alias): CustomFilterResult {
12-
alias = alias ? alias : '';
13-
const pname = `param${randomString()}`;
14-
return {
15-
sql: `("${alias}"."${field}" % :${pname}) == 0`,
16-
params: { [pname]: val },
17-
};
18-
},
19-
},
20-
);
12+
customFilterRegistry.setFilter(new IsMultipleOfCustomFilter());
2113
// Test for (class, field, operation) filter overriding the previous operation filter on a specific field
22-
customFilterRegistry.setFilter(
23-
{ operation: 'isMultipleOf', Entity: TestEntity, field: 'dateType' },
24-
{
25-
apply(field, cmp, val: number, alias): CustomFilterResult {
26-
alias = alias ? alias : '';
27-
const pname = `param${randomString()}`;
28-
return {
29-
sql: `(EXTRACT(EPOCH FROM "${alias}"."${field}") / 3600 / 24) % :${pname}) == 0`,
30-
params: { [pname]: val },
31-
};
32-
},
33-
},
34-
);
14+
customFilterRegistry.setFilter(new IsMultipleOfDateCustomFilter(), { Entity: TestEntity, field: 'dateType' });
3515
// Test for (class, field, operation) filter on a virtual property 'fakePointType' that does not really exist on the entity
36-
customFilterRegistry.setFilter(
37-
{ operation: 'distanceFrom', Entity: TestEntity, field: 'fakePointType' },
38-
{
39-
apply(field, cmp, val: { point: { lat: number; lng: number }; radius: number }, alias): CustomFilterResult {
40-
alias = alias ? alias : '';
41-
const plat = `param${randomString()}`;
42-
const plng = `param${randomString()}`;
43-
const prad = `param${randomString()}`;
44-
return {
45-
sql: `ST_Distance("${alias}"."${field}", ST_MakePoint(:${plat},:${plng})) <= :${prad}`,
46-
params: { [plat]: val.point.lat, [plng]: val.point.lng, [prad]: val.radius },
47-
};
48-
},
49-
},
50-
);
16+
customFilterRegistry.setFilter(new RadiusCustomFilter(), { Entity: TestEntity, field: 'fakePointType' });
5117
return customFilterRegistry;
5218
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const TYPEORM_QUERY_FILTER_KEY = 'nestjs-query:typeorm:query-filter';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ArrayReflector, Class } from '@nestjs-query/core';
2+
import { CustomFilter } from '../query/custom-filter.registry';
3+
import { TYPEORM_QUERY_FILTER_KEY } from './constants';
4+
5+
const reflector = new ArrayReflector(TYPEORM_QUERY_FILTER_KEY);
6+
7+
type TypedClassDecorator<Entity> = <Cls extends Class<Entity>>(DTOClass: Cls) => Cls | void;
8+
9+
export interface TypeormQueryFilterOpts<Entity = unknown> {
10+
filter: Class<CustomFilter>;
11+
fields?: (string | keyof Entity)[];
12+
}
13+
14+
export function TypeormQueryFilter<Entity = unknown>(
15+
opts: TypeormQueryFilterOpts<Entity>,
16+
): TypedClassDecorator<Entity> {
17+
return <Cls extends Class<Entity>>(EntityClass: Cls): Cls | void => {
18+
reflector.append(EntityClass, opts);
19+
};
20+
}
21+
22+
export function getTypeormQueryFilters<Entity = unknown>(EntityClass: Class<Entity>): TypeormQueryFilterOpts[] {
23+
return reflector.get(EntityClass) ?? [];
24+
}

packages/query-typeorm/src/module.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
import { Class } from '@nestjs-query/core';
22
import { TypeOrmModule } from '@nestjs/typeorm';
3-
import { DynamicModule } from '@nestjs/common';
3+
import { DynamicModule, Inject, OnModuleInit, Provider } from '@nestjs/common';
44
import { Connection, ConnectionOptions } from 'typeorm';
5+
import { getTypeormQueryFilters } from './decorators/typeorm-query-filter.decorator';
56
import { createTypeOrmQueryServiceProviders } from './providers';
67

7-
export class NestjsQueryTypeOrmModule {
8+
export const CONFIG_KEY = 'nestjs-query:typeorm:config';
9+
10+
interface NestjsQueryTypeOrmModuleConfig {
11+
entities: Class<unknown>[];
12+
}
13+
14+
export class NestjsQueryTypeOrmModule implements OnModuleInit {
15+
constructor(@Inject(CONFIG_KEY) readonly config: NestjsQueryTypeOrmModuleConfig) {}
16+
17+
onModuleInit() {
18+
for (const entity of this.config.entities) {
19+
const customFilters = getTypeormQueryFilters(entity);
20+
if (customFilters.length > 0) {
21+
console.log({ entity, customFilters });
22+
}
23+
}
24+
}
25+
826
static forFeature(entities: Class<unknown>[], connection?: Connection | ConnectionOptions | string): DynamicModule {
927
const queryServiceProviders = createTypeOrmQueryServiceProviders(entities, connection);
1028
const typeOrmModule = TypeOrmModule.forFeature(entities, connection);
29+
const config: Provider = {
30+
provide: CONFIG_KEY,
31+
useValue: { entities },
32+
};
1133
return {
1234
imports: [typeOrmModule],
1335
module: NestjsQueryTypeOrmModule,
14-
providers: [...queryServiceProviders],
36+
providers: [...queryServiceProviders, config],
1537
exports: [...queryServiceProviders, typeOrmModule],
1638
};
1739
}

packages/query-typeorm/src/query/custom-filter.registry.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,12 @@ import { Class } from '@nestjs-query/core';
22
import merge from 'lodash.merge';
33
import { ObjectLiteral } from 'typeorm';
44

5-
/**
6-
* @internal
7-
*/
85
export type CustomFilterResult = { sql: string; params: ObjectLiteral };
96

10-
/**
11-
* @internal
12-
* Used to create custom filters
13-
*/
14-
export interface CustomFilter<Entity = unknown, T = unknown> {
15-
apply(field: keyof Entity | string, cmp: string, val: T, alias?: string): CustomFilterResult;
7+
export interface CustomFilter<OperationType = string, Entity = unknown> {
8+
readonly operations: OperationType[];
9+
10+
apply(field: keyof Entity | string, cmp: OperationType, val: unknown, alias?: string): CustomFilterResult;
1611
}
1712

1813
type EntityCustomFilters = Record<string | symbol | number, Record<string, CustomFilter>>;
@@ -27,7 +22,7 @@ export class CustomFilterRegistry {
2722
getFilter<Entity = unknown>(
2823
opName: string,
2924
opts?: { klass: Class<Entity>; field?: keyof Entity | string },
30-
): CustomFilter<Entity> | undefined {
25+
): CustomFilter<string, Entity> | undefined {
3126
// Most specific: (class, field, operation) filters.
3227
if (opts && opts.klass && opts.field) {
3328
const flt = this.cfoRegistry.get(opts.klass)?.[opts.field]?.[opName];
@@ -39,24 +34,26 @@ export class CustomFilterRegistry {
3934
return this.oRegistry[opName];
4035
}
4136

42-
// (operation) filter overload
43-
// setFilter<Entity>(opSpec: { operation: string }, filter: CustomFilter<Entity>): void;
4437
// Implementation
4538
setFilter<Entity>(
46-
opSpec: { operation: string; Entity?: Class<Entity>; field?: keyof Entity | string },
47-
filter: CustomFilter<Entity>,
39+
filter: CustomFilter<string, Entity>,
40+
opSpec?: { Entity?: Class<Entity>; field?: keyof Entity | string },
4841
): void {
49-
if (opSpec.Entity && opSpec.field) {
42+
if (opSpec?.Entity && opSpec?.field) {
5043
// (entity/class, field, operation) filter
5144
const { Entity, field } = opSpec;
5245
if (!this.cfoRegistry.has(Entity)) {
5346
this.cfoRegistry.set(Entity, {});
5447
}
5548
const entityFilters = this.cfoRegistry.get(Entity) as EntityCustomFilters;
56-
entityFilters[field] = merge(entityFilters[field], { [opSpec.operation]: filter });
57-
} else if (opSpec.operation) {
49+
for (const op of filter.operations) {
50+
entityFilters[field] = merge(entityFilters[field], { [op]: filter });
51+
}
52+
} else {
5853
// (operation) filter
59-
this.oRegistry[opSpec.operation] = filter;
54+
for (const op of filter.operations) {
55+
this.oRegistry[op] = filter;
56+
}
6057
}
6158
}
6259
}

packages/query-typeorm/src/query/where.builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export class WhereBuilder<Entity> {
171171
// Fallback sqlComparisonBuilder
172172
const opts = Object.keys(cmp) as (keyof FilterFieldComparison<Entity[T]> & string)[];
173173
const sqlComparisons = opts.map((cmpType) => {
174-
const customFilter = this.customFilterRegistry?.getFilter(cmpType, { klass, field });
174+
const customFilter = this.customFilterRegistry?.getFilter(cmpType, { klass: klass, field });
175175
// If we have a registered customfilter for this cmpType, this has priority over the standard sqlComparisonBuilder
176176
if (customFilter) {
177177
return customFilter.apply(field, cmpType, cmp[cmpType], alias);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
export class CustomFilterService {
3+
4+
}

0 commit comments

Comments
 (0)