mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-01-09 04:48:05 +00:00
565 lines
15 KiB
TypeScript
565 lines
15 KiB
TypeScript
//
|
|
// Copyright 2021 Vulcanize, Inc.
|
|
//
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import assert from 'assert';
|
|
import yaml from 'js-yaml';
|
|
import Handlebars from 'handlebars';
|
|
import { Writable } from 'stream';
|
|
|
|
import { VariableDeclaration } from '@solidity-parser/parser/dist/src/ast-types';
|
|
|
|
import { getPgForTs, getTsForGql, getGqlForSol } from './utils/type-mappings';
|
|
import { Param } from './utils/types';
|
|
import { getFieldType } from './utils/subgraph';
|
|
import { getBaseType, isArrayType } from './utils/helpers';
|
|
|
|
const TEMPLATE_FILE = './templates/entity-template.handlebars';
|
|
const TABLES_DIR = './data/entities';
|
|
|
|
export class Entity {
|
|
_entities: Array<any>;
|
|
_templateString: string;
|
|
|
|
constructor () {
|
|
this._entities = [];
|
|
this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString();
|
|
}
|
|
|
|
/**
|
|
* Creates an entity object from the query and stores to be passed to the template.
|
|
* @param name Name of the query.
|
|
* @param params Parameters to the query.
|
|
* @param returnType Return type for the query.
|
|
*/
|
|
addQuery (name: string, params: Array<Param>, returnParameters: VariableDeclaration[]): void {
|
|
// Check if the query is already added.
|
|
if (this._entities.some(entity => entity.className.toLowerCase() === name.toLowerCase())) {
|
|
return;
|
|
}
|
|
|
|
const entityObject: any = {
|
|
className: '',
|
|
indexOn: [],
|
|
columns: [],
|
|
imports: []
|
|
};
|
|
|
|
// eth_call mode: Capitalize first letter of entity name (balanceOf -> BalanceOf).
|
|
// storage mode: Capiltalize second letter of entity name (_balances -> _Balances).
|
|
entityObject.className = (name.charAt(0) === '_')
|
|
? `_${name.charAt(1).toUpperCase()}${name.slice(2)}`
|
|
: `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
|
|
entityObject.imports.push(
|
|
{
|
|
toImport: new Set(['Entity', 'PrimaryGeneratedColumn', 'Column', 'Index']),
|
|
from: 'typeorm'
|
|
}
|
|
);
|
|
|
|
const indexObject = {
|
|
columns: ['blockHash', 'contractAddress'],
|
|
unique: true
|
|
};
|
|
indexObject.columns = indexObject.columns.concat(
|
|
params.map((param) => {
|
|
return param.name;
|
|
})
|
|
);
|
|
entityObject.indexOn.push(indexObject);
|
|
|
|
entityObject.columns.push({
|
|
name: 'id',
|
|
tsType: 'number',
|
|
columnType: 'PrimaryGeneratedColumn',
|
|
columnOptions: []
|
|
});
|
|
entityObject.columns.push({
|
|
name: 'blockHash',
|
|
pgType: 'varchar',
|
|
tsType: 'string',
|
|
columnType: 'Column',
|
|
columnOptions: [
|
|
{
|
|
option: 'length',
|
|
value: 66
|
|
}
|
|
]
|
|
});
|
|
entityObject.columns.push({
|
|
name: 'blockNumber',
|
|
pgType: 'integer',
|
|
tsType: 'number',
|
|
columnType: 'Column'
|
|
});
|
|
entityObject.columns.push({
|
|
name: 'contractAddress',
|
|
pgType: 'varchar',
|
|
tsType: 'string',
|
|
columnType: 'Column',
|
|
columnOptions: [
|
|
{
|
|
option: 'length',
|
|
value: 42
|
|
}
|
|
]
|
|
});
|
|
|
|
entityObject.columns = entityObject.columns.concat(
|
|
params.map((param) => {
|
|
const name = param.name;
|
|
|
|
const gqlType = getGqlForSol(param.type);
|
|
const tsType = getTsForGql(gqlType);
|
|
assert(tsType);
|
|
const pgType = getPgForTs(tsType);
|
|
assert(pgType);
|
|
|
|
const columnOptions = [];
|
|
|
|
if (param.type === 'address') {
|
|
columnOptions.push(
|
|
{
|
|
option: 'length',
|
|
value: 42
|
|
}
|
|
);
|
|
}
|
|
|
|
return {
|
|
name,
|
|
pgType,
|
|
tsType,
|
|
columnType: 'Column',
|
|
columnOptions
|
|
};
|
|
})
|
|
);
|
|
|
|
entityObject.columns = entityObject.columns.concat(
|
|
returnParameters.map((returnParameter, index) => {
|
|
let typeName = returnParameter.typeName;
|
|
assert(typeName);
|
|
|
|
// Handle Mapping type for state variable queries
|
|
while (typeName.type === 'Mapping') {
|
|
typeName = typeName.valueType;
|
|
}
|
|
|
|
const baseType = getBaseType(typeName);
|
|
assert(baseType);
|
|
const gqlReturnType = getGqlForSol(baseType);
|
|
let tsReturnType = getTsForGql(gqlReturnType);
|
|
assert(tsReturnType);
|
|
const pgReturnType = getPgForTs(tsReturnType);
|
|
assert(pgReturnType);
|
|
|
|
const columnOptions = [];
|
|
const isArray = isArrayType(typeName);
|
|
|
|
if (isArray) {
|
|
tsReturnType = tsReturnType.concat('[]');
|
|
|
|
columnOptions.push({
|
|
option: 'array',
|
|
value: true
|
|
});
|
|
}
|
|
|
|
return {
|
|
name: returnParameters.length > 1 ? `value${index}` : 'value',
|
|
pgType: pgReturnType,
|
|
tsType: tsReturnType,
|
|
columnType: 'Column',
|
|
columnOptions
|
|
};
|
|
})
|
|
);
|
|
|
|
entityObject.columns.push({
|
|
name: 'proof',
|
|
pgType: 'text',
|
|
tsType: 'string',
|
|
columnType: 'Column',
|
|
columnOptions: [
|
|
{
|
|
option: 'nullable',
|
|
value: true
|
|
}
|
|
]
|
|
});
|
|
|
|
// Add bigintTransformer column option if required.
|
|
this._addBigIntTransformerOption(entityObject);
|
|
|
|
this._entities.push(entityObject);
|
|
}
|
|
|
|
/**
|
|
* Writes the generated entity files in the given directory.
|
|
* @param entityDir Directory to write the entities to.
|
|
*/
|
|
exportEntities (entityDir: string, subgraphPath: string): void {
|
|
this._addEventEntity();
|
|
this._addSyncStatusEntity();
|
|
this._addContractEntity();
|
|
this._addBlockProgressEntity();
|
|
this._addStateEntity();
|
|
this._addStateSyncStatusEntity();
|
|
|
|
// Add FrothyEntity table only for subgraph watchers
|
|
if (subgraphPath) {
|
|
this._addFrothyEntity();
|
|
}
|
|
|
|
const template = Handlebars.compile(this._templateString);
|
|
this._entities.forEach(entityObj => {
|
|
const entity = template(entityObj);
|
|
const outStream: Writable = entityDir
|
|
? fs.createWriteStream(path.join(entityDir, `${entityObj.className}.ts`))
|
|
: process.stdout;
|
|
outStream.write(entity);
|
|
});
|
|
}
|
|
|
|
addSubgraphEntities (subgraphSchemaDocument: any): void {
|
|
const subgraphTypeDefs = subgraphSchemaDocument.definitions;
|
|
|
|
subgraphTypeDefs.forEach((def: any) => {
|
|
// TODO Handle enum types.
|
|
if (def.kind !== 'ObjectTypeDefinition') {
|
|
return;
|
|
}
|
|
|
|
let entityObject: any = {
|
|
className: def.name.value,
|
|
indexOn: [],
|
|
columns: [],
|
|
imports: []
|
|
};
|
|
|
|
entityObject.imports.push(
|
|
{
|
|
toImport: new Set(['Entity', 'PrimaryColumn', 'Column', 'Index']),
|
|
from: 'typeorm'
|
|
}
|
|
);
|
|
|
|
entityObject.indexOn.push({
|
|
columns: ['blockNumber']
|
|
});
|
|
|
|
// Add common columns.
|
|
entityObject.columns.push({
|
|
name: 'id',
|
|
pgType: 'varchar',
|
|
tsType: 'string',
|
|
columnType: 'PrimaryColumn',
|
|
columnOptions: []
|
|
});
|
|
entityObject.columns.push({
|
|
name: 'blockHash',
|
|
pgType: 'varchar',
|
|
tsType: 'string',
|
|
columnType: 'PrimaryColumn',
|
|
columnOptions: [
|
|
{
|
|
option: 'length',
|
|
value: 66
|
|
}
|
|
]
|
|
});
|
|
entityObject.columns.push({
|
|
name: 'blockNumber',
|
|
pgType: 'integer',
|
|
tsType: 'number',
|
|
columnType: 'Column'
|
|
});
|
|
|
|
// Add subgraph entity specific columns.
|
|
entityObject = this._addSubgraphColumns(subgraphTypeDefs, entityObject, def);
|
|
|
|
// Add is_pruned, is_removed columns.
|
|
entityObject.columns.push(
|
|
{
|
|
name: 'isPruned',
|
|
pgType: 'boolean',
|
|
tsType: 'boolean',
|
|
columnType: 'Column',
|
|
columnOptions: [
|
|
{
|
|
option: 'default',
|
|
value: false
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: 'isRemoved',
|
|
pgType: 'boolean',
|
|
tsType: 'boolean',
|
|
columnType: 'Column',
|
|
columnOptions: [
|
|
{
|
|
option: 'default',
|
|
value: false
|
|
}
|
|
]
|
|
}
|
|
);
|
|
|
|
// Add decimalTransformer column option if required.
|
|
this._addDecimalTransformerOption(entityObject);
|
|
|
|
// Add bigintTransformer column option if required.
|
|
this._addBigIntTransformerOption(entityObject);
|
|
|
|
this._entities.push(entityObject);
|
|
});
|
|
}
|
|
|
|
_addEventEntity (): void {
|
|
const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'Event.yaml'), 'utf8'));
|
|
this._entities.push(entity);
|
|
}
|
|
|
|
_addSyncStatusEntity (): void {
|
|
const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'SyncStatus.yaml'), 'utf8'));
|
|
this._entities.push(entity);
|
|
}
|
|
|
|
_addContractEntity (): void {
|
|
const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'Contract.yaml'), 'utf8'));
|
|
this._entities.push(entity);
|
|
}
|
|
|
|
_addBlockProgressEntity (): void {
|
|
const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'BlockProgress.yaml'), 'utf8'));
|
|
this._entities.push(entity);
|
|
}
|
|
|
|
_addStateEntity (): void {
|
|
const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'State.yaml'), 'utf8'));
|
|
this._entities.push(entity);
|
|
}
|
|
|
|
_addStateSyncStatusEntity (): void {
|
|
const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'StateSyncStatus.yaml'), 'utf8'));
|
|
this._entities.push(entity);
|
|
}
|
|
|
|
_addFrothyEntity (): void {
|
|
const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'FrothyEntity.yaml'), 'utf8'));
|
|
this._entities.push(entity);
|
|
}
|
|
|
|
_addBigIntTransformerOption (entityObject: any): void {
|
|
let importObject = entityObject.imports.find((element: any) => {
|
|
return element.from === '@cerc-io/util';
|
|
});
|
|
|
|
entityObject.columns.forEach((column: any) => {
|
|
if (column.tsType.includes('bigint')) {
|
|
// Check if it is of array type
|
|
if (column.tsType.includes('bigint[]')) {
|
|
// Implement bigintArrayTransformer for array of bigint type.
|
|
column.columnOptions.push(
|
|
{
|
|
option: 'transformer',
|
|
value: 'bigintArrayTransformer'
|
|
}
|
|
);
|
|
|
|
if (importObject) {
|
|
importObject.toImport.add('bigintArrayTransformer');
|
|
} else {
|
|
importObject = {
|
|
toImport: new Set(['bigintArrayTransformer']),
|
|
from: '@cerc-io/util'
|
|
};
|
|
|
|
entityObject.imports.push(importObject);
|
|
}
|
|
} else {
|
|
// Implement bigintTransformer for bigint type.
|
|
column.columnOptions.push(
|
|
{
|
|
option: 'transformer',
|
|
value: 'bigintTransformer'
|
|
}
|
|
);
|
|
|
|
if (importObject) {
|
|
importObject.toImport.add('bigintTransformer');
|
|
} else {
|
|
importObject = {
|
|
toImport: new Set(['bigintTransformer']),
|
|
from: '@cerc-io/util'
|
|
};
|
|
|
|
entityObject.imports.push(importObject);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
_addDecimalTransformerOption (entityObject: any): void {
|
|
let importObject = entityObject.imports.find((element: any) => {
|
|
return element.from === '@cerc-io/util';
|
|
});
|
|
|
|
let isDecimalRequired = false;
|
|
|
|
entityObject.columns.forEach((column: any) => {
|
|
if (column.tsType.includes('Decimal')) {
|
|
isDecimalRequired = true;
|
|
|
|
// Check if it is of array type
|
|
if (column.tsType.includes('Decimal[]')) {
|
|
// Implement decimalArrayTransformer for array of Decimal type.
|
|
column.columnOptions.push(
|
|
{
|
|
option: 'transformer',
|
|
value: 'decimalArrayTransformer'
|
|
}
|
|
);
|
|
|
|
if (importObject) {
|
|
importObject.toImport.add('decimalArrayTransformer');
|
|
} else {
|
|
importObject = {
|
|
toImport: new Set(['decimalArrayTransformer']),
|
|
from: '@cerc-io/util'
|
|
};
|
|
|
|
entityObject.imports.push(importObject);
|
|
}
|
|
} else {
|
|
// Implement decimalTransformer for Decimal type.
|
|
column.columnOptions.push(
|
|
{
|
|
option: 'transformer',
|
|
value: 'decimalTransformer'
|
|
}
|
|
);
|
|
|
|
if (importObject) {
|
|
importObject.toImport.add('decimalTransformer');
|
|
} else {
|
|
importObject = {
|
|
toImport: new Set(['decimalTransformer']),
|
|
from: '@cerc-io/util'
|
|
};
|
|
|
|
entityObject.imports.push(importObject);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (isDecimalRequired) {
|
|
entityObject.imports.push(
|
|
{
|
|
toImport: new Set(['Decimal']),
|
|
from: 'decimal.js'
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
_addSubgraphColumns (subgraphTypeDefs: any, entityObject: any, def: any): any {
|
|
def.fields.forEach((field: any) => {
|
|
if (field.directives.some((directive: any) => directive.name.value === 'derivedFrom')) {
|
|
// Do not add column if it is a derived field.
|
|
return;
|
|
}
|
|
|
|
let name = field.name.value;
|
|
|
|
// Column id is already added.
|
|
if (name === 'id') {
|
|
return;
|
|
}
|
|
|
|
// Handle column with existing name.
|
|
if (['blockHash', 'blockNumber'].includes(name)) {
|
|
name = `_${name}`;
|
|
}
|
|
|
|
const columnObject: any = {
|
|
name,
|
|
columnOptions: [],
|
|
columnType: 'Column'
|
|
};
|
|
|
|
const { typeName, array, nullable } = getFieldType(field.type);
|
|
let tsType = getTsForGql(typeName);
|
|
|
|
if (subgraphTypeDefs.some((typeDef: any) => typeDef.kind === 'EnumTypeDefinition' && typeDef.name.value === typeName)) {
|
|
// Create enum type column.
|
|
|
|
const entityImport = entityObject.imports.find(({ from }: any) => from === '../types');
|
|
|
|
if (!entityImport) {
|
|
entityObject.imports.push(
|
|
{
|
|
toImport: new Set([typeName]),
|
|
from: '../types'
|
|
}
|
|
);
|
|
} else {
|
|
entityImport.toImport.add(typeName);
|
|
}
|
|
|
|
columnObject.columnOptions.push(
|
|
{
|
|
option: 'type',
|
|
value: "'enum'"
|
|
},
|
|
{
|
|
option: 'enum',
|
|
value: typeName
|
|
}
|
|
);
|
|
|
|
columnObject.tsType = typeName;
|
|
} else {
|
|
if (!tsType) {
|
|
tsType = 'string';
|
|
}
|
|
|
|
columnObject.tsType = tsType;
|
|
|
|
// Enum type does not require pgType.
|
|
columnObject.pgType = getPgForTs(tsType);
|
|
}
|
|
|
|
// Handle basic array types.
|
|
if (array) {
|
|
columnObject.columnOptions.push({
|
|
option: 'array',
|
|
value: 'true'
|
|
});
|
|
|
|
columnObject.tsType = `${tsType}[]`;
|
|
}
|
|
|
|
if (nullable) {
|
|
columnObject.columnOptions.push({
|
|
option: 'nullable',
|
|
value: 'true'
|
|
});
|
|
|
|
columnObject.tsType = `${tsType} | null`;
|
|
}
|
|
|
|
entityObject.columns.push(columnObject);
|
|
});
|
|
|
|
return entityObject;
|
|
}
|
|
}
|