@apibara/plugin-drizzle 2.1.0-beta.4 → 2.1.0-beta.41
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 +325 -67
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +172 -4
- package/dist/index.d.mts +172 -4
- package/dist/index.d.ts +172 -4
- package/dist/index.mjs +314 -59
- package/dist/index.mjs.map +1 -0
- package/dist/shared/plugin-drizzle.2d226351.mjs +6 -0
- package/dist/shared/plugin-drizzle.2d226351.mjs.map +1 -0
- package/dist/shared/plugin-drizzle.cae20704.cjs +10 -0
- package/dist/shared/plugin-drizzle.cae20704.cjs.map +1 -0
- package/dist/testing.cjs +14 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +6 -0
- package/dist/testing.d.mts +6 -0
- package/dist/testing.d.ts +6 -0
- package/dist/testing.mjs +12 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +21 -6
- package/src/constants.ts +3 -0
- package/src/helper.ts +219 -0
- package/src/index.ts +191 -21
- package/src/persistence.ts +139 -18
- package/src/storage.ts +88 -23
- package/src/testing.ts +13 -0
- package/src/utils.ts +19 -0
package/dist/index.cjs
CHANGED
|
@@ -4,8 +4,9 @@ const indexer = require('@apibara/indexer');
|
|
|
4
4
|
const plugins = require('@apibara/indexer/plugins');
|
|
5
5
|
const internal = require('@apibara/indexer/internal');
|
|
6
6
|
const plugins$1 = require('@apibara/indexer/internal/plugins');
|
|
7
|
-
const
|
|
7
|
+
const constants = require('./shared/plugin-drizzle.cae20704.cjs');
|
|
8
8
|
const drizzleOrm = require('drizzle-orm');
|
|
9
|
+
const protocol = require('@apibara/protocol');
|
|
9
10
|
const pgCore = require('drizzle-orm/pg-core');
|
|
10
11
|
|
|
11
12
|
class DrizzleStorageError extends Error {
|
|
@@ -35,16 +36,89 @@ function serialize(obj) {
|
|
|
35
36
|
function sleep(ms) {
|
|
36
37
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
38
|
}
|
|
39
|
+
const getIdColumnForTable = (tableName, idColumn) => {
|
|
40
|
+
if (idColumn[tableName]) {
|
|
41
|
+
return idColumn[tableName];
|
|
42
|
+
}
|
|
43
|
+
return idColumn["*"];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function drizzle(options) {
|
|
47
|
+
const {
|
|
48
|
+
connectionString = process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://",
|
|
49
|
+
schema,
|
|
50
|
+
type = "pglite",
|
|
51
|
+
config,
|
|
52
|
+
poolConfig
|
|
53
|
+
} = options ?? {};
|
|
54
|
+
if (isPgliteConnectionString(connectionString) && type === "pglite") {
|
|
55
|
+
const { drizzle: drizzlePGLite } = require("drizzle-orm/pglite");
|
|
56
|
+
return drizzlePGLite({
|
|
57
|
+
schema,
|
|
58
|
+
connection: {
|
|
59
|
+
dataDir: connectionString || "memory://pglite"
|
|
60
|
+
},
|
|
61
|
+
...config || {}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const { Pool } = require("pg");
|
|
65
|
+
const { drizzle: drizzleNode } = require("drizzle-orm/node-postgres");
|
|
66
|
+
const pool = new Pool({
|
|
67
|
+
connectionString,
|
|
68
|
+
...poolConfig || {}
|
|
69
|
+
});
|
|
70
|
+
return drizzleNode(pool, { schema, ...config || {} });
|
|
71
|
+
}
|
|
72
|
+
async function migrate(db, options) {
|
|
73
|
+
const isPglite = isDrizzleKind(db, "PgliteDatabase");
|
|
74
|
+
try {
|
|
75
|
+
if (isPglite) {
|
|
76
|
+
const { migrate: migratePGLite } = require("drizzle-orm/pglite/migrator");
|
|
77
|
+
await migratePGLite(db, options);
|
|
78
|
+
} else {
|
|
79
|
+
const {
|
|
80
|
+
migrate: migrateNode
|
|
81
|
+
} = require("drizzle-orm/node-postgres/migrator");
|
|
82
|
+
await migrateNode(db, options);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new DrizzleStorageError(
|
|
86
|
+
"Failed to apply migrations! Please check if you have generated migrations using drizzle:generate",
|
|
87
|
+
{
|
|
88
|
+
cause: error
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function isPgliteConnectionString(conn) {
|
|
94
|
+
return conn.startsWith("memory://") || conn.startsWith("file://") || conn.startsWith("idb://");
|
|
95
|
+
}
|
|
96
|
+
function isDrizzleKind(value, entityKindValue) {
|
|
97
|
+
if (!value || typeof value !== "object") {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
let cls = Object.getPrototypeOf(value).constructor;
|
|
101
|
+
if (cls) {
|
|
102
|
+
while (cls) {
|
|
103
|
+
if (drizzleOrm.entityKind in cls && cls[drizzleOrm.entityKind] === entityKindValue) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
cls = Object.getPrototypeOf(cls);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
38
111
|
|
|
39
|
-
const CHECKPOINTS_TABLE_NAME = "
|
|
40
|
-
const FILTERS_TABLE_NAME = "
|
|
41
|
-
const SCHEMA_VERSION_TABLE_NAME = "
|
|
42
|
-
const
|
|
112
|
+
const CHECKPOINTS_TABLE_NAME = "checkpoints";
|
|
113
|
+
const FILTERS_TABLE_NAME = "filters";
|
|
114
|
+
const SCHEMA_VERSION_TABLE_NAME = "schema_version";
|
|
115
|
+
const schema$1 = pgCore.pgSchema(constants.SCHEMA_NAME);
|
|
116
|
+
const checkpoints = schema$1.table(CHECKPOINTS_TABLE_NAME, {
|
|
43
117
|
id: pgCore.text("id").notNull().primaryKey(),
|
|
44
118
|
orderKey: pgCore.integer("order_key").notNull(),
|
|
45
119
|
uniqueKey: pgCore.text("unique_key")
|
|
46
120
|
});
|
|
47
|
-
const filters =
|
|
121
|
+
const filters = schema$1.table(
|
|
48
122
|
FILTERS_TABLE_NAME,
|
|
49
123
|
{
|
|
50
124
|
id: pgCore.text("id").notNull(),
|
|
@@ -58,7 +132,16 @@ const filters = pgCore.pgTable(
|
|
|
58
132
|
}
|
|
59
133
|
]
|
|
60
134
|
);
|
|
61
|
-
const
|
|
135
|
+
const chainReorganizations = schema$1.table("chain_reorganizations", {
|
|
136
|
+
id: pgCore.serial("id").primaryKey(),
|
|
137
|
+
indexerId: pgCore.text("indexer_id").notNull(),
|
|
138
|
+
oldHeadOrderKey: pgCore.integer("old_head_order_key"),
|
|
139
|
+
oldHeadUniqueKey: pgCore.text("old_head_unique_key").$type().default(null),
|
|
140
|
+
newHeadOrderKey: pgCore.integer("new_head_order_key").notNull(),
|
|
141
|
+
newHeadUniqueKey: pgCore.text("new_head_unique_key").$type().default(null),
|
|
142
|
+
recordedAt: pgCore.timestamp("recorded_at").defaultNow().notNull()
|
|
143
|
+
});
|
|
144
|
+
const schemaVersion = schema$1.table(SCHEMA_VERSION_TABLE_NAME, {
|
|
62
145
|
k: pgCore.integer("k").notNull().primaryKey(),
|
|
63
146
|
version: pgCore.integer("version").notNull()
|
|
64
147
|
});
|
|
@@ -69,12 +152,38 @@ const MIGRATIONS = [
|
|
|
69
152
|
// Add more migration arrays for future versions
|
|
70
153
|
];
|
|
71
154
|
async function initializePersistentState(tx) {
|
|
72
|
-
await tx.execute(
|
|
73
|
-
|
|
155
|
+
await tx.execute(
|
|
156
|
+
drizzleOrm.sql.raw(`
|
|
157
|
+
CREATE SCHEMA IF NOT EXISTS ${constants.SCHEMA_NAME};
|
|
158
|
+
`)
|
|
159
|
+
);
|
|
160
|
+
await tx.execute(
|
|
161
|
+
drizzleOrm.sql.raw(`
|
|
162
|
+
CREATE TABLE IF NOT EXISTS ${constants.SCHEMA_NAME}.${SCHEMA_VERSION_TABLE_NAME} (
|
|
74
163
|
k INTEGER PRIMARY KEY,
|
|
75
164
|
version INTEGER NOT NULL
|
|
76
165
|
);
|
|
77
|
-
`)
|
|
166
|
+
`)
|
|
167
|
+
);
|
|
168
|
+
await tx.execute(
|
|
169
|
+
drizzleOrm.sql.raw(`
|
|
170
|
+
CREATE TABLE IF NOT EXISTS ${constants.SCHEMA_NAME}.chain_reorganizations (
|
|
171
|
+
id SERIAL PRIMARY KEY,
|
|
172
|
+
indexer_id TEXT NOT NULL,
|
|
173
|
+
old_head_order_key INTEGER,
|
|
174
|
+
old_head_unique_key TEXT DEFAULT NULL,
|
|
175
|
+
new_head_order_key INTEGER NOT NULL,
|
|
176
|
+
new_head_unique_key TEXT DEFAULT NULL,
|
|
177
|
+
recorded_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
178
|
+
);
|
|
179
|
+
`)
|
|
180
|
+
);
|
|
181
|
+
await tx.execute(
|
|
182
|
+
drizzleOrm.sql.raw(`
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_chain_reorgs_indexer_id
|
|
184
|
+
ON ${constants.SCHEMA_NAME}.chain_reorganizations(indexer_id);
|
|
185
|
+
`)
|
|
186
|
+
);
|
|
78
187
|
const versionRows = await tx.select().from(schemaVersion).where(drizzleOrm.eq(schemaVersion.k, 0));
|
|
79
188
|
const storedVersion = versionRows[0]?.version ?? -1;
|
|
80
189
|
if (storedVersion > CURRENT_SCHEMA_VERSION) {
|
|
@@ -84,22 +193,26 @@ async function initializePersistentState(tx) {
|
|
|
84
193
|
}
|
|
85
194
|
try {
|
|
86
195
|
if (storedVersion === -1) {
|
|
87
|
-
await tx.execute(
|
|
88
|
-
|
|
196
|
+
await tx.execute(
|
|
197
|
+
drizzleOrm.sql.raw(`
|
|
198
|
+
CREATE TABLE IF NOT EXISTS ${constants.SCHEMA_NAME}.${CHECKPOINTS_TABLE_NAME} (
|
|
89
199
|
id TEXT PRIMARY KEY,
|
|
90
200
|
order_key INTEGER NOT NULL,
|
|
91
201
|
unique_key TEXT
|
|
92
202
|
);
|
|
93
|
-
`)
|
|
94
|
-
|
|
95
|
-
|
|
203
|
+
`)
|
|
204
|
+
);
|
|
205
|
+
await tx.execute(
|
|
206
|
+
drizzleOrm.sql.raw(`
|
|
207
|
+
CREATE TABLE IF NOT EXISTS ${constants.SCHEMA_NAME}.${FILTERS_TABLE_NAME} (
|
|
96
208
|
id TEXT NOT NULL,
|
|
97
209
|
filter TEXT NOT NULL,
|
|
98
210
|
from_block INTEGER NOT NULL,
|
|
99
211
|
to_block INTEGER DEFAULT NULL,
|
|
100
212
|
PRIMARY KEY (id, from_block)
|
|
101
213
|
);
|
|
102
|
-
`)
|
|
214
|
+
`)
|
|
215
|
+
);
|
|
103
216
|
await tx.insert(schemaVersion).values({
|
|
104
217
|
k: 0,
|
|
105
218
|
version: CURRENT_SCHEMA_VERSION
|
|
@@ -122,6 +235,22 @@ async function initializePersistentState(tx) {
|
|
|
122
235
|
);
|
|
123
236
|
}
|
|
124
237
|
}
|
|
238
|
+
async function recordChainReorganization(props) {
|
|
239
|
+
const { tx, indexerId, oldHead, newHead } = props;
|
|
240
|
+
try {
|
|
241
|
+
await tx.insert(chainReorganizations).values({
|
|
242
|
+
indexerId,
|
|
243
|
+
oldHeadOrderKey: oldHead ? Number(oldHead.orderKey) : null,
|
|
244
|
+
oldHeadUniqueKey: oldHead?.uniqueKey ? oldHead.uniqueKey : null,
|
|
245
|
+
newHeadOrderKey: Number(newHead.orderKey),
|
|
246
|
+
newHeadUniqueKey: newHead.uniqueKey ? newHead.uniqueKey : null
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw new DrizzleStorageError("Failed to record chain reorganization", {
|
|
250
|
+
cause: error
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
125
254
|
async function persistState(props) {
|
|
126
255
|
const { tx, endCursor, filter, indexerId } = props;
|
|
127
256
|
try {
|
|
@@ -134,7 +263,9 @@ async function persistState(props) {
|
|
|
134
263
|
target: checkpoints.id,
|
|
135
264
|
set: {
|
|
136
265
|
orderKey: Number(endCursor.orderKey),
|
|
137
|
-
|
|
266
|
+
// Explicitly set the unique key to `null` to indicate that it has been deleted
|
|
267
|
+
// Otherwise drizzle will not update its value.
|
|
268
|
+
uniqueKey: endCursor.uniqueKey ? endCursor.uniqueKey : null
|
|
138
269
|
}
|
|
139
270
|
});
|
|
140
271
|
if (filter) {
|
|
@@ -180,6 +311,10 @@ async function getState(props) {
|
|
|
180
311
|
async function invalidateState(props) {
|
|
181
312
|
const { tx, cursor, indexerId } = props;
|
|
182
313
|
try {
|
|
314
|
+
await tx.update(checkpoints).set({
|
|
315
|
+
orderKey: Number(cursor.orderKey),
|
|
316
|
+
uniqueKey: cursor.uniqueKey ? cursor.uniqueKey : null
|
|
317
|
+
}).where(drizzleOrm.eq(checkpoints.id, indexerId));
|
|
183
318
|
await tx.delete(filters).where(
|
|
184
319
|
drizzleOrm.and(
|
|
185
320
|
drizzleOrm.eq(filters.id, indexerId),
|
|
@@ -213,11 +348,24 @@ async function finalizeState(props) {
|
|
|
213
348
|
});
|
|
214
349
|
}
|
|
215
350
|
}
|
|
351
|
+
async function resetPersistence(props) {
|
|
352
|
+
const { tx, indexerId } = props;
|
|
353
|
+
try {
|
|
354
|
+
await tx.delete(checkpoints).where(drizzleOrm.eq(checkpoints.id, indexerId));
|
|
355
|
+
await tx.delete(filters).where(drizzleOrm.eq(filters.id, indexerId));
|
|
356
|
+
} catch (error) {
|
|
357
|
+
throw new DrizzleStorageError("Failed to reset persistence state", {
|
|
358
|
+
cause: error
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
216
362
|
|
|
363
|
+
const ROLLBACK_TABLE_NAME = "reorg_rollback";
|
|
364
|
+
const schema = pgCore.pgSchema(constants.SCHEMA_NAME);
|
|
217
365
|
function getReorgTriggerName(table, indexerId) {
|
|
218
366
|
return `${table}_reorg_${indexerId}`;
|
|
219
367
|
}
|
|
220
|
-
|
|
368
|
+
schema.table(ROLLBACK_TABLE_NAME, {
|
|
221
369
|
n: pgCore.serial("n").primaryKey(),
|
|
222
370
|
op: pgCore.char("op", { length: 1 }).$type().notNull(),
|
|
223
371
|
table_name: pgCore.text("table_name").notNull(),
|
|
@@ -228,9 +376,12 @@ pgCore.pgTable("__reorg_rollback", {
|
|
|
228
376
|
});
|
|
229
377
|
async function initializeReorgRollbackTable(tx, indexerId) {
|
|
230
378
|
try {
|
|
379
|
+
await tx.execute(`
|
|
380
|
+
CREATE SCHEMA IF NOT EXISTS ${constants.SCHEMA_NAME};
|
|
381
|
+
`);
|
|
231
382
|
await tx.execute(
|
|
232
383
|
drizzleOrm.sql.raw(`
|
|
233
|
-
CREATE TABLE IF NOT EXISTS
|
|
384
|
+
CREATE TABLE IF NOT EXISTS ${constants.SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(
|
|
234
385
|
n SERIAL PRIMARY KEY,
|
|
235
386
|
op CHAR(1) NOT NULL,
|
|
236
387
|
table_name TEXT NOT NULL,
|
|
@@ -243,7 +394,7 @@ async function initializeReorgRollbackTable(tx, indexerId) {
|
|
|
243
394
|
);
|
|
244
395
|
await tx.execute(
|
|
245
396
|
drizzleOrm.sql.raw(`
|
|
246
|
-
CREATE INDEX IF NOT EXISTS idx_reorg_rollback_indexer_id_cursor ON
|
|
397
|
+
CREATE INDEX IF NOT EXISTS idx_reorg_rollback_indexer_id_cursor ON ${constants.SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(indexer_id, cursor);
|
|
247
398
|
`)
|
|
248
399
|
);
|
|
249
400
|
} catch (error) {
|
|
@@ -254,24 +405,25 @@ async function initializeReorgRollbackTable(tx, indexerId) {
|
|
|
254
405
|
try {
|
|
255
406
|
await tx.execute(
|
|
256
407
|
drizzleOrm.sql.raw(`
|
|
257
|
-
CREATE OR REPLACE FUNCTION reorg_checkpoint()
|
|
408
|
+
CREATE OR REPLACE FUNCTION ${constants.SCHEMA_NAME}.reorg_checkpoint()
|
|
258
409
|
RETURNS TRIGGER AS $$
|
|
259
410
|
DECLARE
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
411
|
+
table_name TEXT := TG_ARGV[0]::TEXT;
|
|
412
|
+
id_col TEXT := TG_ARGV[1]::TEXT;
|
|
413
|
+
order_key INTEGER := TG_ARGV[2]::INTEGER;
|
|
414
|
+
indexer_id TEXT := TG_ARGV[3]::TEXT;
|
|
263
415
|
new_id_value TEXT := row_to_json(NEW.*)->>id_col;
|
|
264
416
|
old_id_value TEXT := row_to_json(OLD.*)->>id_col;
|
|
265
417
|
BEGIN
|
|
266
418
|
IF (TG_OP = 'DELETE') THEN
|
|
267
|
-
INSERT INTO
|
|
268
|
-
SELECT 'D',
|
|
419
|
+
INSERT INTO ${constants.SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
420
|
+
SELECT 'D', table_name, order_key, old_id_value, row_to_json(OLD.*), indexer_id;
|
|
269
421
|
ELSIF (TG_OP = 'UPDATE') THEN
|
|
270
|
-
INSERT INTO
|
|
271
|
-
SELECT 'U',
|
|
422
|
+
INSERT INTO ${constants.SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
423
|
+
SELECT 'U', table_name, order_key, new_id_value, row_to_json(OLD.*), indexer_id;
|
|
272
424
|
ELSIF (TG_OP = 'INSERT') THEN
|
|
273
|
-
INSERT INTO
|
|
274
|
-
SELECT 'I',
|
|
425
|
+
INSERT INTO ${constants.SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
426
|
+
SELECT 'I', table_name, order_key, new_id_value, null, indexer_id;
|
|
275
427
|
END IF;
|
|
276
428
|
RETURN NULL;
|
|
277
429
|
END;
|
|
@@ -287,9 +439,10 @@ async function initializeReorgRollbackTable(tx, indexerId) {
|
|
|
287
439
|
);
|
|
288
440
|
}
|
|
289
441
|
}
|
|
290
|
-
async function registerTriggers(tx, tables, endCursor,
|
|
442
|
+
async function registerTriggers(tx, tables, endCursor, idColumnMap, indexerId) {
|
|
291
443
|
try {
|
|
292
444
|
for (const table of tables) {
|
|
445
|
+
const tableIdColumn = getIdColumnForTable(table, idColumnMap);
|
|
293
446
|
await tx.execute(
|
|
294
447
|
drizzleOrm.sql.raw(
|
|
295
448
|
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`
|
|
@@ -300,7 +453,7 @@ async function registerTriggers(tx, tables, endCursor, idColumn, indexerId) {
|
|
|
300
453
|
CREATE CONSTRAINT TRIGGER ${getReorgTriggerName(table, indexerId)}
|
|
301
454
|
AFTER INSERT OR UPDATE OR DELETE ON ${table}
|
|
302
455
|
DEFERRABLE INITIALLY DEFERRED
|
|
303
|
-
FOR EACH ROW EXECUTE FUNCTION reorg_checkpoint('${
|
|
456
|
+
FOR EACH ROW EXECUTE FUNCTION ${constants.SCHEMA_NAME}.reorg_checkpoint('${table}', '${tableIdColumn}', ${Number(endCursor.orderKey)}, '${indexerId}');
|
|
304
457
|
`)
|
|
305
458
|
);
|
|
306
459
|
}
|
|
@@ -325,11 +478,11 @@ async function removeTriggers(db, tables, indexerId) {
|
|
|
325
478
|
});
|
|
326
479
|
}
|
|
327
480
|
}
|
|
328
|
-
async function invalidate(tx, cursor,
|
|
481
|
+
async function invalidate(tx, cursor, idColumnMap, indexerId) {
|
|
329
482
|
const { rows: result } = await tx.execute(
|
|
330
483
|
drizzleOrm.sql.raw(`
|
|
331
484
|
WITH deleted AS (
|
|
332
|
-
DELETE FROM
|
|
485
|
+
DELETE FROM ${constants.SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
333
486
|
WHERE cursor > ${Number(cursor.orderKey)}
|
|
334
487
|
AND indexer_id = '${indexerId}'
|
|
335
488
|
RETURNING *
|
|
@@ -343,6 +496,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
343
496
|
);
|
|
344
497
|
}
|
|
345
498
|
for (const op of result) {
|
|
499
|
+
const tableIdColumn = getIdColumnForTable(op.table_name, idColumnMap);
|
|
346
500
|
switch (op.op) {
|
|
347
501
|
case "I":
|
|
348
502
|
try {
|
|
@@ -352,7 +506,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
352
506
|
await tx.execute(
|
|
353
507
|
drizzleOrm.sql.raw(`
|
|
354
508
|
DELETE FROM ${op.table_name}
|
|
355
|
-
WHERE ${
|
|
509
|
+
WHERE ${tableIdColumn} = '${op.row_id}'
|
|
356
510
|
`)
|
|
357
511
|
);
|
|
358
512
|
} catch (error) {
|
|
@@ -392,7 +546,9 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
392
546
|
);
|
|
393
547
|
}
|
|
394
548
|
const rowValue = typeof op.row_value === "string" ? JSON.parse(op.row_value) : op.row_value;
|
|
395
|
-
const nonIdKeys = Object.keys(rowValue).filter(
|
|
549
|
+
const nonIdKeys = Object.keys(rowValue).filter(
|
|
550
|
+
(k) => k !== tableIdColumn
|
|
551
|
+
);
|
|
396
552
|
const fields = nonIdKeys.map((c) => `${c} = prev.${c}`).join(", ");
|
|
397
553
|
const query = drizzleOrm.sql.raw(`
|
|
398
554
|
UPDATE ${op.table_name}
|
|
@@ -400,7 +556,7 @@ async function invalidate(tx, cursor, idColumn, indexerId) {
|
|
|
400
556
|
FROM (
|
|
401
557
|
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
402
558
|
) as prev
|
|
403
|
-
WHERE ${op.table_name}.${
|
|
559
|
+
WHERE ${op.table_name}.${tableIdColumn} = '${op.row_id}'
|
|
404
560
|
`);
|
|
405
561
|
await tx.execute(query);
|
|
406
562
|
} catch (error) {
|
|
@@ -422,7 +578,7 @@ async function finalize(tx, cursor, indexerId) {
|
|
|
422
578
|
try {
|
|
423
579
|
await tx.execute(
|
|
424
580
|
drizzleOrm.sql.raw(`
|
|
425
|
-
DELETE FROM
|
|
581
|
+
DELETE FROM ${constants.SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
426
582
|
WHERE cursor <= ${Number(cursor.orderKey)}
|
|
427
583
|
AND indexer_id = '${indexerId}'
|
|
428
584
|
`)
|
|
@@ -433,52 +589,134 @@ async function finalize(tx, cursor, indexerId) {
|
|
|
433
589
|
});
|
|
434
590
|
}
|
|
435
591
|
}
|
|
592
|
+
async function cleanupStorage(tx, tables, indexerId) {
|
|
593
|
+
try {
|
|
594
|
+
for (const table of tables) {
|
|
595
|
+
await tx.execute(
|
|
596
|
+
drizzleOrm.sql.raw(
|
|
597
|
+
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
await tx.execute(
|
|
602
|
+
drizzleOrm.sql.raw(`
|
|
603
|
+
DELETE FROM ${constants.SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
604
|
+
WHERE indexer_id = '${indexerId}'
|
|
605
|
+
`)
|
|
606
|
+
);
|
|
607
|
+
for (const table of tables) {
|
|
608
|
+
try {
|
|
609
|
+
await tx.execute(drizzleOrm.sql.raw(`TRUNCATE TABLE ${table} CASCADE;`));
|
|
610
|
+
} catch (error) {
|
|
611
|
+
throw new DrizzleStorageError(`Failed to truncate table ${table}`, {
|
|
612
|
+
cause: error
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
throw new DrizzleStorageError("Failed to clean up storage", {
|
|
618
|
+
cause: error
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
436
622
|
|
|
437
|
-
const DRIZZLE_PROPERTY = "_drizzle";
|
|
438
623
|
const MAX_RETRIES = 5;
|
|
439
624
|
function useDrizzleStorage(_db) {
|
|
440
625
|
const context = indexer.useIndexerContext();
|
|
441
|
-
if (!context[DRIZZLE_PROPERTY]) {
|
|
626
|
+
if (!context[constants.DRIZZLE_PROPERTY]) {
|
|
442
627
|
throw new DrizzleStorageError(
|
|
443
628
|
"drizzle storage is not available. Did you register the plugin?"
|
|
444
629
|
);
|
|
445
630
|
}
|
|
446
|
-
return context[DRIZZLE_PROPERTY];
|
|
631
|
+
return context[constants.DRIZZLE_PROPERTY];
|
|
632
|
+
}
|
|
633
|
+
function useTestDrizzleStorage() {
|
|
634
|
+
const context = indexer.useIndexerContext();
|
|
635
|
+
if (!context[constants.DRIZZLE_STORAGE_DB_PROPERTY]) {
|
|
636
|
+
throw new DrizzleStorageError(
|
|
637
|
+
"drizzle storage db is not available. Did you register the plugin?"
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return context[constants.DRIZZLE_STORAGE_DB_PROPERTY];
|
|
447
641
|
}
|
|
448
642
|
function drizzleStorage({
|
|
449
643
|
db,
|
|
450
644
|
persistState: enablePersistence = true,
|
|
451
645
|
indexerName: identifier = "default",
|
|
452
|
-
schema,
|
|
453
|
-
idColumn
|
|
646
|
+
schema: _schema,
|
|
647
|
+
idColumn,
|
|
648
|
+
migrate: migrateOptions,
|
|
649
|
+
recordChainReorganizations = false
|
|
454
650
|
}) {
|
|
455
|
-
return plugins.defineIndexerPlugin((indexer) => {
|
|
651
|
+
return plugins.defineIndexerPlugin((indexer$1) => {
|
|
456
652
|
let tableNames = [];
|
|
457
653
|
let indexerId = "";
|
|
654
|
+
const alwaysReindex = process.env["APIBARA_ALWAYS_REINDEX"] === "true";
|
|
655
|
+
let prevFinality;
|
|
656
|
+
const schema = _schema ?? db._.schema ?? {};
|
|
657
|
+
const idColumnMap = {
|
|
658
|
+
"*": typeof idColumn === "string" ? idColumn : "id",
|
|
659
|
+
...typeof idColumn === "object" ? idColumn : {}
|
|
660
|
+
};
|
|
458
661
|
try {
|
|
459
|
-
tableNames = Object.values(schema
|
|
460
|
-
(table) => table.dbName
|
|
461
|
-
);
|
|
662
|
+
tableNames = Object.values(schema).map((table) => table.dbName);
|
|
462
663
|
} catch (error) {
|
|
463
664
|
throw new DrizzleStorageError("Failed to get table names from schema", {
|
|
464
665
|
cause: error
|
|
465
666
|
});
|
|
466
667
|
}
|
|
467
|
-
|
|
468
|
-
const
|
|
668
|
+
for (const table of Object.values(schema)) {
|
|
669
|
+
const columns = table.columns;
|
|
670
|
+
const tableIdColumn = getIdColumnForTable(table.dbName, idColumnMap);
|
|
671
|
+
const columnExists = Object.values(columns).some(
|
|
672
|
+
(column) => column.name === tableIdColumn
|
|
673
|
+
);
|
|
674
|
+
if (!columnExists) {
|
|
675
|
+
throw new DrizzleStorageError(
|
|
676
|
+
`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\`.`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
indexer$1.hooks.hook("plugins:init", async () => {
|
|
681
|
+
const internalContext = plugins$1.useInternalContext();
|
|
682
|
+
const context = indexer.useIndexerContext();
|
|
683
|
+
const logger = plugins.useLogger();
|
|
684
|
+
context[constants.DRIZZLE_STORAGE_DB_PROPERTY] = db;
|
|
685
|
+
const { indexerName: indexerFileName, availableIndexers } = internalContext;
|
|
469
686
|
indexerId = internal.generateIndexerId(indexerFileName, identifier);
|
|
470
687
|
let retries = 0;
|
|
688
|
+
let migrationsApplied = false;
|
|
689
|
+
let cleanupApplied = false;
|
|
471
690
|
while (retries <= MAX_RETRIES) {
|
|
472
691
|
try {
|
|
692
|
+
if (migrateOptions && !migrationsApplied) {
|
|
693
|
+
await migrate(db, migrateOptions);
|
|
694
|
+
migrationsApplied = true;
|
|
695
|
+
logger.success("Migrations applied");
|
|
696
|
+
}
|
|
473
697
|
await withTransaction(db, async (tx) => {
|
|
474
698
|
await initializeReorgRollbackTable(tx, indexerId);
|
|
475
699
|
if (enablePersistence) {
|
|
476
700
|
await initializePersistentState(tx);
|
|
477
701
|
}
|
|
702
|
+
if (alwaysReindex && !cleanupApplied) {
|
|
703
|
+
logger.warn(
|
|
704
|
+
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`
|
|
705
|
+
);
|
|
706
|
+
await cleanupStorage(tx, tableNames, indexerId);
|
|
707
|
+
if (enablePersistence) {
|
|
708
|
+
await resetPersistence({ tx, indexerId });
|
|
709
|
+
}
|
|
710
|
+
cleanupApplied = true;
|
|
711
|
+
logger.success("Tables have been cleaned up for reindexing");
|
|
712
|
+
}
|
|
478
713
|
});
|
|
479
714
|
break;
|
|
480
715
|
} catch (error) {
|
|
481
716
|
if (retries === MAX_RETRIES) {
|
|
717
|
+
if (error instanceof DrizzleStorageError) {
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
482
720
|
throw new DrizzleStorageError(
|
|
483
721
|
"Initialization failed after 5 retries",
|
|
484
722
|
{
|
|
@@ -491,7 +729,7 @@ function drizzleStorage({
|
|
|
491
729
|
}
|
|
492
730
|
}
|
|
493
731
|
});
|
|
494
|
-
indexer.hooks.hook("connect:before", async ({ request }) => {
|
|
732
|
+
indexer$1.hooks.hook("connect:before", async ({ request }) => {
|
|
495
733
|
if (!enablePersistence) {
|
|
496
734
|
return;
|
|
497
735
|
}
|
|
@@ -508,19 +746,19 @@ function drizzleStorage({
|
|
|
508
746
|
}
|
|
509
747
|
});
|
|
510
748
|
});
|
|
511
|
-
indexer.hooks.hook("connect:after", async ({ request }) => {
|
|
749
|
+
indexer$1.hooks.hook("connect:after", async ({ request }) => {
|
|
512
750
|
const cursor = request.startingCursor;
|
|
513
751
|
if (!cursor) {
|
|
514
752
|
return;
|
|
515
753
|
}
|
|
516
754
|
await withTransaction(db, async (tx) => {
|
|
517
|
-
await invalidate(tx, cursor,
|
|
755
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
518
756
|
if (enablePersistence) {
|
|
519
757
|
await invalidateState({ tx, cursor, indexerId });
|
|
520
758
|
}
|
|
521
759
|
});
|
|
522
760
|
});
|
|
523
|
-
indexer.hooks.hook("connect:factory", async ({ request, endCursor }) => {
|
|
761
|
+
indexer$1.hooks.hook("connect:factory", async ({ request, endCursor }) => {
|
|
524
762
|
if (!enablePersistence) {
|
|
525
763
|
return;
|
|
526
764
|
}
|
|
@@ -534,8 +772,8 @@ function drizzleStorage({
|
|
|
534
772
|
});
|
|
535
773
|
}
|
|
536
774
|
});
|
|
537
|
-
indexer.hooks.hook("message:finalize", async ({ message }) => {
|
|
538
|
-
const { cursor } = message
|
|
775
|
+
indexer$1.hooks.hook("message:finalize", async ({ message }) => {
|
|
776
|
+
const { cursor } = message;
|
|
539
777
|
if (!cursor) {
|
|
540
778
|
throw new DrizzleStorageError("Finalized Cursor is undefined");
|
|
541
779
|
}
|
|
@@ -546,59 +784,79 @@ function drizzleStorage({
|
|
|
546
784
|
}
|
|
547
785
|
});
|
|
548
786
|
});
|
|
549
|
-
indexer.hooks.hook("message:invalidate", async ({ message }) => {
|
|
550
|
-
const { cursor } = message
|
|
787
|
+
indexer$1.hooks.hook("message:invalidate", async ({ message }) => {
|
|
788
|
+
const { cursor } = message;
|
|
551
789
|
if (!cursor) {
|
|
552
790
|
throw new DrizzleStorageError("Invalidate Cursor is undefined");
|
|
553
791
|
}
|
|
554
792
|
await withTransaction(db, async (tx) => {
|
|
555
|
-
|
|
793
|
+
let oldHead;
|
|
794
|
+
if (recordChainReorganizations) {
|
|
795
|
+
const { cursor: currentCursor } = await getState({
|
|
796
|
+
tx,
|
|
797
|
+
indexerId
|
|
798
|
+
});
|
|
799
|
+
oldHead = currentCursor;
|
|
800
|
+
await recordChainReorganization({
|
|
801
|
+
tx,
|
|
802
|
+
indexerId,
|
|
803
|
+
oldHead,
|
|
804
|
+
newHead: cursor
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
556
808
|
if (enablePersistence) {
|
|
557
809
|
await invalidateState({ tx, cursor, indexerId });
|
|
558
810
|
}
|
|
559
811
|
});
|
|
560
812
|
});
|
|
561
|
-
indexer.hooks.hook("handler:middleware", async ({ use }) => {
|
|
813
|
+
indexer$1.hooks.hook("handler:middleware", async ({ use }) => {
|
|
562
814
|
use(async (context, next) => {
|
|
563
815
|
try {
|
|
564
|
-
const { endCursor, finality } = context;
|
|
816
|
+
const { endCursor, finality, cursor } = context;
|
|
565
817
|
if (!endCursor) {
|
|
566
818
|
throw new DrizzleStorageError("End Cursor is undefined");
|
|
567
819
|
}
|
|
568
820
|
await withTransaction(db, async (tx) => {
|
|
569
|
-
context[DRIZZLE_PROPERTY] = { db: tx };
|
|
821
|
+
context[constants.DRIZZLE_PROPERTY] = { db: tx };
|
|
822
|
+
if (prevFinality === "pending") {
|
|
823
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
824
|
+
}
|
|
570
825
|
if (finality !== "finalized") {
|
|
571
826
|
await registerTriggers(
|
|
572
827
|
tx,
|
|
573
828
|
tableNames,
|
|
574
829
|
endCursor,
|
|
575
|
-
|
|
830
|
+
idColumnMap,
|
|
576
831
|
indexerId
|
|
577
832
|
);
|
|
578
833
|
}
|
|
579
834
|
await next();
|
|
580
|
-
delete context[DRIZZLE_PROPERTY];
|
|
581
|
-
if (enablePersistence) {
|
|
835
|
+
delete context[constants.DRIZZLE_PROPERTY];
|
|
836
|
+
if (enablePersistence && finality !== "pending") {
|
|
582
837
|
await persistState({
|
|
583
838
|
tx,
|
|
584
839
|
endCursor,
|
|
585
840
|
indexerId
|
|
586
841
|
});
|
|
587
842
|
}
|
|
843
|
+
prevFinality = finality;
|
|
588
844
|
});
|
|
589
845
|
if (finality !== "finalized") {
|
|
590
846
|
await removeTriggers(db, tableNames, indexerId);
|
|
591
847
|
}
|
|
592
848
|
} catch (error) {
|
|
593
849
|
await removeTriggers(db, tableNames, indexerId);
|
|
594
|
-
throw
|
|
595
|
-
cause: error
|
|
596
|
-
});
|
|
850
|
+
throw error;
|
|
597
851
|
}
|
|
598
852
|
});
|
|
599
853
|
});
|
|
600
854
|
});
|
|
601
855
|
}
|
|
602
856
|
|
|
857
|
+
exports.drizzle = drizzle;
|
|
603
858
|
exports.drizzleStorage = drizzleStorage;
|
|
859
|
+
exports.migrate = migrate;
|
|
604
860
|
exports.useDrizzleStorage = useDrizzleStorage;
|
|
861
|
+
exports.useTestDrizzleStorage = useTestDrizzleStorage;
|
|
862
|
+
//# sourceMappingURL=index.cjs.map
|