// // 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 { getTsForSol, getPgForTs, getTsForGql } from './utils/type-mappings'; import { Param } from './utils/types'; import { getFieldType } from './utils/subgraph'; const TEMPLATE_FILE = './templates/entity-template.handlebars'; const TABLES_DIR = './data/entities'; export class Entity { _entities: Array; _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, returnType: string): 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 tsType = getTsForSol(param.type); 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 }; }) ); const tsReturnType = getTsForSol(returnType); assert(tsReturnType); const pgReturnType = getPgForTs(tsReturnType); assert(pgReturnType); entityObject.columns.push({ name: '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): void { this._addEventEntity(); this._addSyncStatusEntity(); this._addContractEntity(); this._addBlockProgressEntity(); this._addIPLDBlockEntity(); this._addIpldStatusEntity(); 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']), from: 'typeorm' } ); // 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 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); } _addIPLDBlockEntity (): void { const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'IPLDBlock.yaml'), 'utf8')); this._entities.push(entity); } _addIpldStatusEntity (): void { const entity = yaml.load(fs.readFileSync(path.resolve(__dirname, TABLES_DIR, 'IpldStatus.yaml'), 'utf8')); this._entities.push(entity); } _addBigIntTransformerOption (entityObject: any): void { let importObject = entityObject.imports.find((element: any) => { return element.from === '@vulcanize/util'; }); entityObject.columns.forEach((column: any) => { // Implement bigintTransformer for bigint type. if (column.tsType === 'bigint') { column.columnOptions.push( { option: 'transformer', value: 'bigintTransformer' } ); if (importObject) { importObject.toImport.add('bigintTransformer'); } else { importObject = { toImport: new Set(['bigintTransformer']), from: '@vulcanize/util' }; entityObject.imports.push(importObject); } } // Implement bigintArrayTransformer for array of bigint type. if (column.tsType === 'bigint[]') { column.columnOptions.push( { option: 'transformer', value: 'bigintArrayTransformer' } ); if (importObject) { importObject.toImport.add('bigintArrayTransformer'); } else { importObject = { toImport: new Set(['bigintArrayTransformer']), from: '@vulcanize/util' }; entityObject.imports.push(importObject); } } }); } _addDecimalTransformerOption (entityObject: any): void { let importObject = entityObject.imports.find((element: any) => { return element.from === '@vulcanize/util'; }); let isDecimalRequired = false; entityObject.columns.forEach((column: any) => { // Implement decimalTransformer for Decimal type. if (column.tsType === 'Decimal') { isDecimalRequired = true; column.columnOptions.push( { option: 'transformer', value: 'decimalTransformer' } ); if (importObject) { importObject.toImport.add('decimalTransformer'); } else { importObject = { toImport: new Set(['decimalTransformer']), from: '@vulcanize/util' }; entityObject.imports.push(importObject); } } // Implement decimalArrayTransformer for array of Decimal type. if (column.tsType === 'Decimal[]') { isDecimalRequired = true; column.columnOptions.push( { option: 'transformer', value: 'decimalArrayTransformer' } ); if (importObject) { importObject.toImport.add('decimalArrayTransformer'); } else { importObject = { toImport: new Set(['decimalArrayTransformer']), from: '@vulcanize/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' }); } entityObject.columns.push(columnObject); }); return entityObject; } }