Handle subgraph schema field with derivedFrom directive (#60)

* Handle subgraph schema field with derivedFrom directive

* Handle derivedFrom directive in eden-watcher

* Fix 1 to N relation error by removing limit from query

* Order by id for derivedFrom relations to match graph-node

* Refactor example subgraph schema entities

* Fix watcher queries to return correct relation field values

* Fix hierarchical query for getting two entities at same block
This commit is contained in:
2021-12-28 16:08:05 +05:30
committed by nabarun
parent b04f6f2fba
commit 238ad21189
26 changed files with 482 additions and 376 deletions
+94 -48
View File
@@ -6,10 +6,12 @@ import assert from 'assert';
import {
Connection,
ConnectionOptions,
FindOneOptions
FindOneOptions,
LessThanOrEqual
} from 'typeorm';
import {
BlockHeight,
Database as BaseDatabase
} from '@vulcanize/util';
@@ -41,7 +43,6 @@ export class Database {
}
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 {
@@ -74,63 +75,108 @@ export class Database {
}
}
async getEntityWithRelations<Entity> (entity: (new () => Entity) | string, id: string, relations: { [key: string]: any }, blockHash?: string): Promise<Entity | undefined> {
async getEntityWithRelations<Entity> (entity: (new () => Entity) | string, id: string, relations: { [key: string]: any }, block: BlockHeight = {}): Promise<Entity | undefined> {
const queryRunner = this._conn.createQueryRunner();
let { hash: blockHash, number: blockNumber } = block;
try {
const repo = queryRunner.manager.getRepository(entity);
let selectQueryBuilder = repo.createQueryBuilder('entity');
const whereOptions: any = { id };
selectQueryBuilder = selectQueryBuilder.where('entity.id = :id', { id })
.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 });
}
if (blockNumber) {
whereOptions.blockNumber = LessThanOrEqual(blockNumber);
}
// TODO: Implement query for nested relations.
Object.entries(relations).forEach(([field, data], index) => {
const { entity: relatedEntity, isArray } = data;
const alias = `relatedEntity${index}`;
if (blockHash) {
whereOptions.blockHash = blockHash;
const block = await this._baseDatabase.getBlockProgress(queryRunner.manager.getRepository('block_progress'), blockHash);
blockNumber = block?.blockNumber;
}
if (isArray) {
// For one to many relational field.
selectQueryBuilder = selectQueryBuilder.leftJoinAndMapMany(
`entity.${field}`,
relatedEntity,
alias,
`${alias}.id IN (SELECT unnest(entity.${field})) AND ${alias}.block_number <= entity.block_number`
)
.addOrderBy(`${alias}.block_number`, 'DESC');
} else {
// For one to one relational field.
selectQueryBuilder = selectQueryBuilder.leftJoinAndMapOne(
`entity.${field}`,
relatedEntity,
alias,
`entity.${field} = ${alias}.id AND ${alias}.block_number <= entity.block_number`
)
.addOrderBy(`${alias}.block_number`, 'DESC');
const findOptions = {
where: whereOptions,
order: {
blockNumber: 'DESC'
}
});
};
return selectQueryBuilder.getOne();
let entityData: any = await repo.findOne(findOptions as FindOneOptions<Entity>);
if (!entityData && findOptions.where.blockHash) {
entityData = await this._baseDatabase.getPrevEntityVersion(queryRunner, repo, findOptions);
}
if (entityData) {
// Populate relational fields.
// TODO: Implement query for nested relations.
const relationQueryPromises = Object.entries(relations).map(async ([field, data]) => {
assert(entityData);
const { entity: relatedEntity, isArray, isDerived, field: derivedField } = data;
const repo = queryRunner.manager.getRepository(relatedEntity);
let selectQueryBuilder = repo.createQueryBuilder('entity');
if (isDerived) {
// For derived relational field.
selectQueryBuilder = selectQueryBuilder.where(`entity.${derivedField} = :id`, { id: entityData.id });
if (isArray) {
selectQueryBuilder = selectQueryBuilder.distinctOn(['entity.id'])
.orderBy('entity.id');
} else {
selectQueryBuilder = selectQueryBuilder.limit(1);
}
} else {
if (isArray) {
// For one to many relational field.
selectQueryBuilder = selectQueryBuilder.where('entity.id IN (:...ids)', { ids: entityData[field] })
.distinctOn(['entity.id'])
.orderBy('entity.id');
// Subquery example if distinctOn is not performant.
//
// SELECT c.*
// FROM
// categories c,
// (
// SELECT id, MAX(block_number) as block_number
// FROM categories
// WHERE
// id IN ('nature', 'tech', 'issues')
// AND
// block_number <= 127
// GROUP BY id
// ) a
// WHERE
// c.id = a.id AND c.block_number = a.block_number
} else {
// For one to one relational field.
selectQueryBuilder = selectQueryBuilder.where('entity.id = :id', { id: entityData[field] })
.limit(1);
}
selectQueryBuilder = selectQueryBuilder.addOrderBy('entity.block_number', 'DESC');
}
if (blockNumber) {
selectQueryBuilder = selectQueryBuilder.andWhere(
'entity.block_number <= :blockNumber',
{ blockNumber }
);
}
if (isArray) {
entityData[field] = await selectQueryBuilder.getMany();
} else {
entityData[field] = await selectQueryBuilder.getOne();
}
});
await Promise.all(relationQueryPromises);
}
return entityData;
} finally {
await queryRunner.release();
}
+7 -1
View File
@@ -53,7 +53,13 @@ export interface Context {
}
}
export const instantiate = async (database: Database, indexer: IndexerInterface, context: Context, filePath: string, data: GraphData = {}): Promise<loader.ResultObject & { exports: any }> => {
export const instantiate = async (
database: Database,
indexer: IndexerInterface,
context: Context,
filePath: string,
data: GraphData = {}
): Promise<loader.ResultObject & { exports: any }> => {
const { abis = {}, dataSource } = data;
const buffer = await fs.readFile(filePath);
const provider = getDefaultProvider(NETWORK_URL);
+3 -3
View File
@@ -11,7 +11,7 @@ import { ContractInterface, utils } from 'ethers';
import { ResultObject } from '@vulcanize/assemblyscript/lib/loader';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { IndexerInterface, getFullBlock } from '@vulcanize/util';
import { IndexerInterface, getFullBlock, BlockHeight } from '@vulcanize/util';
import { createBlock, createEvent, getSubgraphConfig, resolveEntityFieldConflicts } from './utils';
import { Context, instantiate } from './loader';
@@ -177,9 +177,9 @@ export class GraphWatcher {
this._indexer = indexer;
}
async getEntity<Entity> (entity: new () => Entity, id: string, relations: { [key: string]: any }, blockHash?: string): Promise<any> {
async getEntity<Entity> (entity: new () => Entity, id: string, relations: { [key: string]: any }, block?: BlockHeight): Promise<any> {
// Get entity from the database.
const result = await this._database.getEntityWithRelations(entity, id, relations, blockHash) as any;
const result = await this._database.getEntityWithRelations(entity, id, relations, block) as any;
// Resolve any field name conflicts in the entity result.
return resolveEntityFieldConflicts(result);