mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-08-01 04:12:06 +00:00
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:
parent
0d7e3ddc8b
commit
546af92638
@ -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;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -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> (
|
||||||
|
Loading…
Reference in New Issue
Block a user