diff --git a/packages/uni-info-watcher/src/database.ts b/packages/uni-info-watcher/src/database.ts index 2f45271e..e5182a49 100644 --- a/packages/uni-info-watcher/src/database.ts +++ b/packages/uni-info-watcher/src/database.ts @@ -28,6 +28,23 @@ import { SyncStatus } from './entity/SyncStatus'; const DEFAULT_LIMIT = 100; const DEFAULT_SKIP = 0; +const OPERATOR_MAP = { + equals: '=', + gt: '>', + lt: '<', + gte: '>=', + lte: '<=', + in: 'IN', + contains: 'LIKE', + starts: 'LIKE', + ends: 'LIKE' +}; + +export interface BlockHeight { + number?: number; + hash?: string; +} + export enum OrderDirection { asc = 'asc', desc = 'desc' @@ -40,6 +57,14 @@ export interface QueryOptions { orderDirection?: OrderDirection; } +interface Where { + [key: string]: { + value: any; + not: boolean; + operator: keyof typeof OPERATOR_MAP; + } +} + export class Database { _config: ConnectionOptions _conn!: Connection @@ -230,7 +255,8 @@ export class Database { where: whereOptions, order: { blockNumber: 'DESC' - } + }, + relations: ['pool'] }; let entity = await repo.findOne(findOptions as FindOneOptions); @@ -362,17 +388,16 @@ export class Database { return entity; } - async getEntities (entity: new () => Entity, where: { [key: string]: any } = {}, queryOptions: QueryOptions = {}, relations: string[] = []): Promise { - const { blockHash, blockNumber, ...filter } = where; + async getEntities (entity: new () => Entity, block: BlockHeight, where: Where = {}, queryOptions: QueryOptions = {}, relations: string[] = []): Promise { const repo = this._conn.getRepository(entity); const { tableName } = repo.metadata; - let subQuery = await repo.createQueryBuilder('subTable') + let subQuery = repo.createQueryBuilder('subTable') .select('MAX(subTable.block_number)') .where(`subTable.id = ${tableName}.id`); - if (blockHash) { - const { canonicalBlockNumber, blockHashes } = await this._getBranchInfo(blockHash); + if (block.hash) { + const { canonicalBlockNumber, blockHashes } = await this._getBranchInfo(block.hash); subQuery = subQuery .andWhere(new Brackets(qb => { @@ -381,8 +406,8 @@ export class Database { })); } - if (blockNumber) { - subQuery = subQuery.andWhere('subTable.block_number <= :blockNumber', { blockNumber }); + if (block.number) { + subQuery = subQuery.andWhere('subTable.block_number <= :blockNumber', { blockNumber: block.number }); } let selectQueryBuilder = repo.createQueryBuilder(tableName) @@ -393,8 +418,40 @@ export class Database { selectQueryBuilder = selectQueryBuilder.leftJoinAndSelect(`${repo.metadata.tableName}.${relation}`, relation); }); - Object.entries(filter).forEach(([field, value]) => { - selectQueryBuilder = selectQueryBuilder.andWhere(`${tableName}.${field} = :value`, { value }); + Object.entries(where).forEach(([field, filter]) => { + // Form the where clause. + const { not, operator, value } = filter; + const columnMetadata = repo.metadata.findColumnWithPropertyName(field); + assert(columnMetadata); + let whereClause = `${tableName}.${columnMetadata.propertyAliasName} `; + + if (not) { + if (operator === 'equals') { + whereClause += '!'; + } else { + whereClause += 'NOT '; + } + } + + whereClause += `${OPERATOR_MAP[operator]} `; + + if (['contains', 'starts'].some(el => el === operator)) { + whereClause += '%:'; + } else if (operator === 'in') { + whereClause += '(:...'; + } else { + whereClause += ':'; + } + + whereClause += 'value'; + + if (['contains', 'ends'].some(el => el === operator)) { + whereClause += '%'; + } else if (operator === 'in') { + whereClause += ')'; + } + + selectQueryBuilder = selectQueryBuilder.andWhere(whereClause, { value }); }); const { limit = DEFAULT_LIMIT, orderBy, orderDirection, skip = DEFAULT_SKIP } = queryOptions; @@ -404,7 +461,9 @@ export class Database { .limit(limit); if (orderBy) { - selectQueryBuilder = selectQueryBuilder.orderBy(`${tableName}.${orderBy}`, orderDirection === 'desc' ? 'DESC' : 'ASC'); + const columnMetadata = repo.metadata.findColumnWithPropertyName(orderBy); + assert(columnMetadata); + selectQueryBuilder = selectQueryBuilder.orderBy(`${tableName}.${columnMetadata.propertyAliasName}`, orderDirection === 'desc' ? 'DESC' : 'ASC'); } return selectQueryBuilder.getMany(); diff --git a/packages/uni-info-watcher/src/indexer.ts b/packages/uni-info-watcher/src/indexer.ts index d102200c..e475d06a 100644 --- a/packages/uni-info-watcher/src/indexer.ts +++ b/packages/uni-info-watcher/src/indexer.ts @@ -14,7 +14,7 @@ import { convertTokenToDecimal, loadTransaction, safeDiv } from './utils'; import { createTick } from './utils/tick'; import Decimal from 'decimal.js'; import { Position } from './entity/Position'; -import { Database, QueryOptions, OrderDirection } from './database'; +import { Database, QueryOptions, OrderDirection, BlockHeight } from './database'; import { Event } from './entity/Event'; import { ResultEvent, Block, Transaction, PoolCreatedEvent, InitializeEvent, MintEvent, BurnEvent, SwapEvent, IncreaseLiquidityEvent, DecreaseLiquidityEvent, CollectEvent, TransferEvent } from './events'; import { Factory } from './entity/Factory'; @@ -26,7 +26,6 @@ import { Swap } from './entity/Swap'; import { PositionSnapshot } from './entity/PositionSnapshot'; import { SyncStatus } from './entity/SyncStatus'; import { BlockProgress } from './entity/BlockProgress'; -import { BlockHeight } from './resolvers'; const log = debug('vulcanize:indexer'); @@ -37,7 +36,7 @@ export interface ValueResult { } } -export { OrderDirection }; +export { OrderDirection, BlockHeight }; export class Indexer { _db: Database @@ -203,8 +202,31 @@ export class Indexer { return this._db.getToken({ id, blockHash: block.hash, blockNumber: block.number }); } - async getEntities (entity: new () => Entity, where: Partial, queryOptions: QueryOptions, relations?: string[]): Promise { - const res = await this._db.getEntities(entity, where, queryOptions, relations); + async getEntities (entity: new () => Entity, block: BlockHeight, where: { [key: string]: any } = {}, queryOptions: QueryOptions, relations?: string[]): Promise { + where = Object.entries(where).reduce((acc: { [key: string]: any }, [fieldWithSuffix, value]) => { + const [field, ...suffix] = fieldWithSuffix.split('_'); + + acc[field] = { + value, + not: false, + operator: 'equals' + }; + + let operator = suffix.shift(); + + if (operator === 'not') { + acc[field].not = true; + operator = suffix.shift(); + } + + if (operator) { + acc[field].operator = operator; + } + + return acc; + }, {}); + + const res = await this._db.getEntities(entity, block, where, queryOptions, relations); return res; } @@ -368,7 +390,7 @@ export class Indexer { // TODO: In subgraph factory is fetched by hardcoded factory address. // Currently fetching first factory in database as only one exists. - const [factory] = await this._db.getEntities(Factory, { blockHash: block.hash }, { limit: 1 }); + const [factory] = await this._db.getEntities(Factory, { hash: block.hash }, {}, { limit: 1 }); const token0 = pool.token0; const token1 = pool.token1; @@ -504,7 +526,7 @@ export class Indexer { // TODO: In subgraph factory is fetched by hardcoded factory address. // Currently fetching first factory in database as only one exists. - const [factory] = await this._db.getEntities(Factory, { blockHash: block.hash }, { limit: 1 }); + const [factory] = await this._db.getEntities(Factory, { hash: block.hash }, {}, { limit: 1 }); const token0 = pool.token0; const token1 = pool.token1; @@ -622,7 +644,7 @@ export class Indexer { // TODO: In subgraph factory is fetched by hardcoded factory address. // Currently fetching first factory in database as only one exists. - const [factory] = await this._db.getEntities(Factory, { blockHash: block.hash }, { limit: 1 }); + const [factory] = await this._db.getEntities(Factory, { hash: block.hash }, {}, { limit: 1 }); const pool = await this._db.getPool({ id: contractAddress, blockHash: block.hash }); assert(pool); diff --git a/packages/uni-info-watcher/src/resolvers.ts b/packages/uni-info-watcher/src/resolvers.ts index 7660f4d3..97521f73 100644 --- a/packages/uni-info-watcher/src/resolvers.ts +++ b/packages/uni-info-watcher/src/resolvers.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import BigInt from 'apollo-type-bigint'; import debug from 'debug'; -import { Indexer, OrderDirection } from './indexer'; +import { Indexer, OrderDirection, BlockHeight } from './indexer'; import { Burn } from './entity/Burn'; import { Bundle } from './entity/Bundle'; import { Factory } from './entity/Factory'; @@ -12,13 +12,14 @@ import { Pool } from './entity/Pool'; import { Swap } from './entity/Swap'; import { Tick } from './entity/Tick'; import { Token } from './entity/Token'; +import { TokenDayData } from './entity/TokenDayData'; +import { TokenHourData } from './entity/TokenHourData'; +import { Transaction } from './entity/Transaction'; +import { UniswapDayData } from './entity/UniswapDayData'; const log = debug('vulcanize:resolver'); -export interface BlockHeight { - number?: number; - hash?: string; -} +export { BlockHeight }; export const createResolvers = async (indexer: Indexer): Promise => { assert(indexer); @@ -36,25 +37,25 @@ export const createResolvers = async (indexer: Indexer): Promise => { bundles: async (_: any, { block = {}, first }: { first: number, block: BlockHeight }) => { log('bundles', block, first); - return indexer.getEntities(Bundle, { blockHash: block.hash, blockNumber: block.number }, { limit: first }); + return indexer.getEntities(Bundle, block, {}, { limit: first }); }, - burns: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: Partial }) => { + burns: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('burns', first, orderBy, orderDirection, where); - return indexer.getEntities(Burn, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); + return indexer.getEntities(Burn, {}, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); }, factories: async (_: any, { block = {}, first }: { first: number, block: BlockHeight }) => { log('factories', block, first); - return indexer.getEntities(Factory, { blockHash: block.hash, blockNumber: block.number }, { limit: first }); + return indexer.getEntities(Factory, block, {}, { limit: first }); }, - mints: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: Partial }) => { + mints: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('burns', first, orderBy, orderDirection, where); - return indexer.getEntities(Mint, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); + return indexer.getEntities(Mint, {}, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); }, pool: async (_: any, { id, block = {} }: { id: string, block: BlockHeight }) => { @@ -63,32 +64,28 @@ export const createResolvers = async (indexer: Indexer): Promise => { return indexer.getPool(id, block); }, - poolDayDatas: async (_: any, { first, skip, orderBy, orderDirection, where }: { first: number, skip: number, orderBy: string, orderDirection: OrderDirection, where: Partial }) => { + poolDayDatas: async (_: any, { first, skip, orderBy, orderDirection, where }: { first: number, skip: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('poolDayDatas', first, skip, orderBy, orderDirection, where); - return indexer.getEntities(PoolDayData, where, { limit: first, skip, orderBy, orderDirection }); + return indexer.getEntities(PoolDayData, {}, where, { limit: first, skip, orderBy, orderDirection }); }, - pools: async (_: any, { block = {}, first, orderBy, orderDirection, where = {} }: { block: BlockHeight, first: number, orderBy: string, orderDirection: OrderDirection, where: Partial }) => { + pools: async (_: any, { block = {}, first, orderBy, orderDirection, where = {} }: { block: BlockHeight, first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('burns', block, first, orderBy, orderDirection, where); - where.blockHash = block.hash; - where.blockNumber = block.number; - return indexer.getEntities(Pool, where, { limit: first, orderBy, orderDirection }, ['token0', 'token1']); + return indexer.getEntities(Pool, block, where, { limit: first, orderBy, orderDirection }, ['token0', 'token1']); }, - swaps: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: Partial }) => { + swaps: async (_: any, { first, orderBy, orderDirection, where }: { first: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('swaps', first, orderBy, orderDirection, where); - return indexer.getEntities(Swap, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); + return indexer.getEntities(Swap, {}, where, { limit: first, orderBy, orderDirection }, ['pool', 'transaction']); }, - ticks: async (_: any, { block = {}, first, skip, where = {} }: { block: BlockHeight, first: number, skip: number, where: Partial }) => { + ticks: async (_: any, { block = {}, first, skip, where = {} }: { block: BlockHeight, first: number, skip: number, where: { [key: string]: any } }) => { log('ticks', block, first, skip, where); - where.blockHash = block.hash; - where.blockNumber = block.number; - return indexer.getEntities(Tick, where, { limit: first, skip }); + return indexer.getEntities(Tick, block, where, { limit: first, skip }); }, token: async (_: any, { id, block = {} }: { id: string, block: BlockHeight }) => { @@ -97,10 +94,34 @@ export const createResolvers = async (indexer: Indexer): Promise => { return indexer.getToken(id, block); }, - tokens: async (_: any, { orderBy, orderDirection, where }: { orderBy: string, orderDirection: OrderDirection, where: Partial }) => { + tokens: async (_: any, { orderBy, orderDirection, where }: { orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { log('tokens', orderBy, orderDirection, where); - return indexer.getEntities(Token, where, { orderBy, orderDirection }); + return indexer.getEntities(Token, {}, where, { orderBy, orderDirection }); + }, + + tokenDayDatas: async (_: any, { first, skip, orderBy, orderDirection, where }: { first: number, skip: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { + log('tokenDayDatas', first, skip, orderBy, orderDirection, where); + + return indexer.getEntities(TokenDayData, {}, where, { limit: first, skip, orderBy, orderDirection }); + }, + + tokenHourDatas: async (_: any, { first, skip, orderBy, orderDirection, where }: { first: number, skip: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { + log('tokenDayDatas', first, skip, orderBy, orderDirection, where); + + return indexer.getEntities(TokenHourData, {}, where, { limit: first, skip, orderBy, orderDirection }); + }, + + transactions: async (_: any, { first, orderBy, orderDirection }: { first: number, orderBy: string, orderDirection: OrderDirection}) => { + log('transactions', first, orderBy, orderDirection); + + return indexer.getEntities(Transaction, {}, {}, { limit: first, orderBy, orderDirection }, ['burns', 'mints', 'swaps']); + }, + + uniswapDayDatas: async (_: any, { first, skip, orderBy, orderDirection, where }: { first: number, skip: number, orderBy: string, orderDirection: OrderDirection, where: { [key: string]: any } }) => { + log('uniswapDayDatas', first, skip, orderBy, orderDirection, where); + + return indexer.getEntities(UniswapDayData, {}, where, { limit: first, skip, orderBy, orderDirection }); } } }; diff --git a/packages/uni-info-watcher/src/utils/interval-updates.ts b/packages/uni-info-watcher/src/utils/interval-updates.ts index 7683332b..aa8ecf0d 100644 --- a/packages/uni-info-watcher/src/utils/interval-updates.ts +++ b/packages/uni-info-watcher/src/utils/interval-updates.ts @@ -21,7 +21,7 @@ export const updateUniswapDayData = async (db: Database, event: { contractAddres // TODO: In subgraph factory is fetched by hardcoded factory address. // Currently fetching first factory in database as only one exists. - const [factory] = await db.getEntities(Factory, { blockHash: block.hash }, { limit: 1 }); + const [factory] = await db.getEntities(Factory, { hash: block.hash }, {}, { limit: 1 }); const dayID = Math.floor(block.timestamp / 86400); // Rounded. const dayStartTimestamp = dayID * 86400;