Skip to content

Commit

Permalink
Feature/nested filters (#43)
Browse files Browse the repository at this point in the history
* Implement nested filters

* Update deps

---------

Co-authored-by: Alexey Panfilkin <apanfilkin>
  • Loading branch information
Adrinalin4ik authored Aug 13, 2024
1 parent 1ecd69e commit f61ebb7
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 245 deletions.
8 changes: 6 additions & 2 deletions lib/filters/decorators/resolver.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BaseEntity } from "../../common";
import { standardize } from "../../utils/functions";
import { FILTER_DECORATOR_CUSTOM_FIELDS_METADATA_KEY } from "../constants";
import { getFilterFullInputType } from "../input-type-generator";
import { convertFilterParameters } from "../query.builder";
import { EOperationType, convertFilterParameters } from "../query.builder";
import { GraphqlFilterFieldMetadata, GraphqlFilterTypeDecoratorMetadata } from "./field.decorator";

export interface IFilterDecoratorParams {
Expand Down Expand Up @@ -89,6 +89,10 @@ export interface FilterArgs extends Brackets {}
export class FilterPipe implements PipeTransform {
constructor(public readonly args: IFilterPipeArgs) {}
transform(value: any, _metadata: ArgumentMetadata) {
return convertFilterParameters(value, this.args.customFields, this.args.options);
let params = value;
if (!Array.isArray(value)) {
params = [value]
}
return convertFilterParameters(params, EOperationType.AND, this.args.customFields, this.args.options);
}
}
24 changes: 17 additions & 7 deletions lib/filters/input-type-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export type IFilterField<T> = {
};
}

export interface IFilter<T> {
and: IFilterField<T>[];
or: IFilterField<T>[];
}
export type IFilter<T> = {
and?: IFilter<T>[];
or?: IFilter<T>[];
} & IFilterField<T>;

export type RawFilterArgs<T> = IFilter<T> & IFilterField<T>;

Expand Down Expand Up @@ -109,7 +109,7 @@ const generateFilterPropertyType = (field) => {
})

availableOperations.forEach(operationName => {
field.typeFn();
field.typeFn(); // needs to be called to be compiled
Field(() => {
if (arrayLikeOperations.has(OperationQuery[operationName])) {
return [field.typeFn()];
Expand Down Expand Up @@ -209,15 +209,25 @@ function generateFilterInputType<T extends BaseEntity>(classes: T[], name: strin

export const getFilterFullInputType = (classes: BaseEntity[], name: string) => {
const key = `${name}_FilterInputType`;
const baseKey = `${name}_BaseFilterInputType`;
if (filterFullTypes.get(key)) {
return filterFullTypes.get(key);
}
const FilterInputType = generateFilterInputType(classes, name);

@InputType(baseKey)
class BaseEntityInput extends FilterInputType {
@Field(() => [BaseEntityInput], {nullable: true})
and: BaseEntity[];
@Field(() => [BaseEntityInput], {nullable: true})
or: BaseEntity[];
}

@InputType(key)
class EntityWhereInput extends FilterInputType {
@Field(() => [FilterInputType], {nullable: true})
@Field(() => [BaseEntityInput], {nullable: true})
and: BaseEntity[];
@Field(() => [FilterInputType], {nullable: true})
@Field(() => [BaseEntityInput], {nullable: true})
or: BaseEntity[];
}
filterFullTypes.set(key, EntityWhereInput);
Expand Down
79 changes: 37 additions & 42 deletions lib/filters/query.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { GraphqlFilterFieldMetadata } from "./decorators/field.decorator";
import { IFilterDecoratorParams } from "./decorators/resolver.decorator";
import { IFilter, OperationQuery } from "./input-type-generator";

export const convertFilterParameters = <T>(parameters?: IFilter<T>, customFields?: Map<string, GraphqlFilterFieldMetadata>, options?: IFilterDecoratorParams) => {
export enum EOperationType {
AND,
OR
}

export const convertFilterParameters = <T>(parameters?: IFilter<T>[], opType: EOperationType = EOperationType.AND, customFields?: Map<string, GraphqlFilterFieldMetadata>, options?: IFilterDecoratorParams) => {
// For tests purposes and GlobalPipes like ValidationPipe that uses class-transformer to transform object to the class.
// If you provide Brackets instead of object to the decorator, it will use your brackets without processing it.
if ((parameters as any)?.whereFactory) return parameters;
Expand All @@ -15,48 +20,38 @@ export const convertFilterParameters = <T>(parameters?: IFilter<T>, customFields
return;
}

const clonnedParams = {...parameters};

delete clonnedParams.and;
delete clonnedParams.or;
for (const op of parameters) {
if (op.and) {
const innerBrackets = convertFilterParameters<T>(op.and, EOperationType.AND, customFields, options);
if (innerBrackets instanceof Brackets) {
qb.andWhere(innerBrackets)
}
}

if (parameters?.and) {
qb.andWhere(
new Brackets((andBracketsQb) => {
for (const op of parameters?.and) {
const andParameters = recursivelyTransformComparators(op, customFields, options?.sqlAlias);
if (andParameters?.length) {
for (const query of andParameters) {
andBracketsQb.andWhere(query[0], query[1]);
}
}
}
})
)
}
if (parameters?.or) {
qb.orWhere(
new Brackets((orBracketsQb) => {
for (const op of parameters?.or) {
const orParameters = recursivelyTransformComparators(op, customFields, options?.sqlAlias);
if (orParameters?.length) {
for (const query of orParameters) {
orBracketsQb.orWhere(query[0], query[1]);
}
}
}
})
)
}
const basicParameters = recursivelyTransformComparators(clonnedParams, customFields, options?.sqlAlias);
if (basicParameters) {
qb.andWhere(
new Brackets((basicParametersQb) => {
for (const query of basicParameters) {
basicParametersQb.andWhere(query[0], query[1]);
if (op.or) {
const innerBrackets = convertFilterParameters<T>(op.or, EOperationType.OR, customFields, options);
if (innerBrackets instanceof Brackets) {
qb.orWhere(innerBrackets)
}
}


const clonnedOp = {...op};

delete clonnedOp.and;
delete clonnedOp.or;

const basicParameters = recursivelyTransformComparators(clonnedOp, customFields, options?.sqlAlias);
if (basicParameters) {
for (const query of basicParameters) {
if (opType === EOperationType.AND) {
qb.andWhere(query[0], query[1]);
} else {
qb.orWhere(query[0], query[1]);
}
})
)
}
}

}
});
}
Expand Down Expand Up @@ -90,7 +85,7 @@ const recursivelyTransformComparators = (object: Record<string, any>, extendedPa

const buildSqlArgument = (operatorKey: string, field: string, value: any) => {
let result = [];
const argName = `arg_${convertArrayOfStringIntoStringNumber([field])}`
const argName = `arg_${convertArrayOfStringIntoStringNumber([field])}_${Math.floor(Math.random() * 1e6)}`
if (operatorKey === OperationQuery.eq) {
if (value === null || value === 'null') {
result = [`${field} is null`];
Expand Down
22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,28 +56,28 @@
"devDependencies": {
"@apollo/gateway": "^2.8.0",
"@apollo/server": "^4.10.4",
"@nestjs/apollo": "^12.1.0",
"@nestjs/cli": "^10.3.2",
"@nestjs/common": "^10.3.9",
"@nestjs/core": "^10.3.9",
"@nestjs/graphql": ">=12.1.1",
"@nestjs/platform-express": "^10.3.9",
"@nestjs/schematics": "^10.1.1",
"@nestjs/testing": "^10.3.9",
"@nestjs/apollo": "^12.2.0",
"@nestjs/cli": "^10.4.4",
"@nestjs/common": "^10.4.0",
"@nestjs/core": "^10.4.0",
"@nestjs/graphql": ">=12.2.0",
"@nestjs/platform-express": "^10.4.0",
"@nestjs/schematics": "^10.1.3",
"@nestjs/testing": "^10.4.0",
"@nestjs/typeorm": "^10.0.2",
"@types/express": "^4.17.21",
"@types/jest": "29.5.12",
"@types/node": "^20.14.2",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^6.21.0",
"apollo-server-express": "^3.12.1",
"axios": "^1.7.2",
"apollo-server-express": "^3.13.0",
"axios": "^1.7.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"config": "^3.3.11",
"eslint": "^9.4.0",
"graphql": "^16.8.1",
"graphql": "^16.9.0",
"graphql-type-json": "^0.3.2",
"jest": "29.7.0",
"pg": "^8.12.0",
Expand Down
20 changes: 19 additions & 1 deletion test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from './../src/app.module';
import { QUERY1, QUERY2, QUERY3, QUERY4, QUERY5 } from './graphql.query';
import { QUERY1, QUERY2, QUERY3, QUERY4, QUERY5, QUERY6, QUERY7 } from './graphql.query';

describe('AppController (e2e)', () => {
let app: INestApplication;
Expand Down Expand Up @@ -61,4 +61,22 @@ describe('AppController (e2e)', () => {
})
.expect(200);
});
it('Query 6', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
query: QUERY6,
variables: {},
})
.expect(200);
});
it('Query 7', () => {
return request(app.getHttpServer())
.post('/graphql')
.send({
query: QUERY7,
variables: {},
})
.expect(200);
});
});
43 changes: 43 additions & 0 deletions test/graphql.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,46 @@ export const QUERY5 = `
}
`;


export const QUERY6 = `
{
users(where: {
and: {
id: {eq: 1}
or: {
and: {
id: {eq: 2}
is_active: { eq: true }
}
is_active: { eq: true }
id: { eq: 3 }
}
}
}) {
id
}
}
`;

export const QUERY7 = `
{
users(where: {
and: [
{
id: {eq: 1}
or: [
{
and: {
id: {eq: 2}
}
id: { eq: 3 }
}
]
}
]
}) {
id
}
}
`;
Loading

0 comments on commit f61ebb7

Please sign in to comment.