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,125 +836,230 @@ 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,
relations,
block,
alias,
variableSuffix
));
}
filters.forEach((filter, index) => { buildWhereClause<Entity extends ObjectLiteral> (
let { not, operator, value } = filter; repo: Repository<Entity>,
const relation = relations[field]; where: Readonly<Where> = {},
relations: Readonly<{ [key: string]: any }> = {},
// Handle nested relation filter (only one level deep supported) block: Readonly<CanonicalBlockHeight> = {},
if (operator === 'nested' && relation) { alias: string,
const relationRepo = this.conn.getRepository<any>(relation.entity); variableSuffix?: string
const relationTableName = relationRepo.metadata.tableName; ): Brackets {
let relationSubQuery: SelectQueryBuilder<any> = relationRepo.createQueryBuilder(relationTableName, repo.queryRunner) // Chain resulting where clauses in brackets
.select('1'); return new Brackets(whereBuilder => {
Object.entries(where).forEach(([field, filters], whereIndex) => {
if (relation.isDerived) { // Handle and / or operators
const derivationField = relation.field; if (field === 'and' || field === 'or') {
relationSubQuery = relationSubQuery.where(`${relationTableName}.${derivationField} = ${alias}.id`); this.buildWhereClauseWithLogicalFilter(
} else { repo,
// Column has to exist for non-derived fields whereBuilder,
assert(columnMetadata); filters as Where[],
field,
if (relation.isArray) { relations,
relationSubQuery = relationSubQuery.where(`${relationTableName}.id = ANY(${alias}.${columnMetadata.databaseName})`); block,
} else { alias,
relationSubQuery = relationSubQuery.where(`${relationTableName}.id = ${alias}.${columnMetadata.databaseName}`); `${variableSuffix}_${whereIndex}`
} );
}
// 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, value);
selectQueryBuilder = selectQueryBuilder
.andWhere(`EXISTS (${relationSubQuery.getQuery()})`)
.setParameters(relationSubQuery.getParameters());
return; return;
} }
// Column has to exist if it's not a nested filter filters.forEach((filter, fieldIndex) => {
assert(columnMetadata); let { not, operator, value } = filter as Filter;
const columnIsArray = columnMetadata.isArray; const relation = relations[field];
// Form the where clause. // Handle nested relation filter (only one level deep supported)
assert(operator); if (operator === 'nested' && relation) {
let whereClause = ''; this.buildWhereClauseWithNestedFilter(
repo,
whereBuilder,
value,
field,
relation,
block,
alias,
`${variableSuffix}_${whereIndex}`
);
// In case of array field having contains, NOT comes before the field name return;
// Disregards nocase }
if (columnIsArray && operator.includes('contains')) {
if (not) { // Column has to exist if it's not a logical operator or a nested filter
whereClause += 'NOT '; const columnMetadata = repo.metadata.findColumnWithPropertyName(field);
whereClause += `"${alias}"."${columnMetadata.databaseName}" `; assert(columnMetadata);
whereClause += '&& '; const columnIsArray = columnMetadata.isArray;
// Form the where clause.
assert(operator);
let whereClause = '';
// In case of array field having contains, NOT comes before the field name
// Disregards nocase
if (columnIsArray && operator.includes('contains')) {
if (not) {
whereClause += 'NOT ';
whereClause += `"${alias}"."${columnMetadata.databaseName}" `;
whereClause += '&& ';
} else {
whereClause += `"${alias}"."${columnMetadata.databaseName}" `;
whereClause += '@> ';
}
} else { } else {
whereClause += `"${alias}"."${columnMetadata.databaseName}" `; whereClause += `"${alias}"."${columnMetadata.databaseName}" `;
whereClause += '@> ';
}
} else {
whereClause += `"${alias}"."${columnMetadata.databaseName}" `;
if (not) { if (not) {
if (operator === 'equals') { if (operator === 'equals') {
whereClause += '!'; whereClause += '!';
} else { } else {
whereClause += 'NOT '; whereClause += 'NOT ';
}
}
whereClause += `${OPERATOR_MAP[operator]} `;
}
value = this._transformBigValues(value);
if (operator === 'in') {
whereClause += '(:...';
} else {
whereClause += ':';
}
const variableName = `${field}${variableSuffix}_${whereIndex}_${fieldIndex}`;
whereClause += variableName;
if (operator === 'in') {
whereClause += ')';
if (!value.length) {
whereClause = 'FALSE';
} }
} }
whereClause += `${OPERATOR_MAP[operator]} `; if (!columnIsArray) {
} if (['contains', 'contains_nocase', 'ends', 'ends_nocase'].some(el => el === operator)) {
value = `%${value}`;
}
value = this._transformBigValues(value); if (['contains', 'contains_nocase', 'starts', 'starts_nocase'].some(el => el === operator)) {
if (operator === 'in') { value += '%';
whereClause += '(:...'; }
} else {
whereClause += ':';
}
const variableName = `${field}${index}`;
whereClause += variableName;
if (operator === 'in') {
whereClause += ')';
if (!value.length) {
whereClause = 'FALSE';
}
}
if (!columnIsArray) {
if (['contains', 'contains_nocase', 'ends', 'ends_nocase'].some(el => el === operator)) {
value = `%${value}`;
} }
if (['contains', 'contains_nocase', 'starts', 'starts_nocase'].some(el => el === operator)) { whereBuilder.andWhere(whereClause, { [variableName]: value });
value += '%'; });
}
}
selectQueryBuilder = selectQueryBuilder.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> (