@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/src/index.ts CHANGED
@@ -1,5 +1,260 @@
1
- import { DrizzleStorageError } from "./utils";
1
+ import { useIndexerContext } from "@apibara/indexer";
2
+ import { defineIndexerPlugin } from "@apibara/indexer/plugins";
2
3
 
3
- export function drizzleStorage<TFilter, TBlock>() {
4
- throw new DrizzleStorageError("Not implemented");
4
+ import type {
5
+ ExtractTablesWithRelations,
6
+ TablesRelationalConfig,
7
+ } from "drizzle-orm";
8
+
9
+ import type { Cursor, DataFinality } from "@apibara/protocol";
10
+ import type {
11
+ PgDatabase,
12
+ PgQueryResultHKT,
13
+ PgTransaction,
14
+ } from "drizzle-orm/pg-core";
15
+ import {
16
+ finalizeState,
17
+ getState,
18
+ initializePersistentState,
19
+ invalidateState,
20
+ persistState,
21
+ } from "./persistence";
22
+ import {
23
+ finalize,
24
+ initializeReorgRollbackTable,
25
+ invalidate,
26
+ registerTriggers,
27
+ removeTriggers,
28
+ } from "./storage";
29
+ import { DrizzleStorageError, withTransaction } from "./utils";
30
+
31
+ const DRIZZLE_PROPERTY = "_drizzle";
32
+
33
+ export type DrizzleStorage<
34
+ TQueryResult extends PgQueryResultHKT,
35
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
36
+ TSchema extends
37
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
38
+ > = {
39
+ db: PgTransaction<TQueryResult, TFullSchema, TSchema>;
40
+ };
41
+
42
+ export function useDrizzleStorage<
43
+ TQueryResult extends PgQueryResultHKT,
44
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
45
+ TSchema extends
46
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
47
+ >(
48
+ _db?: PgDatabase<TQueryResult, TFullSchema, TSchema>,
49
+ ): DrizzleStorage<TQueryResult, TFullSchema, TSchema> {
50
+ const context = useIndexerContext();
51
+
52
+ if (!context[DRIZZLE_PROPERTY]) {
53
+ throw new DrizzleStorageError(
54
+ "drizzle storage is not available. Did you register the plugin?",
55
+ );
56
+ }
57
+
58
+ return context[DRIZZLE_PROPERTY];
59
+ }
60
+
61
+ export interface DrizzleStorageOptions<
62
+ TQueryResult extends PgQueryResultHKT,
63
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
64
+ TSchema extends
65
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
66
+ > {
67
+ db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
68
+ persistState?: boolean;
69
+ indexerName?: string;
70
+ schema?: Record<string, unknown>;
71
+ idColumn?: string;
72
+ }
73
+
74
+ /**
75
+ * Creates a plugin that uses Drizzle as the storage layer.
76
+ *
77
+ * Supports storing the indexer's state and provides a simple Key-Value store.
78
+ * @param options.db - The Drizzle database instance.
79
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
80
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
81
+ * @param options.schema - The schema of the database.
82
+ * @param options.idColumn - The column to use as the id. Defaults to 'id'.
83
+ */
84
+ export function drizzleStorage<
85
+ TFilter,
86
+ TBlock,
87
+ TQueryResult extends PgQueryResultHKT,
88
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
89
+ TSchema extends
90
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
91
+ >({
92
+ db,
93
+ persistState: enablePersistence = true,
94
+ indexerName = "default",
95
+ schema,
96
+ idColumn = "id",
97
+ }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>) {
98
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
99
+ let tableNames: string[] = [];
100
+
101
+ try {
102
+ tableNames = Object.values((schema as TSchema) ?? db._.schema ?? {}).map(
103
+ (table) => table.dbName,
104
+ );
105
+ } catch (error) {
106
+ throw new DrizzleStorageError("Failed to get table names from schema", {
107
+ cause: error,
108
+ });
109
+ }
110
+
111
+ indexer.hooks.hook("run:before", async () => {
112
+ await withTransaction(db, async (tx) => {
113
+ await initializeReorgRollbackTable(tx);
114
+ if (enablePersistence) {
115
+ await initializePersistentState(tx);
116
+ }
117
+ });
118
+ });
119
+
120
+ indexer.hooks.hook("connect:before", async ({ request }) => {
121
+ if (!enablePersistence) {
122
+ return;
123
+ }
124
+
125
+ await withTransaction(db, async (tx) => {
126
+ const { cursor, filter } = await getState<
127
+ TFilter,
128
+ TQueryResult,
129
+ TFullSchema,
130
+ TSchema
131
+ >({
132
+ tx,
133
+ indexerName,
134
+ });
135
+ if (cursor) {
136
+ request.startingCursor = cursor;
137
+ }
138
+ if (filter) {
139
+ request.filter[1] = filter;
140
+ }
141
+ });
142
+ });
143
+
144
+ indexer.hooks.hook("connect:after", async ({ request }) => {
145
+ // On restart, we need to invalidate data for blocks that were processed but not persisted.
146
+ const cursor = request.startingCursor;
147
+
148
+ if (!cursor) {
149
+ return;
150
+ }
151
+
152
+ await withTransaction(db, async (tx) => {
153
+ await invalidate(tx, cursor, idColumn);
154
+
155
+ if (enablePersistence) {
156
+ await invalidateState({ tx, cursor, indexerName });
157
+ }
158
+ });
159
+ });
160
+
161
+ indexer.hooks.hook("connect:factory", async ({ request, endCursor }) => {
162
+ if (!enablePersistence) {
163
+ return;
164
+ }
165
+ // We can call this hook because this hook is called inside the transaction of handler:middleware
166
+ // so we have access to the transaction from the context
167
+ const { db: tx } = useDrizzleStorage(db);
168
+
169
+ if (endCursor && request.filter[1]) {
170
+ await persistState({
171
+ tx,
172
+ endCursor,
173
+ filter: request.filter[1],
174
+ indexerName,
175
+ });
176
+ }
177
+ });
178
+
179
+ indexer.hooks.hook("message:finalize", async ({ message }) => {
180
+ const { cursor } = message.finalize;
181
+
182
+ if (!cursor) {
183
+ throw new DrizzleStorageError("Finalized Cursor is undefined");
184
+ }
185
+
186
+ await withTransaction(db, async (tx) => {
187
+ await finalize(tx, cursor);
188
+
189
+ if (enablePersistence) {
190
+ await finalizeState({ tx, cursor, indexerName });
191
+ }
192
+ });
193
+ });
194
+
195
+ indexer.hooks.hook("message:invalidate", async ({ message }) => {
196
+ const { cursor } = message.invalidate;
197
+
198
+ if (!cursor) {
199
+ throw new DrizzleStorageError("Invalidate Cursor is undefined");
200
+ }
201
+
202
+ await withTransaction(db, async (tx) => {
203
+ await invalidate(tx, cursor, idColumn);
204
+
205
+ if (enablePersistence) {
206
+ await invalidateState({ tx, cursor, indexerName });
207
+ }
208
+ });
209
+ });
210
+
211
+ indexer.hooks.hook("handler:middleware", async ({ use }) => {
212
+ use(async (context, next) => {
213
+ try {
214
+ const { endCursor, finality } = context as {
215
+ endCursor: Cursor;
216
+ finality: DataFinality;
217
+ };
218
+
219
+ if (!endCursor) {
220
+ throw new DrizzleStorageError("End Cursor is undefined");
221
+ }
222
+
223
+ await withTransaction(db, async (tx) => {
224
+ context[DRIZZLE_PROPERTY] = { db: tx } as DrizzleStorage<
225
+ TQueryResult,
226
+ TFullSchema,
227
+ TSchema
228
+ >;
229
+
230
+ if (finality !== "finalized") {
231
+ await registerTriggers(tx, tableNames, endCursor, idColumn);
232
+ }
233
+
234
+ await next();
235
+ delete context[DRIZZLE_PROPERTY];
236
+
237
+ if (enablePersistence) {
238
+ await persistState({
239
+ tx,
240
+ endCursor,
241
+ indexerName,
242
+ });
243
+ }
244
+ });
245
+
246
+ if (finality !== "finalized") {
247
+ // remove trigger outside of the transaction or it won't be triggered.
248
+ await removeTriggers(db, tableNames);
249
+ }
250
+ } catch (error) {
251
+ await removeTriggers(db, tableNames);
252
+
253
+ throw new DrizzleStorageError("Failed to run handler:middleware", {
254
+ cause: error,
255
+ });
256
+ }
257
+ });
258
+ });
259
+ });
5
260
  }
@@ -1,24 +1,18 @@
1
- /*
2
1
  import type { Cursor } from "@apibara/protocol";
3
- import {
4
- type ExtractTablesWithRelations,
5
- type TablesRelationalConfig,
6
- and,
7
- eq,
8
- isNull,
2
+ import { and, eq, gt, isNull, lt } from "drizzle-orm";
3
+ import type {
4
+ ExtractTablesWithRelations,
5
+ TablesRelationalConfig,
9
6
  } from "drizzle-orm";
10
- import {
11
- type PgDatabase,
12
- type PgQueryResultHKT,
13
- integer,
14
- pgTable,
15
- primaryKey,
16
- text,
17
- } from "drizzle-orm/pg-core";
18
- import { deserialize, serialize } from "../vcr";
19
- import { defineIndexerPlugin } from "./config";
20
-
21
- export const checkpoints = pgTable("checkpoints", {
7
+ import type { PgQueryResultHKT, PgTransaction } from "drizzle-orm/pg-core";
8
+ import { integer, pgTable, primaryKey, text } from "drizzle-orm/pg-core";
9
+ import { DrizzleStorageError, deserialize, serialize } from "./utils";
10
+
11
+ const CHECKPOINTS_TABLE_NAME = "__indexer_checkpoints";
12
+ const FILTERS_TABLE_NAME = "__indexer_filters";
13
+ const SCHEMA_VERSION_TABLE_NAME = "__indexer_schema_version";
14
+
15
+ export const checkpoints = pgTable(CHECKPOINTS_TABLE_NAME, {
22
16
  id: text("id").notNull().primaryKey(),
23
17
  orderKey: integer("order_key").notNull(),
24
18
  uniqueKey: text("unique_key")
@@ -28,167 +22,281 @@ export const checkpoints = pgTable("checkpoints", {
28
22
  });
29
23
 
30
24
  export const filters = pgTable(
31
- "filters",
25
+ FILTERS_TABLE_NAME,
32
26
  {
33
27
  id: text("id").notNull(),
34
28
  filter: text("filter").notNull(),
35
29
  fromBlock: integer("from_block").notNull(),
36
- toBlock: integer("to_block"),
30
+ toBlock: integer("to_block").$type<number | null>().default(null),
37
31
  },
38
- (table) => ({
39
- pk: primaryKey({ columns: [table.id, table.fromBlock] }),
40
- }),
32
+ (table) => [
33
+ {
34
+ pk: primaryKey({ columns: [table.id, table.fromBlock] }),
35
+ },
36
+ ],
41
37
  );
42
38
 
43
- export function drizzlePersistence<
44
- TFilter,
45
- TBlock,
46
- TTxnParams,
39
+ export const schemaVersion = pgTable(SCHEMA_VERSION_TABLE_NAME, {
40
+ k: integer("k").notNull().primaryKey(),
41
+ version: integer("version").notNull(),
42
+ });
43
+
44
+ export const CURRENT_SCHEMA_VERSION = 0;
45
+
46
+ // migrations for future schema updates
47
+ const MIGRATIONS: string[][] = [
48
+ // migrations[0]: v0 -> v1 (for future use)
49
+ [],
50
+ // Add more migration arrays for future versions
51
+ ];
52
+
53
+ export async function initializePersistentState<
47
54
  TQueryResult extends PgQueryResultHKT,
48
55
  TFullSchema extends Record<string, unknown> = Record<string, never>,
49
56
  TSchema extends
50
57
  TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
51
- >({
52
- database,
53
- indexerName = "default",
54
- }: {
55
- database: PgDatabase<TQueryResult, TFullSchema, TSchema>;
56
- indexerName?: string;
57
- }) {
58
- return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
59
- let store: DrizzlePersistence<TFilter, TQueryResult, TFullSchema, TSchema>;
58
+ >(tx: PgTransaction<TQueryResult, TFullSchema, TSchema>) {
59
+ // Create schema version table
60
+ await tx.execute(`
61
+ CREATE TABLE IF NOT EXISTS ${SCHEMA_VERSION_TABLE_NAME} (
62
+ k INTEGER PRIMARY KEY,
63
+ version INTEGER NOT NULL
64
+ );
65
+ `);
60
66
 
61
- indexer.hooks.hook("run:before", async () => {
62
- store = new DrizzlePersistence(database, indexerName);
63
- // Tables are created by user via migrations in Drizzle
64
- });
67
+ // Get current schema version
68
+ const versionRows = await tx
69
+ .select()
70
+ .from(schemaVersion)
71
+ .where(eq(schemaVersion.k, 0));
65
72
 
66
- indexer.hooks.hook("connect:before", async ({ request }) => {
67
- const { cursor, filter } = await store.get();
73
+ const storedVersion = versionRows[0]?.version ?? -1;
68
74
 
69
- if (cursor) {
70
- request.startingCursor = cursor;
71
- }
75
+ // Check for incompatible version
76
+ if (storedVersion > CURRENT_SCHEMA_VERSION) {
77
+ throw new DrizzleStorageError(
78
+ `Database Persistence schema version v${storedVersion} is newer than supported version v${CURRENT_SCHEMA_VERSION}`,
79
+ );
80
+ }
72
81
 
73
- if (filter) {
74
- request.filter[1] = filter;
75
- }
76
- });
82
+ // Begin schema updates
83
+ try {
84
+ if (storedVersion === -1) {
85
+ // First time initialization
86
+ await tx.execute(`
87
+ CREATE TABLE IF NOT EXISTS ${CHECKPOINTS_TABLE_NAME} (
88
+ id TEXT PRIMARY KEY,
89
+ order_key INTEGER NOT NULL,
90
+ unique_key TEXT NOT NULL DEFAULT ''
91
+ );
92
+ `);
77
93
 
78
- indexer.hooks.hook("transaction:commit", async ({ endCursor }) => {
79
- if (endCursor) {
80
- await store.put({ cursor: endCursor });
81
- }
82
- });
94
+ await tx.execute(`
95
+ CREATE TABLE IF NOT EXISTS ${FILTERS_TABLE_NAME} (
96
+ id TEXT NOT NULL,
97
+ filter TEXT NOT NULL,
98
+ from_block INTEGER NOT NULL,
99
+ to_block INTEGER DEFAULT NULL,
100
+ PRIMARY KEY (id, from_block)
101
+ );
102
+ `);
83
103
 
84
- indexer.hooks.hook("connect:factory", async ({ request, endCursor }) => {
85
- if (request.filter[1]) {
86
- await store.put({ cursor: endCursor, filter: request.filter[1] });
104
+ // Set initial schema version
105
+ await tx.insert(schemaVersion).values({
106
+ k: 0,
107
+ version: CURRENT_SCHEMA_VERSION,
108
+ });
109
+ } else {
110
+ // Run any necessary migrations
111
+ let currentVersion = storedVersion;
112
+ while (currentVersion < CURRENT_SCHEMA_VERSION) {
113
+ const migrationStatements = MIGRATIONS[currentVersion];
114
+ for (const statement of migrationStatements) {
115
+ await tx.execute(statement);
116
+ }
117
+ currentVersion++;
87
118
  }
88
- });
89
- });
119
+
120
+ // Update schema version
121
+ await tx
122
+ .update(schemaVersion)
123
+ .set({ version: CURRENT_SCHEMA_VERSION })
124
+ .where(eq(schemaVersion.k, 0));
125
+ }
126
+ } catch (error) {
127
+ throw new DrizzleStorageError(
128
+ "Failed to initialize or migrate database schema",
129
+ { cause: error },
130
+ );
131
+ }
90
132
  }
91
133
 
92
- export class DrizzlePersistence<
134
+ export async function persistState<
93
135
  TFilter,
94
136
  TQueryResult extends PgQueryResultHKT,
95
137
  TFullSchema extends Record<string, unknown> = Record<string, never>,
96
138
  TSchema extends
97
139
  TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
98
- > {
99
- constructor(
100
- private _db: PgDatabase<TQueryResult, TFullSchema, TSchema>,
101
- private _indexerName: string,
102
- ) {}
103
-
104
- public async get(): Promise<{ cursor?: Cursor; filter?: TFilter }> {
105
- const cursor = await this._getCheckpoint();
106
- const filter = await this._getFilter();
107
-
108
- return { cursor, filter };
109
- }
140
+ >(props: {
141
+ tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
142
+ endCursor: Cursor;
143
+ filter?: TFilter;
144
+ indexerName: string;
145
+ }) {
146
+ const { tx, endCursor, filter, indexerName } = props;
110
147
 
111
- public async put({ cursor, filter }: { cursor?: Cursor; filter?: TFilter }) {
112
- if (cursor) {
113
- await this._putCheckpoint(cursor);
148
+ try {
149
+ if (endCursor) {
150
+ await tx
151
+ .insert(checkpoints)
152
+ .values({
153
+ id: indexerName,
154
+ orderKey: Number(endCursor.orderKey),
155
+ uniqueKey: endCursor.uniqueKey,
156
+ })
157
+ .onConflictDoUpdate({
158
+ target: checkpoints.id,
159
+ set: {
160
+ orderKey: Number(endCursor.orderKey),
161
+ uniqueKey: endCursor.uniqueKey,
162
+ },
163
+ });
114
164
 
115
165
  if (filter) {
116
- await this._putFilter(filter, cursor);
166
+ await tx
167
+ .update(filters)
168
+ .set({ toBlock: Number(endCursor.orderKey) })
169
+ .where(and(eq(filters.id, indexerName), isNull(filters.toBlock)));
170
+
171
+ await tx
172
+ .insert(filters)
173
+ .values({
174
+ id: indexerName,
175
+ filter: serialize(filter),
176
+ fromBlock: Number(endCursor.orderKey),
177
+ toBlock: null,
178
+ })
179
+ .onConflictDoUpdate({
180
+ target: [filters.id, filters.fromBlock],
181
+ set: {
182
+ filter: serialize(filter),
183
+ fromBlock: Number(endCursor.orderKey),
184
+ toBlock: null,
185
+ },
186
+ });
117
187
  }
118
188
  }
189
+ } catch (error) {
190
+ throw new DrizzleStorageError("Failed to persist state", {
191
+ cause: error,
192
+ });
119
193
  }
194
+ }
120
195
 
121
- // --- CHECKPOINTS TABLE METHODS ---
196
+ export async function getState<
197
+ TFilter,
198
+ TQueryResult extends PgQueryResultHKT,
199
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
200
+ TSchema extends
201
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
202
+ >(props: {
203
+ tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
204
+ indexerName: string;
205
+ }): Promise<{ cursor?: Cursor; filter?: TFilter }> {
206
+ const { tx, indexerName } = props;
122
207
 
123
- private async _getCheckpoint(): Promise<Cursor | undefined> {
124
- const rows = await this._db
208
+ try {
209
+ const checkpointRows = await tx
125
210
  .select()
126
211
  .from(checkpoints)
127
- .where(eq(checkpoints.id, this._indexerName));
128
-
129
- const row = rows[0];
130
- if (!row) return undefined;
131
-
132
- return {
133
- orderKey: BigInt(row.orderKey),
134
- uniqueKey: row.uniqueKey,
135
- };
136
- }
137
-
138
- private async _putCheckpoint(cursor: Cursor) {
139
- await this._db
140
- .insert(checkpoints)
141
- .values({
142
- id: this._indexerName,
143
- orderKey: Number(cursor.orderKey),
144
- uniqueKey: cursor.uniqueKey,
145
- })
146
- .onConflictDoUpdate({
147
- target: checkpoints.id,
148
- set: {
149
- orderKey: Number(cursor.orderKey),
150
- uniqueKey: cursor.uniqueKey,
151
- },
152
- });
153
- }
212
+ .where(eq(checkpoints.id, indexerName));
154
213
 
155
- // --- FILTERS TABLE METHODS ---
214
+ const cursor = checkpointRows[0]
215
+ ? {
216
+ orderKey: BigInt(checkpointRows[0].orderKey),
217
+ uniqueKey: checkpointRows[0].uniqueKey,
218
+ }
219
+ : undefined;
156
220
 
157
- private async _getFilter(): Promise<TFilter | undefined> {
158
- const rows = await this._db
221
+ const filterRows = await tx
159
222
  .select()
160
223
  .from(filters)
161
- .where(and(eq(filters.id, this._indexerName), isNull(filters.toBlock)));
224
+ .where(and(eq(filters.id, indexerName), isNull(filters.toBlock)));
162
225
 
163
- const row = rows[0];
226
+ const filter = filterRows[0]
227
+ ? deserialize<TFilter>(filterRows[0].filter)
228
+ : undefined;
164
229
 
165
- if (!row) return undefined;
166
-
167
- return deserialize(row.filter) as TFilter;
230
+ return { cursor, filter };
231
+ } catch (error) {
232
+ throw new DrizzleStorageError("Failed to get persistent state", {
233
+ cause: error,
234
+ });
168
235
  }
236
+ }
169
237
 
170
- private async _putFilter(filter: TFilter, endCursor: Cursor) {
171
- // Update existing filter's to_block
172
- await this._db
238
+ export async function invalidateState<
239
+ TQueryResult extends PgQueryResultHKT,
240
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
241
+ TSchema extends
242
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
243
+ >(props: {
244
+ tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
245
+ cursor: Cursor;
246
+ indexerName: string;
247
+ }) {
248
+ const { tx, cursor, indexerName } = props;
249
+
250
+ try {
251
+ await tx
252
+ .delete(filters)
253
+ .where(
254
+ and(
255
+ eq(filters.id, indexerName),
256
+ gt(filters.fromBlock, Number(cursor.orderKey)),
257
+ ),
258
+ );
259
+
260
+ await tx
173
261
  .update(filters)
174
- .set({ toBlock: Number(endCursor.orderKey) })
175
- .where(and(eq(filters.id, this._indexerName), isNull(filters.toBlock)));
176
-
177
- // Insert new filter
178
- await this._db
179
- .insert(filters)
180
- .values({
181
- id: this._indexerName,
182
- filter: serialize(filter as Record<string, unknown>),
183
- fromBlock: Number(endCursor.orderKey),
184
- })
185
- .onConflictDoUpdate({
186
- target: [filters.id, filters.fromBlock],
187
- set: {
188
- filter: serialize(filter as Record<string, unknown>),
189
- fromBlock: Number(endCursor.orderKey),
190
- },
191
- });
262
+ .set({ toBlock: null })
263
+ .where(
264
+ and(
265
+ eq(filters.id, indexerName),
266
+ gt(filters.toBlock, Number(cursor.orderKey)),
267
+ ),
268
+ );
269
+ } catch (error) {
270
+ throw new DrizzleStorageError("Failed to invalidate state", {
271
+ cause: error,
272
+ });
273
+ }
274
+ }
275
+
276
+ export async function finalizeState<
277
+ TQueryResult extends PgQueryResultHKT,
278
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
279
+ TSchema extends
280
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
281
+ >(props: {
282
+ tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
283
+ cursor: Cursor;
284
+ indexerName: string;
285
+ }) {
286
+ const { tx, cursor, indexerName } = props;
287
+
288
+ try {
289
+ await tx
290
+ .delete(filters)
291
+ .where(
292
+ and(
293
+ eq(filters.id, indexerName),
294
+ lt(filters.toBlock, Number(cursor.orderKey)),
295
+ ),
296
+ );
297
+ } catch (error) {
298
+ throw new DrizzleStorageError("Failed to finalize state", {
299
+ cause: error,
300
+ });
192
301
  }
193
302
  }
194
- */