Skip to content

Commit

Permalink
Force UTC timezone to make some tests (e.g. typeorm-query-service.spe…
Browse files Browse the repository at this point in the history
…c) deterministic

Implement type-based custom filters + field specific custom filters
Filter registration through decorators performed on module initialization
Tests
  • Loading branch information
luca-nardelli committed Nov 15, 2021
1 parent 098f83a commit 45aff05
Show file tree
Hide file tree
Showing 36 changed files with 1,161 additions and 160 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"lint": "eslint --cache --ext=.ts .",
"lint:no-cache": "eslint --ext=.ts .",
"lint:fix": "eslint --fix --ext=.ts .",
"jest": "jest --runInBand --coverage",
"jest:e2e": "jest --runInBand --config=./jest.e2e.config.js",
"jest:unit": "jest --coverage --config=./jest.unit.config.js",
"jest": "TZ=UTC jest --runInBand --coverage",
"jest:e2e": "TZ=UTC jest --runInBand --config=./jest.e2e.config.js",
"jest:unit": "TZ=UTC jest --coverage --config=./jest.unit.config.js",
"coverage": "cat ./coverage/lcov.info | coveralls",
"prepare": "husky install"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/helpers/filter.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ export class FilterBuilder {
throw new Error(`unknown comparison ${JSON.stringify(fieldOrNested)}`);
}
const nestedFilterFn = this.build(value);
return (dto?: DTO) => nestedFilterFn(dto ? dto[fieldOrNested] : null);
return (dto?: DTO) => nestedFilterFn(dto ? dto[fieldOrNested] : undefined);
}
}
3 changes: 2 additions & 1 deletion packages/core/src/interfaces/filter.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,6 @@ type FilterGrouping<T> = {
* ```
*
* @typeparam T - the type of object to filter on.
* @typeparam C - custom filters defined on the object.
*/
export type Filter<T> = FilterGrouping<T> & FilterComparisons<T>;
export type Filter<T, C = Record<string, any>> = FilterGrouping<T> & FilterComparisons<T> & { [K in keyof C]: C[K] };
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ export const CONNECTION_OPTIONS: ConnectionOptions = {
logging: false,
};

export function createTestConnection(): Promise<Connection> {
return createConnection(CONNECTION_OPTIONS);
// eslint-disable-next-line @typescript-eslint/ban-types
export function createTestConnection(opts?: { extraEntities: (string | Function)[] }): Promise<Connection> {
const connOpts: ConnectionOptions = {
...CONNECTION_OPTIONS,
entities: [...(CONNECTION_OPTIONS.entities || []), ...(opts?.extraEntities ?? [])],
};
return createConnection(connOpts);
}

export function closeTestConnection(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ColumnType } from 'typeorm';
import { randomString } from '../../src/common';
import { CustomFilter, CustomFilterResult } from '../../src/query/custom-filter.registry';
import { TypeOrmQueryFilter } from '../../src/decorators/typeorm-query-filter.decorator';

type IsMultipleOfOpType = 'isMultipleOf';

@TypeOrmQueryFilter()
export class IsMultipleOfCustomFilter implements CustomFilter<IsMultipleOfOpType> {
readonly operations: IsMultipleOfOpType[] = ['isMultipleOf'];

readonly types: ColumnType[] = [Number, 'integer'];

apply(field: string, cmp: IsMultipleOfOpType, val: unknown, alias?: string): CustomFilterResult {
alias = alias ? alias : '';
const pname = `param${randomString()}`;
return {
sql: `(${alias}.${field} % :${pname}) == 0`,
params: { [pname]: val },
};
}
}

@TypeOrmQueryFilter()
export class IsMultipleOfDateCustomFilter implements CustomFilter<IsMultipleOfOpType> {
readonly operations: IsMultipleOfOpType[] = ['isMultipleOf'];

readonly types: ColumnType[] = [Date, 'date', 'datetime'];

apply(field: string, cmp: IsMultipleOfOpType, val: unknown, alias?: string): CustomFilterResult {
alias = alias ? alias : '';
const pname = `param${randomString()}`;
return {
sql: `(EXTRACT(EPOCH FROM ${alias}.${field}) / 3600 / 24) % :${pname}) == 0`,
params: { [pname]: val },
};
}
}

type RadiusCustomFilterOp = 'distanceFrom';

@TypeOrmQueryFilter({
autoRegister: false,
})
export class RadiusCustomFilter implements CustomFilter<RadiusCustomFilterOp> {
readonly operations: RadiusCustomFilterOp[] = ['distanceFrom'];

apply(
field: string,
cmp: RadiusCustomFilterOp,
val: { point: { lat: number; lng: number }; radius: number },
alias?: string,
): CustomFilterResult {
alias = alias ? alias : '';
const plat = `param${randomString()}`;
const plng = `param${randomString()}`;
const prad = `param${randomString()}`;
return {
sql: `ST_Distance(${alias}.${field}, ST_MakePoint(:${plat},:${plng})) <= :${prad}`,
params: { [plat]: val.point.lat, [plng]: val.point.lng, [prad]: val.radius },
};
}
}
13 changes: 8 additions & 5 deletions packages/query-typeorm/__tests__/__fixtures__/seeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,33 @@ export const TEST_SOFT_DELETE_ENTITIES: TestSoftDeleteEntity[] = [1, 2, 3, 4, 5,
};
});

export const TEST_RELATIONS: TestRelation[] = TEST_ENTITIES.reduce(
(relations, te) => [
// Generate different numberTypes so we can use them for filters later on
export const TEST_RELATIONS: TestRelation[] = TEST_ENTITIES.reduce((relations, te) => {
return [
...relations,
{
testRelationPk: `test-relations-${te.testEntityPk}-1`,
relationName: `${te.stringType}-test-relation-one`,
testEntityId: te.testEntityPk,
uniDirectionalTestEntityId: te.testEntityPk,
numberType: te.numberType * 10 + 1,
},
{
testRelationPk: `test-relations-${te.testEntityPk}-2`,
relationName: `${te.stringType}-test-relation-two`,
testEntityId: te.testEntityPk,
uniDirectionalTestEntityId: te.testEntityPk,
numberType: te.numberType * 10 + 2,
},
{
testRelationPk: `test-relations-${te.testEntityPk}-3`,
relationName: `${te.stringType}-test-relation-three`,
testEntityId: te.testEntityPk,
uniDirectionalTestEntityId: te.testEntityPk,
numberType: te.numberType * 10 + 3,
},
],
[] as TestRelation[],
);
];
}, [] as TestRelation[]);

export const TEST_RELATIONS_OF_RELATION = TEST_RELATIONS.map<Partial<RelationOfTestRelationEntity>>((testRelation) => ({
relationName: `test-relation-of-${testRelation.relationName}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ManyToOne, Column, Entity, JoinColumn, ManyToMany, OneToOne, OneToMany, PrimaryColumn } from 'typeorm';
import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, PrimaryColumn } from 'typeorm';
import { TestEntityRelationEntity } from './test-entity-relation.entity';
import { TestEntity } from './test.entity';
import { RelationOfTestRelationEntity } from './relation-of-test-relation.entity';
Expand All @@ -17,6 +17,9 @@ export class TestRelation {
@Column({ name: 'uni_directional_test_entity_id', nullable: true })
uniDirectionalTestEntityId?: string;

@Column({ name: 'number_type' })
numberType?: number;

@ManyToOne(() => TestEntity, (te) => te.testRelations, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'test_entity_id' })
testEntity?: TestEntity;
Expand Down
8 changes: 7 additions & 1 deletion packages/query-typeorm/__tests__/__fixtures__/test.entity.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Column, Entity, OneToMany, ManyToMany, JoinTable, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, OneToMany, OneToOne, PrimaryColumn } from 'typeorm';
import { WithTypeormQueryFilter } from '../../src/decorators/with-typeorm-entity-query-filter.decorator';
import { RadiusCustomFilter } from './custom-filters.services';
import { TestEntityRelationEntity } from './test-entity-relation.entity';
import { TestRelation } from './test-relation.entity';

@Entity()
@WithTypeormQueryFilter<TestEntity>({
filter: RadiusCustomFilter,
fields: ['fakePointType'],
})
export class TestEntity {
@PrimaryColumn({ name: 'test_entity_pk' })
testEntityPk!: string;
Expand Down
73 changes: 73 additions & 0 deletions packages/query-typeorm/__tests__/metadata/typeorm-metadata.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
import { getQueryTypeormMetadata, QueryTypeormEntityMetadata } from '../../src/common';
import { closeTestConnection, createTestConnection, getTestConnection } from '../__fixtures__/connection.fixture';
import { TestEntityRelationEntity } from '../__fixtures__/test-entity-relation.entity';
import { TestRelation } from '../__fixtures__/test-relation.entity';
import { TestEntity } from '../__fixtures__/test.entity';

describe('TypeormMetadata', (): void => {
class TestEmbedded {
@Column({ type: 'text' })
stringType!: string;

@Column({ type: 'boolean' })
boolType!: boolean;
}

@Entity()
class TestMetadataEntity {
@PrimaryColumn({ type: 'text' })
pk!: string;

@Column({ type: 'text' })
stringType!: string;

@Column({ type: 'boolean' })
boolType!: boolean;

@Column({ type: 'integer' })
numberType!: number;

@Column({ type: 'date' })
dateType!: Date;

@Column({ type: 'datetime' })
datetimeType!: Date;

@Column({ type: 'simple-json' })
jsonType!: any;

@Column(() => TestEmbedded)
embeddedType!: TestEmbedded;
}

beforeEach(() => createTestConnection({ extraEntities: [TestMetadataEntity] }));
afterEach(() => closeTestConnection());

it('Test metadata', (): void => {
const meta = getQueryTypeormMetadata(getTestConnection());
console.log(meta);
// Implicit column types
expect(meta.get(TestEntity)).toMatchObject({
testEntityPk: { metaType: 'property', type: String },
stringType: { metaType: 'property', type: String },
dateType: { metaType: 'property', type: Date },
boolType: { metaType: 'property', type: Boolean },
oneTestRelation: { metaType: 'relation', type: TestRelation },
testRelations: { metaType: 'relation', type: TestRelation },
manyTestRelations: { metaType: 'relation', type: TestRelation },
manyToManyUniDirectional: { metaType: 'relation', type: TestRelation },
testEntityRelation: { metaType: 'relation', type: TestEntityRelationEntity },
} as QueryTypeormEntityMetadata<TestEntity>);
// Explicit column types
expect(meta.get(TestMetadataEntity)).toMatchObject({
pk: { metaType: 'property', type: 'text' },
stringType: { metaType: 'property', type: 'text' },
boolType: { metaType: 'property', type: 'boolean' },
numberType: { metaType: 'property', type: 'integer' },
dateType: { metaType: 'property', type: 'date' },
datetimeType: { metaType: 'property', type: 'datetime' },
jsonType: { metaType: 'property', type: 'simple-json' },
} as QueryTypeormEntityMetadata<TestMetadataEntity>);
});
});
3 changes: 2 additions & 1 deletion packages/query-typeorm/__tests__/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { NestjsQueryTypeOrmModule } from '../src';
describe('NestjsQueryTypeOrmModule', () => {
it('should create a module', () => {
class TestEntity {}

const typeOrmModule = NestjsQueryTypeOrmModule.forFeature([TestEntity]);
expect(typeOrmModule.imports).toHaveLength(1);
expect(typeOrmModule.module).toBe(NestjsQueryTypeOrmModule);
expect(typeOrmModule.providers).toHaveLength(1);
expect(typeOrmModule.providers).toHaveLength(3);
expect(typeOrmModule.exports).toHaveLength(2);
});
});
23 changes: 18 additions & 5 deletions packages/query-typeorm/__tests__/providers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { getQueryServiceToken } from '@nestjs-query/core';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { mock, instance } from 'ts-mockito';
import { createConnection, Repository } from 'typeorm';
import { instance, mock } from 'ts-mockito';
import { createTypeOrmQueryServiceProviders } from '../src/providers';
import { TypeOrmQueryService } from '../src/services';
import { CustomFilterRegistry } from '../src/query';

describe('createTypeOrmQueryServiceProviders', () => {
it('should create a provider for the entity', () => {
it('should create a provider for the entity', async () => {
class TestEntity {}

// We need a connection in order to extract entity metadata
const conn = await createConnection({
type: 'sqlite',
database: ':memory:',
dropSchema: true,
entities: [TestEntity],
synchronize: true,
logging: false,
});
const mockRepo = mock<Repository<TestEntity>>(Repository);
const providers = createTypeOrmQueryServiceProviders([TestEntity]);
const providers = createTypeOrmQueryServiceProviders([TestEntity], conn);
expect(providers).toHaveLength(1);
expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity));
expect(providers[0].inject).toEqual([getRepositoryToken(TestEntity)]);
expect(providers[0].inject).toEqual([getRepositoryToken(TestEntity), CustomFilterRegistry]);
expect(providers[0].useFactory(instance(mockRepo))).toBeInstanceOf(TypeOrmQueryService);

await conn.close();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@ exports[`FilterQueryBuilder #delete with sorting should ignore sorting 1`] = `DE

exports[`FilterQueryBuilder #delete with sorting should ignore sorting 2`] = `Array []`;

exports[`FilterQueryBuilder #select with custom filter should add custom filters 1`] = `SELECT "TestEntity"."test_entity_pk" AS "TestEntity_test_entity_pk", "TestEntity"."string_type" AS "TestEntity_string_type", "TestEntity"."bool_type" AS "TestEntity_bool_type", "TestEntity"."number_type" AS "TestEntity_number_type", "TestEntity"."date_type" AS "TestEntity_date_type", "TestEntity"."oneTestRelationTestRelationPk" AS "TestEntity_oneTestRelationTestRelationPk" FROM "test_entity" "TestEntity" WHERE ("TestEntity"."number_type" >= ? OR "TestEntity"."number_type" <= ? OR ("TestEntity"."number_type" % ?) == 0) AND ((EXTRACT(EPOCH FROM "TestEntity"."date_type") / 3600 / 24) % ?) == 0) AND (ST_Distance(TestEntity.fakePointType, ST_MakePoint(?,?)) <= ?)`;

exports[`FilterQueryBuilder #select with custom filter should add custom filters 2`] = `
Array [
1,
10,
5,
3,
45.3,
9.5,
50000,
]
`;

exports[`FilterQueryBuilder #select with custom filter should add custom filters with aggregate 1`] = `SELECT MAX("TestEntity"."number_type") AS "MAX_numberType" FROM "test_entity" "TestEntity" WHERE ("TestEntity"."number_type" >= ? OR "TestEntity"."number_type" <= ? OR ("TestEntity"."number_type" % ?) == 0) AND ((EXTRACT(EPOCH FROM "TestEntity"."date_type") / 3600 / 24) % ?) == 0) AND (ST_Distance(TestEntity.fakePointType, ST_MakePoint(?,?)) <= ?)`;

exports[`FilterQueryBuilder #select with custom filter should add custom filters with aggregate 2`] = `
Array [
1,
10,
5,
3,
45.3,
9.5,
50000,
]
`;

exports[`FilterQueryBuilder #select with filter should call whereBuilder#build if there is a filter 1`] = `SELECT "TestEntity"."test_entity_pk" AS "TestEntity_test_entity_pk", "TestEntity"."string_type" AS "TestEntity_string_type", "TestEntity"."bool_type" AS "TestEntity_bool_type", "TestEntity"."number_type" AS "TestEntity_number_type", "TestEntity"."date_type" AS "TestEntity_date_type", "TestEntity"."oneTestRelationTestRelationPk" AS "TestEntity_oneTestRelationTestRelationPk" FROM "test_entity" "TestEntity" WHERE "TestEntity"."string_type" = 'foo'`;

exports[`FilterQueryBuilder #select with filter should call whereBuilder#build if there is a filter 2`] = `Array []`;
Expand Down
Loading

0 comments on commit 45aff05

Please sign in to comment.