@apibara/plugin-drizzle 2.0.0-beta.27 → 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/src/storage.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { Cursor } from "@apibara/protocol";
|
|
2
|
+
import {
|
|
3
|
+
type ExtractTablesWithRelations,
|
|
4
|
+
type TablesRelationalConfig,
|
|
5
|
+
sql,
|
|
6
|
+
} from "drizzle-orm";
|
|
7
|
+
import {
|
|
8
|
+
type PgDatabase,
|
|
9
|
+
type PgQueryResultHKT,
|
|
10
|
+
type PgTransaction,
|
|
11
|
+
char,
|
|
12
|
+
integer,
|
|
13
|
+
jsonb,
|
|
14
|
+
pgTable,
|
|
15
|
+
serial,
|
|
16
|
+
text,
|
|
17
|
+
} from "drizzle-orm/pg-core";
|
|
18
|
+
import { DrizzleStorageError } from "./utils";
|
|
19
|
+
export type ReorgOperation = "I" | "U" | "D";
|
|
20
|
+
|
|
21
|
+
export const reorgRollbackTable = pgTable("__reorg_rollback", {
|
|
22
|
+
n: serial("n").primaryKey(),
|
|
23
|
+
op: char("op", { length: 1 }).$type<ReorgOperation>().notNull(),
|
|
24
|
+
table_name: text("table_name").notNull(),
|
|
25
|
+
cursor: integer("cursor").notNull(),
|
|
26
|
+
row_id: text("row_id"),
|
|
27
|
+
row_value: jsonb("row_value"),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type ReorgRollbackRow = typeof reorgRollbackTable.$inferSelect;
|
|
31
|
+
|
|
32
|
+
export async function initializeReorgRollbackTable<
|
|
33
|
+
TQueryResult extends PgQueryResultHKT,
|
|
34
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
35
|
+
TSchema extends
|
|
36
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
37
|
+
>(tx: PgTransaction<TQueryResult, TFullSchema, TSchema>) {
|
|
38
|
+
try {
|
|
39
|
+
// Create the audit log table
|
|
40
|
+
await tx.execute(
|
|
41
|
+
sql.raw(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS __reorg_rollback(
|
|
43
|
+
n SERIAL PRIMARY KEY,
|
|
44
|
+
op CHAR(1) NOT NULL,
|
|
45
|
+
table_name TEXT NOT NULL,
|
|
46
|
+
cursor INTEGER NOT NULL,
|
|
47
|
+
row_id TEXT,
|
|
48
|
+
row_value JSONB
|
|
49
|
+
);
|
|
50
|
+
`),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Create the trigger function
|
|
54
|
+
await tx.execute(
|
|
55
|
+
sql.raw(`
|
|
56
|
+
CREATE OR REPLACE FUNCTION reorg_checkpoint()
|
|
57
|
+
RETURNS TRIGGER AS $$
|
|
58
|
+
DECLARE
|
|
59
|
+
id_col TEXT := TG_ARGV[0]::TEXT;
|
|
60
|
+
order_key INTEGER := TG_ARGV[1]::INTEGER;
|
|
61
|
+
new_id_value TEXT := row_to_json(NEW.*)->>id_col;
|
|
62
|
+
old_id_value TEXT := row_to_json(OLD.*)->>id_col;
|
|
63
|
+
BEGIN
|
|
64
|
+
IF (TG_OP = 'DELETE') THEN
|
|
65
|
+
INSERT INTO __reorg_rollback(op, table_name, cursor, row_id, row_value)
|
|
66
|
+
SELECT 'D', TG_TABLE_NAME, order_key, old_id_value, row_to_json(OLD.*);
|
|
67
|
+
ELSIF (TG_OP = 'UPDATE') THEN
|
|
68
|
+
INSERT INTO __reorg_rollback(op, table_name, cursor, row_id, row_value)
|
|
69
|
+
SELECT 'U', TG_TABLE_NAME, order_key, new_id_value, row_to_json(OLD.*);
|
|
70
|
+
ELSIF (TG_OP = 'INSERT') THEN
|
|
71
|
+
INSERT INTO __reorg_rollback(op, table_name, cursor, row_id, row_value)
|
|
72
|
+
SELECT 'I', TG_TABLE_NAME, order_key, new_id_value, null;
|
|
73
|
+
END IF;
|
|
74
|
+
RETURN NULL;
|
|
75
|
+
END;
|
|
76
|
+
$$ LANGUAGE plpgsql;
|
|
77
|
+
`),
|
|
78
|
+
);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new DrizzleStorageError("Failed to initialize reorg rollback table", {
|
|
81
|
+
cause: error,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function registerTriggers<
|
|
87
|
+
TQueryResult extends PgQueryResultHKT,
|
|
88
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
89
|
+
TSchema extends
|
|
90
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
91
|
+
>(
|
|
92
|
+
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
93
|
+
tables: string[],
|
|
94
|
+
endCursor: Cursor,
|
|
95
|
+
idColumn: string,
|
|
96
|
+
) {
|
|
97
|
+
try {
|
|
98
|
+
for (const table of tables) {
|
|
99
|
+
await tx.execute(
|
|
100
|
+
sql.raw(`DROP TRIGGER IF EXISTS ${table}_reorg ON ${table};`),
|
|
101
|
+
);
|
|
102
|
+
await tx.execute(
|
|
103
|
+
sql.raw(`
|
|
104
|
+
CREATE CONSTRAINT TRIGGER ${table}_reorg
|
|
105
|
+
AFTER INSERT OR UPDATE OR DELETE ON ${table}
|
|
106
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
107
|
+
FOR EACH ROW EXECUTE FUNCTION reorg_checkpoint('${idColumn}', ${`${Number(endCursor.orderKey)}`});
|
|
108
|
+
`),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new DrizzleStorageError("Failed to register triggers", {
|
|
113
|
+
cause: error,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function removeTriggers<
|
|
119
|
+
TQueryResult extends PgQueryResultHKT,
|
|
120
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
121
|
+
TSchema extends
|
|
122
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
123
|
+
>(db: PgDatabase<TQueryResult, TFullSchema, TSchema>, tables: string[]) {
|
|
124
|
+
try {
|
|
125
|
+
for (const table of tables) {
|
|
126
|
+
await db.execute(
|
|
127
|
+
sql.raw(`DROP TRIGGER IF EXISTS ${table}_reorg ON ${table};`),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new DrizzleStorageError("Failed to remove triggers", {
|
|
132
|
+
cause: error,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function invalidate<
|
|
138
|
+
TQueryResult extends PgQueryResultHKT,
|
|
139
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
140
|
+
TSchema extends
|
|
141
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
142
|
+
>(
|
|
143
|
+
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
144
|
+
cursor: Cursor,
|
|
145
|
+
idColumn: string,
|
|
146
|
+
) {
|
|
147
|
+
// Get and delete operations after cursor in one query, ordered by newest first
|
|
148
|
+
const { rows: result } = (await tx.execute(
|
|
149
|
+
sql.raw(`
|
|
150
|
+
WITH deleted AS (
|
|
151
|
+
DELETE FROM __reorg_rollback
|
|
152
|
+
WHERE cursor > ${Number(cursor.orderKey)}
|
|
153
|
+
RETURNING *
|
|
154
|
+
)
|
|
155
|
+
SELECT * FROM deleted ORDER BY n DESC;
|
|
156
|
+
`),
|
|
157
|
+
)) as { rows: ReorgRollbackRow[] };
|
|
158
|
+
|
|
159
|
+
if (!Array.isArray(result)) {
|
|
160
|
+
throw new DrizzleStorageError(
|
|
161
|
+
"Invalid result format from reorg_rollback query",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Process each operation in reverse order
|
|
166
|
+
for (const op of result) {
|
|
167
|
+
switch (op.op) {
|
|
168
|
+
case "I":
|
|
169
|
+
try {
|
|
170
|
+
if (!op.row_id) {
|
|
171
|
+
throw new DrizzleStorageError("Insert operation has no row_id");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await tx.execute(
|
|
175
|
+
sql.raw(`
|
|
176
|
+
DELETE FROM ${op.table_name}
|
|
177
|
+
WHERE ${idColumn} = '${op.row_id}'
|
|
178
|
+
`),
|
|
179
|
+
);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new DrizzleStorageError(
|
|
182
|
+
"Failed to invalidate | Operation - I",
|
|
183
|
+
{
|
|
184
|
+
cause: error,
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case "D":
|
|
192
|
+
try {
|
|
193
|
+
// For deletes, reinsert the row using json_populate_record
|
|
194
|
+
if (!op.row_value) {
|
|
195
|
+
throw new DrizzleStorageError("Delete operation has no row_value");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await tx.execute(
|
|
199
|
+
sql.raw(`
|
|
200
|
+
INSERT INTO ${op.table_name}
|
|
201
|
+
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
202
|
+
`),
|
|
203
|
+
);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new DrizzleStorageError(
|
|
206
|
+
"Failed to invalidate | Operation - D",
|
|
207
|
+
{
|
|
208
|
+
cause: error,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case "U":
|
|
216
|
+
try {
|
|
217
|
+
if (!op.row_value || !op.row_id) {
|
|
218
|
+
throw new DrizzleStorageError(
|
|
219
|
+
"Update operation has no row_value or row_id",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// For updates, restore previous values
|
|
224
|
+
|
|
225
|
+
const rowValue =
|
|
226
|
+
typeof op.row_value === "string"
|
|
227
|
+
? JSON.parse(op.row_value)
|
|
228
|
+
: op.row_value;
|
|
229
|
+
|
|
230
|
+
const nonIdKeys = Object.keys(rowValue).filter((k) => k !== idColumn);
|
|
231
|
+
|
|
232
|
+
const fields = nonIdKeys.map((c) => `${c} = prev.${c}`).join(", ");
|
|
233
|
+
|
|
234
|
+
const query = sql.raw(`
|
|
235
|
+
UPDATE ${op.table_name}
|
|
236
|
+
SET ${fields}
|
|
237
|
+
FROM (
|
|
238
|
+
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
239
|
+
) as prev
|
|
240
|
+
WHERE ${op.table_name}.${idColumn} = '${op.row_id}'
|
|
241
|
+
`);
|
|
242
|
+
|
|
243
|
+
await tx.execute(query);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new DrizzleStorageError(
|
|
246
|
+
"Failed to invalidate | Operation - U",
|
|
247
|
+
{
|
|
248
|
+
cause: error,
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
default: {
|
|
255
|
+
throw new DrizzleStorageError(`Unknown operation: ${op.op}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function finalize<
|
|
262
|
+
TQueryResult extends PgQueryResultHKT,
|
|
263
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
264
|
+
TSchema extends
|
|
265
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
266
|
+
>(tx: PgTransaction<TQueryResult, TFullSchema, TSchema>, cursor: Cursor) {
|
|
267
|
+
try {
|
|
268
|
+
await tx.execute(
|
|
269
|
+
sql.raw(`
|
|
270
|
+
DELETE FROM __reorg_rollback
|
|
271
|
+
WHERE cursor <= ${Number(cursor.orderKey)}
|
|
272
|
+
`),
|
|
273
|
+
);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new DrizzleStorageError("Failed to finalize", {
|
|
276
|
+
cause: error,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtractTablesWithRelations,
|
|
3
|
+
TablesRelationalConfig,
|
|
4
|
+
} from "drizzle-orm";
|
|
5
|
+
import type {
|
|
6
|
+
PgDatabase,
|
|
7
|
+
PgQueryResultHKT,
|
|
8
|
+
PgTransaction,
|
|
9
|
+
} from "drizzle-orm/pg-core";
|
|
10
|
+
|
|
1
11
|
export class DrizzleStorageError extends Error {
|
|
2
|
-
constructor(message: string) {
|
|
3
|
-
super(message);
|
|
12
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
13
|
+
super(message, options);
|
|
4
14
|
this.name = "DrizzleStorageError";
|
|
5
15
|
}
|
|
6
16
|
}
|
|
17
|
+
|
|
18
|
+
export async function withTransaction<
|
|
19
|
+
TQueryResult extends PgQueryResultHKT,
|
|
20
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
21
|
+
TSchema extends
|
|
22
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
23
|
+
>(
|
|
24
|
+
db: PgDatabase<TQueryResult, TFullSchema, TSchema>,
|
|
25
|
+
cb: (db: PgTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<void>,
|
|
26
|
+
) {
|
|
27
|
+
return await db.transaction(async (txnDb) => {
|
|
28
|
+
return await cb(txnDb);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function deserialize<T>(str: string): T {
|
|
33
|
+
return JSON.parse(str, (_, value) =>
|
|
34
|
+
typeof value === "string" && value.match(/^\d+n$/)
|
|
35
|
+
? BigInt(value.slice(0, -1))
|
|
36
|
+
: value,
|
|
37
|
+
) as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function serialize<T>(obj: T): string {
|
|
41
|
+
return JSON.stringify(
|
|
42
|
+
obj,
|
|
43
|
+
(_, value) => (typeof value === "bigint" ? `${value.toString()}n` : value),
|
|
44
|
+
"\t",
|
|
45
|
+
);
|
|
46
|
+
}
|