mirror of
https://github.com/cerc-io/watcher-ts
synced 2026-09-10 01:34:06 +00:00
Add a CLI to compare entity query results from two GQL endpoints (#57)
* Add CLI to compare entities from two GQL endpoints * Print out result diffs in compare-entity CLI * Get the colorized result diff in compare-entity CLI * Read query dir from config file or as an arg * Make config file arg required * Make queries in Example schema similar to that in graph-node * Get non-colorized output on choosing raw-json diff option * Make queries in eden-watcher similar to that in graph-node
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright 2021 Vulcanize, Inc.
|
||||
//
|
||||
|
||||
import assert from 'assert';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { gql } from '@apollo/client/core';
|
||||
import { GraphQLClient, GraphQLConfig } from '@vulcanize/ipld-eth-client';
|
||||
|
||||
export class Client {
|
||||
_config: GraphQLConfig;
|
||||
_graphqlClient: GraphQLClient;
|
||||
_queryDir: string;
|
||||
|
||||
constructor (config: GraphQLConfig, queryDir: string) {
|
||||
this._config = config;
|
||||
this._queryDir = path.resolve(process.cwd(), queryDir);
|
||||
|
||||
const { gqlEndpoint } = config;
|
||||
assert(gqlEndpoint, 'Missing gql endpoint');
|
||||
|
||||
this._graphqlClient = new GraphQLClient(config);
|
||||
}
|
||||
|
||||
async getEntity ({ blockHash, queryName, id }: { blockHash: string, queryName: string, id: string }): Promise<any> {
|
||||
const entityQuery = fs.readFileSync(path.resolve(this._queryDir, `${queryName}.gql`), 'utf8');
|
||||
|
||||
return this._graphqlClient.query(
|
||||
gql(entityQuery),
|
||||
{
|
||||
id,
|
||||
blockHash
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// Copyright 2021 Vulcanize, Inc.
|
||||
//
|
||||
|
||||
import yargs from 'yargs';
|
||||
import 'reflect-metadata';
|
||||
import path from 'path';
|
||||
import toml from 'toml';
|
||||
import fs from 'fs-extra';
|
||||
import assert from 'assert';
|
||||
import util from 'util';
|
||||
import { diffString, diff } from 'json-diff';
|
||||
|
||||
import { Client } from './client';
|
||||
|
||||
interface EndpointConfig {
|
||||
gqlEndpoint1: string;
|
||||
gqlEndpoint2: string;
|
||||
}
|
||||
|
||||
interface QueryConfig {
|
||||
queryDir: string;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
endpoints: EndpointConfig;
|
||||
queries: QueryConfig;
|
||||
}
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const argv = await yargs.parserConfiguration({
|
||||
'parse-numbers': false
|
||||
}).options({
|
||||
configFile: {
|
||||
alias: 'cf',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
describe: 'Configuration file path (toml)'
|
||||
},
|
||||
queryDir: {
|
||||
alias: 'qf',
|
||||
type: 'string',
|
||||
describe: 'Path to queries directory'
|
||||
},
|
||||
blockHash: {
|
||||
alias: 'b',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
describe: 'Blockhash'
|
||||
},
|
||||
queryName: {
|
||||
alias: 'q',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
describe: 'Query name'
|
||||
},
|
||||
entityId: {
|
||||
alias: 'i',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
describe: 'Id of the entity to be queried'
|
||||
},
|
||||
rawJson: {
|
||||
alias: 'j',
|
||||
type: 'boolean',
|
||||
describe: 'Whether to print out raw diff object',
|
||||
default: false
|
||||
}
|
||||
}).argv;
|
||||
|
||||
const config: Config = await getConfig(argv.configFile);
|
||||
|
||||
const { client1, client2 } = await getClients(config, argv.queryDir);
|
||||
|
||||
const queryName = argv.queryName;
|
||||
const id = argv.entityId;
|
||||
const blockHash = argv.blockHash;
|
||||
|
||||
const result1 = await client1.getEntity({ blockHash, queryName, id });
|
||||
const result2 = await client2.getEntity({ blockHash, queryName, id });
|
||||
|
||||
// Getting the diff of two result objects.
|
||||
let resultDiff;
|
||||
if (argv.rawJson) {
|
||||
resultDiff = diff(result1, result2);
|
||||
|
||||
if (resultDiff) {
|
||||
// Use util.inspect to extend depth limit in the output.
|
||||
resultDiff = util.inspect(diff(result1, result2), false, null);
|
||||
}
|
||||
} else {
|
||||
resultDiff = diffString(result1, result2);
|
||||
}
|
||||
|
||||
if (resultDiff) {
|
||||
console.log(resultDiff);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfig (configFile: string): Promise<Config> {
|
||||
const configFilePath = path.resolve(configFile);
|
||||
const fileExists = await fs.pathExists(configFilePath);
|
||||
if (!fileExists) {
|
||||
throw new Error(`Config file not found: ${configFilePath}`);
|
||||
}
|
||||
|
||||
const config = toml.parse(await fs.readFile(configFilePath, 'utf8'));
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function getClients (config: Config, queryDir?: string): Promise<{
|
||||
client1: Client,
|
||||
client2: Client
|
||||
}> {
|
||||
assert(config.endpoints, 'Missing endpoints config');
|
||||
|
||||
const gqlEndpoint1 = config.endpoints.gqlEndpoint1;
|
||||
const gqlEndpoint2 = config.endpoints.gqlEndpoint2;
|
||||
|
||||
assert(gqlEndpoint1, 'Missing endpoint one');
|
||||
assert(gqlEndpoint2, 'Missing endpoint two');
|
||||
|
||||
if (!queryDir) {
|
||||
assert(config.queries, 'Missing queries config');
|
||||
queryDir = config.queries.queryDir;
|
||||
}
|
||||
|
||||
assert(queryDir, 'Query directory not provided');
|
||||
|
||||
const client1 = new Client({
|
||||
gqlEndpoint: gqlEndpoint1
|
||||
}, queryDir);
|
||||
|
||||
const client2 = new Client({
|
||||
gqlEndpoint: gqlEndpoint2
|
||||
}, queryDir);
|
||||
|
||||
return {
|
||||
client1,
|
||||
client2
|
||||
};
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.log(err);
|
||||
}).finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -40,7 +40,8 @@ export class Database {
|
||||
return this._baseDatabase.close();
|
||||
}
|
||||
|
||||
async getEntity<Entity> (entity: (new () => Entity) | string, id: string, blockHash: string): Promise<Entity | undefined> {
|
||||
async getEntity<Entity> (entity: (new () => Entity) | string, id: string, blockHash?: string): Promise<Entity | undefined> {
|
||||
// TODO: Take block number as an optional argument
|
||||
const queryRunner = this._conn.createQueryRunner();
|
||||
|
||||
try {
|
||||
@@ -73,33 +74,36 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async getEntityWithRelations<Entity> (entity: (new () => Entity) | string, id: string, blockHash: string, relations: { [key: string]: any }): Promise<Entity | undefined> {
|
||||
async getEntityWithRelations<Entity> (entity: (new () => Entity) | string, id: string, relations: { [key: string]: any }, blockHash?: string): Promise<Entity | undefined> {
|
||||
const queryRunner = this._conn.createQueryRunner();
|
||||
|
||||
try {
|
||||
const repo = queryRunner.manager.getRepository(entity);
|
||||
|
||||
// Fetching blockHash for previous entity in frothy region.
|
||||
const { blockHash: entityblockHash, blockNumber, id: frothyId } = await this._baseDatabase.getFrothyEntity(queryRunner, repo, { blockHash, id });
|
||||
|
||||
let selectQueryBuilder = repo.createQueryBuilder('entity');
|
||||
|
||||
if (frothyId) {
|
||||
// If entity found in frothy region.
|
||||
selectQueryBuilder = selectQueryBuilder.where('entity.block_hash = :entityblockHash', { entityblockHash });
|
||||
} else {
|
||||
// If entity not in frothy region.
|
||||
const canonicalBlockNumber = blockNumber + 1;
|
||||
selectQueryBuilder = selectQueryBuilder.where('entity.id = :id', { id })
|
||||
.orderBy('entity.block_number', 'DESC')
|
||||
.limit(1);
|
||||
|
||||
selectQueryBuilder = selectQueryBuilder.innerJoinAndSelect('block_progress', 'block', 'block.block_hash = entity.block_hash')
|
||||
.where('block.is_pruned = false')
|
||||
.andWhere('entity.block_number <= :canonicalBlockNumber', { canonicalBlockNumber })
|
||||
.orderBy('entity.block_number', 'DESC')
|
||||
.limit(1);
|
||||
// Use blockHash if provided.
|
||||
if (blockHash) {
|
||||
// Fetching blockHash for previous entity in frothy region.
|
||||
const { blockHash: entityblockHash, blockNumber, id: frothyId } = await this._baseDatabase.getFrothyEntity(queryRunner, repo, { blockHash, id });
|
||||
|
||||
if (frothyId) {
|
||||
// If entity found in frothy region.
|
||||
selectQueryBuilder = selectQueryBuilder.andWhere('entity.block_hash = :entityblockHash', { entityblockHash });
|
||||
} else {
|
||||
// If entity not in frothy region.
|
||||
const canonicalBlockNumber = blockNumber + 1;
|
||||
|
||||
selectQueryBuilder = selectQueryBuilder.innerJoinAndSelect('block_progress', 'block', 'block.block_hash = entity.block_hash')
|
||||
.andWhere('block.is_pruned = false')
|
||||
.andWhere('entity.block_number <= :canonicalBlockNumber', { canonicalBlockNumber });
|
||||
}
|
||||
}
|
||||
|
||||
selectQueryBuilder = selectQueryBuilder.andWhere('entity.id = :id', { id });
|
||||
|
||||
// TODO: Implement query for nested relations.
|
||||
Object.entries(relations).forEach(([field, data], index) => {
|
||||
const { entity: relatedEntity, isArray } = data;
|
||||
|
||||
@@ -177,9 +177,9 @@ export class GraphWatcher {
|
||||
this._indexer = indexer;
|
||||
}
|
||||
|
||||
async getEntity<Entity> (entity: new () => Entity, id: string, blockHash: string, relations: { [key: string]: any }): Promise<any> {
|
||||
async getEntity<Entity> (entity: new () => Entity, id: string, relations: { [key: string]: any }, blockHash?: string): Promise<any> {
|
||||
// Get entity from the database.
|
||||
const result = await this._database.getEntityWithRelations(entity, id, blockHash, relations) as any;
|
||||
const result = await this._database.getEntityWithRelations(entity, id, relations, blockHash) as any;
|
||||
|
||||
// Resolve any field name conflicts in the entity result.
|
||||
return resolveEntityFieldConflicts(result);
|
||||
|
||||
Reference in New Issue
Block a user