@apibara/plugin-drizzle 2.1.0-beta.20 → 2.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +52 -27
- package/dist/index.d.cts +34 -4
- package/dist/index.d.mts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.mjs +52 -27
- package/package.json +3 -3
- package/src/index.ts +82 -27
- package/src/storage.ts +19 -7
- package/src/utils.ts +19 -0
package/dist/index.cjs
CHANGED
|
@@ -46,6 +46,12 @@ function serialize(obj) {
|
|
|
46
46
|
function sleep(ms) {
|
|
47
47
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
48
|
}
|
|
49
|
+
const getIdColumnForTable = (tableName, idColumn) => {
|
|
50
|
+
if (idColumn[tableName]) {
|
|
51
|
+
return idColumn[tableName];
|
|
52
|
+
}
|
|
53
|
+
return idColumn["*"];
|
|
54
|
+
};
|
|
49
55
|
|
|
50
56
|
function drizzle(options) {
|
|
51
57
|
const {
|
|
@@ -375,9 +381,10 @@ async function initializeReorgRollbackTable(tx, indexerId) {
|
|
|
375
381
|
);
|
|
376
382
|
}
|
|
377
383
|
}
|
|
378
|
-
async function registerTriggers(tx, tables, endCursor,
|
|
384
|
+
async function registerTriggers(tx, tables, endCursor, idColumnMap, indexerId) {
|
|
379
385
|
try {
|
|
380
386
|
for (const table of tables) {
|
|
387
|
+
const tableIdColumn = getIdColumnForTable(table, idColumnMap);
|
|
381
388
|
await tx.execute(
|
|
382
389
|
drizzleOrm.sql.raw(
|
|
383
390
|
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`
|
|
@@ -388,7 +395,7 @@ async function registerTriggers(tx, tables, endCursor, idColumn, indexerId) {
|
|
|
388
395
|
CREATE CONSTRAINT TRIGGER ${getReorgTriggerName(table, indexerId)}
|
|
389
396
|
AFTER INSERT OR UPDATE OR DELETE ON ${table}
|
|
390
397
|
DEFERRABLE INITIALLY DEFERRED
|
|
391
|
-
FOR EACH ROW EXECUTE FUNCTION ${constants.SCHEMA_NAME}.reorg_checkpoint('${
|
|
398
|
+
FOR EACH ROW EXECUTE FUNCTION ${constants.SCHEMA_NAME}.reorg_checkpoint('${tableIdColumn}', ${Number(endCursor.orderKey)}, '${indexerId}');
|
|
392
399
|
`)
|
|
393
400
|
);
|
|
394
401
|
}
|
|
@@ -413,7 +420,7 @@ async function removeTriggers(db, tables, indexerId) {
|
|
|
413
420
|
});
|
|
414
421
|
}
|
|
415
422
|
}
|
|
416
|
-
async function invalidate(tx, cursor,
|
|
423
|
+
async function invalidate(tx, cursor, idColumnMap, indexerId) {
|
|
417
424
|
const { rows: result } = await tx.execute(
|
|
418
425
|
drizzleOrm.sql.raw(`
|
|
419
426
|
WITH deleted AS (
|
|
@@ -431,6 +438,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
431
438
|
);
|
|
432
439
|
}
|
|
433
440
|
for (const op of result) {
|
|
441
|
+
const tableIdColumn = getIdColumnForTable(op.table_name, idColumnMap);
|
|
434
442
|
switch (op.op) {
|
|
435
443
|
case "I":
|
|
436
444
|
try {
|
|
@@ -440,7 +448,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
440
448
|
await tx.execute(
|
|
441
449
|
drizzleOrm.sql.raw(`
|
|
442
450
|
DELETE FROM ${op.table_name}
|
|
443
|
-
WHERE ${
|
|
451
|
+
WHERE ${tableIdColumn} = '${op.row_id}'
|
|
444
452
|
`)
|
|
445
453
|
);
|
|
446
454
|
} catch (error) {
|
|
@@ -480,7 +488,9 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
480
488
|
);
|
|
481
489
|
}
|
|
482
490
|
const rowValue = typeof op.row_value === "string" ? JSON.parse(op.row_value) : op.row_value;
|
|
483
|
-
const nonIdKeys = Object.keys(rowValue).filter(
|
|
491
|
+
const nonIdKeys = Object.keys(rowValue).filter(
|
|
492
|
+
(k) => k !== tableIdColumn
|
|
493
|
+
);
|
|
484
494
|
const fields = nonIdKeys.map((c) => `${c} = prev.${c}`).join(", ");
|
|
485
495
|
const query = drizzleOrm.sql.raw(`
|
|
486
496
|
UPDATE ${op.table_name}
|
|
@@ -488,7 +498,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
488
498
|
FROM (
|
|
489
499
|
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
490
500
|
) as prev
|
|
491
|
-
WHERE ${op.table_name}.${
|
|
501
|
+
WHERE ${op.table_name}.${tableIdColumn} = '${op.row_id}'
|
|
492
502
|
`);
|
|
493
503
|
await tx.execute(query);
|
|
494
504
|
} catch (error) {
|
|
@@ -566,8 +576,8 @@ function drizzleStorage({
|
|
|
566
576
|
db,
|
|
567
577
|
persistState: enablePersistence = true,
|
|
568
578
|
indexerName: identifier = "default",
|
|
569
|
-
schema,
|
|
570
|
-
idColumn
|
|
579
|
+
schema: _schema,
|
|
580
|
+
idColumn,
|
|
571
581
|
migrate: migrateOptions
|
|
572
582
|
}) {
|
|
573
583
|
return plugins.defineIndexerPlugin((indexer$1) => {
|
|
@@ -575,15 +585,30 @@ function drizzleStorage({
|
|
|
575
585
|
let indexerId = "";
|
|
576
586
|
const alwaysReindex = process.env["APIBARA_ALWAYS_REINDEX"] === "true";
|
|
577
587
|
let prevFinality;
|
|
588
|
+
const schema = _schema ?? db._.schema ?? {};
|
|
589
|
+
const idColumnMap = {
|
|
590
|
+
"*": typeof idColumn === "string" ? idColumn : "id",
|
|
591
|
+
...typeof idColumn === "object" ? idColumn : {}
|
|
592
|
+
};
|
|
578
593
|
try {
|
|
579
|
-
tableNames = Object.values(schema
|
|
580
|
-
(table) => table.dbName
|
|
581
|
-
);
|
|
594
|
+
tableNames = Object.values(schema).map((table) => table.dbName);
|
|
582
595
|
} catch (error) {
|
|
583
596
|
throw new DrizzleStorageError("Failed to get table names from schema", {
|
|
584
597
|
cause: error
|
|
585
598
|
});
|
|
586
599
|
}
|
|
600
|
+
for (const table of Object.values(schema)) {
|
|
601
|
+
const columns = table.columns;
|
|
602
|
+
const tableIdColumn = getIdColumnForTable(table.dbName, idColumnMap);
|
|
603
|
+
const columnExists = Object.values(columns).some(
|
|
604
|
+
(column) => column.name === tableIdColumn
|
|
605
|
+
);
|
|
606
|
+
if (!columnExists) {
|
|
607
|
+
throw new DrizzleStorageError(
|
|
608
|
+
`Column \`"${tableIdColumn}"\` does not exist in table \`"${table.dbName}"\`. Make sure the table has the specified column or provide a valid \`idColumn\` mapping to \`drizzleStorage\`.`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
587
612
|
indexer$1.hooks.hook("run:before", async () => {
|
|
588
613
|
const internalContext = plugins$1.useInternalContext();
|
|
589
614
|
const context = indexer.useIndexerContext();
|
|
@@ -591,20 +616,9 @@ function drizzleStorage({
|
|
|
591
616
|
context[constants.DRIZZLE_STORAGE_DB_PROPERTY] = db;
|
|
592
617
|
const { indexerName: indexerFileName, availableIndexers } = internalContext;
|
|
593
618
|
indexerId = internal.generateIndexerId(indexerFileName, identifier);
|
|
594
|
-
if (alwaysReindex) {
|
|
595
|
-
logger.warn(
|
|
596
|
-
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`
|
|
597
|
-
);
|
|
598
|
-
await withTransaction(db, async (tx) => {
|
|
599
|
-
await cleanupStorage(tx, tableNames, indexerId);
|
|
600
|
-
if (enablePersistence) {
|
|
601
|
-
await resetPersistence({ tx, indexerId });
|
|
602
|
-
}
|
|
603
|
-
logger.success("Tables have been cleaned up for reindexing");
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
619
|
let retries = 0;
|
|
607
620
|
let migrationsApplied = false;
|
|
621
|
+
let cleanupApplied = false;
|
|
608
622
|
while (retries <= MAX_RETRIES) {
|
|
609
623
|
try {
|
|
610
624
|
if (migrateOptions && !migrationsApplied) {
|
|
@@ -617,6 +631,17 @@ function drizzleStorage({
|
|
|
617
631
|
if (enablePersistence) {
|
|
618
632
|
await initializePersistentState(tx);
|
|
619
633
|
}
|
|
634
|
+
if (alwaysReindex && !cleanupApplied) {
|
|
635
|
+
logger.warn(
|
|
636
|
+
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`
|
|
637
|
+
);
|
|
638
|
+
await cleanupStorage(tx, tableNames, indexerId);
|
|
639
|
+
if (enablePersistence) {
|
|
640
|
+
await resetPersistence({ tx, indexerId });
|
|
641
|
+
}
|
|
642
|
+
cleanupApplied = true;
|
|
643
|
+
logger.success("Tables have been cleaned up for reindexing");
|
|
644
|
+
}
|
|
620
645
|
});
|
|
621
646
|
break;
|
|
622
647
|
} catch (error) {
|
|
@@ -659,7 +684,7 @@ function drizzleStorage({
|
|
|
659
684
|
return;
|
|
660
685
|
}
|
|
661
686
|
await withTransaction(db, async (tx) => {
|
|
662
|
-
await invalidate(tx, cursor,
|
|
687
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
663
688
|
if (enablePersistence) {
|
|
664
689
|
await invalidateState({ tx, cursor, indexerId });
|
|
665
690
|
}
|
|
@@ -697,7 +722,7 @@ function drizzleStorage({
|
|
|
697
722
|
throw new DrizzleStorageError("Invalidate Cursor is undefined");
|
|
698
723
|
}
|
|
699
724
|
await withTransaction(db, async (tx) => {
|
|
700
|
-
await invalidate(tx, cursor,
|
|
725
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
701
726
|
if (enablePersistence) {
|
|
702
727
|
await invalidateState({ tx, cursor, indexerId });
|
|
703
728
|
}
|
|
@@ -713,14 +738,14 @@ function drizzleStorage({
|
|
|
713
738
|
await withTransaction(db, async (tx) => {
|
|
714
739
|
context[constants.DRIZZLE_PROPERTY] = { db: tx };
|
|
715
740
|
if (prevFinality === "pending") {
|
|
716
|
-
await invalidate(tx, cursor,
|
|
741
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
717
742
|
}
|
|
718
743
|
if (finality !== "finalized") {
|
|
719
744
|
await registerTriggers(
|
|
720
745
|
tx,
|
|
721
746
|
tableNames,
|
|
722
747
|
endCursor,
|
|
723
|
-
|
|
748
|
+
idColumnMap,
|
|
724
749
|
indexerId
|
|
725
750
|
);
|
|
726
751
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -113,6 +113,13 @@ type MigrateOptions = MigrationConfig;
|
|
|
113
113
|
*/
|
|
114
114
|
declare function migrate<TSchema extends Record<string, unknown>>(db: PgliteDatabase<TSchema> | NodePgDatabase<TSchema>, options: MigrateOptions): Promise<void>;
|
|
115
115
|
|
|
116
|
+
interface IdColumnMap extends Record<string, string> {
|
|
117
|
+
/**
|
|
118
|
+
* Wildcard mapping for all tables.
|
|
119
|
+
*/
|
|
120
|
+
"*": string;
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
type DrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> = {
|
|
117
124
|
db: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
118
125
|
};
|
|
@@ -135,9 +142,32 @@ interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSche
|
|
|
135
142
|
*/
|
|
136
143
|
schema?: Record<string, unknown>;
|
|
137
144
|
/**
|
|
138
|
-
* The column to use as the
|
|
145
|
+
* The column to use as the primary identifier for each table.
|
|
146
|
+
*
|
|
147
|
+
* This identifier is used for tracking changes during reorgs and rollbacks.
|
|
148
|
+
*
|
|
149
|
+
* Can be specified in two ways:
|
|
150
|
+
*
|
|
151
|
+
* 1. As a single string that applies to all tables:
|
|
152
|
+
* ```ts
|
|
153
|
+
* idColumn: "_id" // Uses "_id" column for all tables
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* 2. As an object mapping table names to their ID columns:
|
|
157
|
+
* ```ts
|
|
158
|
+
* idColumn: {
|
|
159
|
+
* transfers: "transaction_hash", // Use "transaction_hash" for transfers table
|
|
160
|
+
* blocks: "block_number", // Use "block_number" for blocks table
|
|
161
|
+
* "*": "_id" // Use "_id" for all other tables | defaults to "id"
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* The special "*" key acts as a fallback for any tables not explicitly mapped.
|
|
166
|
+
*
|
|
167
|
+
* @default "id"
|
|
168
|
+
* @type {string | Partial<IdColumnMap>}
|
|
139
169
|
*/
|
|
140
|
-
idColumn?: string
|
|
170
|
+
idColumn?: string | Partial<IdColumnMap>;
|
|
141
171
|
/**
|
|
142
172
|
* The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.
|
|
143
173
|
*/
|
|
@@ -154,6 +184,6 @@ interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSche
|
|
|
154
184
|
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
155
185
|
* @param options.migrate - The options for the database migration. when provided, the database will automatically run migrations before the indexer runs.
|
|
156
186
|
*/
|
|
157
|
-
declare function drizzleStorage<TFilter, TBlock, TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>({ db, persistState: enablePersistence, indexerName: identifier, schema, idColumn, migrate: migrateOptions, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
187
|
+
declare function drizzleStorage<TFilter, TBlock, TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>({ db, persistState: enablePersistence, indexerName: identifier, schema: _schema, idColumn, migrate: migrateOptions, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
158
188
|
|
|
159
|
-
export { type Database, type DrizzleOptions, type DrizzleStorage, type DrizzleStorageOptions, type MigrateOptions, type NodePgDatabase, type NodePgDrizzleOptions, type PgliteDatabase, type PgliteDrizzleOptions, drizzle, drizzleStorage, migrate, useDrizzleStorage };
|
|
189
|
+
export { type Database, type DrizzleOptions, type DrizzleStorage, type DrizzleStorageOptions, type IdColumnMap, type MigrateOptions, type NodePgDatabase, type NodePgDrizzleOptions, type PgliteDatabase, type PgliteDrizzleOptions, drizzle, drizzleStorage, migrate, useDrizzleStorage };
|
package/dist/index.d.mts
CHANGED
|
@@ -113,6 +113,13 @@ type MigrateOptions = MigrationConfig;
|
|
|
113
113
|
*/
|
|
114
114
|
declare function migrate<TSchema extends Record<string, unknown>>(db: PgliteDatabase<TSchema> | NodePgDatabase<TSchema>, options: MigrateOptions): Promise<void>;
|
|
115
115
|
|
|
116
|
+
interface IdColumnMap extends Record<string, string> {
|
|
117
|
+
/**
|
|
118
|
+
* Wildcard mapping for all tables.
|
|
119
|
+
*/
|
|
120
|
+
"*": string;
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
type DrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> = {
|
|
117
124
|
db: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
118
125
|
};
|
|
@@ -135,9 +142,32 @@ interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSche
|
|
|
135
142
|
*/
|
|
136
143
|
schema?: Record<string, unknown>;
|
|
137
144
|
/**
|
|
138
|
-
* The column to use as the
|
|
145
|
+
* The column to use as the primary identifier for each table.
|
|
146
|
+
*
|
|
147
|
+
* This identifier is used for tracking changes during reorgs and rollbacks.
|
|
148
|
+
*
|
|
149
|
+
* Can be specified in two ways:
|
|
150
|
+
*
|
|
151
|
+
* 1. As a single string that applies to all tables:
|
|
152
|
+
* ```ts
|
|
153
|
+
* idColumn: "_id" // Uses "_id" column for all tables
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* 2. As an object mapping table names to their ID columns:
|
|
157
|
+
* ```ts
|
|
158
|
+
* idColumn: {
|
|
159
|
+
* transfers: "transaction_hash", // Use "transaction_hash" for transfers table
|
|
160
|
+
* blocks: "block_number", // Use "block_number" for blocks table
|
|
161
|
+
* "*": "_id" // Use "_id" for all other tables | defaults to "id"
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* The special "*" key acts as a fallback for any tables not explicitly mapped.
|
|
166
|
+
*
|
|
167
|
+
* @default "id"
|
|
168
|
+
* @type {string | Partial<IdColumnMap>}
|
|
139
169
|
*/
|
|
140
|
-
idColumn?: string
|
|
170
|
+
idColumn?: string | Partial<IdColumnMap>;
|
|
141
171
|
/**
|
|
142
172
|
* The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.
|
|
143
173
|
*/
|
|
@@ -154,6 +184,6 @@ interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSche
|
|
|
154
184
|
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
155
185
|
* @param options.migrate - The options for the database migration. when provided, the database will automatically run migrations before the indexer runs.
|
|
156
186
|
*/
|
|
157
|
-
declare function drizzleStorage<TFilter, TBlock, TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>({ db, persistState: enablePersistence, indexerName: identifier, schema, idColumn, migrate: migrateOptions, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
187
|
+
declare function drizzleStorage<TFilter, TBlock, TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>({ db, persistState: enablePersistence, indexerName: identifier, schema: _schema, idColumn, migrate: migrateOptions, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
158
188
|
|
|
159
|
-
export { type Database, type DrizzleOptions, type DrizzleStorage, type DrizzleStorageOptions, type MigrateOptions, type NodePgDatabase, type NodePgDrizzleOptions, type PgliteDatabase, type PgliteDrizzleOptions, drizzle, drizzleStorage, migrate, useDrizzleStorage };
|
|
189
|
+
export { type Database, type DrizzleOptions, type DrizzleStorage, type DrizzleStorageOptions, type IdColumnMap, type MigrateOptions, type NodePgDatabase, type NodePgDrizzleOptions, type PgliteDatabase, type PgliteDrizzleOptions, drizzle, drizzleStorage, migrate, useDrizzleStorage };
|
package/dist/index.d.ts
CHANGED
|
@@ -113,6 +113,13 @@ type MigrateOptions = MigrationConfig;
|
|
|
113
113
|
*/
|
|
114
114
|
declare function migrate<TSchema extends Record<string, unknown>>(db: PgliteDatabase<TSchema> | NodePgDatabase<TSchema>, options: MigrateOptions): Promise<void>;
|
|
115
115
|
|
|
116
|
+
interface IdColumnMap extends Record<string, string> {
|
|
117
|
+
/**
|
|
118
|
+
* Wildcard mapping for all tables.
|
|
119
|
+
*/
|
|
120
|
+
"*": string;
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
type DrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> = {
|
|
117
124
|
db: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
118
125
|
};
|
|
@@ -135,9 +142,32 @@ interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSche
|
|
|
135
142
|
*/
|
|
136
143
|
schema?: Record<string, unknown>;
|
|
137
144
|
/**
|
|
138
|
-
* The column to use as the
|
|
145
|
+
* The column to use as the primary identifier for each table.
|
|
146
|
+
*
|
|
147
|
+
* This identifier is used for tracking changes during reorgs and rollbacks.
|
|
148
|
+
*
|
|
149
|
+
* Can be specified in two ways:
|
|
150
|
+
*
|
|
151
|
+
* 1. As a single string that applies to all tables:
|
|
152
|
+
* ```ts
|
|
153
|
+
* idColumn: "_id" // Uses "_id" column for all tables
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* 2. As an object mapping table names to their ID columns:
|
|
157
|
+
* ```ts
|
|
158
|
+
* idColumn: {
|
|
159
|
+
* transfers: "transaction_hash", // Use "transaction_hash" for transfers table
|
|
160
|
+
* blocks: "block_number", // Use "block_number" for blocks table
|
|
161
|
+
* "*": "_id" // Use "_id" for all other tables | defaults to "id"
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* The special "*" key acts as a fallback for any tables not explicitly mapped.
|
|
166
|
+
*
|
|
167
|
+
* @default "id"
|
|
168
|
+
* @type {string | Partial<IdColumnMap>}
|
|
139
169
|
*/
|
|
140
|
-
idColumn?: string
|
|
170
|
+
idColumn?: string | Partial<IdColumnMap>;
|
|
141
171
|
/**
|
|
142
172
|
* The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.
|
|
143
173
|
*/
|
|
@@ -154,6 +184,6 @@ interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSche
|
|
|
154
184
|
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
155
185
|
* @param options.migrate - The options for the database migration. when provided, the database will automatically run migrations before the indexer runs.
|
|
156
186
|
*/
|
|
157
|
-
declare function drizzleStorage<TFilter, TBlock, TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>({ db, persistState: enablePersistence, indexerName: identifier, schema, idColumn, migrate: migrateOptions, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
187
|
+
declare function drizzleStorage<TFilter, TBlock, TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>({ db, persistState: enablePersistence, indexerName: identifier, schema: _schema, idColumn, migrate: migrateOptions, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
158
188
|
|
|
159
|
-
export { type Database, type DrizzleOptions, type DrizzleStorage, type DrizzleStorageOptions, type MigrateOptions, type NodePgDatabase, type NodePgDrizzleOptions, type PgliteDatabase, type PgliteDrizzleOptions, drizzle, drizzleStorage, migrate, useDrizzleStorage };
|
|
189
|
+
export { type Database, type DrizzleOptions, type DrizzleStorage, type DrizzleStorageOptions, type IdColumnMap, type MigrateOptions, type NodePgDatabase, type NodePgDrizzleOptions, type PgliteDatabase, type PgliteDrizzleOptions, drizzle, drizzleStorage, migrate, useDrizzleStorage };
|
package/dist/index.mjs
CHANGED
|
@@ -40,6 +40,12 @@ function serialize(obj) {
|
|
|
40
40
|
function sleep(ms) {
|
|
41
41
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
42
|
}
|
|
43
|
+
const getIdColumnForTable = (tableName, idColumn) => {
|
|
44
|
+
if (idColumn[tableName]) {
|
|
45
|
+
return idColumn[tableName];
|
|
46
|
+
}
|
|
47
|
+
return idColumn["*"];
|
|
48
|
+
};
|
|
43
49
|
|
|
44
50
|
function drizzle(options) {
|
|
45
51
|
const {
|
|
@@ -369,9 +375,10 @@ async function initializeReorgRollbackTable(tx, indexerId) {
|
|
|
369
375
|
);
|
|
370
376
|
}
|
|
371
377
|
}
|
|
372
|
-
async function registerTriggers(tx, tables, endCursor,
|
|
378
|
+
async function registerTriggers(tx, tables, endCursor, idColumnMap, indexerId) {
|
|
373
379
|
try {
|
|
374
380
|
for (const table of tables) {
|
|
381
|
+
const tableIdColumn = getIdColumnForTable(table, idColumnMap);
|
|
375
382
|
await tx.execute(
|
|
376
383
|
sql.raw(
|
|
377
384
|
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`
|
|
@@ -382,7 +389,7 @@ async function registerTriggers(tx, tables, endCursor, idColumn, indexerId) {
|
|
|
382
389
|
CREATE CONSTRAINT TRIGGER ${getReorgTriggerName(table, indexerId)}
|
|
383
390
|
AFTER INSERT OR UPDATE OR DELETE ON ${table}
|
|
384
391
|
DEFERRABLE INITIALLY DEFERRED
|
|
385
|
-
FOR EACH ROW EXECUTE FUNCTION ${SCHEMA_NAME}.reorg_checkpoint('${
|
|
392
|
+
FOR EACH ROW EXECUTE FUNCTION ${SCHEMA_NAME}.reorg_checkpoint('${tableIdColumn}', ${Number(endCursor.orderKey)}, '${indexerId}');
|
|
386
393
|
`)
|
|
387
394
|
);
|
|
388
395
|
}
|
|
@@ -407,7 +414,7 @@ async function removeTriggers(db, tables, indexerId) {
|
|
|
407
414
|
});
|
|
408
415
|
}
|
|
409
416
|
}
|
|
410
|
-
async function invalidate(tx, cursor,
|
|
417
|
+
async function invalidate(tx, cursor, idColumnMap, indexerId) {
|
|
411
418
|
const { rows: result } = await tx.execute(
|
|
412
419
|
sql.raw(`
|
|
413
420
|
WITH deleted AS (
|
|
@@ -425,6 +432,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
425
432
|
);
|
|
426
433
|
}
|
|
427
434
|
for (const op of result) {
|
|
435
|
+
const tableIdColumn = getIdColumnForTable(op.table_name, idColumnMap);
|
|
428
436
|
switch (op.op) {
|
|
429
437
|
case "I":
|
|
430
438
|
try {
|
|
@@ -434,7 +442,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
434
442
|
await tx.execute(
|
|
435
443
|
sql.raw(`
|
|
436
444
|
DELETE FROM ${op.table_name}
|
|
437
|
-
WHERE ${
|
|
445
|
+
WHERE ${tableIdColumn} = '${op.row_id}'
|
|
438
446
|
`)
|
|
439
447
|
);
|
|
440
448
|
} catch (error) {
|
|
@@ -474,7 +482,9 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
474
482
|
);
|
|
475
483
|
}
|
|
476
484
|
const rowValue = typeof op.row_value === "string" ? JSON.parse(op.row_value) : op.row_value;
|
|
477
|
-
const nonIdKeys = Object.keys(rowValue).filter(
|
|
485
|
+
const nonIdKeys = Object.keys(rowValue).filter(
|
|
486
|
+
(k) => k !== tableIdColumn
|
|
487
|
+
);
|
|
478
488
|
const fields = nonIdKeys.map((c) => `${c} = prev.${c}`).join(", ");
|
|
479
489
|
const query = sql.raw(`
|
|
480
490
|
UPDATE ${op.table_name}
|
|
@@ -482,7 +492,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
482
492
|
FROM (
|
|
483
493
|
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
484
494
|
) as prev
|
|
485
|
-
WHERE ${op.table_name}.${
|
|
495
|
+
WHERE ${op.table_name}.${tableIdColumn} = '${op.row_id}'
|
|
486
496
|
`);
|
|
487
497
|
await tx.execute(query);
|
|
488
498
|
} catch (error) {
|
|
@@ -560,8 +570,8 @@ function drizzleStorage({
|
|
|
560
570
|
db,
|
|
561
571
|
persistState: enablePersistence = true,
|
|
562
572
|
indexerName: identifier = "default",
|
|
563
|
-
schema,
|
|
564
|
-
idColumn
|
|
573
|
+
schema: _schema,
|
|
574
|
+
idColumn,
|
|
565
575
|
migrate: migrateOptions
|
|
566
576
|
}) {
|
|
567
577
|
return defineIndexerPlugin((indexer) => {
|
|
@@ -569,15 +579,30 @@ function drizzleStorage({
|
|
|
569
579
|
let indexerId = "";
|
|
570
580
|
const alwaysReindex = process.env["APIBARA_ALWAYS_REINDEX"] === "true";
|
|
571
581
|
let prevFinality;
|
|
582
|
+
const schema = _schema ?? db._.schema ?? {};
|
|
583
|
+
const idColumnMap = {
|
|
584
|
+
"*": typeof idColumn === "string" ? idColumn : "id",
|
|
585
|
+
...typeof idColumn === "object" ? idColumn : {}
|
|
586
|
+
};
|
|
572
587
|
try {
|
|
573
|
-
tableNames = Object.values(schema
|
|
574
|
-
(table) => table.dbName
|
|
575
|
-
);
|
|
588
|
+
tableNames = Object.values(schema).map((table) => table.dbName);
|
|
576
589
|
} catch (error) {
|
|
577
590
|
throw new DrizzleStorageError("Failed to get table names from schema", {
|
|
578
591
|
cause: error
|
|
579
592
|
});
|
|
580
593
|
}
|
|
594
|
+
for (const table of Object.values(schema)) {
|
|
595
|
+
const columns = table.columns;
|
|
596
|
+
const tableIdColumn = getIdColumnForTable(table.dbName, idColumnMap);
|
|
597
|
+
const columnExists = Object.values(columns).some(
|
|
598
|
+
(column) => column.name === tableIdColumn
|
|
599
|
+
);
|
|
600
|
+
if (!columnExists) {
|
|
601
|
+
throw new DrizzleStorageError(
|
|
602
|
+
`Column \`"${tableIdColumn}"\` does not exist in table \`"${table.dbName}"\`. Make sure the table has the specified column or provide a valid \`idColumn\` mapping to \`drizzleStorage\`.`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
581
606
|
indexer.hooks.hook("run:before", async () => {
|
|
582
607
|
const internalContext = useInternalContext();
|
|
583
608
|
const context = useIndexerContext();
|
|
@@ -585,20 +610,9 @@ function drizzleStorage({
|
|
|
585
610
|
context[DRIZZLE_STORAGE_DB_PROPERTY] = db;
|
|
586
611
|
const { indexerName: indexerFileName, availableIndexers } = internalContext;
|
|
587
612
|
indexerId = generateIndexerId(indexerFileName, identifier);
|
|
588
|
-
if (alwaysReindex) {
|
|
589
|
-
logger.warn(
|
|
590
|
-
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`
|
|
591
|
-
);
|
|
592
|
-
await withTransaction(db, async (tx) => {
|
|
593
|
-
await cleanupStorage(tx, tableNames, indexerId);
|
|
594
|
-
if (enablePersistence) {
|
|
595
|
-
await resetPersistence({ tx, indexerId });
|
|
596
|
-
}
|
|
597
|
-
logger.success("Tables have been cleaned up for reindexing");
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
613
|
let retries = 0;
|
|
601
614
|
let migrationsApplied = false;
|
|
615
|
+
let cleanupApplied = false;
|
|
602
616
|
while (retries <= MAX_RETRIES) {
|
|
603
617
|
try {
|
|
604
618
|
if (migrateOptions && !migrationsApplied) {
|
|
@@ -611,6 +625,17 @@ function drizzleStorage({
|
|
|
611
625
|
if (enablePersistence) {
|
|
612
626
|
await initializePersistentState(tx);
|
|
613
627
|
}
|
|
628
|
+
if (alwaysReindex && !cleanupApplied) {
|
|
629
|
+
logger.warn(
|
|
630
|
+
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`
|
|
631
|
+
);
|
|
632
|
+
await cleanupStorage(tx, tableNames, indexerId);
|
|
633
|
+
if (enablePersistence) {
|
|
634
|
+
await resetPersistence({ tx, indexerId });
|
|
635
|
+
}
|
|
636
|
+
cleanupApplied = true;
|
|
637
|
+
logger.success("Tables have been cleaned up for reindexing");
|
|
638
|
+
}
|
|
614
639
|
});
|
|
615
640
|
break;
|
|
616
641
|
} catch (error) {
|
|
@@ -653,7 +678,7 @@ function drizzleStorage({
|
|
|
653
678
|
return;
|
|
654
679
|
}
|
|
655
680
|
await withTransaction(db, async (tx) => {
|
|
656
|
-
await invalidate(tx, cursor,
|
|
681
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
657
682
|
if (enablePersistence) {
|
|
658
683
|
await invalidateState({ tx, cursor, indexerId });
|
|
659
684
|
}
|
|
@@ -691,7 +716,7 @@ function drizzleStorage({
|
|
|
691
716
|
throw new DrizzleStorageError("Invalidate Cursor is undefined");
|
|
692
717
|
}
|
|
693
718
|
await withTransaction(db, async (tx) => {
|
|
694
|
-
await invalidate(tx, cursor,
|
|
719
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
695
720
|
if (enablePersistence) {
|
|
696
721
|
await invalidateState({ tx, cursor, indexerId });
|
|
697
722
|
}
|
|
@@ -707,14 +732,14 @@ function drizzleStorage({
|
|
|
707
732
|
await withTransaction(db, async (tx) => {
|
|
708
733
|
context[DRIZZLE_PROPERTY] = { db: tx };
|
|
709
734
|
if (prevFinality === "pending") {
|
|
710
|
-
await invalidate(tx, cursor,
|
|
735
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
711
736
|
}
|
|
712
737
|
if (finality !== "finalized") {
|
|
713
738
|
await registerTriggers(
|
|
714
739
|
tx,
|
|
715
740
|
tableNames,
|
|
716
741
|
endCursor,
|
|
717
|
-
|
|
742
|
+
idColumnMap,
|
|
718
743
|
indexerId
|
|
719
744
|
);
|
|
720
745
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apibara/plugin-drizzle",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"vitest": "^1.6.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@apibara/indexer": "2.1.0-beta.
|
|
49
|
-
"@apibara/protocol": "2.1.0-beta.
|
|
48
|
+
"@apibara/indexer": "2.1.0-beta.21",
|
|
49
|
+
"@apibara/protocol": "2.1.0-beta.21",
|
|
50
50
|
"postgres-range": "^1.1.4"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/index.ts
CHANGED
|
@@ -32,10 +32,18 @@ import {
|
|
|
32
32
|
registerTriggers,
|
|
33
33
|
removeTriggers,
|
|
34
34
|
} from "./storage";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
DrizzleStorageError,
|
|
37
|
+
type IdColumnMap,
|
|
38
|
+
getIdColumnForTable,
|
|
39
|
+
sleep,
|
|
40
|
+
withTransaction,
|
|
41
|
+
} from "./utils";
|
|
36
42
|
|
|
37
43
|
export * from "./helper";
|
|
38
44
|
|
|
45
|
+
export type { IdColumnMap };
|
|
46
|
+
|
|
39
47
|
const MAX_RETRIES = 5;
|
|
40
48
|
|
|
41
49
|
export type DrizzleStorage<
|
|
@@ -89,9 +97,32 @@ export interface DrizzleStorageOptions<
|
|
|
89
97
|
*/
|
|
90
98
|
schema?: Record<string, unknown>;
|
|
91
99
|
/**
|
|
92
|
-
* The column to use as the
|
|
100
|
+
* The column to use as the primary identifier for each table.
|
|
101
|
+
*
|
|
102
|
+
* This identifier is used for tracking changes during reorgs and rollbacks.
|
|
103
|
+
*
|
|
104
|
+
* Can be specified in two ways:
|
|
105
|
+
*
|
|
106
|
+
* 1. As a single string that applies to all tables:
|
|
107
|
+
* ```ts
|
|
108
|
+
* idColumn: "_id" // Uses "_id" column for all tables
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* 2. As an object mapping table names to their ID columns:
|
|
112
|
+
* ```ts
|
|
113
|
+
* idColumn: {
|
|
114
|
+
* transfers: "transaction_hash", // Use "transaction_hash" for transfers table
|
|
115
|
+
* blocks: "block_number", // Use "block_number" for blocks table
|
|
116
|
+
* "*": "_id" // Use "_id" for all other tables | defaults to "id"
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* The special "*" key acts as a fallback for any tables not explicitly mapped.
|
|
121
|
+
*
|
|
122
|
+
* @default "id"
|
|
123
|
+
* @type {string | Partial<IdColumnMap>}
|
|
93
124
|
*/
|
|
94
|
-
idColumn?: string
|
|
125
|
+
idColumn?: string | Partial<IdColumnMap>;
|
|
95
126
|
/**
|
|
96
127
|
* The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.
|
|
97
128
|
*/
|
|
@@ -120,8 +151,8 @@ export function drizzleStorage<
|
|
|
120
151
|
db,
|
|
121
152
|
persistState: enablePersistence = true,
|
|
122
153
|
indexerName: identifier = "default",
|
|
123
|
-
schema,
|
|
124
|
-
idColumn
|
|
154
|
+
schema: _schema,
|
|
155
|
+
idColumn,
|
|
125
156
|
migrate: migrateOptions,
|
|
126
157
|
}: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>) {
|
|
127
158
|
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
@@ -129,17 +160,37 @@ export function drizzleStorage<
|
|
|
129
160
|
let indexerId = "";
|
|
130
161
|
const alwaysReindex = process.env["APIBARA_ALWAYS_REINDEX"] === "true";
|
|
131
162
|
let prevFinality: DataFinality | undefined;
|
|
163
|
+
const schema: TSchema = (_schema as TSchema) ?? db._.schema ?? {};
|
|
164
|
+
const idColumnMap: IdColumnMap = {
|
|
165
|
+
"*": typeof idColumn === "string" ? idColumn : "id",
|
|
166
|
+
...(typeof idColumn === "object" ? idColumn : {}),
|
|
167
|
+
};
|
|
132
168
|
|
|
133
169
|
try {
|
|
134
|
-
tableNames = Object.values(
|
|
135
|
-
(table) => table.dbName,
|
|
136
|
-
);
|
|
170
|
+
tableNames = Object.values(schema).map((table) => table.dbName);
|
|
137
171
|
} catch (error) {
|
|
138
172
|
throw new DrizzleStorageError("Failed to get table names from schema", {
|
|
139
173
|
cause: error,
|
|
140
174
|
});
|
|
141
175
|
}
|
|
142
176
|
|
|
177
|
+
// Check if specified idColumn exists in all the tables in schema
|
|
178
|
+
for (const table of Object.values(schema)) {
|
|
179
|
+
const columns = table.columns;
|
|
180
|
+
const tableIdColumn = getIdColumnForTable(table.dbName, idColumnMap);
|
|
181
|
+
|
|
182
|
+
const columnExists = Object.values(columns).some(
|
|
183
|
+
(column) => column.name === tableIdColumn,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (!columnExists) {
|
|
187
|
+
throw new DrizzleStorageError(
|
|
188
|
+
`Column \`"${tableIdColumn}"\` does not exist in table \`"${table.dbName}"\`. ` +
|
|
189
|
+
"Make sure the table has the specified column or provide a valid `idColumn` mapping to `drizzleStorage`.",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
143
194
|
indexer.hooks.hook("run:before", async () => {
|
|
144
195
|
const internalContext = useInternalContext();
|
|
145
196
|
const context = useIndexerContext();
|
|
@@ -153,25 +204,11 @@ export function drizzleStorage<
|
|
|
153
204
|
|
|
154
205
|
indexerId = generateIndexerId(indexerFileName, identifier);
|
|
155
206
|
|
|
156
|
-
if (alwaysReindex) {
|
|
157
|
-
logger.warn(
|
|
158
|
-
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`,
|
|
159
|
-
);
|
|
160
|
-
await withTransaction(db, async (tx) => {
|
|
161
|
-
await cleanupStorage(tx, tableNames, indexerId);
|
|
162
|
-
|
|
163
|
-
if (enablePersistence) {
|
|
164
|
-
await resetPersistence({ tx, indexerId });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
logger.success("Tables have been cleaned up for reindexing");
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
207
|
let retries = 0;
|
|
172
208
|
|
|
173
209
|
// incase the migrations are already applied, we don't want to run them again
|
|
174
210
|
let migrationsApplied = false;
|
|
211
|
+
let cleanupApplied = false;
|
|
175
212
|
|
|
176
213
|
while (retries <= MAX_RETRIES) {
|
|
177
214
|
try {
|
|
@@ -186,6 +223,22 @@ export function drizzleStorage<
|
|
|
186
223
|
if (enablePersistence) {
|
|
187
224
|
await initializePersistentState(tx);
|
|
188
225
|
}
|
|
226
|
+
|
|
227
|
+
if (alwaysReindex && !cleanupApplied) {
|
|
228
|
+
logger.warn(
|
|
229
|
+
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
await cleanupStorage(tx, tableNames, indexerId);
|
|
233
|
+
|
|
234
|
+
if (enablePersistence) {
|
|
235
|
+
await resetPersistence({ tx, indexerId });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
cleanupApplied = true;
|
|
239
|
+
|
|
240
|
+
logger.success("Tables have been cleaned up for reindexing");
|
|
241
|
+
}
|
|
189
242
|
});
|
|
190
243
|
break;
|
|
191
244
|
} catch (error) {
|
|
@@ -239,7 +292,8 @@ export function drizzleStorage<
|
|
|
239
292
|
}
|
|
240
293
|
|
|
241
294
|
await withTransaction(db, async (tx) => {
|
|
242
|
-
|
|
295
|
+
// Use the appropriate idColumn for each table when calling invalidate
|
|
296
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
243
297
|
|
|
244
298
|
if (enablePersistence) {
|
|
245
299
|
await invalidateState({ tx, cursor, indexerId });
|
|
@@ -289,7 +343,8 @@ export function drizzleStorage<
|
|
|
289
343
|
}
|
|
290
344
|
|
|
291
345
|
await withTransaction(db, async (tx) => {
|
|
292
|
-
|
|
346
|
+
// Use the appropriate idColumn for each table when calling invalidate
|
|
347
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
293
348
|
|
|
294
349
|
if (enablePersistence) {
|
|
295
350
|
await invalidateState({ tx, cursor, indexerId });
|
|
@@ -319,7 +374,7 @@ export function drizzleStorage<
|
|
|
319
374
|
|
|
320
375
|
if (prevFinality === "pending") {
|
|
321
376
|
// invalidate if previous block's finality was "pending"
|
|
322
|
-
await invalidate(tx, cursor,
|
|
377
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
323
378
|
}
|
|
324
379
|
|
|
325
380
|
if (finality !== "finalized") {
|
|
@@ -327,7 +382,7 @@ export function drizzleStorage<
|
|
|
327
382
|
tx,
|
|
328
383
|
tableNames,
|
|
329
384
|
endCursor,
|
|
330
|
-
|
|
385
|
+
idColumnMap,
|
|
331
386
|
indexerId,
|
|
332
387
|
);
|
|
333
388
|
}
|
package/src/storage.ts
CHANGED
|
@@ -16,7 +16,11 @@ import {
|
|
|
16
16
|
text,
|
|
17
17
|
} from "drizzle-orm/pg-core";
|
|
18
18
|
import { SCHEMA_NAME } from "./constants";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
DrizzleStorageError,
|
|
21
|
+
type IdColumnMap,
|
|
22
|
+
getIdColumnForTable,
|
|
23
|
+
} from "./utils";
|
|
20
24
|
|
|
21
25
|
const ROLLBACK_TABLE_NAME = "reorg_rollback";
|
|
22
26
|
|
|
@@ -125,11 +129,14 @@ export async function registerTriggers<
|
|
|
125
129
|
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
126
130
|
tables: string[],
|
|
127
131
|
endCursor: Cursor,
|
|
128
|
-
|
|
132
|
+
idColumnMap: IdColumnMap,
|
|
129
133
|
indexerId: string,
|
|
130
134
|
) {
|
|
131
135
|
try {
|
|
132
136
|
for (const table of tables) {
|
|
137
|
+
// Determine the column ID for this specific table
|
|
138
|
+
const tableIdColumn = getIdColumnForTable(table, idColumnMap);
|
|
139
|
+
|
|
133
140
|
await tx.execute(
|
|
134
141
|
sql.raw(
|
|
135
142
|
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`,
|
|
@@ -140,7 +147,7 @@ export async function registerTriggers<
|
|
|
140
147
|
CREATE CONSTRAINT TRIGGER ${getReorgTriggerName(table, indexerId)}
|
|
141
148
|
AFTER INSERT OR UPDATE OR DELETE ON ${table}
|
|
142
149
|
DEFERRABLE INITIALLY DEFERRED
|
|
143
|
-
FOR EACH ROW EXECUTE FUNCTION ${SCHEMA_NAME}.reorg_checkpoint('${
|
|
150
|
+
FOR EACH ROW EXECUTE FUNCTION ${SCHEMA_NAME}.reorg_checkpoint('${tableIdColumn}', ${Number(endCursor.orderKey)}, '${indexerId}');
|
|
144
151
|
`),
|
|
145
152
|
);
|
|
146
153
|
}
|
|
@@ -184,7 +191,7 @@ export async function invalidate<
|
|
|
184
191
|
>(
|
|
185
192
|
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
186
193
|
cursor: Cursor,
|
|
187
|
-
|
|
194
|
+
idColumnMap: IdColumnMap,
|
|
188
195
|
indexerId: string,
|
|
189
196
|
) {
|
|
190
197
|
// Get and delete operations after cursor in one query, ordered by newest first
|
|
@@ -208,6 +215,9 @@ export async function invalidate<
|
|
|
208
215
|
|
|
209
216
|
// Process each operation in reverse order
|
|
210
217
|
for (const op of result) {
|
|
218
|
+
// Determine the column ID for this specific table
|
|
219
|
+
const tableIdColumn = getIdColumnForTable(op.table_name, idColumnMap);
|
|
220
|
+
|
|
211
221
|
switch (op.op) {
|
|
212
222
|
case "I":
|
|
213
223
|
try {
|
|
@@ -218,7 +228,7 @@ export async function invalidate<
|
|
|
218
228
|
await tx.execute(
|
|
219
229
|
sql.raw(`
|
|
220
230
|
DELETE FROM ${op.table_name}
|
|
221
|
-
WHERE ${
|
|
231
|
+
WHERE ${tableIdColumn} = '${op.row_id}'
|
|
222
232
|
`),
|
|
223
233
|
);
|
|
224
234
|
} catch (error) {
|
|
@@ -271,7 +281,9 @@ export async function invalidate<
|
|
|
271
281
|
? JSON.parse(op.row_value)
|
|
272
282
|
: op.row_value;
|
|
273
283
|
|
|
274
|
-
const nonIdKeys = Object.keys(rowValue).filter(
|
|
284
|
+
const nonIdKeys = Object.keys(rowValue).filter(
|
|
285
|
+
(k) => k !== tableIdColumn,
|
|
286
|
+
);
|
|
275
287
|
|
|
276
288
|
const fields = nonIdKeys.map((c) => `${c} = prev.${c}`).join(", ");
|
|
277
289
|
|
|
@@ -281,7 +293,7 @@ export async function invalidate<
|
|
|
281
293
|
FROM (
|
|
282
294
|
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
283
295
|
) as prev
|
|
284
|
-
WHERE ${op.table_name}.${
|
|
296
|
+
WHERE ${op.table_name}.${tableIdColumn} = '${op.row_id}'
|
|
285
297
|
`);
|
|
286
298
|
|
|
287
299
|
await tx.execute(query);
|
package/src/utils.ts
CHANGED
|
@@ -48,3 +48,22 @@ export function serialize<T>(obj: T): string {
|
|
|
48
48
|
export function sleep(ms: number) {
|
|
49
49
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
export interface IdColumnMap extends Record<string, string> {
|
|
53
|
+
/**
|
|
54
|
+
* Wildcard mapping for all tables.
|
|
55
|
+
*/
|
|
56
|
+
"*": string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const getIdColumnForTable = (
|
|
60
|
+
tableName: string,
|
|
61
|
+
idColumn: IdColumnMap,
|
|
62
|
+
): string => {
|
|
63
|
+
// If there's a specific mapping for this table, use it
|
|
64
|
+
if (idColumn[tableName]) {
|
|
65
|
+
return idColumn[tableName];
|
|
66
|
+
}
|
|
67
|
+
// Default fallback
|
|
68
|
+
return idColumn["*"];
|
|
69
|
+
};
|