diff --git a/lerna.json b/lerna.json index bfc5de4e..bf0b3e43 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.2.105", + "version": "0.2.106", "npmClient": "yarn", "useWorkspaces": true, "command": { diff --git a/packages/cache/package.json b/packages/cache/package.json index 7f8fb3c6..24b19313 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/cache", - "version": "0.2.105", + "version": "0.2.106", "description": "Generic object cache", "main": "dist/index.js", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 1bf12be3..fee221ca 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/cli", - "version": "0.2.105", + "version": "0.2.106", "main": "dist/index.js", "license": "AGPL-3.0", "scripts": { @@ -15,13 +15,13 @@ }, "dependencies": { "@apollo/client": "^3.7.1", - "@cerc-io/cache": "^0.2.105", - "@cerc-io/ipld-eth-client": "^0.2.105", + "@cerc-io/cache": "^0.2.106", + "@cerc-io/ipld-eth-client": "^0.2.106", "@cerc-io/libp2p": "^0.42.2-laconic-0.1.4", "@cerc-io/nitro-node": "^0.1.15", - "@cerc-io/peer": "^0.2.105", - "@cerc-io/rpc-eth-client": "^0.2.105", - "@cerc-io/util": "^0.2.105", + "@cerc-io/peer": "^0.2.106", + "@cerc-io/rpc-eth-client": "^0.2.106", + "@cerc-io/util": "^0.2.106", "@ethersproject/providers": "^5.4.4", "@graphql-tools/utils": "^9.1.1", "@ipld/dag-cbor": "^8.0.0", diff --git a/packages/codegen/package.json b/packages/codegen/package.json index 7f75f6dc..a3c944d2 100644 --- a/packages/codegen/package.json +++ b/packages/codegen/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/codegen", - "version": "0.2.105", + "version": "0.2.106", "description": "Code generator", "private": true, "main": "index.js", @@ -20,7 +20,7 @@ }, "homepage": "https://github.com/cerc-io/watcher-ts#readme", "dependencies": { - "@cerc-io/util": "^0.2.105", + "@cerc-io/util": "^0.2.106", "@graphql-tools/load-files": "^6.5.2", "@npmcli/package-json": "^5.0.0", "@poanet/solidity-flattener": "https://github.com/vulcanize/solidity-flattener.git", diff --git a/packages/codegen/src/templates/package-template.handlebars b/packages/codegen/src/templates/package-template.handlebars index 044d6b2e..e01ea43a 100644 --- a/packages/codegen/src/templates/package-template.handlebars +++ b/packages/codegen/src/templates/package-template.handlebars @@ -41,12 +41,12 @@ "homepage": "https://github.com/cerc-io/watcher-ts#readme", "dependencies": { "@apollo/client": "^3.3.19", - "@cerc-io/cli": "^0.2.105", - "@cerc-io/ipld-eth-client": "^0.2.105", - "@cerc-io/solidity-mapper": "^0.2.105", - "@cerc-io/util": "^0.2.105", + "@cerc-io/cli": "^0.2.106", + "@cerc-io/ipld-eth-client": "^0.2.106", + "@cerc-io/solidity-mapper": "^0.2.106", + "@cerc-io/util": "^0.2.106", {{#if (subgraphPath)}} - "@cerc-io/graph-node": "^0.2.105", + "@cerc-io/graph-node": "^0.2.106", {{/if}} "@ethersproject/providers": "^5.4.4", "debug": "^4.3.1", diff --git a/packages/graph-node/package.json b/packages/graph-node/package.json index 79362cc0..642be2c5 100644 --- a/packages/graph-node/package.json +++ b/packages/graph-node/package.json @@ -1,10 +1,10 @@ { "name": "@cerc-io/graph-node", - "version": "0.2.105", + "version": "0.2.106", "main": "dist/index.js", "license": "AGPL-3.0", "devDependencies": { - "@cerc-io/solidity-mapper": "^0.2.105", + "@cerc-io/solidity-mapper": "^0.2.106", "@ethersproject/providers": "^5.4.4", "@graphprotocol/graph-ts": "^0.22.0", "@nomiclabs/hardhat-ethers": "^2.0.2", @@ -51,9 +51,9 @@ "dependencies": { "@apollo/client": "^3.3.19", "@cerc-io/assemblyscript": "0.19.10-watcher-ts-0.1.2", - "@cerc-io/cache": "^0.2.105", - "@cerc-io/ipld-eth-client": "^0.2.105", - "@cerc-io/util": "^0.2.105", + "@cerc-io/cache": "^0.2.106", + "@cerc-io/ipld-eth-client": "^0.2.106", + "@cerc-io/util": "^0.2.106", "@types/json-diff": "^0.5.2", "@types/yargs": "^17.0.0", "bn.js": "^4.11.9", diff --git a/packages/graph-node/src/watcher.ts b/packages/graph-node/src/watcher.ts index 4f230014..7a6dc18d 100644 --- a/packages/graph-node/src/watcher.ts +++ b/packages/graph-node/src/watcher.ts @@ -28,10 +28,6 @@ import { Transaction, EthClient, DEFAULT_LIMIT, - FILTER_CHANGE_BLOCK, - Where, - Filter, - OPERATOR_MAP, ExtraEventData, EthFullTransaction } from '@cerc-io/util'; @@ -379,7 +375,7 @@ export class GraphWatcher { const dbTx = await this._database.createTransactionRunner(); try { - where = this._buildFilter(where); + where = this._database.buildFilter(where); if (!queryOptions.limit) { queryOptions.limit = DEFAULT_LIMIT; @@ -514,76 +510,6 @@ 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; - } - - 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('_'); - - if (!acc[field]) { - acc[field] = []; - } - - let op = suffix.shift(); - - // If op is "" (different from undefined), it means it's a nested filter on a relation field - if (op === '') { - (acc[field] as Filter[]).push({ - // Parse nested filter value - value: this._buildFilter(value), - not: false, - operator: 'nested' - }); - - return acc; - } - - const filter: Filter = { - value, - not: false, - operator: 'equals' - }; - - if (op === 'not') { - filter.not = true; - op = suffix.shift(); - } - - if (op) { - filter.operator = op 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 = `${op}_nocase` as keyof typeof OPERATOR_MAP; - } - - (acc[field] as Filter[]).push(filter); - - return acc; - }, {}); - } - _getSelectionsFromGQLInfo (queryInfo: GraphQLResolveInfo): readonly SelectionNode[] { const [fieldNode] = queryInfo.fieldNodes; const selectionSet = fieldNode.selectionSet; diff --git a/packages/ipld-eth-client/package.json b/packages/ipld-eth-client/package.json index 2d5bfe04..dee53e3a 100644 --- a/packages/ipld-eth-client/package.json +++ b/packages/ipld-eth-client/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/ipld-eth-client", - "version": "0.2.105", + "version": "0.2.106", "description": "IPLD ETH Client", "main": "dist/index.js", "scripts": { @@ -20,8 +20,8 @@ "homepage": "https://github.com/cerc-io/watcher-ts#readme", "dependencies": { "@apollo/client": "^3.7.1", - "@cerc-io/cache": "^0.2.105", - "@cerc-io/util": "^0.2.105", + "@cerc-io/cache": "^0.2.106", + "@cerc-io/util": "^0.2.106", "cross-fetch": "^3.1.4", "debug": "^4.3.1", "ethers": "^5.4.4", diff --git a/packages/peer/package.json b/packages/peer/package.json index 0b944728..ef20f4ab 100644 --- a/packages/peer/package.json +++ b/packages/peer/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/peer", - "version": "0.2.105", + "version": "0.2.106", "description": "libp2p module", "main": "dist/index.js", "exports": "./dist/index.js", diff --git a/packages/rpc-eth-client/package.json b/packages/rpc-eth-client/package.json index e97b52cf..b13e3e3b 100644 --- a/packages/rpc-eth-client/package.json +++ b/packages/rpc-eth-client/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/rpc-eth-client", - "version": "0.2.105", + "version": "0.2.106", "description": "RPC ETH Client", "main": "dist/index.js", "scripts": { @@ -19,9 +19,9 @@ }, "homepage": "https://github.com/cerc-io/watcher-ts#readme", "dependencies": { - "@cerc-io/cache": "^0.2.105", - "@cerc-io/ipld-eth-client": "^0.2.105", - "@cerc-io/util": "^0.2.105", + "@cerc-io/cache": "^0.2.106", + "@cerc-io/ipld-eth-client": "^0.2.106", + "@cerc-io/util": "^0.2.106", "chai": "^4.3.4", "ethers": "^5.4.4", "left-pad": "^1.3.0", diff --git a/packages/solidity-mapper/package.json b/packages/solidity-mapper/package.json index c66102cc..df25a1d2 100644 --- a/packages/solidity-mapper/package.json +++ b/packages/solidity-mapper/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/solidity-mapper", - "version": "0.2.105", + "version": "0.2.106", "main": "dist/index.js", "license": "AGPL-3.0", "devDependencies": { diff --git a/packages/test/package.json b/packages/test/package.json index de3217d5..6d8eff84 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/test", - "version": "0.2.105", + "version": "0.2.106", "main": "dist/index.js", "license": "AGPL-3.0", "private": true, diff --git a/packages/tracing-client/package.json b/packages/tracing-client/package.json index 69bd802b..fa8553d8 100644 --- a/packages/tracing-client/package.json +++ b/packages/tracing-client/package.json @@ -1,6 +1,6 @@ { "name": "@cerc-io/tracing-client", - "version": "0.2.105", + "version": "0.2.106", "description": "ETH VM tracing client", "main": "dist/index.js", "scripts": { diff --git a/packages/util/package.json b/packages/util/package.json index a0928792..586573fe 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,13 +1,13 @@ { "name": "@cerc-io/util", - "version": "0.2.105", + "version": "0.2.106", "main": "dist/index.js", "license": "AGPL-3.0", "dependencies": { "@apollo/utils.keyvaluecache": "^1.0.1", "@cerc-io/nitro-node": "^0.1.15", - "@cerc-io/peer": "^0.2.105", - "@cerc-io/solidity-mapper": "^0.2.105", + "@cerc-io/peer": "^0.2.106", + "@cerc-io/solidity-mapper": "^0.2.106", "@cerc-io/ts-channel": "1.0.3-ts-nitro-0.1.1", "@ethersproject/properties": "^5.7.0", "@ethersproject/providers": "^5.4.4", @@ -54,7 +54,7 @@ "yargs": "^17.0.1" }, "devDependencies": { - "@cerc-io/cache": "^0.2.105", + "@cerc-io/cache": "^0.2.106", "@nomiclabs/hardhat-waffle": "^2.0.1", "@types/bunyan": "^1.8.8", "@types/express": "^4.17.14", diff --git a/packages/util/src/graph/database.ts b/packages/util/src/graph/database.ts index 401fceef..27d1d63f 100644 --- a/packages/util/src/graph/database.ts +++ b/packages/util/src/graph/database.ts @@ -17,11 +17,11 @@ import { } from 'typeorm'; import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'; import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer'; -import { ArgumentNode, FieldNode, GraphQLResolveInfo, SelectionNode, IntValueNode, EnumValueNode } from 'graphql'; +import { ArgumentNode, FieldNode, GraphQLResolveInfo, SelectionNode, IntValueNode, EnumValueNode, ObjectValueNode, ObjectFieldNode } from 'graphql'; import _ from 'lodash'; import debug from 'debug'; -import { Database as BaseDatabase, QueryOptions, Where, CanonicalBlockHeight, OrderDirection } from '../database'; +import { Database as BaseDatabase, QueryOptions, Where, CanonicalBlockHeight, Filter, OPERATOR_MAP, OrderDirection } from '../database'; import { BlockProgressInterface } from '../types'; import { cachePrunedEntitiesCount, eventProcessingLoadEntityCacheHitCount, eventProcessingLoadEntityCount, eventProcessingLoadEntityDBQueryDuration } from '../metrics'; import { ServerConfig } from '../config'; @@ -280,16 +280,10 @@ export class GraphDatabase { // Filter out __typename field in GQL for loading relations. childSelections = childSelections.filter(selection => !(selection.kind === 'Field' && selection.name.value === '__typename')); - // Parse arguments on a plural selection field - let relationWhere: Where = {}; - let relationQueryOptions: QueryOptions = {}; - if (isDerived || isArray) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ where: relationWhere, queryOptions: relationQueryOptions } = this._getSelectionFieldArguments(selection)); - } + // Parse selection's arguments + let { where: relationWhere, queryOptions: relationQueryOptions } = this._getSelectionFieldArguments(selection, queryInfo); if (isDerived) { - // TODO: Merge with relationWhere const where: Where = { [foreignKey]: [{ value: entityData.id, @@ -297,13 +291,19 @@ export class GraphDatabase { operator: 'equals' }] }; + relationWhere = _.mergeWith(relationWhere, where, (objValue: any, srcValue: any) => { + if (Array.isArray(objValue)) { + // Overwrite the array in the target object with the source array + return srcValue; + } + }); const relatedEntities = await this.getEntities( queryRunner, relationEntity, relationsMap, block, - where, + relationWhere, relationQueryOptions, childSelections, queryInfo @@ -315,7 +315,6 @@ export class GraphDatabase { } if (isArray) { - // TODO: Merge with relationWhere const where: Where = { id: [{ value: entityData[field], @@ -323,13 +322,19 @@ export class GraphDatabase { operator: 'in' }] }; + relationWhere = _.mergeWith(relationWhere, where, (objValue: any, srcValue: any) => { + if (Array.isArray(objValue)) { + // Overwrite the array in the target object with the source array + return srcValue; + } + }); const relatedEntities = await this.getEntities( queryRunner, relationEntity, relationsMap, block, - where, + relationWhere, relationQueryOptions, childSelections, queryInfo @@ -799,6 +804,7 @@ export class GraphDatabase { queryInfo: GraphQLResolveInfo ): Promise { assert(selection.kind === 'Field'); + const field = selection.name.value; const { entity: relationEntity, isArray, isDerived, field: foreignKey } = relations[field]; let childSelections = selection.selectionSet?.selections || []; @@ -806,6 +812,9 @@ export class GraphDatabase { // Filter out __typename field in GQL for loading relations. childSelections = childSelections.filter(selection => !(selection.kind === 'Field' && selection.name.value === '__typename')); + // Parse selection's arguments + let { where: relationWhere, queryOptions: relationQueryOptions } = this._getSelectionFieldArguments(selection, queryInfo); + if (isDerived) { const where: Where = { [foreignKey]: [{ @@ -814,14 +823,20 @@ export class GraphDatabase { operator: 'in' }] }; + relationWhere = _.mergeWith(relationWhere, where, (objValue: any, srcValue: any) => { + if (Array.isArray(objValue)) { + // Overwrite the array in the target object with the source array + return srcValue; + } + }); const relatedEntities = await this.getEntities( queryRunner, relationEntity, relationsMap, block, - where, - {}, + relationWhere, + relationQueryOptions, childSelections, queryInfo ); @@ -866,14 +881,20 @@ export class GraphDatabase { operator: 'in' }] }; + relationWhere = _.mergeWith(relationWhere, where, (objValue: any, srcValue: any) => { + if (Array.isArray(objValue)) { + // Overwrite the array in the target object with the source array + return srcValue; + } + }); const relatedEntities = await this.getEntities( queryRunner, relationEntity, relationsMap, block, - where, - {}, + relationWhere, + relationQueryOptions, childSelections, queryInfo ); @@ -918,14 +939,20 @@ export class GraphDatabase { operator: 'in' }] }; + relationWhere = _.mergeWith(relationWhere, where, (objValue: any, srcValue: any) => { + if (Array.isArray(objValue)) { + // Overwrite the array in the target object with the source array + return srcValue; + } + }); const relatedEntities = await this.getEntities( queryRunner, relationEntity, relationsMap, block, - where, - {}, + relationWhere, + relationQueryOptions, childSelections, queryInfo ); @@ -1339,6 +1366,76 @@ export class GraphDatabase { ); } + 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; + } + + 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('_'); + + if (!acc[field]) { + acc[field] = []; + } + + let op = suffix.shift(); + + // If op is "" (different from undefined), it means it's a nested filter on a relation field + if (op === '') { + (acc[field] as Filter[]).push({ + // Parse nested filter value + value: this.buildFilter(value), + not: false, + operator: 'nested' + }); + + return acc; + } + + const filter: Filter = { + value, + not: false, + operator: 'equals' + }; + + if (op === 'not') { + filter.not = true; + op = suffix.shift(); + } + + if (op) { + filter.operator = op 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 = `${op}_nocase` as keyof typeof OPERATOR_MAP; + } + + (acc[field] as Filter[]).push(filter); + + return acc; + }, {}); + } + _measureCachedPrunedEntities (): void { const totalEntities = Array.from(this.cachedEntities.latestPrunedEntities.values()) .reduce((acc, idEntitiesMap) => acc + idEntitiesMap.size, 0); @@ -1359,32 +1456,39 @@ export class GraphDatabase { }, []); } - _getSelectionFieldArguments (fieldNode: FieldNode): { where: Where, queryOptions: QueryOptions } { - const where: Where = {}; + _getSelectionFieldArguments (fieldNode: FieldNode, queryInfo: GraphQLResolveInfo): { where: Where, queryOptions: QueryOptions } { + let where: Where = {}; const queryOptions: QueryOptions = {}; fieldNode.arguments?.forEach((arg: ArgumentNode) => { switch (arg.name.value) { case 'where': - // TODO: Parse ArgumentNode to build where filter - // where = this.buildFilter(arg.value); + where = this.buildFilter(this._buildWhereFromArgumentNode(arg, queryInfo)); break; - case 'first': - queryOptions.limit = Number((arg.value as IntValueNode).value); + case 'first': { + const argValue = (arg.value.kind === 'Variable') ? queryInfo.variableValues[arg.value.name.value] : (arg.value as IntValueNode).value; + queryOptions.limit = Number(argValue); break; + } - case 'skip': - queryOptions.skip = Number((arg.value as IntValueNode).value); + case 'skip': { + const argValue = (arg.value.kind === 'Variable') ? queryInfo.variableValues[arg.value.name.value] : (arg.value as IntValueNode).value; + queryOptions.skip = Number(argValue); break; + } - case 'orderBy': - queryOptions.orderBy = (arg.value as EnumValueNode).value; + case 'orderBy': { + const argValue = (arg.value.kind === 'Variable') ? queryInfo.variableValues[arg.value.name.value] : (arg.value as EnumValueNode).value; + queryOptions.orderBy = String(argValue); break; + } - case 'orderDirection': - queryOptions.orderDirection = (arg.value as EnumValueNode).value as OrderDirection; + case 'orderDirection': { + const argValue = (arg.value.kind === 'Variable') ? queryInfo.variableValues[arg.value.name.value] : (arg.value as EnumValueNode).value; + queryOptions.orderDirection = argValue as OrderDirection; break; + } default: throw new Error('Unrecognized query argument'); @@ -1395,4 +1499,34 @@ export class GraphDatabase { return { where, queryOptions }; } + + _buildWhereFromArgumentNode (arg: ArgumentNode, queryInfo: GraphQLResolveInfo): { [key: string]: any } { + // TODO: Handle all types of filters on nested fields + + return (arg.value as ObjectValueNode).fields.reduce((acc: { [key: string]: any }, fieldNode: ObjectFieldNode) => { + switch (fieldNode.value.kind) { + case 'BooleanValue' : + case 'EnumValue' : + case 'FloatValue' : + case 'IntValue' : + case 'StringValue' : + acc[fieldNode.name.value] = fieldNode.value.value; + break; + + case 'NullValue': + acc[fieldNode.name.value] = null; + break; + + case 'Variable': + acc[fieldNode.name.value] = queryInfo.variableValues[fieldNode.value.name.value]; + break; + + case 'ListValue': + case 'ObjectValue': + throw new Error(`Nested filter type ${fieldNode.value.kind} not supported`); + } + + return acc; + }, {}); + } }