mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-07-27 10:42:06 +00:00
Latest entity queries to optimize frontend app GQL requests (#232)
* Add latest entity table query pattern * Add subscriber config to typeorm
This commit is contained in:
parent
408a3927c0
commit
a084b4e40c
@ -44,7 +44,8 @@ export class Database implements DatabaseInterface {
|
|||||||
|
|
||||||
this._config = {
|
this._config = {
|
||||||
...config,
|
...config,
|
||||||
entities: [path.join(__dirname, 'entity/*')]
|
entities: [path.join(__dirname, 'entity/*')],
|
||||||
|
subscribers: [path.join(__dirname, 'entity/Subscriber.*')]
|
||||||
};
|
};
|
||||||
|
|
||||||
this._baseDatabase = new BaseDatabase(this._config);
|
this._baseDatabase = new BaseDatabase(this._config);
|
||||||
|
@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm';
|
import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm';
|
||||||
|
|
||||||
|
import { afterEntityInsertOrUpdate } from '@cerc-io/graph-node';
|
||||||
|
|
||||||
import { FrothyEntity } from './FrothyEntity';
|
import { FrothyEntity } from './FrothyEntity';
|
||||||
import { ENTITIES } from '../database';
|
import { ENTITIES } from '../database';
|
||||||
import { afterEntityInsertOrUpdate } from '@cerc-io/graph-node';
|
|
||||||
|
|
||||||
@EventSubscriber()
|
@EventSubscriber()
|
||||||
export class EntitySubscriber implements EntitySubscriberInterface {
|
export class EntitySubscriber implements EntitySubscriberInterface {
|
||||||
|
@ -68,16 +68,23 @@ export class Database {
|
|||||||
_conn!: Connection
|
_conn!: Connection
|
||||||
_baseDatabase: BaseDatabase
|
_baseDatabase: BaseDatabase
|
||||||
_entityQueryTypeMap: Map<new() => any, ENTITY_QUERY_TYPE>
|
_entityQueryTypeMap: Map<new() => any, ENTITY_QUERY_TYPE>
|
||||||
|
_entityToLatestEntityMap: Map<new () => any, new () => any> = new Map()
|
||||||
|
|
||||||
_cachedEntities: CachedEntities = {
|
_cachedEntities: CachedEntities = {
|
||||||
frothyBlocks: new Map(),
|
frothyBlocks: new Map(),
|
||||||
latestPrunedEntities: new Map()
|
latestPrunedEntities: new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (serverConfig: ServerConfig, baseDatabase: BaseDatabase, entityQueryTypeMap: Map<new() => any, ENTITY_QUERY_TYPE> = new Map()) {
|
constructor (
|
||||||
|
serverConfig: ServerConfig,
|
||||||
|
baseDatabase: BaseDatabase,
|
||||||
|
entityQueryTypeMap: Map<new () => any, ENTITY_QUERY_TYPE> = new Map(),
|
||||||
|
entityToLatestEntityMap: Map<new () => any, new () => any> = new Map()
|
||||||
|
) {
|
||||||
this._serverConfig = serverConfig;
|
this._serverConfig = serverConfig;
|
||||||
this._baseDatabase = baseDatabase;
|
this._baseDatabase = baseDatabase;
|
||||||
this._entityQueryTypeMap = entityQueryTypeMap;
|
this._entityQueryTypeMap = entityQueryTypeMap;
|
||||||
|
this._entityToLatestEntityMap = entityToLatestEntityMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
get cachedEntities () {
|
get cachedEntities () {
|
||||||
@ -367,8 +374,21 @@ export class Database {
|
|||||||
queryOptions: QueryOptions = {},
|
queryOptions: QueryOptions = {},
|
||||||
selections: ReadonlyArray<SelectionNode> = []
|
selections: ReadonlyArray<SelectionNode> = []
|
||||||
): Promise<Entity[]> {
|
): Promise<Entity[]> {
|
||||||
let entities: Entity[];
|
let entities: Entity[] = [];
|
||||||
|
const latestEntity = this._entityToLatestEntityMap.get(entity);
|
||||||
|
|
||||||
|
if (latestEntity) {
|
||||||
|
if (!Object.keys(block).length) {
|
||||||
|
// Use latest entity tables if block height not passed.
|
||||||
|
entities = await this.getEntitiesLatest(
|
||||||
|
queryRunner,
|
||||||
|
entity,
|
||||||
|
latestEntity,
|
||||||
|
where,
|
||||||
|
queryOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// Use different suitable query patterns based on entities.
|
// Use different suitable query patterns based on entities.
|
||||||
switch (this._entityQueryTypeMap.get(entity)) {
|
switch (this._entityQueryTypeMap.get(entity)) {
|
||||||
case ENTITY_QUERY_TYPE.SINGULAR:
|
case ENTITY_QUERY_TYPE.SINGULAR:
|
||||||
@ -402,6 +422,7 @@ export class Database {
|
|||||||
entities = await this.getEntitiesGroupBy(queryRunner, entity, block, where, queryOptions);
|
entities = await this.getEntitiesGroupBy(queryRunner, entity, block, where, queryOptions);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!entities.length) {
|
if (!entities.length) {
|
||||||
return [];
|
return [];
|
||||||
@ -805,6 +826,42 @@ export class Database {
|
|||||||
return entities as Entity[];
|
return entities as Entity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEntitiesLatest<Entity> (
|
||||||
|
queryRunner: QueryRunner,
|
||||||
|
entity: new () => Entity,
|
||||||
|
latestEntity: new () => any,
|
||||||
|
where: Where = {},
|
||||||
|
queryOptions: QueryOptions = {}
|
||||||
|
): Promise<Entity[]> {
|
||||||
|
const repo = queryRunner.manager.getRepository(entity);
|
||||||
|
const { tableName } = repo.metadata;
|
||||||
|
|
||||||
|
let selectQueryBuilder = repo.createQueryBuilder(tableName)
|
||||||
|
.innerJoin(
|
||||||
|
latestEntity,
|
||||||
|
'latest',
|
||||||
|
`latest.id = ${tableName}.id AND latest.blockHash = ${tableName}.blockHash`
|
||||||
|
);
|
||||||
|
|
||||||
|
selectQueryBuilder = this._baseDatabase.buildQuery(repo, selectQueryBuilder, where, 'latest');
|
||||||
|
|
||||||
|
if (queryOptions.orderBy) {
|
||||||
|
selectQueryBuilder = this._baseDatabase.orderQuery(repo, selectQueryBuilder, queryOptions, '', 'latest');
|
||||||
|
}
|
||||||
|
|
||||||
|
selectQueryBuilder = this._baseDatabase.orderQuery(repo, selectQueryBuilder, { ...queryOptions, orderBy: 'id' }, '', 'latest');
|
||||||
|
|
||||||
|
if (queryOptions.skip) {
|
||||||
|
selectQueryBuilder = selectQueryBuilder.offset(queryOptions.skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryOptions.limit) {
|
||||||
|
selectQueryBuilder = selectQueryBuilder.limit(queryOptions.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectQueryBuilder.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
async loadEntitiesRelations<Entity> (
|
async loadEntitiesRelations<Entity> (
|
||||||
queryRunner: QueryRunner,
|
queryRunner: QueryRunner,
|
||||||
block: BlockHeight,
|
block: BlockHeight,
|
||||||
|
@ -899,10 +899,18 @@ export const updateEntitiesFromState = async (database: Database, indexer: Index
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const afterEntityInsertOrUpdate = async<Entity> (frothyEntityType: EntityTarget<Entity>, entities: Set<any>, event: InsertEvent<any> | UpdateEvent<any>): Promise<void> => {
|
export const afterEntityInsertOrUpdate = async<Entity> (
|
||||||
|
frothyEntityType: EntityTarget<Entity>,
|
||||||
|
entities: Set<any>,
|
||||||
|
event: InsertEvent<any> | UpdateEvent<any>,
|
||||||
|
entityToLatestEntityMap: Map<new () => any, new () => any> = new Map()
|
||||||
|
): Promise<void> => {
|
||||||
const entity = event.entity;
|
const entity = event.entity;
|
||||||
|
|
||||||
// TODO: Check and return if entity is being pruned (is_pruned flag update)
|
// Return if the entity is being pruned
|
||||||
|
if (entity.isPruned) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Insert the entity details in FrothyEntity table
|
// Insert the entity details in FrothyEntity table
|
||||||
if (entities.has(entity.constructor)) {
|
if (entities.has(entity.constructor)) {
|
||||||
@ -922,5 +930,26 @@ export const afterEntityInsertOrUpdate = async<Entity> (frothyEntityType: Entity
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOOD: Update latest entity tables
|
// Get latest entity's type
|
||||||
|
const entityTarget = entityToLatestEntityMap.get(entity.constructor);
|
||||||
|
|
||||||
|
if (!entityTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get latest entity's fields to be updated
|
||||||
|
const latestEntityRepo = event.manager.getRepository(entityTarget);
|
||||||
|
const latestEntityFields = latestEntityRepo.metadata.columns.map(column => column.propertyName);
|
||||||
|
const fieldsToUpdate = latestEntityRepo.metadata.columns.map(column => column.databaseName).filter(val => val !== 'id');
|
||||||
|
|
||||||
|
// Create a latest entity instance and upsert in the db
|
||||||
|
const latestEntity = event.manager.create(entityTarget, _.pick(entity, latestEntityFields));
|
||||||
|
await event.manager.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(entityTarget)
|
||||||
|
.values(latestEntity)
|
||||||
|
.orUpdate(
|
||||||
|
{ conflict_target: ['id'], overwrite: fieldsToUpdate }
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
};
|
};
|
||||||
|
@ -788,20 +788,29 @@ export class Database {
|
|||||||
return repo.save(entity);
|
return repo.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildQuery<Entity> (repo: Repository<Entity>, selectQueryBuilder: SelectQueryBuilder<Entity>, where: Where = {}): SelectQueryBuilder<Entity> {
|
buildQuery<Entity> (
|
||||||
|
repo: Repository<Entity>,
|
||||||
|
selectQueryBuilder: SelectQueryBuilder<Entity>,
|
||||||
|
where: Where = {},
|
||||||
|
alias?: string
|
||||||
|
): SelectQueryBuilder<Entity> {
|
||||||
|
if (!alias) {
|
||||||
|
alias = selectQueryBuilder.alias;
|
||||||
|
}
|
||||||
|
|
||||||
Object.entries(where).forEach(([field, filters]) => {
|
Object.entries(where).forEach(([field, filters]) => {
|
||||||
filters.forEach((filter, index) => {
|
filters.forEach((filter, index) => {
|
||||||
// Form the where clause.
|
// Form the where clause.
|
||||||
let { not, operator, value } = filter;
|
let { not, operator, value } = filter;
|
||||||
const columnMetadata = repo.metadata.findColumnWithPropertyName(field);
|
const columnMetadata = repo.metadata.findColumnWithPropertyName(field);
|
||||||
assert(columnMetadata);
|
assert(columnMetadata);
|
||||||
let whereClause = `"${selectQueryBuilder.alias}"."${columnMetadata.databaseName}" `;
|
let whereClause = `"${alias}"."${columnMetadata.databaseName}" `;
|
||||||
|
|
||||||
if (columnMetadata.relationMetadata) {
|
if (columnMetadata.relationMetadata) {
|
||||||
// For relation fields, use the id column.
|
// For relation fields, use the id column.
|
||||||
const idColumn = columnMetadata.relationMetadata.joinColumns.find(column => column.referencedColumn?.propertyName === 'id');
|
const idColumn = columnMetadata.relationMetadata.joinColumns.find(column => column.referencedColumn?.propertyName === 'id');
|
||||||
assert(idColumn);
|
assert(idColumn);
|
||||||
whereClause = `"${selectQueryBuilder.alias}"."${idColumn.databaseName}" `;
|
whereClause = `"${alias}"."${idColumn.databaseName}" `;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (not) {
|
if (not) {
|
||||||
@ -853,8 +862,13 @@ export class Database {
|
|||||||
repo: Repository<Entity>,
|
repo: Repository<Entity>,
|
||||||
selectQueryBuilder: SelectQueryBuilder<Entity>,
|
selectQueryBuilder: SelectQueryBuilder<Entity>,
|
||||||
orderOptions: { orderBy?: string, orderDirection?: string },
|
orderOptions: { orderBy?: string, orderDirection?: string },
|
||||||
columnPrefix = ''
|
columnPrefix = '',
|
||||||
|
alias?: string
|
||||||
): SelectQueryBuilder<Entity> {
|
): SelectQueryBuilder<Entity> {
|
||||||
|
if (!alias) {
|
||||||
|
alias = selectQueryBuilder.alias;
|
||||||
|
}
|
||||||
|
|
||||||
const { orderBy, orderDirection } = orderOptions;
|
const { orderBy, orderDirection } = orderOptions;
|
||||||
assert(orderBy);
|
assert(orderBy);
|
||||||
|
|
||||||
@ -862,7 +876,7 @@ export class Database {
|
|||||||
assert(columnMetadata);
|
assert(columnMetadata);
|
||||||
|
|
||||||
return selectQueryBuilder.addOrderBy(
|
return selectQueryBuilder.addOrderBy(
|
||||||
`"${selectQueryBuilder.alias}"."${columnPrefix}${columnMetadata.databaseName}"`,
|
`"${alias}"."${columnPrefix}${columnMetadata.databaseName}"`,
|
||||||
orderDirection === 'desc' ? 'DESC' : 'ASC'
|
orderDirection === 'desc' ? 'DESC' : 'ASC'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user