@apibara/plugin-drizzle 2.1.0-beta.1 → 2.1.0-beta.10

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,5 @@
1
1
  import { useIndexerContext } from "@apibara/indexer";
2
- import { defineIndexerPlugin } from "@apibara/indexer/plugins";
2
+ import { defineIndexerPlugin, useLogger } from "@apibara/indexer/plugins";
3
3
 
4
4
  import type {
5
5
  ExtractTablesWithRelations,
@@ -14,14 +14,18 @@ import type {
14
14
  PgQueryResultHKT,
15
15
  PgTransaction,
16
16
  } from "drizzle-orm/pg-core";
17
+ import { DRIZZLE_PROPERTY, DRIZZLE_STORAGE_DB_PROPERTY } from "./constants";
18
+ import { type MigrateOptions, migrate } from "./helper";
17
19
  import {
18
20
  finalizeState,
19
21
  getState,
20
22
  initializePersistentState,
21
23
  invalidateState,
22
24
  persistState,
25
+ resetPersistence,
23
26
  } from "./persistence";
24
27
  import {
28
+ cleanupStorage,
25
29
  finalize,
26
30
  initializeReorgRollbackTable,
27
31
  invalidate,
@@ -30,7 +34,8 @@ import {
30
34
  } from "./storage";
31
35
  import { DrizzleStorageError, sleep, withTransaction } from "./utils";
32
36
 
33
- const DRIZZLE_PROPERTY = "_drizzle";
37
+ export * from "./helper";
38
+
34
39
  const MAX_RETRIES = 5;
35
40
 
36
41
  export type DrizzleStorage<
@@ -67,11 +72,30 @@ export interface DrizzleStorageOptions<
67
72
  TSchema extends
68
73
  TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
69
74
  > {
75
+ /**
76
+ * The Drizzle database instance.
77
+ */
70
78
  db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
79
+ /**
80
+ * Whether to persist the indexer's state. Defaults to true.
81
+ */
71
82
  persistState?: boolean;
83
+ /**
84
+ * The name of the indexer. Default value is 'default'.
85
+ */
72
86
  indexerName?: string;
87
+ /**
88
+ * The schema of the database.
89
+ */
73
90
  schema?: Record<string, unknown>;
91
+ /**
92
+ * The column to use as the id. Defaults to 'id'.
93
+ */
74
94
  idColumn?: string;
95
+ /**
96
+ * The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.
97
+ */
98
+ migrate?: MigrateOptions;
75
99
  }
76
100
 
77
101
  /**
@@ -83,6 +107,7 @@ export interface DrizzleStorageOptions<
83
107
  * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
84
108
  * @param options.schema - The schema of the database.
85
109
  * @param options.idColumn - The column to use as the id. Defaults to 'id'.
110
+ * @param options.migrate - The options for the database migration. when provided, the database will automatically run migrations before the indexer runs.
86
111
  */
87
112
  export function drizzleStorage<
88
113
  TFilter,
@@ -97,10 +122,12 @@ export function drizzleStorage<
97
122
  indexerName: identifier = "default",
98
123
  schema,
99
124
  idColumn = "id",
125
+ migrate: migrateOptions,
100
126
  }: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>) {
101
127
  return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
102
128
  let tableNames: string[] = [];
103
129
  let indexerId = "";
130
+ const alwaysReindex = process.env["APIBARA_ALWAYS_REINDEX"] === "true";
104
131
 
105
132
  try {
106
133
  tableNames = Object.values((schema as TSchema) ?? db._.schema ?? {}).map(
@@ -113,15 +140,46 @@ export function drizzleStorage<
113
140
  }
114
141
 
115
142
  indexer.hooks.hook("run:before", async () => {
143
+ const internalContext = useInternalContext();
144
+ const context = useIndexerContext();
145
+ const logger = useLogger();
146
+
147
+ // For testing purposes using vcr.
148
+ context[DRIZZLE_STORAGE_DB_PROPERTY] = db;
149
+
116
150
  const { indexerName: indexerFileName, availableIndexers } =
117
- useInternalContext();
151
+ internalContext;
118
152
 
119
153
  indexerId = generateIndexerId(indexerFileName, identifier);
120
154
 
155
+ if (alwaysReindex) {
156
+ logger.warn(
157
+ `Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`,
158
+ );
159
+ await withTransaction(db, async (tx) => {
160
+ await cleanupStorage(tx, tableNames, indexerId);
161
+
162
+ if (enablePersistence) {
163
+ await resetPersistence({ tx, indexerId });
164
+ }
165
+
166
+ logger.success("Tables have been cleaned up for reindexing");
167
+ });
168
+ }
169
+
121
170
  let retries = 0;
122
171
 
172
+ // incase the migrations are already applied, we don't want to run them again
173
+ let migrationsApplied = false;
174
+
123
175
  while (retries <= MAX_RETRIES) {
124
176
  try {
177
+ if (migrateOptions && !migrationsApplied) {
178
+ // @ts-ignore type mismatch for db
179
+ await migrate(db, migrateOptions);
180
+ migrationsApplied = true;
181
+ logger.success("Migrations applied");
182
+ }
125
183
  await withTransaction(db, async (tx) => {
126
184
  await initializeReorgRollbackTable(tx, indexerId);
127
185
  if (enablePersistence) {
@@ -131,6 +189,9 @@ export function drizzleStorage<
131
189
  break;
132
190
  } catch (error) {
133
191
  if (retries === MAX_RETRIES) {
192
+ if (error instanceof DrizzleStorageError) {
193
+ throw error;
194
+ }
134
195
  throw new DrizzleStorageError(
135
196
  "Initialization failed after 5 retries",
136
197
  {
@@ -297,3 +297,24 @@ export async function finalizeState<
297
297
  });
298
298
  }
299
299
  }
300
+
301
+ export async function resetPersistence<
302
+ TQueryResult extends PgQueryResultHKT,
303
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
304
+ TSchema extends
305
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
306
+ >(props: {
307
+ tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
308
+ indexerId: string;
309
+ }) {
310
+ const { tx, indexerId } = props;
311
+
312
+ try {
313
+ await tx.delete(checkpoints).where(eq(checkpoints.id, indexerId));
314
+ await tx.delete(filters).where(eq(filters.id, indexerId));
315
+ } catch (error) {
316
+ throw new DrizzleStorageError("Failed to reset persistence state", {
317
+ cause: error,
318
+ });
319
+ }
320
+ }
package/src/storage.ts CHANGED
@@ -316,3 +316,45 @@ export async function finalize<
316
316
  });
317
317
  }
318
318
  }
319
+
320
+ export async function cleanupStorage<
321
+ TQueryResult extends PgQueryResultHKT,
322
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
323
+ TSchema extends
324
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
325
+ >(
326
+ tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
327
+ tables: string[],
328
+ indexerId: string,
329
+ ) {
330
+ try {
331
+ for (const table of tables) {
332
+ await tx.execute(
333
+ sql.raw(
334
+ `DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`,
335
+ ),
336
+ );
337
+ }
338
+
339
+ await tx.execute(
340
+ sql.raw(`
341
+ DELETE FROM __reorg_rollback
342
+ WHERE indexer_id = '${indexerId}'
343
+ `),
344
+ );
345
+
346
+ for (const table of tables) {
347
+ try {
348
+ await tx.execute(sql.raw(`TRUNCATE TABLE ${table} CASCADE;`));
349
+ } catch (error) {
350
+ throw new DrizzleStorageError(`Failed to truncate table ${table}`, {
351
+ cause: error,
352
+ });
353
+ }
354
+ }
355
+ } catch (error) {
356
+ throw new DrizzleStorageError("Failed to clean up storage", {
357
+ cause: error,
358
+ });
359
+ }
360
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { VcrResult } from "@apibara/indexer/testing";
2
+ import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
3
+ import { DRIZZLE_STORAGE_DB_PROPERTY } from "./constants";
4
+
5
+ export function getTestDatabase(context: VcrResult) {
6
+ const db = context[DRIZZLE_STORAGE_DB_PROPERTY];
7
+
8
+ if (!db) {
9
+ throw new Error("Drizzle database not found in context");
10
+ }
11
+
12
+ return db as PgDatabase<PgQueryResultHKT>;
13
+ }