Support logical filter operations in plural GQL queries (#449)

* Handle logical operators in where input

* Handle logical filter operators while building the database query

* Refactor code to build where clause for database query
This commit is contained in:
prathamesh0 2023-11-06 11:17:54 +05:30 committed by GitHub
parent 0d7e3ddc8b
commit 546af92638
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 232 additions and 113 deletions

View File

@ -472,23 +472,28 @@ export class GraphWatcher {
return acc; return acc;
} }
if (['and', 'or'].includes(fieldWithSuffix)) {
assert(Array.isArray(value));
// Parse all the comibations given in the array
acc[fieldWithSuffix] = value.map(w => {
return this._buildFilter(w);
});
return acc;
}
const [field, ...suffix] = fieldWithSuffix.split('_'); const [field, ...suffix] = fieldWithSuffix.split('_');
if (!acc[field]) { if (!acc[field]) {
acc[field] = []; acc[field] = [];
} }
const filter: Filter = { let op = suffix.shift();
value,
not: false,
operator: 'equals'
};
let operator = suffix.shift(); // If op is "" (different from undefined), it means it's a nested filter on a relation field
if (op === '') {
// If the operator is "" (different from undefined), it means it's a nested filter on a relation field (acc[field] as Filter[]).push({
if (operator === '') {
acc[field].push({
// Parse nested filter value // Parse nested filter value
value: this._buildFilter(value), value: this._buildFilter(value),
not: false, not: false,
@ -498,21 +503,27 @@ export class GraphWatcher {
return acc; return acc;
} }
if (operator === 'not') { const filter: Filter = {
value,
not: false,
operator: 'equals'
};
if (op === 'not') {
filter.not = true; filter.not = true;
operator = suffix.shift(); op = suffix.shift();
} }
if (operator) { if (op) {
filter.operator = operator as keyof typeof OPERATOR_MAP; filter.operator = op as keyof typeof OPERATOR_MAP;
} }
// If filter field ends with "nocase", use case insensitive version of the operator // If filter field ends with "nocase", use case insensitive version of the operator
if (suffix[suffix.length - 1] === 'nocase') { if (suffix[suffix.length - 1] === 'nocase') {
filter.operator = `${operator}_nocase` as keyof typeof OPERATOR_MAP; filter.operator = `${op}_nocase` as keyof typeof OPERATOR_MAP;
} }
acc[field].push(filter); (acc[field] as Filter[]).push(filter);
return acc; return acc;
}, {}); }, {});

View File

@ -17,7 +17,8 @@ import {
ObjectLiteral, ObjectLiteral,
QueryRunner, QueryRunner,
Repository, Repository,
SelectQueryBuilder SelectQueryBuilder,
WhereExpressionBuilder
} from 'typeorm'; } from 'typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import _ from 'lodash'; import _ from 'lodash';
@ -75,7 +76,9 @@ export interface Filter {
} }
export interface Where { export interface Where {
[key: string]: Filter[]; // Where[] in case of and / or operators
// Filter[] in others
[key: string]: Filter[] | Where[];
} }
export type Relation = string | { property: string, alias: string } export type Relation = string | { property: string, alias: string }
@ -833,60 +836,72 @@ export class Database {
where: Readonly<Where> = {}, where: Readonly<Where> = {},
relations: Readonly<{ [key: string]: any }> = {}, relations: Readonly<{ [key: string]: any }> = {},
block: Readonly<CanonicalBlockHeight> = {}, block: Readonly<CanonicalBlockHeight> = {},
alias?: string alias?: string,
variableSuffix = ''
): SelectQueryBuilder<Entity> { ): SelectQueryBuilder<Entity> {
if (!alias) { if (!alias) {
alias = selectQueryBuilder.alias; alias = selectQueryBuilder.alias;
} }
Object.entries(where).forEach(([field, filters]) => { return selectQueryBuilder.andWhere(this.buildWhereClause(
const columnMetadata = repo.metadata.findColumnWithPropertyName(field); repo,
where,
filters.forEach((filter, index) => { relations,
let { not, operator, value } = filter; block,
const relation = relations[field]; alias,
variableSuffix
// Handle nested relation filter (only one level deep supported) ));
if (operator === 'nested' && relation) {
const relationRepo = this.conn.getRepository<any>(relation.entity);
const relationTableName = relationRepo.metadata.tableName;
let relationSubQuery: SelectQueryBuilder<any> = relationRepo.createQueryBuilder(relationTableName, repo.queryRunner)
.select('1');
if (relation.isDerived) {
const derivationField = relation.field;
relationSubQuery = relationSubQuery.where(`${relationTableName}.${derivationField} = ${alias}.id`);
} else {
// Column has to exist for non-derived fields
assert(columnMetadata);
if (relation.isArray) {
relationSubQuery = relationSubQuery.where(`${relationTableName}.id = ANY(${alias}.${columnMetadata.databaseName})`);
} else {
relationSubQuery = relationSubQuery.where(`${relationTableName}.id = ${alias}.${columnMetadata.databaseName}`);
}
} }
// canonicalBlockHashes take precedence over block number if provided buildWhereClause<Entity extends ObjectLiteral> (
if (block.canonicalBlockHashes) { repo: Repository<Entity>,
relationSubQuery = relationSubQuery where: Readonly<Where> = {},
.andWhere(new Brackets(qb => { relations: Readonly<{ [key: string]: any }> = {},
qb.where(`${relationTableName}.block_hash IN (:...relationBlockHashes)`, { relationBlockHashes: block.canonicalBlockHashes }) block: Readonly<CanonicalBlockHeight> = {},
.orWhere(`${relationTableName}.block_number <= :relationCanonicalBlockNumber`, { relationCanonicalBlockNumber: block.number }); alias: string,
})); variableSuffix?: string
} else if (block.number) { ): Brackets {
relationSubQuery = relationSubQuery.andWhere(`${relationTableName}.block_number <= :blockNumber`, { blockNumber: block.number }); // Chain resulting where clauses in brackets
} return new Brackets(whereBuilder => {
Object.entries(where).forEach(([field, filters], whereIndex) => {
relationSubQuery = this.buildQuery(relationRepo, relationSubQuery, value); // Handle and / or operators
selectQueryBuilder = selectQueryBuilder if (field === 'and' || field === 'or') {
.andWhere(`EXISTS (${relationSubQuery.getQuery()})`) this.buildWhereClauseWithLogicalFilter(
.setParameters(relationSubQuery.getParameters()); repo,
whereBuilder,
filters as Where[],
field,
relations,
block,
alias,
`${variableSuffix}_${whereIndex}`
);
return; return;
} }
// Column has to exist if it's not a nested filter filters.forEach((filter, fieldIndex) => {
let { not, operator, value } = filter as Filter;
const relation = relations[field];
// Handle nested relation filter (only one level deep supported)
if (operator === 'nested' && relation) {
this.buildWhereClauseWithNestedFilter(
repo,
whereBuilder,
value,
field,
relation,
block,
alias,
`${variableSuffix}_${whereIndex}`
);
return;
}
// Column has to exist if it's not a logical operator or a nested filter
const columnMetadata = repo.metadata.findColumnWithPropertyName(field);
assert(columnMetadata); assert(columnMetadata);
const columnIsArray = columnMetadata.isArray; const columnIsArray = columnMetadata.isArray;
@ -926,7 +941,7 @@ export class Database {
whereClause += ':'; whereClause += ':';
} }
const variableName = `${field}${index}`; const variableName = `${field}${variableSuffix}_${whereIndex}_${fieldIndex}`;
whereClause += variableName; whereClause += variableName;
if (operator === 'in') { if (operator === 'in') {
@ -947,11 +962,104 @@ export class Database {
} }
} }
selectQueryBuilder = selectQueryBuilder.andWhere(whereClause, { [variableName]: value }); whereBuilder.andWhere(whereClause, { [variableName]: value });
}); });
}); });
});
}
return selectQueryBuilder; buildWhereClauseWithLogicalFilter<Entity extends ObjectLiteral> (
repo: Repository<Entity>,
whereBuilder: WhereExpressionBuilder,
wheres: ReadonlyArray<Where> = [],
operator: 'and' | 'or',
relations: Readonly<{ [key: string]: any }> = {},
block: Readonly<CanonicalBlockHeight> = {},
alias: string,
variableSuffix?: string
): void {
switch (operator) {
case 'and': {
whereBuilder.andWhere(new Brackets(andWhereBuilder => {
// Chain all where clauses using AND
wheres.forEach(w => {
andWhereBuilder.andWhere(this.buildWhereClause(
repo,
w,
relations,
block,
alias,
variableSuffix
));
});
}));
break;
}
case 'or': {
whereBuilder.andWhere(new Brackets(orWhereBuilder => {
// Chain all where clauses using OR
wheres.forEach(w => {
orWhereBuilder.orWhere(this.buildWhereClause(
repo,
w,
relations,
block,
alias,
variableSuffix
));
});
}));
break;
}
}
}
buildWhereClauseWithNestedFilter<Entity extends ObjectLiteral> (
repo: Repository<Entity>,
whereBuilder: WhereExpressionBuilder,
where: Readonly<Where> = {},
field: string,
relation: Readonly<any> = {},
block: Readonly<CanonicalBlockHeight> = {},
alias: string,
variableSuffix?: string
): void {
const relationRepo = this.conn.getRepository<any>(relation.entity);
const relationTableName = relationRepo.metadata.tableName;
let relationSubQuery: SelectQueryBuilder<any> = relationRepo.createQueryBuilder(relationTableName, repo.queryRunner)
.select('1');
if (relation.isDerived) {
const derivationField = relation.field;
relationSubQuery = relationSubQuery.where(`${relationTableName}.${derivationField} = ${alias}.id`);
} else {
// Column has to exist for non-derived fields
const columnMetadata = repo.metadata.findColumnWithPropertyName(field);
assert(columnMetadata);
if (relation.isArray) {
relationSubQuery = relationSubQuery.where(`${relationTableName}.id = ANY(${alias}.${columnMetadata.databaseName})`);
} else {
relationSubQuery = relationSubQuery.where(`${relationTableName}.id = ${alias}.${columnMetadata.databaseName}`);
}
}
// canonicalBlockHashes take precedence over block number if provided
if (block.canonicalBlockHashes) {
relationSubQuery = relationSubQuery
.andWhere(new Brackets(qb => {
qb.where(`${relationTableName}.block_hash IN (:...relationBlockHashes)`, { relationBlockHashes: block.canonicalBlockHashes })
.orWhere(`${relationTableName}.block_number <= :relationCanonicalBlockNumber`, { relationCanonicalBlockNumber: block.number });
}));
} else if (block.number) {
relationSubQuery = relationSubQuery.andWhere(`${relationTableName}.block_number <= :blockNumber`, { blockNumber: block.number });
}
relationSubQuery = this.buildQuery(relationRepo, relationSubQuery, where, {}, block, undefined, variableSuffix);
whereBuilder.andWhere(`EXISTS (${relationSubQuery.getQuery()})`, relationSubQuery.getParameters());
} }
orderQuery<Entity extends ObjectLiteral> ( orderQuery<Entity extends ObjectLiteral> (