Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested filters for plural queries on subgraph entities #446

Merged
merged 5 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 64 additions & 45 deletions packages/graph-node/src/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import {
Transaction,
EthClient,
DEFAULT_LIMIT,
FILTER_CHANGE_BLOCK
FILTER_CHANGE_BLOCK,
Where,
Filter,
OPERATOR_MAP
} from '@cerc-io/util';

import { Context, GraphData, instantiate } from './loader';
Expand Down Expand Up @@ -322,50 +325,7 @@ export class GraphWatcher {
const dbTx = await this._database.createTransactionRunner();

try {
where = Object.entries(where).reduce((acc: { [key: string]: any }, [fieldWithSuffix, value]) => {
if (fieldWithSuffix === FILTER_CHANGE_BLOCK) {
assert(value.number_gte && typeof value.number_gte === 'number');

// Maintain util.Where type
acc[FILTER_CHANGE_BLOCK] = [{
value: value.number_gte
}];

return acc;
}

const [field, ...suffix] = fieldWithSuffix.split('_');

if (!acc[field]) {
acc[field] = [];
}

const filter = {
value,
not: false,
operator: 'equals'
};

let operator = suffix.shift();

if (operator === 'not') {
filter.not = true;
operator = suffix.shift();
}

if (operator) {
filter.operator = operator;
}

// If filter field ends with "nocase", use case insensitive version of the operator
if (suffix[suffix.length - 1] === 'nocase') {
filter.operator = `${operator}_nocase`;
}

acc[field].push(filter);

return acc;
}, {});
where = this._buildFilter(where);

if (!queryOptions.limit) {
queryOptions.limit = DEFAULT_LIMIT;
Expand Down Expand Up @@ -498,6 +458,65 @@ export class GraphWatcher {

return transaction;
}

_buildFilter (where: { [key: string]: any } = {}): Where {
return Object.entries(where).reduce((acc: Where, [fieldWithSuffix, value]) => {
if (fieldWithSuffix === FILTER_CHANGE_BLOCK) {
assert(value.number_gte && typeof value.number_gte === 'number');

acc[FILTER_CHANGE_BLOCK] = [{
value: value.number_gte,
not: false
}];

return acc;
}

const [field, ...suffix] = fieldWithSuffix.split('_');

if (!acc[field]) {
acc[field] = [];
}

const filter: Filter = {
value,
not: false,
operator: 'equals'
};

let operator = suffix.shift();

// If the operator is "" (different from undefined), it means it's a nested filter on a relation field
if (operator === '') {
acc[field].push({
// Parse nested filter value
value: this._buildFilter(value),
not: false,
operator: 'nested'
});

return acc;
}

if (operator === 'not') {
filter.not = true;
operator = suffix.shift();
}

if (operator) {
filter.operator = operator as keyof typeof OPERATOR_MAP;
}

// If filter field ends with "nocase", use case insensitive version of the operator
if (suffix[suffix.length - 1] === 'nocase') {
filter.operator = `${operator}_nocase` as keyof typeof OPERATOR_MAP;
}

acc[field].push(filter);

return acc;
}, {});
}
}

export const getGraphDbAndWatcher = async (
Expand Down
63 changes: 53 additions & 10 deletions packages/util/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import assert from 'assert';
import {
Between,
Brackets,
Connection,
ConnectionOptions,
createConnection,
Expand Down Expand Up @@ -38,7 +39,8 @@ export const OPERATOR_MAP = {
ends: 'LIKE',
contains_nocase: 'ILIKE',
starts_nocase: 'ILIKE',
ends_nocase: 'ILIKE'
ends_nocase: 'ILIKE',
nested: ''
};

const INSERT_EVENTS_BATCH = 100;
Expand All @@ -48,6 +50,10 @@ export interface BlockHeight {
hash?: string;
}

export interface CanonicalBlockHeight extends BlockHeight {
canonicalBlockHashes?: string[];
}

export enum OrderDirection {
asc = 'asc',
desc = 'desc'
Expand All @@ -60,12 +66,15 @@ export interface QueryOptions {
orderDirection?: OrderDirection;
}

export interface Filter {
// eslint-disable-next-line no-use-before-define
value: any | Where;
not: boolean;
operator?: keyof typeof OPERATOR_MAP;
}

export interface Where {
[key: string]: [{
value: any;
not: boolean;
operator: keyof typeof OPERATOR_MAP;
}]
[key: string]: Filter[];
}

export type Relation = string | { property: string, alias: string }
Expand Down Expand Up @@ -828,19 +837,52 @@ export class Database {
buildQuery<Entity extends ObjectLiteral> (
repo: Repository<Entity>,
selectQueryBuilder: SelectQueryBuilder<Entity>,
where: Where = {},
where: Readonly<Where> = {},
relations: Readonly<{ [key: string]: any }> = {},
block: Readonly<CanonicalBlockHeight> = {},
alias?: string
): SelectQueryBuilder<Entity> {
if (!alias) {
alias = selectQueryBuilder.alias;
}

Object.entries(where).forEach(([field, filters]) => {
// TODO: Handle nested filters on derived and array fields
const columnMetadata = repo.metadata.findColumnWithPropertyName(field);
assert(columnMetadata);

filters.forEach((filter, index) => {
// Form the where clause.
let { not, operator, value } = filter;
const columnMetadata = repo.metadata.findColumnWithPropertyName(field);
assert(columnMetadata);

// Handle nested relation filter
const relation = relations[field];
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')
.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, value);
selectQueryBuilder = selectQueryBuilder
.andWhere(`EXISTS (${relationSubQuery.getQuery()})`)
.setParameters(relationSubQuery.getParameters());

return;
}

// Form the where clause.
let whereClause = `"${alias}"."${columnMetadata.databaseName}" `;

if (columnMetadata.relationMetadata) {
Expand All @@ -858,6 +900,7 @@ export class Database {
}
}

assert(operator);
whereClause += `${OPERATOR_MAP[operator]} `;

value = this._transformBigIntValues(value);
Expand Down
Loading
Loading