@apibara/plugin-drizzle 2.0.0-beta.28 → 2.0.0-beta.29
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 +535 -4
- package/dist/index.d.cts +27 -2
- package/dist/index.d.mts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.mjs +535 -5
- package/package.json +10 -5
- package/src/index.ts +258 -3
- package/src/persistence.ts +245 -137
- package/src/storage.ts +279 -0
- package/src/utils.ts +42 -2
- package/src/persistence.test.ts +0 -7
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,545 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const indexer = require('@apibara/indexer');
|
|
4
|
+
const plugins = require('@apibara/indexer/plugins');
|
|
5
|
+
const drizzleOrm = require('drizzle-orm');
|
|
6
|
+
const pgCore = require('drizzle-orm/pg-core');
|
|
7
|
+
|
|
3
8
|
class DrizzleStorageError extends Error {
|
|
4
|
-
constructor(message) {
|
|
5
|
-
super(message);
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options);
|
|
6
11
|
this.name = "DrizzleStorageError";
|
|
7
12
|
}
|
|
8
13
|
}
|
|
14
|
+
async function withTransaction(db, cb) {
|
|
15
|
+
return await db.transaction(async (txnDb) => {
|
|
16
|
+
return await cb(txnDb);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function deserialize(str) {
|
|
20
|
+
return JSON.parse(
|
|
21
|
+
str,
|
|
22
|
+
(_, value) => typeof value === "string" && value.match(/^\d+n$/) ? BigInt(value.slice(0, -1)) : value
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
function serialize(obj) {
|
|
26
|
+
return JSON.stringify(
|
|
27
|
+
obj,
|
|
28
|
+
(_, value) => typeof value === "bigint" ? `${value.toString()}n` : value,
|
|
29
|
+
" "
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CHECKPOINTS_TABLE_NAME = "__indexer_checkpoints";
|
|
34
|
+
const FILTERS_TABLE_NAME = "__indexer_filters";
|
|
35
|
+
const SCHEMA_VERSION_TABLE_NAME = "__indexer_schema_version";
|
|
36
|
+
const checkpoints = pgCore.pgTable(CHECKPOINTS_TABLE_NAME, {
|
|
37
|
+
id: pgCore.text("id").notNull().primaryKey(),
|
|
38
|
+
orderKey: pgCore.integer("order_key").notNull(),
|
|
39
|
+
uniqueKey: pgCore.text("unique_key").$type().notNull().default(void 0)
|
|
40
|
+
});
|
|
41
|
+
const filters = pgCore.pgTable(
|
|
42
|
+
FILTERS_TABLE_NAME,
|
|
43
|
+
{
|
|
44
|
+
id: pgCore.text("id").notNull(),
|
|
45
|
+
filter: pgCore.text("filter").notNull(),
|
|
46
|
+
fromBlock: pgCore.integer("from_block").notNull(),
|
|
47
|
+
toBlock: pgCore.integer("to_block").$type().default(null)
|
|
48
|
+
},
|
|
49
|
+
(table) => [
|
|
50
|
+
{
|
|
51
|
+
pk: pgCore.primaryKey({ columns: [table.id, table.fromBlock] })
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
);
|
|
55
|
+
const schemaVersion = pgCore.pgTable(SCHEMA_VERSION_TABLE_NAME, {
|
|
56
|
+
k: pgCore.integer("k").notNull().primaryKey(),
|
|
57
|
+
version: pgCore.integer("version").notNull()
|
|
58
|
+
});
|
|
59
|
+
const CURRENT_SCHEMA_VERSION = 0;
|
|
60
|
+
const MIGRATIONS = [
|
|
61
|
+
// migrations[0]: v0 -> v1 (for future use)
|
|
62
|
+
[]
|
|
63
|
+
// Add more migration arrays for future versions
|
|
64
|
+
];
|
|
65
|
+
async function initializePersistentState(tx) {
|
|
66
|
+
await tx.execute(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_VERSION_TABLE_NAME} (
|
|
68
|
+
k INTEGER PRIMARY KEY,
|
|
69
|
+
version INTEGER NOT NULL
|
|
70
|
+
);
|
|
71
|
+
`);
|
|
72
|
+
const versionRows = await tx.select().from(schemaVersion).where(drizzleOrm.eq(schemaVersion.k, 0));
|
|
73
|
+
const storedVersion = versionRows[0]?.version ?? -1;
|
|
74
|
+
if (storedVersion > CURRENT_SCHEMA_VERSION) {
|
|
75
|
+
throw new DrizzleStorageError(
|
|
76
|
+
`Database Persistence schema version v${storedVersion} is newer than supported version v${CURRENT_SCHEMA_VERSION}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
if (storedVersion === -1) {
|
|
81
|
+
await tx.execute(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS ${CHECKPOINTS_TABLE_NAME} (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
order_key INTEGER NOT NULL,
|
|
85
|
+
unique_key TEXT NOT NULL DEFAULT ''
|
|
86
|
+
);
|
|
87
|
+
`);
|
|
88
|
+
await tx.execute(`
|
|
89
|
+
CREATE TABLE IF NOT EXISTS ${FILTERS_TABLE_NAME} (
|
|
90
|
+
id TEXT NOT NULL,
|
|
91
|
+
filter TEXT NOT NULL,
|
|
92
|
+
from_block INTEGER NOT NULL,
|
|
93
|
+
to_block INTEGER DEFAULT NULL,
|
|
94
|
+
PRIMARY KEY (id, from_block)
|
|
95
|
+
);
|
|
96
|
+
`);
|
|
97
|
+
await tx.insert(schemaVersion).values({
|
|
98
|
+
k: 0,
|
|
99
|
+
version: CURRENT_SCHEMA_VERSION
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
let currentVersion = storedVersion;
|
|
103
|
+
while (currentVersion < CURRENT_SCHEMA_VERSION) {
|
|
104
|
+
const migrationStatements = MIGRATIONS[currentVersion];
|
|
105
|
+
for (const statement of migrationStatements) {
|
|
106
|
+
await tx.execute(statement);
|
|
107
|
+
}
|
|
108
|
+
currentVersion++;
|
|
109
|
+
}
|
|
110
|
+
await tx.update(schemaVersion).set({ version: CURRENT_SCHEMA_VERSION }).where(drizzleOrm.eq(schemaVersion.k, 0));
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw new DrizzleStorageError(
|
|
114
|
+
"Failed to initialize or migrate database schema",
|
|
115
|
+
{ cause: error }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function persistState(props) {
|
|
120
|
+
const { tx, endCursor, filter, indexerName } = props;
|
|
121
|
+
try {
|
|
122
|
+
if (endCursor) {
|
|
123
|
+
await tx.insert(checkpoints).values({
|
|
124
|
+
id: indexerName,
|
|
125
|
+
orderKey: Number(endCursor.orderKey),
|
|
126
|
+
uniqueKey: endCursor.uniqueKey
|
|
127
|
+
}).onConflictDoUpdate({
|
|
128
|
+
target: checkpoints.id,
|
|
129
|
+
set: {
|
|
130
|
+
orderKey: Number(endCursor.orderKey),
|
|
131
|
+
uniqueKey: endCursor.uniqueKey
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
if (filter) {
|
|
135
|
+
await tx.update(filters).set({ toBlock: Number(endCursor.orderKey) }).where(drizzleOrm.and(drizzleOrm.eq(filters.id, indexerName), drizzleOrm.isNull(filters.toBlock)));
|
|
136
|
+
await tx.insert(filters).values({
|
|
137
|
+
id: indexerName,
|
|
138
|
+
filter: serialize(filter),
|
|
139
|
+
fromBlock: Number(endCursor.orderKey),
|
|
140
|
+
toBlock: null
|
|
141
|
+
}).onConflictDoUpdate({
|
|
142
|
+
target: [filters.id, filters.fromBlock],
|
|
143
|
+
set: {
|
|
144
|
+
filter: serialize(filter),
|
|
145
|
+
fromBlock: Number(endCursor.orderKey),
|
|
146
|
+
toBlock: null
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new DrizzleStorageError("Failed to persist state", {
|
|
153
|
+
cause: error
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function getState(props) {
|
|
158
|
+
const { tx, indexerName } = props;
|
|
159
|
+
try {
|
|
160
|
+
const checkpointRows = await tx.select().from(checkpoints).where(drizzleOrm.eq(checkpoints.id, indexerName));
|
|
161
|
+
const cursor = checkpointRows[0] ? {
|
|
162
|
+
orderKey: BigInt(checkpointRows[0].orderKey),
|
|
163
|
+
uniqueKey: checkpointRows[0].uniqueKey
|
|
164
|
+
} : void 0;
|
|
165
|
+
const filterRows = await tx.select().from(filters).where(drizzleOrm.and(drizzleOrm.eq(filters.id, indexerName), drizzleOrm.isNull(filters.toBlock)));
|
|
166
|
+
const filter = filterRows[0] ? deserialize(filterRows[0].filter) : void 0;
|
|
167
|
+
return { cursor, filter };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new DrizzleStorageError("Failed to get persistent state", {
|
|
170
|
+
cause: error
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function invalidateState(props) {
|
|
175
|
+
const { tx, cursor, indexerName } = props;
|
|
176
|
+
try {
|
|
177
|
+
await tx.delete(filters).where(
|
|
178
|
+
drizzleOrm.and(
|
|
179
|
+
drizzleOrm.eq(filters.id, indexerName),
|
|
180
|
+
drizzleOrm.gt(filters.fromBlock, Number(cursor.orderKey))
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
await tx.update(filters).set({ toBlock: null }).where(
|
|
184
|
+
drizzleOrm.and(
|
|
185
|
+
drizzleOrm.eq(filters.id, indexerName),
|
|
186
|
+
drizzleOrm.gt(filters.toBlock, Number(cursor.orderKey))
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new DrizzleStorageError("Failed to invalidate state", {
|
|
191
|
+
cause: error
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function finalizeState(props) {
|
|
196
|
+
const { tx, cursor, indexerName } = props;
|
|
197
|
+
try {
|
|
198
|
+
await tx.delete(filters).where(
|
|
199
|
+
drizzleOrm.and(
|
|
200
|
+
drizzleOrm.eq(filters.id, indexerName),
|
|
201
|
+
drizzleOrm.lt(filters.toBlock, Number(cursor.orderKey))
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new DrizzleStorageError("Failed to finalize state", {
|
|
206
|
+
cause: error
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
9
210
|
|
|
10
|
-
|
|
11
|
-
|
|
211
|
+
pgCore.pgTable("__reorg_rollback", {
|
|
212
|
+
n: pgCore.serial("n").primaryKey(),
|
|
213
|
+
op: pgCore.char("op", { length: 1 }).$type().notNull(),
|
|
214
|
+
table_name: pgCore.text("table_name").notNull(),
|
|
215
|
+
cursor: pgCore.integer("cursor").notNull(),
|
|
216
|
+
row_id: pgCore.text("row_id"),
|
|
217
|
+
row_value: pgCore.jsonb("row_value")
|
|
218
|
+
});
|
|
219
|
+
async function initializeReorgRollbackTable(tx) {
|
|
220
|
+
try {
|
|
221
|
+
await tx.execute(
|
|
222
|
+
drizzleOrm.sql.raw(`
|
|
223
|
+
CREATE TABLE IF NOT EXISTS __reorg_rollback(
|
|
224
|
+
n SERIAL PRIMARY KEY,
|
|
225
|
+
op CHAR(1) NOT NULL,
|
|
226
|
+
table_name TEXT NOT NULL,
|
|
227
|
+
cursor INTEGER NOT NULL,
|
|
228
|
+
row_id TEXT,
|
|
229
|
+
row_value JSONB
|
|
230
|
+
);
|
|
231
|
+
`)
|
|
232
|
+
);
|
|
233
|
+
await tx.execute(
|
|
234
|
+
drizzleOrm.sql.raw(`
|
|
235
|
+
CREATE OR REPLACE FUNCTION reorg_checkpoint()
|
|
236
|
+
RETURNS TRIGGER AS $$
|
|
237
|
+
DECLARE
|
|
238
|
+
id_col TEXT := TG_ARGV[0]::TEXT;
|
|
239
|
+
order_key INTEGER := TG_ARGV[1]::INTEGER;
|
|
240
|
+
new_id_value TEXT := row_to_json(NEW.*)->>id_col;
|
|
241
|
+
old_id_value TEXT := row_to_json(OLD.*)->>id_col;
|
|
242
|
+
BEGIN
|
|
243
|
+
IF (TG_OP = 'DELETE') THEN
|
|
244
|
+
INSERT INTO __reorg_rollback(op, table_name, cursor, row_id, row_value)
|
|
245
|
+
SELECT 'D', TG_TABLE_NAME, order_key, old_id_value, row_to_json(OLD.*);
|
|
246
|
+
ELSIF (TG_OP = 'UPDATE') THEN
|
|
247
|
+
INSERT INTO __reorg_rollback(op, table_name, cursor, row_id, row_value)
|
|
248
|
+
SELECT 'U', TG_TABLE_NAME, order_key, new_id_value, row_to_json(OLD.*);
|
|
249
|
+
ELSIF (TG_OP = 'INSERT') THEN
|
|
250
|
+
INSERT INTO __reorg_rollback(op, table_name, cursor, row_id, row_value)
|
|
251
|
+
SELECT 'I', TG_TABLE_NAME, order_key, new_id_value, null;
|
|
252
|
+
END IF;
|
|
253
|
+
RETURN NULL;
|
|
254
|
+
END;
|
|
255
|
+
$$ LANGUAGE plpgsql;
|
|
256
|
+
`)
|
|
257
|
+
);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw new DrizzleStorageError("Failed to initialize reorg rollback table", {
|
|
260
|
+
cause: error
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function registerTriggers(tx, tables, endCursor, idColumn) {
|
|
265
|
+
try {
|
|
266
|
+
for (const table of tables) {
|
|
267
|
+
await tx.execute(
|
|
268
|
+
drizzleOrm.sql.raw(`DROP TRIGGER IF EXISTS ${table}_reorg ON ${table};`)
|
|
269
|
+
);
|
|
270
|
+
await tx.execute(
|
|
271
|
+
drizzleOrm.sql.raw(`
|
|
272
|
+
CREATE CONSTRAINT TRIGGER ${table}_reorg
|
|
273
|
+
AFTER INSERT OR UPDATE OR DELETE ON ${table}
|
|
274
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
275
|
+
FOR EACH ROW EXECUTE FUNCTION reorg_checkpoint('${idColumn}', ${`${Number(endCursor.orderKey)}`});
|
|
276
|
+
`)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new DrizzleStorageError("Failed to register triggers", {
|
|
281
|
+
cause: error
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function removeTriggers(db, tables) {
|
|
286
|
+
try {
|
|
287
|
+
for (const table of tables) {
|
|
288
|
+
await db.execute(
|
|
289
|
+
drizzleOrm.sql.raw(`DROP TRIGGER IF EXISTS ${table}_reorg ON ${table};`)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
throw new DrizzleStorageError("Failed to remove triggers", {
|
|
294
|
+
cause: error
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function invalidate(tx, cursor, idColumn) {
|
|
299
|
+
const { rows: result } = await tx.execute(
|
|
300
|
+
drizzleOrm.sql.raw(`
|
|
301
|
+
WITH deleted AS (
|
|
302
|
+
DELETE FROM __reorg_rollback
|
|
303
|
+
WHERE cursor > ${Number(cursor.orderKey)}
|
|
304
|
+
RETURNING *
|
|
305
|
+
)
|
|
306
|
+
SELECT * FROM deleted ORDER BY n DESC;
|
|
307
|
+
`)
|
|
308
|
+
);
|
|
309
|
+
if (!Array.isArray(result)) {
|
|
310
|
+
throw new DrizzleStorageError(
|
|
311
|
+
"Invalid result format from reorg_rollback query"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
for (const op of result) {
|
|
315
|
+
switch (op.op) {
|
|
316
|
+
case "I":
|
|
317
|
+
try {
|
|
318
|
+
if (!op.row_id) {
|
|
319
|
+
throw new DrizzleStorageError("Insert operation has no row_id");
|
|
320
|
+
}
|
|
321
|
+
await tx.execute(
|
|
322
|
+
drizzleOrm.sql.raw(`
|
|
323
|
+
DELETE FROM ${op.table_name}
|
|
324
|
+
WHERE ${idColumn} = '${op.row_id}'
|
|
325
|
+
`)
|
|
326
|
+
);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
throw new DrizzleStorageError(
|
|
329
|
+
"Failed to invalidate | Operation - I",
|
|
330
|
+
{
|
|
331
|
+
cause: error
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
case "D":
|
|
337
|
+
try {
|
|
338
|
+
if (!op.row_value) {
|
|
339
|
+
throw new DrizzleStorageError("Delete operation has no row_value");
|
|
340
|
+
}
|
|
341
|
+
await tx.execute(
|
|
342
|
+
drizzleOrm.sql.raw(`
|
|
343
|
+
INSERT INTO ${op.table_name}
|
|
344
|
+
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
345
|
+
`)
|
|
346
|
+
);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
throw new DrizzleStorageError(
|
|
349
|
+
"Failed to invalidate | Operation - D",
|
|
350
|
+
{
|
|
351
|
+
cause: error
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
case "U":
|
|
357
|
+
try {
|
|
358
|
+
if (!op.row_value || !op.row_id) {
|
|
359
|
+
throw new DrizzleStorageError(
|
|
360
|
+
"Update operation has no row_value or row_id"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const rowValue = typeof op.row_value === "string" ? JSON.parse(op.row_value) : op.row_value;
|
|
364
|
+
const nonIdKeys = Object.keys(rowValue).filter((k) => k !== idColumn);
|
|
365
|
+
const fields = nonIdKeys.map((c) => `${c} = prev.${c}`).join(", ");
|
|
366
|
+
const query = drizzleOrm.sql.raw(`
|
|
367
|
+
UPDATE ${op.table_name}
|
|
368
|
+
SET ${fields}
|
|
369
|
+
FROM (
|
|
370
|
+
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
371
|
+
) as prev
|
|
372
|
+
WHERE ${op.table_name}.${idColumn} = '${op.row_id}'
|
|
373
|
+
`);
|
|
374
|
+
await tx.execute(query);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new DrizzleStorageError(
|
|
377
|
+
"Failed to invalidate | Operation - U",
|
|
378
|
+
{
|
|
379
|
+
cause: error
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
default: {
|
|
385
|
+
throw new DrizzleStorageError(`Unknown operation: ${op.op}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function finalize(tx, cursor) {
|
|
391
|
+
try {
|
|
392
|
+
await tx.execute(
|
|
393
|
+
drizzleOrm.sql.raw(`
|
|
394
|
+
DELETE FROM __reorg_rollback
|
|
395
|
+
WHERE cursor <= ${Number(cursor.orderKey)}
|
|
396
|
+
`)
|
|
397
|
+
);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
throw new DrizzleStorageError("Failed to finalize", {
|
|
400
|
+
cause: error
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const DRIZZLE_PROPERTY = "_drizzle";
|
|
406
|
+
function useDrizzleStorage(_db) {
|
|
407
|
+
const context = indexer.useIndexerContext();
|
|
408
|
+
if (!context[DRIZZLE_PROPERTY]) {
|
|
409
|
+
throw new DrizzleStorageError(
|
|
410
|
+
"drizzle storage is not available. Did you register the plugin?"
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return context[DRIZZLE_PROPERTY];
|
|
414
|
+
}
|
|
415
|
+
function drizzleStorage({
|
|
416
|
+
db,
|
|
417
|
+
persistState: enablePersistence = true,
|
|
418
|
+
indexerName = "default",
|
|
419
|
+
schema,
|
|
420
|
+
idColumn = "id"
|
|
421
|
+
}) {
|
|
422
|
+
return plugins.defineIndexerPlugin((indexer) => {
|
|
423
|
+
let tableNames = [];
|
|
424
|
+
try {
|
|
425
|
+
tableNames = Object.values(schema ?? db._.schema ?? {}).map(
|
|
426
|
+
(table) => table.dbName
|
|
427
|
+
);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
throw new DrizzleStorageError("Failed to get table names from schema", {
|
|
430
|
+
cause: error
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
indexer.hooks.hook("run:before", async () => {
|
|
434
|
+
await withTransaction(db, async (tx) => {
|
|
435
|
+
await initializeReorgRollbackTable(tx);
|
|
436
|
+
if (enablePersistence) {
|
|
437
|
+
await initializePersistentState(tx);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
indexer.hooks.hook("connect:before", async ({ request }) => {
|
|
442
|
+
if (!enablePersistence) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
await withTransaction(db, async (tx) => {
|
|
446
|
+
const { cursor, filter } = await getState({
|
|
447
|
+
tx,
|
|
448
|
+
indexerName
|
|
449
|
+
});
|
|
450
|
+
if (cursor) {
|
|
451
|
+
request.startingCursor = cursor;
|
|
452
|
+
}
|
|
453
|
+
if (filter) {
|
|
454
|
+
request.filter[1] = filter;
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
indexer.hooks.hook("connect:after", async ({ request }) => {
|
|
459
|
+
const cursor = request.startingCursor;
|
|
460
|
+
if (!cursor) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
await withTransaction(db, async (tx) => {
|
|
464
|
+
await invalidate(tx, cursor, idColumn);
|
|
465
|
+
if (enablePersistence) {
|
|
466
|
+
await invalidateState({ tx, cursor, indexerName });
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
indexer.hooks.hook("connect:factory", async ({ request, endCursor }) => {
|
|
471
|
+
if (!enablePersistence) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const { db: tx } = useDrizzleStorage();
|
|
475
|
+
if (endCursor && request.filter[1]) {
|
|
476
|
+
await persistState({
|
|
477
|
+
tx,
|
|
478
|
+
endCursor,
|
|
479
|
+
filter: request.filter[1],
|
|
480
|
+
indexerName
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
indexer.hooks.hook("message:finalize", async ({ message }) => {
|
|
485
|
+
const { cursor } = message.finalize;
|
|
486
|
+
if (!cursor) {
|
|
487
|
+
throw new DrizzleStorageError("Finalized Cursor is undefined");
|
|
488
|
+
}
|
|
489
|
+
await withTransaction(db, async (tx) => {
|
|
490
|
+
await finalize(tx, cursor);
|
|
491
|
+
if (enablePersistence) {
|
|
492
|
+
await finalizeState({ tx, cursor, indexerName });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
indexer.hooks.hook("message:invalidate", async ({ message }) => {
|
|
497
|
+
const { cursor } = message.invalidate;
|
|
498
|
+
if (!cursor) {
|
|
499
|
+
throw new DrizzleStorageError("Invalidate Cursor is undefined");
|
|
500
|
+
}
|
|
501
|
+
await withTransaction(db, async (tx) => {
|
|
502
|
+
await invalidate(tx, cursor, idColumn);
|
|
503
|
+
if (enablePersistence) {
|
|
504
|
+
await invalidateState({ tx, cursor, indexerName });
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
indexer.hooks.hook("handler:middleware", async ({ use }) => {
|
|
509
|
+
use(async (context, next) => {
|
|
510
|
+
try {
|
|
511
|
+
const { endCursor, finality } = context;
|
|
512
|
+
if (!endCursor) {
|
|
513
|
+
throw new DrizzleStorageError("End Cursor is undefined");
|
|
514
|
+
}
|
|
515
|
+
await withTransaction(db, async (tx) => {
|
|
516
|
+
context[DRIZZLE_PROPERTY] = { db: tx };
|
|
517
|
+
if (finality !== "finalized") {
|
|
518
|
+
await registerTriggers(tx, tableNames, endCursor, idColumn);
|
|
519
|
+
}
|
|
520
|
+
await next();
|
|
521
|
+
delete context[DRIZZLE_PROPERTY];
|
|
522
|
+
if (enablePersistence) {
|
|
523
|
+
await persistState({
|
|
524
|
+
tx,
|
|
525
|
+
endCursor,
|
|
526
|
+
indexerName
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
if (finality !== "finalized") {
|
|
531
|
+
await removeTriggers(db, tableNames);
|
|
532
|
+
}
|
|
533
|
+
} catch (error) {
|
|
534
|
+
await removeTriggers(db, tableNames);
|
|
535
|
+
throw new DrizzleStorageError("Failed to run handler:middleware", {
|
|
536
|
+
cause: error
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
});
|
|
12
542
|
}
|
|
13
543
|
|
|
14
544
|
exports.drizzleStorage = drizzleStorage;
|
|
545
|
+
exports.useDrizzleStorage = useDrizzleStorage;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
|
|
2
|
+
import { TablesRelationalConfig, ExtractTablesWithRelations } from 'drizzle-orm';
|
|
3
|
+
import { PgQueryResultHKT, PgTransaction, PgDatabase } from 'drizzle-orm/pg-core';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
type DrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> = {
|
|
6
|
+
db: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
7
|
+
};
|
|
8
|
+
declare function useDrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>(_db?: PgDatabase<TQueryResult, TFullSchema, TSchema>): DrizzleStorage<TQueryResult, TFullSchema, TSchema>;
|
|
9
|
+
interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> {
|
|
10
|
+
db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
|
|
11
|
+
persistState?: boolean;
|
|
12
|
+
indexerName?: string;
|
|
13
|
+
schema?: Record<string, unknown>;
|
|
14
|
+
idColumn?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a plugin that uses Drizzle as the storage layer.
|
|
18
|
+
*
|
|
19
|
+
* Supports storing the indexer's state and provides a simple Key-Value store.
|
|
20
|
+
* @param options.db - The Drizzle database instance.
|
|
21
|
+
* @param options.persistState - Whether to persist the indexer's state. Defaults to true.
|
|
22
|
+
* @param options.indexerName - The name of the indexer. Defaults value is 'default'.
|
|
23
|
+
* @param options.schema - The schema of the database.
|
|
24
|
+
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
25
|
+
*/
|
|
26
|
+
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, schema, idColumn, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
27
|
+
|
|
28
|
+
export { type DrizzleStorage, type DrizzleStorageOptions, drizzleStorage, useDrizzleStorage };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
|
|
2
|
+
import { TablesRelationalConfig, ExtractTablesWithRelations } from 'drizzle-orm';
|
|
3
|
+
import { PgQueryResultHKT, PgTransaction, PgDatabase } from 'drizzle-orm/pg-core';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
type DrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> = {
|
|
6
|
+
db: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
7
|
+
};
|
|
8
|
+
declare function useDrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>(_db?: PgDatabase<TQueryResult, TFullSchema, TSchema>): DrizzleStorage<TQueryResult, TFullSchema, TSchema>;
|
|
9
|
+
interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> {
|
|
10
|
+
db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
|
|
11
|
+
persistState?: boolean;
|
|
12
|
+
indexerName?: string;
|
|
13
|
+
schema?: Record<string, unknown>;
|
|
14
|
+
idColumn?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a plugin that uses Drizzle as the storage layer.
|
|
18
|
+
*
|
|
19
|
+
* Supports storing the indexer's state and provides a simple Key-Value store.
|
|
20
|
+
* @param options.db - The Drizzle database instance.
|
|
21
|
+
* @param options.persistState - Whether to persist the indexer's state. Defaults to true.
|
|
22
|
+
* @param options.indexerName - The name of the indexer. Defaults value is 'default'.
|
|
23
|
+
* @param options.schema - The schema of the database.
|
|
24
|
+
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
25
|
+
*/
|
|
26
|
+
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, schema, idColumn, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
27
|
+
|
|
28
|
+
export { type DrizzleStorage, type DrizzleStorageOptions, drizzleStorage, useDrizzleStorage };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
|
|
2
|
+
import { TablesRelationalConfig, ExtractTablesWithRelations } from 'drizzle-orm';
|
|
3
|
+
import { PgQueryResultHKT, PgTransaction, PgDatabase } from 'drizzle-orm/pg-core';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
type DrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> = {
|
|
6
|
+
db: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
7
|
+
};
|
|
8
|
+
declare function useDrizzleStorage<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>>(_db?: PgDatabase<TQueryResult, TFullSchema, TSchema>): DrizzleStorage<TQueryResult, TFullSchema, TSchema>;
|
|
9
|
+
interface DrizzleStorageOptions<TQueryResult extends PgQueryResultHKT, TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>> {
|
|
10
|
+
db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
|
|
11
|
+
persistState?: boolean;
|
|
12
|
+
indexerName?: string;
|
|
13
|
+
schema?: Record<string, unknown>;
|
|
14
|
+
idColumn?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a plugin that uses Drizzle as the storage layer.
|
|
18
|
+
*
|
|
19
|
+
* Supports storing the indexer's state and provides a simple Key-Value store.
|
|
20
|
+
* @param options.db - The Drizzle database instance.
|
|
21
|
+
* @param options.persistState - Whether to persist the indexer's state. Defaults to true.
|
|
22
|
+
* @param options.indexerName - The name of the indexer. Defaults value is 'default'.
|
|
23
|
+
* @param options.schema - The schema of the database.
|
|
24
|
+
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
25
|
+
*/
|
|
26
|
+
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, schema, idColumn, }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
|
|
27
|
+
|
|
28
|
+
export { type DrizzleStorage, type DrizzleStorageOptions, drizzleStorage, useDrizzleStorage };
|