Skip to content

Commit

Permalink
feat: add interceptors
Browse files Browse the repository at this point in the history
  • Loading branch information
kostyazgara committed Oct 27, 2022
1 parent 642e24e commit 5e05cda
Show file tree
Hide file tree
Showing 76 changed files with 1,530 additions and 20 deletions.
136 changes: 126 additions & 10 deletions .pnp.cjs

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/core/lib/execution-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ export class ExecutionContext<
}

constructor(
public readonly buildQueryBuilder: (
trx?: Knex.Transaction,
) => Knex.QueryBuilder<TRecord, TResult>,
public readonly buildQueryBuilder: () => Knex.QueryBuilder<
TRecord,
TResult
>,
private readonly _queryBuilder: Knex.QueryBuilder<TRecord, TResult>,
private readonly _rawBuilder: Knex.RawBuilder<TRecord, TResult>,
private readonly _constructorRef: Function,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/lib/interfaces/database-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export interface DatabaseOptions<TRecord, TResult> {

export interface SelectDatabaseOptions<TRecord, TResult>
extends DatabaseOptions<TRecord, TResult>,
AliasableRepositoryDatabaseOptions {}
AliasableRepositoryDatabaseOptions {
[field: string]: unknown;
}
2 changes: 1 addition & 1 deletion packages/core/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export class Repository<
handler?: Function,
): Knex.QueryBuilder<TRecord, TResult> {
const context = new ExecutionContext(
(trx?: Knex.Transaction) => this.pureQueryBuilder(trx),
() => this.queryBuilder(options, handler),
queryBuilder,
this.rawBuilder(options?.transaction),
this.constructor,
Expand Down
6 changes: 6 additions & 0 deletions packages/filter/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*
!package.json
!lib/**/*.js
!index.js
!index.d.ts
!*.d.ts
11 changes: 11 additions & 0 deletions packages/filter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `@knexion/filter`

> TODO: description
## Usage

```
const filter = require('@knexion/filter');
// TODO: DEMONSTRATE API
```
1 change: 1 addition & 0 deletions packages/filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib';
5 changes: 5 additions & 0 deletions packages/filter/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
2 changes: 2 additions & 0 deletions packages/filter/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './interceptors';
export * from './interfaces';
42 changes: 42 additions & 0 deletions packages/filter/lib/interceptors/filter-by-array.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Observable } from 'rxjs';
import {
addPrefixColumn,
ExecutionContext,
RepositoryInterceptor,
RepositoryInterceptorNext,
SelectDatabaseOptions,
} from '@knexion/core';
import { FilterOptions } from '../interfaces';

type FilterByArrayOperator = 'in' | string;

export class FilterByArrayInterceptor<TRecord, TResult>
implements RepositoryInterceptor<TRecord, TResult>
{
constructor(
private readonly name: keyof TRecord,
private readonly value: string[],
private readonly operator: FilterByArrayOperator = 'in',
private readonly options: FilterOptions = {},
) {}

public intercept(
context: ExecutionContext<
TRecord,
TResult,
SelectDatabaseOptions<TRecord, TResult>
>,
next: RepositoryInterceptorNext,
): Observable<unknown> {
const { useAlias = true } = this.options;
const column = useAlias
? addPrefixColumn(this.name as string, context.options.alias)
: (this.name as string);
if (this.operator === 'in') {
context.queryBuilder.whereIn(column, this.value);
} else {
context.queryBuilder.where(column, this.operator, this.value);
}
return next.handle();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Observable } from 'rxjs';
import {
addPrefixColumn,
ExecutionContext,
RepositoryInterceptor,
RepositoryInterceptorNext,
SelectDatabaseOptions,
} from '@knexion/core';
import { buildRangeNumberFilter } from '../utils';
import { FilterOptions, RangeNumberFilter } from '../interfaces';

export class FilterByNumberRangeInterceptor<TRecord, TResult>
implements RepositoryInterceptor<TRecord, TResult>
{
constructor(
private readonly name: keyof TRecord,
private readonly value: RangeNumberFilter,
private readonly options: FilterOptions = {},
) {}
public intercept(
context: ExecutionContext<
TRecord,
TResult,
SelectDatabaseOptions<TRecord, TResult>
>,
next: RepositoryInterceptorNext,
): Observable<unknown> {
const { useAlias = true } = this.options;
const column = useAlias
? addPrefixColumn(this.name as string, context.options.alias)
: (this.name as string);
buildRangeNumberFilter(context.queryBuilder, column, this.value);
return next.handle();
}
}
80 changes: 80 additions & 0 deletions packages/filter/lib/interceptors/filter-timestamp.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Observable } from 'rxjs';
import { Knex } from 'knex';
import {
addPrefixColumn,
ExecutionContext,
RepositoryInterceptor,
RepositoryInterceptorNext,
SelectDatabaseOptions,
} from '@knexion/core';
import { RangeNumberFilter, TimestampFilter } from '../interfaces';

export class FilterTimestampInterceptor<TRecord, TResult>
implements RepositoryInterceptor<TRecord, TResult>
{
constructor(private readonly timestampField: keyof TRecord) {}

public intercept(
context: ExecutionContext<
TRecord,
TResult,
SelectDatabaseOptions<TRecord, TResult>
>,
next: RepositoryInterceptorNext,
): Observable<unknown> {
if (!context.options[this.timestampField as string]) {
return next.handle();
}
const timestampFilter = context.options[this.timestampField as string];
if (this.isNumber(timestampFilter)) {
context.queryBuilder.where(
addPrefixColumn('created_at', context.options.alias),
timestampFilter,
);
} else {
this.buildComplexDateFilterQuery(
context.queryBuilder,
timestampFilter,
context.options,
);
}
return next.handle();
}

private isNumber(dateFilter: number | TimestampFilter): dateFilter is number {
return typeof dateFilter === 'number';
}

private buildComplexDateFilterQuery(
queryBuilder: Knex.QueryBuilder<TRecord, TResult>,
dateFilter: RangeNumberFilter,
options: SelectDatabaseOptions<TRecord, TResult>,
): void {
Object.entries(dateFilter).forEach(([operator, value]) =>
this.getQueryForDateOperator(
queryBuilder,
operator as 'gt' | 'gte' | 'lt' | 'lte',
value,
options.alias,
),
);
}

private getQueryForDateOperator(
queryBuilder: Knex.QueryBuilder<TRecord, TResult>,
operator: 'gt' | 'gte' | 'lt' | 'lte',
value: number,
alias?: string,
): void {
const createdAtColumnName = addPrefixColumn('created_at', alias);
if (operator === 'gt') {
queryBuilder.where(createdAtColumnName, '>', value);
} else if (operator === 'gte') {
queryBuilder.where(createdAtColumnName, '>=', value);
} else if (operator === 'lt') {
queryBuilder.where(createdAtColumnName, '<', value);
} else if (operator === 'lte') {
queryBuilder.where(createdAtColumnName, '<=', value);
}
}
}
50 changes: 50 additions & 0 deletions packages/filter/lib/interceptors/filter.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Observable } from 'rxjs';
import {
addPrefixColumn,
ExecutionContext,
RepositoryInterceptor,
RepositoryInterceptorNext,
SelectDatabaseOptions,
} from '@knexion/core';
import { FilterObject, FilterObjectOptions } from '../interfaces';
import { isPlainObject } from '@nestjs/common/utils/shared.utils';

export class FilterInterceptor<TRecord, TResult>
implements RepositoryInterceptor<TRecord, TResult>
{
constructor(private readonly options: FilterObjectOptions = {}) {}

public intercept(
context: ExecutionContext<
TRecord,
TResult,
SelectDatabaseOptions<TRecord, TResult>
>,
next: RepositoryInterceptorNext,
): Observable<unknown> {
const filterObject = context.options[this.options.optionKey ?? 'filter'];
if (isPlainObject(filterObject)) {
context.queryBuilder.where(
this.appendAliasToFilterColumns(
filterObject as FilterObject<TRecord>,
context.options.alias,
),
);
}

return next.handle();
}

private appendAliasToFilterColumns(
filter: Record<string, unknown>,
alias?: string,
): Record<string, unknown> {
return Object.fromEntries(
Object.entries(filter).map(([name, value]) => {
const { useAlias = true } = this.options;
const column = useAlias ? addPrefixColumn(name, alias) : name;
return [column, value];
}),
);
}
}
4 changes: 4 additions & 0 deletions packages/filter/lib/interceptors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './filter.interceptor';
export * from './filter-by-array.interceptor';
export * from './filter-by-number-range.interceptor';
export * from './filter-timestamp.interceptor';
11 changes: 11 additions & 0 deletions packages/filter/lib/interfaces/filter-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface FilterOptions {
useAlias?: boolean;
}

export type FilterObject<TRecord> = Partial<
Record<keyof TRecord, string | number | null>
>;

export interface FilterObjectOptions extends FilterOptions {
optionKey?: string;
}
3 changes: 3 additions & 0 deletions packages/filter/lib/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './filter-options.interface';
export * from './range-number-filter.interface';
export * from './timestamp-filter.interface';
21 changes: 21 additions & 0 deletions packages/filter/lib/interfaces/range-number-filter.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface RangeNumberFilter {
/**
* Return records where the field is after this value.
*/
gt?: number;

/**
* Return records where the field is after or equal to this value.
*/
gte?: number;

/**
* Return records where the field is before this value.
*/
lt?: number;

/**
* Return records where the field is before or equal to this value.
*/
lte?: number;
}
3 changes: 3 additions & 0 deletions packages/filter/lib/interfaces/timestamp-filter.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { RangeNumberFilter } from './range-number-filter.interface';

export type TimestampFilter = number | RangeNumberFilter;
34 changes: 34 additions & 0 deletions packages/filter/lib/utils/build-range-number-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Knex } from 'knex';
import { RangeNumberFilter } from '../interfaces';

const getQueryForDateOperator = (
queryBuilder: Knex.QueryBuilder,
name: string,
operator: 'gt' | 'gte' | 'lt' | 'lte',
value: number,
): void => {
if (operator === 'gt') {
queryBuilder.where(name, '>', value);
} else if (operator === 'gte') {
queryBuilder.where(name, '>=', value);
} else if (operator === 'lt') {
queryBuilder.where(name, '<', value);
} else if (operator === 'lte') {
queryBuilder.where(name, '<=', value);
}
};

export const buildRangeNumberFilter = (
queryBuilder: Knex.QueryBuilder,
name: string,
numberFilter: RangeNumberFilter,
): void => {
Object.entries(numberFilter).forEach(([operator, value]) =>
getQueryForDateOperator(
queryBuilder,
name as string,
operator as 'gt' | 'gte' | 'lt' | 'lte',
value,
),
);
};
1 change: 1 addition & 0 deletions packages/filter/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './build-range-number-filter';
Loading

0 comments on commit 5e05cda

Please sign in to comment.