UNPKG

envio

Version:

A latency and sync speed optimized, developer friendly blockchain data indexer.

1,209 lines (1,165 loc) • 52.9 kB
// Generated by ReScript, PLEASE EDIT WITH CARE import * as Fs from "fs"; import * as Env from "./Env.res.mjs"; import * as Sink from "./Sink.res.mjs"; import * as Path from "path"; import * as Table from "./db/Table.res.mjs"; import * as Utils from "./Utils.res.mjs"; import * as Config from "./Config.res.mjs"; import * as Hasura from "./Hasura.res.mjs"; import * as Hrtime from "./bindings/Hrtime.res.mjs"; import * as Schema from "./db/Schema.res.mjs"; import * as Logging from "./Logging.res.mjs"; import * as Internal from "./Internal.res.mjs"; import Postgres from "postgres"; import * as EventUtils from "./EventUtils.res.mjs"; import * as Prometheus from "./Prometheus.res.mjs"; import * as Persistence from "./Persistence.res.mjs"; import * as ChainFetcher from "./ChainFetcher.res.mjs"; import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js"; import * as EntityHistory from "./db/EntityHistory.res.mjs"; import * as InternalTable from "./db/InternalTable.res.mjs"; import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js"; import * as Child_process from "child_process"; import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js"; import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js"; import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js"; import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js"; import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs"; import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js"; let getCacheRowCountFnName = "get_cache_row_count"; function makeClient() { return Postgres({ host: Env.Db.host, port: Env.Db.port, database: Env.Db.database, username: Env.Db.user, password: Env.Db.password, ssl: Env.Db.ssl, max: Env.Db.maxConnections, onnotice: Primitive_object.equal(Env.userLogLevel, "warn") || Primitive_object.equal(Env.userLogLevel, "error") ? undefined : _str => {}, transform: { undefined: null } }); } function makeCreateIndexQuery(tableName, indexFields, pgSchema) { let indexName = tableName + "_" + indexFields.join("_"); let index = indexFields.map(idx => `"` + idx + `"`).join(", "); return `CREATE INDEX IF NOT EXISTS "` + indexName + `" ON "` + pgSchema + `"."` + tableName + `"(` + index + `);`; } function directionToSql(direction) { if (direction === "Asc") { return ""; } else { return " DESC"; } } function directionToIndexName(direction) { if (direction === "Asc") { return ""; } else { return "_desc"; } } function makeCreateCompositeIndexQuery(tableName, indexFields, pgSchema) { let indexName = tableName + "_" + indexFields.map(f => f.fieldName + directionToIndexName(f.direction)).join("_"); let index = indexFields.map(f => `"` + f.fieldName + `"` + directionToSql(f.direction)).join(", "); return `CREATE INDEX IF NOT EXISTS "` + indexName + `" ON "` + pgSchema + `"."` + tableName + `"(` + index + `);`; } function makeCreateTableIndicesQuery(table, pgSchema) { let tableName = table.tableName; let createIndex = indexField => makeCreateIndexQuery(tableName, [indexField], pgSchema); let createCompositeIndex = indexFields => makeCreateCompositeIndexQuery(tableName, indexFields, pgSchema); let singleIndices = Table.getSingleIndices(table); let compositeIndices = Table.getCompositeIndices(table); return singleIndices.map(createIndex).join("\n") + compositeIndices.map(createCompositeIndex).join("\n"); } function makeCreateTableQuery(table, pgSchema, isNumericArrayAsText) { let fieldsMapped = Table.getFields(table).map(field => { let defaultValue = field.defaultValue; let isNullable = field.isNullable; let fieldName = Table.getPgDbFieldName(field); return `"` + fieldName + `" ` + Table.getPgFieldType(field.fieldType, pgSchema, field.isArray, isNumericArrayAsText, isNullable) + ( defaultValue !== undefined ? ` DEFAULT ` + defaultValue : ( isNullable ? `` : ` NOT NULL` ) ); }).join(", "); let primaryKeyFieldNames = Table.getPgPrimaryKeyFieldNames(table); let primaryKey = primaryKeyFieldNames.map(field => `"` + field + `"`).join(", "); return `CREATE TABLE IF NOT EXISTS "` + pgSchema + `"."` + table.tableName + `"(` + fieldsMapped + ( primaryKeyFieldNames.length !== 0 ? `, PRIMARY KEY(` + primaryKey + `)` : "" ) + `);`; } let entityHistoryCache = new WeakMap(); function getEntityHistory(entityConfig) { let cache = entityHistoryCache.get(entityConfig); if (cache !== undefined) { return cache; } let dataFields = Stdlib_Array.filterMap(entityConfig.table.fields, field => { if (field.TAG !== "Field") { return; } let field$1 = field._0; let match = field$1.fieldName; if (match === "id") { return { TAG: "Field", _0: { fieldName: "id", fieldType: field$1.fieldType, fieldSchema: field$1.fieldSchema, isArray: field$1.isArray, isNullable: field$1.isNullable, isPrimaryKey: true, isIndex: field$1.isIndex, linkedEntity: field$1.linkedEntity, defaultValue: field$1.defaultValue, description: field$1.description, postgresDbName: field$1.postgresDbName, clickhouseDbName: field$1.clickhouseDbName } }; } else { return { TAG: "Field", _0: { fieldName: field$1.fieldName, fieldType: field$1.fieldType, fieldSchema: field$1.fieldSchema, isArray: field$1.isArray, isNullable: true, isPrimaryKey: field$1.isPrimaryKey, isIndex: false, linkedEntity: field$1.linkedEntity, defaultValue: field$1.defaultValue, description: field$1.description, postgresDbName: field$1.postgresDbName, clickhouseDbName: field$1.clickhouseDbName } }; } }); let actionField = Table.mkField(EntityHistory.changeFieldName, EntityHistory.changeFieldType, S$RescriptSchema.never, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined); let checkpointIdField = Table.mkField(EntityHistory.checkpointIdFieldName, EntityHistory.checkpointIdFieldType, EntityHistory.unsafeCheckpointIdSchema, undefined, undefined, undefined, true, undefined, undefined, undefined, undefined, undefined); let entityTableName = entityConfig.table.tableName; let historyTableName = EntityHistory.historyTableName(entityTableName, entityConfig.index); let table = Table.mkTable(historyTableName, undefined, dataFields.concat([ checkpointIdField, actionField ]), undefined); let setChangeSchema = EntityHistory.makeSetUpdateSchema(entityConfig.schema); let cache_setChangeSchemaRows = S$RescriptSchema.array(setChangeSchema); let cache$1 = { table: table, setChangeSchema: setChangeSchema, setChangeSchemaRows: cache_setChangeSchemaRows }; entityHistoryCache.set(entityConfig, cache$1); return cache$1; } function makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigsOpt, entitiesOpt, enumsOpt, isEmptyPgSchemaOpt) { let chainConfigs = chainConfigsOpt !== undefined ? chainConfigsOpt : []; let entities = entitiesOpt !== undefined ? entitiesOpt : []; let enums = enumsOpt !== undefined ? enumsOpt : []; let isEmptyPgSchema = isEmptyPgSchemaOpt !== undefined ? isEmptyPgSchemaOpt : false; let generalTables = [ InternalTable.Chains.table, InternalTable.EnvioInfo.table, InternalTable.Checkpoints.table, InternalTable.RawEvents.table ]; let allTables = generalTables.slice(); let allEntityTables = []; entities.forEach(entityConfig => { allEntityTables.push(entityConfig.table); allTables.push(entityConfig.table); allTables.push(getEntityHistory(entityConfig).table); }); let derivedSchema = Schema.make(allEntityTables); let query = { contents: ( isEmptyPgSchema && pgSchema === "public" ? `CREATE SCHEMA IF NOT EXISTS "` + pgSchema + `";\n` : `DROP SCHEMA IF EXISTS "` + pgSchema + `" CASCADE; CREATE SCHEMA "` + pgSchema + `";\n` ) + (`GRANT ALL ON SCHEMA "` + pgSchema + `" TO "` + pgUser + `"; GRANT ALL ON SCHEMA "` + pgSchema + `" TO public;`) }; enums.forEach(enumConfig => { let enumCreateQuery = `CREATE TYPE "` + pgSchema + `".` + enumConfig.name + ` AS ENUM(` + enumConfig.variants.map(v => `'` + v + `'`).join(", ") + `);`; query.contents = query.contents + "\n" + enumCreateQuery; }); allTables.forEach(table => { query.contents = query.contents + "\n" + makeCreateTableQuery(table, pgSchema, isHasuraEnabled); }); allTables.forEach(table => { let indices = makeCreateTableIndicesQuery(table, pgSchema); if (indices !== "") { query.contents = query.contents + "\n" + indices; return; } }); entities.forEach(entity => { Table.getDerivedFromFields(entity.table).forEach(derivedFromField => { let indexField = Utils.unwrapResultExn(Schema.getDerivedFromPgFieldName(derivedSchema, derivedFromField)); query.contents = query.contents + "\n" + makeCreateIndexQuery(derivedFromField.derivedFromEntity, [indexField], pgSchema); }); }); query.contents = query.contents + "\n" + InternalTable.Views.makeMetaViewQuery(pgSchema); query.contents = query.contents + "\n" + InternalTable.Views.makeChainMetadataViewQuery(pgSchema); let initialChainsValuesQuery = InternalTable.Chains.makeInitialValuesQuery(pgSchema, chainConfigs); if (initialChainsValuesQuery !== undefined) { query.contents = query.contents + "\n" + initialChainsValuesQuery; } return [ query.contents, `CREATE OR REPLACE FUNCTION ` + getCacheRowCountFnName + `(table_name text) RETURNS integer AS $$ DECLARE result integer; BEGIN EXECUTE format('SELECT COUNT(*) FROM "` + pgSchema + `".%I', table_name) INTO result; RETURN result; END; $$ LANGUAGE plpgsql;` ]; } function makeLoadQuery(pgSchema, tableName, condition) { return `SELECT * FROM "` + pgSchema + `"."` + tableName + `" WHERE ` + condition + `;`; } function makeFilterCondition(filter, table, params) { let getQueryFieldOrThrow = fieldName => { let queryField = Table.queryFields(table)[fieldName]; if (queryField !== undefined) { return queryField; } throw { RE_EXN_ID: Persistence.StorageError, message: `Failed loading "` + table.tableName + `" from storage. The table doesn't have the field "` + fieldName + `".`, reason: { RE_EXN_ID: Table.NonExistingTableField, _1: fieldName }, Error: new Error() }; }; let serializeParamOrThrow = (queryField, fieldName, fieldValue, isArray) => { let param; try { param = S$RescriptSchema.reverseConvertToJsonOrThrow(fieldValue, isArray ? queryField.arrayFieldSchema : queryField.fieldSchema); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); throw { RE_EXN_ID: Persistence.StorageError, message: `Failed loading "` + table.tableName + `" from storage by field "` + fieldName + `". Couldn't serialize provided value.`, reason: exn, Error: new Error() }; } params.push(param); return `$` + params.length.toString(); }; let scalarCondition = (fieldName, fieldValue, op) => { let queryField = getQueryFieldOrThrow(fieldName); return `"` + queryField.pgDbFieldName + `" ` + op + ` ` + serializeParamOrThrow(queryField, fieldName, fieldValue, false); }; switch (filter.operator) { case "=" : return scalarCondition(filter.fieldName, filter.fieldValue, "="); case ">" : return scalarCondition(filter.fieldName, filter.fieldValue, ">"); case "<" : return scalarCondition(filter.fieldName, filter.fieldValue, "<"); case "in" : let fieldName = filter.fieldName; let queryField = getQueryFieldOrThrow(fieldName); return `"` + queryField.pgDbFieldName + `" = ANY(` + serializeParamOrThrow(queryField, fieldName, filter.fieldValue, true) + `)`; case "and" : let filters = filter.filters; if (filters.length !== 0) { return `(` + filters.map(filter => makeFilterCondition(filter, table, params)).join(" AND ") + `)`; } throw { RE_EXN_ID: Persistence.StorageError, message: `Failed loading "` + table.tableName + `" from storage. The "and" filter must contain at least one nested filter.`, reason: new Error(`Empty "and" filter`), Error: new Error() }; } } function makeDeleteByIdQuery(pgSchema, tableName) { return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = $1;`; } function makeDeleteByIdsQuery(pgSchema, tableName) { return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = ANY($1::text[]);`; } function makeLoadAllQuery(pgSchema, tableName) { return `SELECT * FROM "` + pgSchema + `"."` + tableName + `";`; } function makeInsertUnnestSetQuery(pgSchema, table, itemSchema, isRawEvents) { let match = Table.toSqlParams(table, itemSchema, pgSchema); let quotedNonPrimaryFieldNames = match.quotedNonPrimaryFieldNames; let primaryKeyFieldNames = Table.getPgPrimaryKeyFieldNames(table); return `INSERT INTO "` + pgSchema + `"."` + table.tableName + `" (` + match.quotedFieldNames.join(", ") + `) SELECT * FROM unnest(` + match.arrayFieldTypes.map((arrayFieldType, idx) => `$` + (idx + 1 | 0).toString() + `::` + arrayFieldType).join(",") + `)` + ( isRawEvents || primaryKeyFieldNames.length === 0 ? `` : `ON CONFLICT(` + primaryKeyFieldNames.map(fieldName => `"` + fieldName + `"`).join(",") + `) DO ` + ( Utils.$$Array.isEmpty(quotedNonPrimaryFieldNames) ? `NOTHING` : `UPDATE SET ` + quotedNonPrimaryFieldNames.map(fieldName => fieldName + ` = EXCLUDED.` + fieldName).join(",") ) ) + ";"; } function makeInsertValuesSetQuery(pgSchema, table, itemSchema, itemsCount) { let match = Table.toSqlParams(table, itemSchema, pgSchema); let quotedNonPrimaryFieldNames = match.quotedNonPrimaryFieldNames; let quotedFieldNames = match.quotedFieldNames; let primaryKeyFieldNames = Table.getPgPrimaryKeyFieldNames(table); let fieldsCount = quotedFieldNames.length; let placeholders = ""; for (let idx = 1; idx <= itemsCount; ++idx) { if (idx > 1) { placeholders = placeholders + ","; } placeholders = placeholders + "("; for (let fieldIdx = 0; fieldIdx < fieldsCount; ++fieldIdx) { if (fieldIdx > 0) { placeholders = placeholders + ","; } placeholders = placeholders + (`$` + ((fieldIdx * itemsCount | 0) + idx | 0).toString()); } placeholders = placeholders + ")"; } return `INSERT INTO "` + pgSchema + `"."` + table.tableName + `" (` + quotedFieldNames.join(", ") + `) VALUES` + placeholders + ( primaryKeyFieldNames.length !== 0 ? `ON CONFLICT(` + primaryKeyFieldNames.map(fieldName => `"` + fieldName + `"`).join(",") + `) DO ` + ( Utils.$$Array.isEmpty(quotedNonPrimaryFieldNames) ? `NOTHING` : `UPDATE SET ` + quotedNonPrimaryFieldNames.map(fieldName => fieldName + ` = EXCLUDED.` + fieldName).join(",") ) : `` ) + ";"; } function makeTableBatchSetQuery(pgSchema, table, itemSchema) { let match = Table.toSqlParams(table, itemSchema, pgSchema); let isRawEvents = table.tableName === InternalTable.RawEvents.table.tableName; let isHistoryUpdate = table.tableName.startsWith(EntityHistory.historyTablePrefix); if ((isRawEvents || !match.hasArrayField) && !isHistoryUpdate) { return { query: makeInsertUnnestSetQuery(pgSchema, table, itemSchema, isRawEvents), convertOrThrow: S$RescriptSchema.compile(S$RescriptSchema.unnest(match.dbSchema), "Output", "Input", "Sync", false), isInsertValues: false }; } else { return { query: makeInsertValuesSetQuery(pgSchema, table, itemSchema, 500), convertOrThrow: S$RescriptSchema.compile(S$RescriptSchema.preprocess(S$RescriptSchema.unnest(itemSchema), param => ({ s: prim => prim.flat(1) })), "Output", "Input", "Sync", false), isInsertValues: true }; } } function chunkArray(arr, chunkSize) { let chunks = []; let i = 0; while (i < arr.length) { let chunk = arr.slice(i, i + chunkSize | 0); chunks.push(chunk); i = i + chunkSize | 0; }; return chunks; } function removeInvalidUtf8DeepInPlace(value) { if (typeof value === "string") { return value.replaceAll("\x00", ""); } else if (typeof value === "object" && value !== null) { Utils.Dict.forEachWithKey(value, (v, k) => { value[k] = removeInvalidUtf8DeepInPlace(v); }); return value; } else { return value; } } function removeInvalidUtf8InPlace(items) { items.forEach(item => { removeInvalidUtf8DeepInPlace(item); }); } let pgErrorMessageSchema = S$RescriptSchema.object(s => s.f("message", S$RescriptSchema.string)); let PgEncodingError = /* @__PURE__ */Primitive_exceptions.create("PgStorage.PgEncodingError"); function classifyWriteError(specificError, table, exn) { let normalizedExn = Primitive_exceptions.internalToException(exn.RE_EXN_ID === "JsExn" || exn.RE_EXN_ID !== Persistence.StorageError ? exn : exn.reason); if (normalizedExn.RE_EXN_ID === "JsExn") { let val; try { val = S$RescriptSchema.parseOrThrow(normalizedExn._1, pgErrorMessageSchema); } catch (exn$1) { return; } switch (val) { case "current transaction is aborted, commands ignored until end of transaction block" : return; case "invalid byte sequence for encoding \"UTF8\": 0x00" : case "unsupported Unicode escape sequence" : specificError.contents = { RE_EXN_ID: PgEncodingError, table: table }; return; default: specificError.contents = Utils.prettifyExn(exn); return; } } else { if (normalizedExn.RE_EXN_ID !== S$RescriptSchema.Raised) { return; } throw normalizedExn; } } let setQueryCache = new WeakMap(); async function setOrThrow(sql, items, table, itemSchema, pgSchema) { if (items.length === 0) { return; } let cached = setQueryCache.get(table); let data; if (cached !== undefined) { data = Primitive_option.valFromOption(cached); } else { let newQuery = makeTableBatchSetQuery(pgSchema, table, itemSchema); setQueryCache.set(table, newQuery); data = newQuery; } try { if (!data.isInsertValues) { return await sql.unsafe(data.query, data.convertOrThrow(items), {prepare: true}); } let chunks = chunkArray(items, 500); let responses = []; chunks.forEach(chunk => { let chunkSize = chunk.length; let isFullChunk = chunkSize === 500; let params = data.convertOrThrow(chunk); let response = isFullChunk ? sql.unsafe(data.query, params, {prepare: true}) : sql.unsafe(makeInsertValuesSetQuery(pgSchema, table, itemSchema, chunkSize), params); responses.push(response); }); await Promise.all(responses); return; } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); if (exn.RE_EXN_ID === S$RescriptSchema.Raised) { throw { RE_EXN_ID: Persistence.StorageError, message: `Failed to convert items for table "` + table.tableName + `"`, reason: exn, Error: new Error() }; } throw { RE_EXN_ID: Persistence.StorageError, message: `Failed to insert items into table "` + table.tableName + `"`, reason: Utils.prettifyExn(exn), Error: new Error() }; } } function makeSchemaTableNamesQuery(pgSchema) { return `SELECT table_name FROM information_schema.tables WHERE table_schema = '` + pgSchema + `';`; } let cacheTablePrefixLength = Internal.cacheTablePrefix.length; function makeSchemaCacheTableInfoQuery(pgSchema) { return `SELECT t.table_name, ` + getCacheRowCountFnName + `(t.table_name) as count FROM information_schema.tables t WHERE t.table_schema = '` + pgSchema + `' AND t.table_name LIKE '` + Internal.cacheTablePrefix + `%';`; } let psqlExecState = { contents: "Unknown" }; async function getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containerName) { let promise = psqlExecState.contents; if (typeof promise === "object") { if (promise.TAG === "Pending") { return await promise._0; } else { return promise._0; } } let promise$1 = new Promise((resolve, _reject) => { let binary = "psql"; Child_process.exec(binary + ` --version`, (error, param, param$1) => { if (error === null) { return resolve({ TAG: "Ok", _0: binary + ` -h ` + pgHost + ` -p ` + pgPort.toString() + ` -U ` + pgUser + ` -d ` + pgDatabase }); } let binary$1 = `docker exec -i -u ` + pgUser + ` ` + containerName + ` psql`; Child_process.exec(binary$1 + ` --version`, (error, param, param$1) => { if (error === null) { return resolve({ TAG: "Ok", _0: binary$1 + ` -h ` + pgHost + ` -p ` + (5432).toString() + ` -U ` + pgUser + ` -d ` + pgDatabase }); } else { return resolve({ TAG: "Error", _0: `Please check if "psql" binary is installed or Docker container "` + containerName + `" is running.` }); } }); }); }); psqlExecState.contents = { TAG: "Pending", _0: promise$1 }; let result = await promise$1; psqlExecState.contents = { TAG: "Resolved", _0: result }; return result; } async function deleteByIdsOrThrow(sql, pgSchema, ids, table) { try { await ( ids.length !== 1 ? sql.unsafe(makeDeleteByIdsQuery(pgSchema, table.tableName), [ids], {prepare: true}) : sql.unsafe(makeDeleteByIdQuery(pgSchema, table.tableName), ids, {prepare: true}) ); return; } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); throw { RE_EXN_ID: Persistence.StorageError, message: `Failed deleting "` + table.tableName + `" from storage by ids`, reason: exn, Error: new Error() }; } } function makeInsertDeleteUpdatesQuery(entityConfig, pgSchema) { let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index); let allHistoryFieldNames = Stdlib_Array.filterMap(entityConfig.table.fields, fieldOrDerived => { if (fieldOrDerived.TAG === "Field") { return Table.getPgDbFieldName(fieldOrDerived._0); } }); allHistoryFieldNames.push(EntityHistory.checkpointIdFieldName); allHistoryFieldNames.push(EntityHistory.changeFieldName); let allHistoryFieldNamesStr = allHistoryFieldNames.map(name => `"` + name + `"`).join(", "); let selectParts = allHistoryFieldNames.map(fieldName => { if (fieldName === Table.idFieldName) { return `u.` + Table.idFieldName; } else if (fieldName === EntityHistory.checkpointIdFieldName) { return `u.` + EntityHistory.checkpointIdFieldName; } else if (fieldName === EntityHistory.changeFieldName) { return `'` + "DELETE" + `'`; } else { return "NULL"; } }); let selectPartsStr = selectParts.join(", "); let checkpointIdPgType = Table.getPgFieldType(EntityHistory.checkpointIdFieldType, pgSchema, false, false, false); return `INSERT INTO "` + pgSchema + `"."` + historyTableName + `" (` + allHistoryFieldNamesStr + `) SELECT ` + selectPartsStr + ` FROM UNNEST($1::text[], $2::` + checkpointIdPgType + `[]) AS u(` + Table.idFieldName + `, ` + EntityHistory.checkpointIdFieldName + `)`; } function executeSet(sql, items, dbFunction) { if (items.length !== 0) { return dbFunction(sql, items); } else { return Promise.resolve(); } } function convertFieldsToJson(fields) { if (fields !== undefined) { return Utils.Dict.mapValues(fields, value => { if (typeof value === "bigint") { return value.toString(); } else { return value; } }); } else { return {}; } } function makeRawEvent(eventItem, config) { let event = eventItem.event; let blockNumber = eventItem.blockNumber; let eventConfig = eventItem.eventConfig; let logIndex = event.logIndex; let eventId = EventUtils.packEventIndex(blockNumber, logIndex); let blockFields = convertFieldsToJson(event.block); let transactionFields = convertFieldsToJson(event.transaction); config.ecosystem.cleanUpRawEventFieldsInPlace(blockFields); let params = S$RescriptSchema.reverseConvertOrThrow(event.params, eventConfig.paramsRawEventSchema); let params$1 = params === null ? "null" : params; return { chain_id: eventItem.chain, event_id: eventId, event_name: eventConfig.name, contract_name: eventConfig.contractName, block_number: blockNumber, log_index: logIndex, src_address: event.srcAddress, block_hash: eventItem.blockHash, block_timestamp: eventItem.timestamp, block_fields: blockFields, transaction_fields: transactionFields, params: params$1 }; } async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, chainMetaData, escapeTables) { try { let shouldSaveHistory = Config.shouldSaveHistory(config, isInReorgThreshold); let specificError = { contents: undefined }; let rawEvents; if (config.enableRawEvents) { let rows = Stdlib_Array.filterMap(batch.items, item => { if (item.kind === 0) { return makeRawEvent(item, config); } }); if (escapeTables !== undefined && Primitive_option.valFromOption(escapeTables).has(InternalTable.RawEvents.table)) { removeInvalidUtf8InPlace(rows); } rawEvents = rows; } else { rawEvents = []; } let setRawEvents = async sql => { try { return await executeSet(sql, rawEvents, (sql, items) => setOrThrow(sql, items, InternalTable.RawEvents.table, InternalTable.RawEvents.schema, pgSchema)); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); return classifyWriteError(specificError, InternalTable.RawEvents.table, exn); } }; let setEntities = updatedEntities.map(param => { let entityConfig = param.entityConfig; let entitiesToSet = []; let idsToDelete = []; let diffCheckpointId = Stdlib_Option.map(rollback, r => r.diffCheckpointId); let batchSetUpdates = []; let batchDeleteEntityIds = []; let batchDeleteCheckpointIds = []; let idsWithDiff = new Set(); let latestChangeById = {}; let orderedIds = []; param.changes.forEach(change => { let entityId = change.entityId; if (Stdlib_Option.isNone(latestChangeById[entityId])) { orderedIds.push(entityId); } latestChangeById[entityId] = change; if (!shouldSaveHistory) { return; } if (change.checkpointId === diffCheckpointId) { idsWithDiff.add(entityId); return; } if (change.type === "SET") { batchSetUpdates.push(change); return; } batchDeleteEntityIds.push(change.entityId); batchDeleteCheckpointIds.push(change.checkpointId); }); let backfillHistoryIds = new Set(); orderedIds.forEach(entityId => { let match = latestChangeById[entityId]; if (match.type === "SET") { entitiesToSet.push(match.entity); } else { idsToDelete.push(match.entityId); } if (shouldSaveHistory && !idsWithDiff.has(entityId)) { backfillHistoryIds.add(entityId); return; } }); let shouldRemoveInvalidUtf8 = escapeTables !== undefined ? Primitive_option.valFromOption(escapeTables).has(entityConfig.table) : false; return async sql => { try { let promises = []; if (shouldSaveHistory) { if (backfillHistoryIds.size !== 0) { await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.name, entityConfig.index, Array.from(backfillHistoryIds)); } if (Utils.$$Array.notEmpty(batchDeleteCheckpointIds)) { promises.push(sql.unsafe(makeInsertDeleteUpdatesQuery(entityConfig, pgSchema), [ batchDeleteEntityIds, Utils.$$BigInt.arrayToStringArray(batchDeleteCheckpointIds) ], {prepare: true})); } if (Utils.$$Array.notEmpty(batchSetUpdates)) { if (shouldRemoveInvalidUtf8) { removeInvalidUtf8InPlace(batchSetUpdates.map(batchSetUpdate => { if (batchSetUpdate.type === "SET") { return batchSetUpdate.entity; } else { return Stdlib_JsError.throwWithMessage("Expected Set action"); } })); } let entityHistory = getEntityHistory(entityConfig); promises.push(setOrThrow(sql, batchSetUpdates, entityHistory.table, entityHistory.setChangeSchema, pgSchema)); } } if (Utils.$$Array.notEmpty(entitiesToSet)) { if (shouldRemoveInvalidUtf8) { removeInvalidUtf8InPlace(entitiesToSet); } promises.push(setOrThrow(sql, entitiesToSet, entityConfig.table, entityConfig.schema, pgSchema)); } if (Utils.$$Array.notEmpty(idsToDelete)) { promises.push(deleteByIdsOrThrow(sql, pgSchema, idsToDelete, entityConfig.table)); } await Promise.all(promises); return; } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); return classifyWriteError(specificError, entityConfig.table, exn); } }; }); let rollbackTables; if (rollback !== undefined) { let rollbackTargetCheckpointId = rollback.targetCheckpointId; rollbackTables = sql => { let promises = allEntities.map(entityConfig => EntityHistory.rollback(sql, pgSchema, entityConfig.name, entityConfig.index, rollbackTargetCheckpointId)); promises.push(InternalTable.Checkpoints.rollback(sql, pgSchema, rollbackTargetCheckpointId)); return Promise.all(promises); }; } else { rollbackTables = undefined; } try { await Promise.all([ sql.begin(async sql => { if (rollbackTables !== undefined) { await rollbackTables(sql); } let setOperations = [ sql => InternalTable.Chains.setProgressedChains(sql, pgSchema, Utils.Dict.mapValuesToArray(batch.progressedChainsById, chainAfterBatch => ({ chainId: chainAfterBatch.fetchState.chainId, progressBlockNumber: chainAfterBatch.progressBlockNumber, sourceBlockNumber: chainAfterBatch.sourceBlockNumber, totalEventsProcessed: chainAfterBatch.totalEventsProcessed }))), setRawEvents ].concat(setEntities); if (chainMetaData !== undefined) { setOperations.push(sql => InternalTable.Chains.setMeta(sql, pgSchema, chainMetaData)); } if (shouldSaveHistory) { setOperations.push(sql => InternalTable.Checkpoints.insert(sql, pgSchema, batch.checkpointIds, batch.checkpointChainIds, batch.checkpointBlockNumbers, batch.checkpointBlockHashes, batch.checkpointEventsProcessed)); } await Promise.all(setOperations.map(dbFunc => dbFunc(sql))); if (sinkPromise === undefined) { return; } let exn = await sinkPromise; if (exn === undefined) { return; } throw exn; }), Promise.all(updatedEffectsCache.map(param => setEffectCacheOrThrow(param.effect, param.items, param.shouldInitialize))) ]); let specificError$1 = specificError.contents; if (specificError$1 === undefined) { return; } throw specificError$1; } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); let specificError$2 = specificError.contents; throw specificError$2 !== undefined ? specificError$2 : exn; } } catch (raw_exn$1) { let exn$1 = Primitive_exceptions.internalToException(raw_exn$1); if (exn$1.RE_EXN_ID === PgEncodingError) { let escapeTables$1 = escapeTables !== undefined ? Primitive_option.valFromOption(escapeTables) : new Set(); escapeTables$1.add(exn$1.table); return await writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, chainMetaData, Primitive_option.some(escapeTables$1)); } throw exn$1; } } function makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema) { let dataFieldNames = Stdlib_Array.filterMap(entityConfig.table.fields, fieldOrDerived => { if (fieldOrDerived.TAG === "Field") { return Table.getPgDbFieldName(fieldOrDerived._0); } }); let dataFieldsCommaSeparated = dataFieldNames.map(name => `"` + name + `"`).join(", "); let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index); return `SELECT DISTINCT ON (` + Table.idFieldName + `) ` + dataFieldsCommaSeparated + ` FROM "` + pgSchema + `"."` + historyTableName + `" WHERE "` + EntityHistory.checkpointIdFieldName + `" <= $1 AND EXISTS ( SELECT 1 FROM "` + pgSchema + `"."` + historyTableName + `" h WHERE h.` + Table.idFieldName + ` = "` + historyTableName + `".` + Table.idFieldName + ` AND h."` + EntityHistory.checkpointIdFieldName + `" > $1 ) ORDER BY ` + Table.idFieldName + `, "` + EntityHistory.checkpointIdFieldName + `" DESC`; } function makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema) { let historyTableName = EntityHistory.historyTableName(entityConfig.name, entityConfig.index); return `SELECT DISTINCT ` + Table.idFieldName + ` FROM "` + pgSchema + `"."` + historyTableName + `" WHERE "` + EntityHistory.checkpointIdFieldName + `" > $1 AND NOT EXISTS ( SELECT 1 FROM "` + pgSchema + `"."` + historyTableName + `" h WHERE h.` + Table.idFieldName + ` = "` + historyTableName + `".` + Table.idFieldName + ` AND h."` + EntityHistory.checkpointIdFieldName + `" <= $1 )`; } function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isHasuraEnabled, sink, onInitialize, onNewTables) { let containerName = "envio-postgres"; let psqlExecOptions_env = Object.fromEntries([ [ "PGPASSWORD", pgPassword ], [ "PATH", process.env.PATH ] ]); let psqlExecOptions = { env: psqlExecOptions_env }; let cacheDirPath = Path.resolve(".envio", "cache"); let isInitialized = async () => Utils.$$Array.notEmpty(await sql.unsafe(`SELECT table_schema FROM information_schema.tables WHERE table_schema = '` + pgSchema + `' AND (table_name = '` + "event_sync_state" + `' OR table_name = '` + InternalTable.Chains.table.tableName + `');`)); let restoreEffectCache = async withUpload => { if (withUpload) { let nothingToUploadErrorMessage = "Nothing to upload."; let match = await Promise.all([ Stdlib_Promise.$$catch(Fs.promises.readdir(cacheDirPath).then(e => ({ TAG: "Ok", _0: e })), param => Promise.resolve({ TAG: "Error", _0: nothingToUploadErrorMessage })), getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containerName) ]); let exit = 0; let message; let entries = match[0]; if (entries.TAG === "Ok") { let psqlExec = match[1]; if (psqlExec.TAG === "Ok") { let psqlExec$1 = psqlExec._0; let cacheFiles = entries._0.filter(entry => entry.endsWith(".tsv")); await Promise.all(cacheFiles.map(entry => { let effectName = entry.slice(0, -4); let table = Internal.makeCacheTable(effectName); return sql.unsafe(makeCreateTableQuery(table, pgSchema, false)).then(() => { let inputFile = Path.join(cacheDirPath, entry); let command = psqlExec$1 + ` -c 'COPY "` + pgSchema + `"."` + table.tableName + `" FROM STDIN WITH (FORMAT text, HEADER);' < ` + inputFile; return new Promise((resolve, reject) => { Child_process.exec(command, psqlExecOptions, (error, stdout, param) => { if (error === null) { return resolve(stdout); } else { return reject(error); } }); }); }); })); Logging.info("Successfully uploaded cache."); } else { message = match[1]._0; exit = 1; } } else { message = entries._0; exit = 1; } if (exit === 1) { if (message === nothingToUploadErrorMessage) { Logging.info("No cache found to upload."); } else { Logging.error(`Failed to upload cache, continuing without it. ` + message); } } } let cacheTableInfo = await sql.unsafe(makeSchemaCacheTableInfoQuery(pgSchema)); if (withUpload && Utils.$$Array.notEmpty(cacheTableInfo) && onNewTables !== undefined) { await onNewTables(cacheTableInfo.map(info => info.table_name)); } let cache = {}; cacheTableInfo.forEach(param => { let effectName = param.table_name.slice(cacheTablePrefixLength); cache[effectName] = { effectName: effectName, count: param.count }; }); return cache; }; let initialize = async (chainConfigsOpt, entitiesOpt, enumsOpt, envioInfo) => { let chainConfigs = chainConfigsOpt !== undefined ? chainConfigsOpt : []; let entities = entitiesOpt !== undefined ? entitiesOpt : []; let enums = enumsOpt !== undefined ? enumsOpt : []; let pgEntities = entities.filter(e => e.storage.postgres); let chEntities = entities.filter(e => e.storage.clickhouse); let schemaTableNames = await sql.unsafe(makeSchemaTableNamesQuery(pgSchema)); if (Utils.$$Array.notEmpty(schemaTableNames) && !schemaTableNames.some(table => table.table_name === InternalTable.Chains.table.tableName ? true : table.table_name === "event_sync_state")) { Stdlib_JsError.throwWithMessage(`Cannot run Envio migrations on PostgreSQL schema "` + pgSchema + `" because it contains non-Envio tables. Running migrations would delete all data in this schema.\n\nTo resolve this:\n1. If you want to use this schema, first backup any important data, then drop it with: "pnpm envio local db-migrate down"\n2. Or specify a different schema name by setting the "ENVIO_PG_SCHEMA" environment variable\n3. Or manually drop the schema in your database if you're certain the data is not needed.`); } if (sink !== undefined) { await sink.initialize(chainConfigs, chEntities, enums); } let queries = makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigs, pgEntities, enums, Utils.$$Array.isEmpty(schemaTableNames)); await sql.begin(async sql => { await Promise.all(queries.map(query => sql.unsafe(query))); return await InternalTable.EnvioInfo.write(sql, pgSchema, envioInfo); }); let ids = []; let addrChainIds = []; let addrContractNames = []; chainConfigs.forEach(chain => { chain.contracts.forEach(contract => { contract.addresses.forEach(address => { ids.push(Config.EnvioAddresses.makeId(chain.id, address)); addrChainIds.push(chain.id); addrContractNames.push(contract.name); }); }); }); if (ids.length !== 0) { await sql.unsafe(`INSERT INTO "` + pgSchema + `"."` + Config.EnvioAddresses.table.tableName + `" ("id", "chain_id", "registration_block", "registration_log_index", "contract_name") SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::text[]) AS t(id, chain_id, contract_name);`, [ ids, addrChainIds, addrContractNames ]); } let cache = await restoreEffectCache(true); if (onInitialize !== undefined) { await onInitialize(); } return { cleanRun: true, cache: cache, chains: chainConfigs.map(chainConfig => ({ id: chainConfig.id, startBlock: chainConfig.startBlock, endBlock: chainConfig.endBlock, maxReorgDepth: chainConfig.maxReorgDepth, progressBlockNumber: -1, numEventsProcessed: 0, firstEventBlockNumber: undefined, timestampCaughtUpToHeadOrEndblock: undefined, indexingAddresses: ChainFetcher.configAddresses(chainConfig), sourceBlockNumber: 0 })), checkpointId: InternalTable.Checkpoints.initialCheckpointId, reorgCheckpoints: [], envioInfo: envioInfo }; }; let loadOrThrow = async (filter, table) => { let params = []; let condition = makeFilterCondition(filter, table, params); let rows; try { rows = await sql.unsafe(makeLoadQuery(pgSchema, table.tableName, condition), params, {prepare: true}); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); throw { RE_EXN_ID: Persistence.StorageError, message: `Failed loading "` + table.tableName + `" from storage by condition: ` + condition, reason: exn, Error: new Error() }; } try { return S$RescriptSchema.parseOrThrow(rows, Table.pgRowsSchema(table)); } catch (raw_exn$1) { let exn$1 = Primitive_exceptions.internalToException(raw_exn$1); throw { RE_EXN_ID: Persistence.StorageError, message: `Failed to parse "` + table.tableName + `" loaded from storage by condition: ` + condition, reason: exn$1, Error: new Error() }; } }; let setOrThrow$1 = (items, table, itemSchema) => setOrThrow(sql, items, table, itemSchema, pgSchema); let setEffectCacheOrThrow = async (effect, items, initialize) => { let match = effect.storageMeta; let table = match.table; if (initialize) { await sql.unsafe(makeCreateTableQuery(table, pgSchema, false)); if (onNewTables !== undefined) { await onNewTables([table.tableName]); } } return await setOrThrow$1(items, table, match.itemSchema); }; let dumpEffectCache = async () => { try { let cacheTableInfo = (await sql.unsafe(makeSchemaCacheTableInfoQuery(pgSchema))).filter(i => i.count > 0); if (!Utils.$$Array.notEmpty(cacheTableInfo)) { return; } try { await Fs.promises.access(cacheDirPath); } catch (exn) { await Fs.promises.mkdir(cacheDirPath, { recursive: true }); } let psqlExec = await getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containerName); if (psqlExec.TAG !== "Ok") { return Logging.error(`Failed to dump cache. ` + psqlExec._0); } let psqlExec$1 = psqlExec._0; Logging.info(`Dumping cache: ` + cacheTableInfo.map(param => param.table_name + " (" + param.count.toString() + " rows)").join(", ")); let promises = cacheTableInfo.map(async param => { let tableName = param.table_name; let cacheName = tableName.slice(cacheTablePrefixLength); let outputFile = Path.join(cacheDirPath, cacheName + ".tsv"); let command = psqlExec$1 + ` -c 'COPY "` + pgSchema + `"."` + tableName + `" TO STDOUT WITH (FORMAT text, HEADER);' > ` + outputFile; return new Promise((resolve, reject) => { Child_process.exec(command, psqlExecOptions, (error, stdout, param) => { if (error === null) { return resolve(stdout); } else { return reject(error); } }); }); }); await Promise.all(promises); return Logging.info(`Successfully dumped cache to ` + cacheDirPath); } catch (raw_exn) { let exn$1 = Primitive_exceptions.internalToException(raw_exn); return Logging.errorWithExn(Utils.prettifyExn(exn$1), `Failed to dump cache.`); } }; let resumeInitialState = async () => { let match = await Promise.all([ restoreEffectCache(false), InternalTable.Chains.getInitialState(sql, pgSchema).then(rawInitialStates => rawInitialStates.map(rawInitialState => ({ id: rawInitialState.id, startBlock: rawInitialState.startBlock, endBlock: Primitive_option.fromNull(rawInitialState.endBlock), maxReorgDepth: rawInitialState.maxReorgDepth, progressBlockNumber: rawInitialState.progressBlockNumber, numEventsProcessed: rawInitialState.numEventsProcessed, firstEventBlockNumber: Primitive_option.fromNull(rawInitialState.firstEventBlockNumber), timestampCaughtUpToHeadOrEndblock: Primitive_option.fromNull(rawInitialState.timestampCaughtUpToHeadOrEndblock), indexingAddresses: rawInitialState.indexingAddresses, sourceBlockNumber: rawInitialState.sourceBlockNumber }))), sql.unsafe(InternalTable.Checkpoints.makeCommitedCheckpointIdQuery(pgSchema)), sql.unsafe(InternalTable.Checkpoints.makeGetReorgCheckpointsQuery(pgSchema)), InternalTable.EnvioInfo.read(sql, pgSchema) ]); let checkpointId = BigInt(match[2][0].id); let reorgCheckpoints = match[3].map(raw => ({ id: BigInt(raw.id), chain_id: raw.chain_id, block_number: raw.block_number, block_hash: raw.block_hash })); if (sink !== undefined) { await sink.resume(checkpointId); } return { cleanRun: false, cache: match[0], chains: match[1], checkpointId: checkpointId, reorgCheckpoints: reorgCheckpoints, envioInfo: match[4] }; }; let reset = async () => { let query = `DROP SCHEMA IF EXISTS "` + pgSchema + `" CASCADE;`; return await sql.unsafe(query); }; let setChainMeta = chainsData => InternalTable.Chains.setMeta(sql, pgSchema, chainsData).then(param => (undefined)); let pruneStaleCheckpoints = safeCheckpointId => InternalTable.Checkpoints.pruneStaleCheckpoints(sql, pgSchema, safeCheckpointId); let pruneStaleEntityHistory = (entityName, entityIndex, safeCheckpointId) => EntityHistory.pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, safeCheckpointId); let getRollbackTargetCheckpoint = (reorgChainId, lastKnownValidBlockNumber) => InternalTable.Checkpoints.getRollbackTargetCheckpoint(sql, pgSchema, reorgChainId, lastKnownValidBlockNumber); let getRollbackProgressDiff = rollbackTargetCheckpointId => InternalTable.Checkpoints.getRollbackProgressDiff(sql, pgSchema, rollbackTargetCheckpointId); let getRollbackData = async (entityConfig, rollbackTargetCheckpointId) => await Promise.all([ sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}), sql.unsafe(makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}) ]); let writeBatchMethod = async (batch, rollback, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities, chainMetaData) => { let pgUpdates = []; let chUpdates = []; for (let i = 0, i_finish = updatedEntities.length; i < i_finish; ++i) { let update = updatedEntities[i]; let entityConfig = update.entityConfig; if (entityConfig.storage.postgres) { pgUpdates.push(update); } if (entityConfig.storage.clickhouse) { chUpdates.push(update); } } let sinkPromise; if (sink !== undefined) { let timerRef = Hrtime.makeTimer(); sinkPromise = sink.writeBatch(batch, chUpdates).then(() => { Prometheus.StorageWrite.increment(sink.name, Hrtime.toSecondsFloat(Hrtime.timeSince(timerRef))); }).catch(exn => exn); } else { sinkPromise = undefined; } let primaryTimerRef = Hrtime.makeTimer(); await writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, chainMetaData, undefined); return Prometheus.StorageWrite.increment("postgres", Hrtime.toSecondsFloat(Hrtime.timeSince(primaryTimerRef))); }; let close = () => sql.end(); return { name: "postgres", isInitialized: isInitialized, initialize: initialize, resumeInitialState: resumeInitialState, loadOrThrow: loadOrThrow, dumpEffectCache: dumpEffectCache, reset: reset, setChainMeta: setChainMeta, pruneStaleCheckpoints: pruneStaleCheckpoints, pruneStaleEntityHistory: pruneStaleEntityHistory, getRollbackTargetCheckpoint: getRollbackTargetCheckpoint, getRollbackProgressDiff: getRollbackProgressDiff, getRollbackData: getRollbackData, writeBatch: writeBatchMethod, close: close }; } function makeStorageFromEnv(config, sqlOpt, pgSchemaOpt, isHasuraEnabledOpt) { let sql = sqlOpt !== undefined ? Primitive_option.valFromOption(sqlOpt) : makeClient(); let pgSchema = pgSchemaOpt !== undefined ? pgSchemaOpt : Env.Db.publicSchema; let isHasuraEnabled = isHasuraEnabledOpt !== undefined ? isHasuraEnabledOpt : Env.Hasura.enabled; let tmp; if (config.storage.clickhouse) { let host = Env.ClickHouse.host(); let username = Env.ClickHouse.username(); let password = Env.ClickHouse.password(); let database = Env.ClickHouse.database(); let missing = []; let checkEnv = (opt, name) => { if (opt !== undefined) { return; } else { missing.push(name); return;