@apibara/plugin-drizzle 2.1.0-beta.3 → 2.1.0-beta.30
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 +250 -64
- package/dist/index.d.cts +165 -4
- package/dist/index.d.mts +165 -4
- package/dist/index.d.ts +165 -4
- package/dist/index.mjs +239 -55
- package/dist/shared/plugin-drizzle.2d226351.mjs +5 -0
- package/dist/shared/plugin-drizzle.cae20704.cjs +9 -0
- package/dist/testing.cjs +13 -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 +11 -0
- package/package.json +21 -6
- package/src/constants.ts +3 -0
- package/src/helper.ts +219 -0
- package/src/index.ts +142 -17
- package/src/persistence.ts +60 -18
- package/src/storage.ts +88 -23
- package/src/testing.ts +13 -0
- package/src/utils.ts +19 -0
package/src/helper.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { PGlite, PGliteOptions } from "@electric-sql/pglite";
|
|
2
|
+
import type { DrizzleConfig } from "drizzle-orm";
|
|
3
|
+
import { entityKind } from "drizzle-orm";
|
|
4
|
+
import type { MigrationConfig } from "drizzle-orm/migrator";
|
|
5
|
+
import type { NodePgDatabase as OriginalNodePgDatabase } from "drizzle-orm/node-postgres";
|
|
6
|
+
import type { PgliteDatabase as OriginalPgliteDatabase } from "drizzle-orm/pglite";
|
|
7
|
+
import type pg from "pg";
|
|
8
|
+
import { DrizzleStorageError } from "./utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Union type of all possible drizzle database options
|
|
12
|
+
*/
|
|
13
|
+
export type DrizzleOptions = PgliteDrizzleOptions | NodePgDrizzleOptions;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration options for Node-Postgres database connection
|
|
17
|
+
*/
|
|
18
|
+
export type NodePgDrizzleOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* Type of database to use -
|
|
21
|
+
* - "pglite" - PGLite database
|
|
22
|
+
* - "node-postgres" - Node-Postgres database
|
|
23
|
+
* @default "pglite"
|
|
24
|
+
*/
|
|
25
|
+
type: "node-postgres";
|
|
26
|
+
/**
|
|
27
|
+
* Connection string to use for the database
|
|
28
|
+
* @default ""
|
|
29
|
+
*/
|
|
30
|
+
connectionString?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Pool configuration options for Node-Postgres
|
|
33
|
+
*/
|
|
34
|
+
poolConfig?: pg.PoolConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Additional drizzle configuration options
|
|
37
|
+
*/
|
|
38
|
+
config?: Omit<DrizzleConfig, "schema">;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Configuration options for PGLite database connection
|
|
43
|
+
*/
|
|
44
|
+
export type PgliteDrizzleOptions = {
|
|
45
|
+
/**
|
|
46
|
+
* Type of database to use -
|
|
47
|
+
* - "pglite" - PGLite database
|
|
48
|
+
* - "node-postgres" - Node-Postgres database
|
|
49
|
+
*/
|
|
50
|
+
type?: "pglite";
|
|
51
|
+
/**
|
|
52
|
+
* Connection string to use for the database
|
|
53
|
+
* @default process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://pglite"
|
|
54
|
+
*/
|
|
55
|
+
connectionString?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Pool configuration is not supported for PGLite
|
|
58
|
+
*/
|
|
59
|
+
poolConfig?: never;
|
|
60
|
+
/**
|
|
61
|
+
* Additional drizzle configuration options with PGLite specific connection options
|
|
62
|
+
*/
|
|
63
|
+
config?: Omit<DrizzleConfig, "schema"> & {
|
|
64
|
+
connection?:
|
|
65
|
+
| (PGliteOptions & {
|
|
66
|
+
dataDir?: string;
|
|
67
|
+
})
|
|
68
|
+
| string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extended PGLite database type with client information
|
|
74
|
+
*/
|
|
75
|
+
export type PgliteDatabase<TSchema extends Record<string, unknown>> =
|
|
76
|
+
OriginalPgliteDatabase<TSchema> & {
|
|
77
|
+
$client: PGlite;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extended Node-Postgres database type with client information
|
|
82
|
+
*/
|
|
83
|
+
export type NodePgDatabase<TSchema extends Record<string, unknown>> =
|
|
84
|
+
OriginalNodePgDatabase<TSchema> & {
|
|
85
|
+
$client: pg.Pool;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type Database<
|
|
89
|
+
TOptions extends DrizzleOptions,
|
|
90
|
+
TSchema extends Record<string, unknown>,
|
|
91
|
+
> = TOptions extends PgliteDrizzleOptions
|
|
92
|
+
? PgliteDatabase<TSchema>
|
|
93
|
+
: NodePgDatabase<TSchema>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a new Drizzle database instance based on the provided options
|
|
97
|
+
*
|
|
98
|
+
* @important connectionString defaults to process.env["POSTGRES_CONNECTION_STRING"], if not set, it defaults to "memory://" (in-memory pglite)
|
|
99
|
+
*
|
|
100
|
+
* @param options - Configuration options for the database connection
|
|
101
|
+
* @returns A configured Drizzle database instance
|
|
102
|
+
* @throws {Error} If an invalid database type is specified
|
|
103
|
+
*/
|
|
104
|
+
export function drizzle<
|
|
105
|
+
TSchema extends Record<string, unknown>,
|
|
106
|
+
TOptions extends DrizzleOptions,
|
|
107
|
+
>(
|
|
108
|
+
options?: TOptions & {
|
|
109
|
+
/**
|
|
110
|
+
* Schema to use for the database
|
|
111
|
+
* @default {}
|
|
112
|
+
*/
|
|
113
|
+
schema?: TSchema;
|
|
114
|
+
},
|
|
115
|
+
): Database<TOptions, TSchema> {
|
|
116
|
+
const {
|
|
117
|
+
connectionString = process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://",
|
|
118
|
+
schema,
|
|
119
|
+
type = "pglite",
|
|
120
|
+
config,
|
|
121
|
+
poolConfig,
|
|
122
|
+
} = options ?? {};
|
|
123
|
+
|
|
124
|
+
if (isPgliteConnectionString(connectionString) && type === "pglite") {
|
|
125
|
+
const { drizzle: drizzlePGLite } = require("drizzle-orm/pglite");
|
|
126
|
+
|
|
127
|
+
return drizzlePGLite({
|
|
128
|
+
schema: schema as TSchema,
|
|
129
|
+
connection: {
|
|
130
|
+
dataDir: connectionString || "memory://pglite",
|
|
131
|
+
},
|
|
132
|
+
...(config || {}),
|
|
133
|
+
}) as Database<TOptions, TSchema>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { Pool } = require("pg");
|
|
137
|
+
const { drizzle: drizzleNode } = require("drizzle-orm/node-postgres");
|
|
138
|
+
const pool = new Pool({
|
|
139
|
+
connectionString,
|
|
140
|
+
...(poolConfig || {}),
|
|
141
|
+
});
|
|
142
|
+
return drizzleNode(pool, { schema, ...(config || {}) }) as Database<
|
|
143
|
+
TOptions,
|
|
144
|
+
TSchema
|
|
145
|
+
>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Options for database migration
|
|
150
|
+
*/
|
|
151
|
+
export type MigrateOptions = MigrationConfig;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Performs database migration based on the provided configuration
|
|
155
|
+
* @param db - The database instance to migrate
|
|
156
|
+
* @param options - Migration configuration options
|
|
157
|
+
*
|
|
158
|
+
* @important This function runs migrations on the database instance provided to the `drizzleStorage` plugin.
|
|
159
|
+
* It automatically detects the type of database and runs the appropriate migrate function
|
|
160
|
+
* (PGLite or Node-Postgres).
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* await migrate(db, { migrationsFolder: "./drizzle" });
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export async function migrate<TSchema extends Record<string, unknown>>(
|
|
168
|
+
db: PgliteDatabase<TSchema> | NodePgDatabase<TSchema>,
|
|
169
|
+
options: MigrateOptions,
|
|
170
|
+
) {
|
|
171
|
+
const isPglite = isDrizzleKind(db, "PgliteDatabase");
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
if (isPglite) {
|
|
175
|
+
const { migrate: migratePGLite } = require("drizzle-orm/pglite/migrator");
|
|
176
|
+
await migratePGLite(db as PgliteDatabase<TSchema>, options);
|
|
177
|
+
} else {
|
|
178
|
+
const {
|
|
179
|
+
migrate: migrateNode,
|
|
180
|
+
} = require("drizzle-orm/node-postgres/migrator");
|
|
181
|
+
await migrateNode(db as NodePgDatabase<TSchema>, options);
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw new DrizzleStorageError(
|
|
185
|
+
"Failed to apply migrations! Please check if you have generated migrations using drizzle:generate",
|
|
186
|
+
{
|
|
187
|
+
cause: error,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isPgliteConnectionString(conn: string) {
|
|
194
|
+
return (
|
|
195
|
+
conn.startsWith("memory://") ||
|
|
196
|
+
conn.startsWith("file://") ||
|
|
197
|
+
conn.startsWith("idb://")
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isDrizzleKind(value: unknown, entityKindValue: string) {
|
|
202
|
+
if (!value || typeof value !== "object") {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
// https://github.com/drizzle-team/drizzle-orm/blob/f39f885779800982e90dd3c89aba6df3217a6fd2/drizzle-orm/src/entity.ts#L29-L41
|
|
206
|
+
let cls = Object.getPrototypeOf(value).constructor;
|
|
207
|
+
if (cls) {
|
|
208
|
+
// Traverse the prototype chain to find the entityKind
|
|
209
|
+
while (cls) {
|
|
210
|
+
// https://github.com/drizzle-team/drizzle-orm/blob/f39f885779800982e90dd3c89aba6df3217a6fd2/drizzle-orm/src/pglite/driver.ts#L41
|
|
211
|
+
if (entityKind in cls && cls[entityKind] === entityKindValue) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
cls = Object.getPrototypeOf(cls);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return false;
|
|
219
|
+
}
|
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,23 +14,36 @@ 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,
|
|
28
32
|
registerTriggers,
|
|
29
33
|
removeTriggers,
|
|
30
34
|
} from "./storage";
|
|
31
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
DrizzleStorageError,
|
|
37
|
+
type IdColumnMap,
|
|
38
|
+
getIdColumnForTable,
|
|
39
|
+
sleep,
|
|
40
|
+
withTransaction,
|
|
41
|
+
} from "./utils";
|
|
42
|
+
|
|
43
|
+
export * from "./helper";
|
|
44
|
+
|
|
45
|
+
export type { IdColumnMap };
|
|
32
46
|
|
|
33
|
-
const DRIZZLE_PROPERTY = "_drizzle";
|
|
34
47
|
const MAX_RETRIES = 5;
|
|
35
48
|
|
|
36
49
|
export type DrizzleStorage<
|
|
@@ -67,11 +80,53 @@ export interface DrizzleStorageOptions<
|
|
|
67
80
|
TSchema extends
|
|
68
81
|
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
69
82
|
> {
|
|
83
|
+
/**
|
|
84
|
+
* The Drizzle database instance.
|
|
85
|
+
*/
|
|
70
86
|
db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
|
|
87
|
+
/**
|
|
88
|
+
* Whether to persist the indexer's state. Defaults to true.
|
|
89
|
+
*/
|
|
71
90
|
persistState?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The name of the indexer. Default value is 'default'.
|
|
93
|
+
*/
|
|
72
94
|
indexerName?: string;
|
|
95
|
+
/**
|
|
96
|
+
* The schema of the database.
|
|
97
|
+
*/
|
|
73
98
|
schema?: Record<string, unknown>;
|
|
74
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The column to use as the primary identifier for each table.
|
|
101
|
+
*
|
|
102
|
+
* This identifier is used for tracking changes during reorgs and rollbacks.
|
|
103
|
+
*
|
|
104
|
+
* Can be specified in two ways:
|
|
105
|
+
*
|
|
106
|
+
* 1. As a single string that applies to all tables:
|
|
107
|
+
* ```ts
|
|
108
|
+
* idColumn: "_id" // Uses "_id" column for all tables
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* 2. As an object mapping table names to their ID columns:
|
|
112
|
+
* ```ts
|
|
113
|
+
* idColumn: {
|
|
114
|
+
* transfers: "transaction_hash", // Use "transaction_hash" for transfers table
|
|
115
|
+
* blocks: "block_number", // Use "block_number" for blocks table
|
|
116
|
+
* "*": "_id" // Use "_id" for all other tables | defaults to "id"
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* The special "*" key acts as a fallback for any tables not explicitly mapped.
|
|
121
|
+
*
|
|
122
|
+
* @default "id"
|
|
123
|
+
* @type {string | Partial<IdColumnMap>}
|
|
124
|
+
*/
|
|
125
|
+
idColumn?: string | Partial<IdColumnMap>;
|
|
126
|
+
/**
|
|
127
|
+
* The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.
|
|
128
|
+
*/
|
|
129
|
+
migrate?: MigrateOptions;
|
|
75
130
|
}
|
|
76
131
|
|
|
77
132
|
/**
|
|
@@ -83,6 +138,7 @@ export interface DrizzleStorageOptions<
|
|
|
83
138
|
* @param options.indexerName - The name of the indexer. Defaults value is 'default'.
|
|
84
139
|
* @param options.schema - The schema of the database.
|
|
85
140
|
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
141
|
+
* @param options.migrate - The options for the database migration. when provided, the database will automatically run migrations before the indexer runs.
|
|
86
142
|
*/
|
|
87
143
|
export function drizzleStorage<
|
|
88
144
|
TFilter,
|
|
@@ -95,42 +151,101 @@ export function drizzleStorage<
|
|
|
95
151
|
db,
|
|
96
152
|
persistState: enablePersistence = true,
|
|
97
153
|
indexerName: identifier = "default",
|
|
98
|
-
schema,
|
|
99
|
-
idColumn
|
|
154
|
+
schema: _schema,
|
|
155
|
+
idColumn,
|
|
156
|
+
migrate: migrateOptions,
|
|
100
157
|
}: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>) {
|
|
101
158
|
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
102
159
|
let tableNames: string[] = [];
|
|
103
160
|
let indexerId = "";
|
|
161
|
+
const alwaysReindex = process.env["APIBARA_ALWAYS_REINDEX"] === "true";
|
|
162
|
+
let prevFinality: DataFinality | undefined;
|
|
163
|
+
const schema: TSchema = (_schema as TSchema) ?? db._.schema ?? {};
|
|
164
|
+
const idColumnMap: IdColumnMap = {
|
|
165
|
+
"*": typeof idColumn === "string" ? idColumn : "id",
|
|
166
|
+
...(typeof idColumn === "object" ? idColumn : {}),
|
|
167
|
+
};
|
|
104
168
|
|
|
105
169
|
try {
|
|
106
|
-
tableNames = Object.values(
|
|
107
|
-
(table) => table.dbName,
|
|
108
|
-
);
|
|
170
|
+
tableNames = Object.values(schema).map((table) => table.dbName);
|
|
109
171
|
} catch (error) {
|
|
110
172
|
throw new DrizzleStorageError("Failed to get table names from schema", {
|
|
111
173
|
cause: error,
|
|
112
174
|
});
|
|
113
175
|
}
|
|
114
176
|
|
|
177
|
+
// Check if specified idColumn exists in all the tables in schema
|
|
178
|
+
for (const table of Object.values(schema)) {
|
|
179
|
+
const columns = table.columns;
|
|
180
|
+
const tableIdColumn = getIdColumnForTable(table.dbName, idColumnMap);
|
|
181
|
+
|
|
182
|
+
const columnExists = Object.values(columns).some(
|
|
183
|
+
(column) => column.name === tableIdColumn,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (!columnExists) {
|
|
187
|
+
throw new DrizzleStorageError(
|
|
188
|
+
`Column \`"${tableIdColumn}"\` does not exist in table \`"${table.dbName}"\`. ` +
|
|
189
|
+
"Make sure the table has the specified column or provide a valid `idColumn` mapping to `drizzleStorage`.",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
115
194
|
indexer.hooks.hook("run:before", async () => {
|
|
195
|
+
const internalContext = useInternalContext();
|
|
196
|
+
const context = useIndexerContext();
|
|
197
|
+
const logger = useLogger();
|
|
198
|
+
|
|
199
|
+
// For testing purposes using vcr.
|
|
200
|
+
context[DRIZZLE_STORAGE_DB_PROPERTY] = db;
|
|
201
|
+
|
|
116
202
|
const { indexerName: indexerFileName, availableIndexers } =
|
|
117
|
-
|
|
203
|
+
internalContext;
|
|
118
204
|
|
|
119
205
|
indexerId = generateIndexerId(indexerFileName, identifier);
|
|
120
206
|
|
|
121
207
|
let retries = 0;
|
|
122
208
|
|
|
209
|
+
// incase the migrations are already applied, we don't want to run them again
|
|
210
|
+
let migrationsApplied = false;
|
|
211
|
+
let cleanupApplied = false;
|
|
212
|
+
|
|
123
213
|
while (retries <= MAX_RETRIES) {
|
|
124
214
|
try {
|
|
215
|
+
if (migrateOptions && !migrationsApplied) {
|
|
216
|
+
// @ts-ignore type mismatch for db
|
|
217
|
+
await migrate(db, migrateOptions);
|
|
218
|
+
migrationsApplied = true;
|
|
219
|
+
logger.success("Migrations applied");
|
|
220
|
+
}
|
|
125
221
|
await withTransaction(db, async (tx) => {
|
|
126
222
|
await initializeReorgRollbackTable(tx, indexerId);
|
|
127
223
|
if (enablePersistence) {
|
|
128
224
|
await initializePersistentState(tx);
|
|
129
225
|
}
|
|
226
|
+
|
|
227
|
+
if (alwaysReindex && !cleanupApplied) {
|
|
228
|
+
logger.warn(
|
|
229
|
+
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
await cleanupStorage(tx, tableNames, indexerId);
|
|
233
|
+
|
|
234
|
+
if (enablePersistence) {
|
|
235
|
+
await resetPersistence({ tx, indexerId });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
cleanupApplied = true;
|
|
239
|
+
|
|
240
|
+
logger.success("Tables have been cleaned up for reindexing");
|
|
241
|
+
}
|
|
130
242
|
});
|
|
131
243
|
break;
|
|
132
244
|
} catch (error) {
|
|
133
245
|
if (retries === MAX_RETRIES) {
|
|
246
|
+
if (error instanceof DrizzleStorageError) {
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
134
249
|
throw new DrizzleStorageError(
|
|
135
250
|
"Initialization failed after 5 retries",
|
|
136
251
|
{
|
|
@@ -177,7 +292,8 @@ export function drizzleStorage<
|
|
|
177
292
|
}
|
|
178
293
|
|
|
179
294
|
await withTransaction(db, async (tx) => {
|
|
180
|
-
|
|
295
|
+
// Use the appropriate idColumn for each table when calling invalidate
|
|
296
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
181
297
|
|
|
182
298
|
if (enablePersistence) {
|
|
183
299
|
await invalidateState({ tx, cursor, indexerId });
|
|
@@ -204,7 +320,7 @@ export function drizzleStorage<
|
|
|
204
320
|
});
|
|
205
321
|
|
|
206
322
|
indexer.hooks.hook("message:finalize", async ({ message }) => {
|
|
207
|
-
const { cursor } = message
|
|
323
|
+
const { cursor } = message;
|
|
208
324
|
|
|
209
325
|
if (!cursor) {
|
|
210
326
|
throw new DrizzleStorageError("Finalized Cursor is undefined");
|
|
@@ -220,14 +336,15 @@ export function drizzleStorage<
|
|
|
220
336
|
});
|
|
221
337
|
|
|
222
338
|
indexer.hooks.hook("message:invalidate", async ({ message }) => {
|
|
223
|
-
const { cursor } = message
|
|
339
|
+
const { cursor } = message;
|
|
224
340
|
|
|
225
341
|
if (!cursor) {
|
|
226
342
|
throw new DrizzleStorageError("Invalidate Cursor is undefined");
|
|
227
343
|
}
|
|
228
344
|
|
|
229
345
|
await withTransaction(db, async (tx) => {
|
|
230
|
-
|
|
346
|
+
// Use the appropriate idColumn for each table when calling invalidate
|
|
347
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
231
348
|
|
|
232
349
|
if (enablePersistence) {
|
|
233
350
|
await invalidateState({ tx, cursor, indexerId });
|
|
@@ -238,7 +355,8 @@ export function drizzleStorage<
|
|
|
238
355
|
indexer.hooks.hook("handler:middleware", async ({ use }) => {
|
|
239
356
|
use(async (context, next) => {
|
|
240
357
|
try {
|
|
241
|
-
const { endCursor, finality } = context as {
|
|
358
|
+
const { endCursor, finality, cursor } = context as {
|
|
359
|
+
cursor: Cursor;
|
|
242
360
|
endCursor: Cursor;
|
|
243
361
|
finality: DataFinality;
|
|
244
362
|
};
|
|
@@ -254,12 +372,17 @@ export function drizzleStorage<
|
|
|
254
372
|
TSchema
|
|
255
373
|
>;
|
|
256
374
|
|
|
375
|
+
if (prevFinality === "pending") {
|
|
376
|
+
// invalidate if previous block's finality was "pending"
|
|
377
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
378
|
+
}
|
|
379
|
+
|
|
257
380
|
if (finality !== "finalized") {
|
|
258
381
|
await registerTriggers(
|
|
259
382
|
tx,
|
|
260
383
|
tableNames,
|
|
261
384
|
endCursor,
|
|
262
|
-
|
|
385
|
+
idColumnMap,
|
|
263
386
|
indexerId,
|
|
264
387
|
);
|
|
265
388
|
}
|
|
@@ -267,13 +390,15 @@ export function drizzleStorage<
|
|
|
267
390
|
await next();
|
|
268
391
|
delete context[DRIZZLE_PROPERTY];
|
|
269
392
|
|
|
270
|
-
if (enablePersistence) {
|
|
393
|
+
if (enablePersistence && finality !== "pending") {
|
|
271
394
|
await persistState({
|
|
272
395
|
tx,
|
|
273
396
|
endCursor,
|
|
274
397
|
indexerId,
|
|
275
398
|
});
|
|
276
399
|
}
|
|
400
|
+
|
|
401
|
+
prevFinality = finality;
|
|
277
402
|
});
|
|
278
403
|
|
|
279
404
|
if (finality !== "finalized") {
|
package/src/persistence.ts
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import { type Cursor, normalizeCursor } from "@apibara/protocol";
|
|
2
|
-
import { and, eq, gt, isNull, lt } from "drizzle-orm";
|
|
2
|
+
import { and, eq, gt, isNull, lt, sql } from "drizzle-orm";
|
|
3
3
|
import type {
|
|
4
4
|
ExtractTablesWithRelations,
|
|
5
5
|
TablesRelationalConfig,
|
|
6
6
|
} from "drizzle-orm";
|
|
7
7
|
import type { PgQueryResultHKT, PgTransaction } from "drizzle-orm/pg-core";
|
|
8
|
-
import { integer,
|
|
8
|
+
import { integer, pgSchema, primaryKey, text } from "drizzle-orm/pg-core";
|
|
9
|
+
import { SCHEMA_NAME } from "./constants";
|
|
9
10
|
import { DrizzleStorageError, deserialize, serialize } from "./utils";
|
|
10
11
|
|
|
11
|
-
const CHECKPOINTS_TABLE_NAME = "
|
|
12
|
-
const FILTERS_TABLE_NAME = "
|
|
13
|
-
const SCHEMA_VERSION_TABLE_NAME = "
|
|
12
|
+
const CHECKPOINTS_TABLE_NAME = "checkpoints";
|
|
13
|
+
const FILTERS_TABLE_NAME = "filters";
|
|
14
|
+
const SCHEMA_VERSION_TABLE_NAME = "schema_version";
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const schema = pgSchema(SCHEMA_NAME);
|
|
17
|
+
|
|
18
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
19
|
+
export const checkpoints = schema.table(CHECKPOINTS_TABLE_NAME, {
|
|
16
20
|
id: text("id").notNull().primaryKey(),
|
|
17
21
|
orderKey: integer("order_key").notNull(),
|
|
18
22
|
uniqueKey: text("unique_key"),
|
|
19
23
|
});
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
26
|
+
export const filters = schema.table(
|
|
22
27
|
FILTERS_TABLE_NAME,
|
|
23
28
|
{
|
|
24
29
|
id: text("id").notNull(),
|
|
@@ -33,7 +38,8 @@ export const filters = pgTable(
|
|
|
33
38
|
],
|
|
34
39
|
);
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
42
|
+
export const schemaVersion = schema.table(SCHEMA_VERSION_TABLE_NAME, {
|
|
37
43
|
k: integer("k").notNull().primaryKey(),
|
|
38
44
|
version: integer("version").notNull(),
|
|
39
45
|
});
|
|
@@ -53,13 +59,22 @@ export async function initializePersistentState<
|
|
|
53
59
|
TSchema extends
|
|
54
60
|
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
55
61
|
>(tx: PgTransaction<TQueryResult, TFullSchema, TSchema>) {
|
|
62
|
+
// Create schema if it doesn't exist
|
|
63
|
+
await tx.execute(
|
|
64
|
+
sql.raw(`
|
|
65
|
+
CREATE SCHEMA IF NOT EXISTS ${SCHEMA_NAME};
|
|
66
|
+
`),
|
|
67
|
+
);
|
|
68
|
+
|
|
56
69
|
// Create schema version table
|
|
57
|
-
await tx.execute(
|
|
58
|
-
|
|
70
|
+
await tx.execute(
|
|
71
|
+
sql.raw(`
|
|
72
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${SCHEMA_VERSION_TABLE_NAME} (
|
|
59
73
|
k INTEGER PRIMARY KEY,
|
|
60
74
|
version INTEGER NOT NULL
|
|
61
75
|
);
|
|
62
|
-
`)
|
|
76
|
+
`),
|
|
77
|
+
);
|
|
63
78
|
|
|
64
79
|
// Get current schema version
|
|
65
80
|
const versionRows = await tx
|
|
@@ -80,23 +95,27 @@ export async function initializePersistentState<
|
|
|
80
95
|
try {
|
|
81
96
|
if (storedVersion === -1) {
|
|
82
97
|
// First time initialization
|
|
83
|
-
await tx.execute(
|
|
84
|
-
|
|
98
|
+
await tx.execute(
|
|
99
|
+
sql.raw(`
|
|
100
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${CHECKPOINTS_TABLE_NAME} (
|
|
85
101
|
id TEXT PRIMARY KEY,
|
|
86
102
|
order_key INTEGER NOT NULL,
|
|
87
103
|
unique_key TEXT
|
|
88
104
|
);
|
|
89
|
-
`)
|
|
105
|
+
`),
|
|
106
|
+
);
|
|
90
107
|
|
|
91
|
-
await tx.execute(
|
|
92
|
-
|
|
108
|
+
await tx.execute(
|
|
109
|
+
sql.raw(`
|
|
110
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${FILTERS_TABLE_NAME} (
|
|
93
111
|
id TEXT NOT NULL,
|
|
94
112
|
filter TEXT NOT NULL,
|
|
95
113
|
from_block INTEGER NOT NULL,
|
|
96
114
|
to_block INTEGER DEFAULT NULL,
|
|
97
115
|
PRIMARY KEY (id, from_block)
|
|
98
116
|
);
|
|
99
|
-
`)
|
|
117
|
+
`),
|
|
118
|
+
);
|
|
100
119
|
|
|
101
120
|
// Set initial schema version
|
|
102
121
|
await tx.insert(schemaVersion).values({
|
|
@@ -155,7 +174,9 @@ export async function persistState<
|
|
|
155
174
|
target: checkpoints.id,
|
|
156
175
|
set: {
|
|
157
176
|
orderKey: Number(endCursor.orderKey),
|
|
158
|
-
|
|
177
|
+
// Explicitly set the unique key to `null` to indicate that it has been deleted
|
|
178
|
+
// Otherwise drizzle will not update its value.
|
|
179
|
+
uniqueKey: endCursor.uniqueKey ? endCursor.uniqueKey : null,
|
|
159
180
|
},
|
|
160
181
|
});
|
|
161
182
|
|
|
@@ -297,3 +318,24 @@ export async function finalizeState<
|
|
|
297
318
|
});
|
|
298
319
|
}
|
|
299
320
|
}
|
|
321
|
+
|
|
322
|
+
export async function resetPersistence<
|
|
323
|
+
TQueryResult extends PgQueryResultHKT,
|
|
324
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
325
|
+
TSchema extends
|
|
326
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
327
|
+
>(props: {
|
|
328
|
+
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
329
|
+
indexerId: string;
|
|
330
|
+
}) {
|
|
331
|
+
const { tx, indexerId } = props;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
await tx.delete(checkpoints).where(eq(checkpoints.id, indexerId));
|
|
335
|
+
await tx.delete(filters).where(eq(filters.id, indexerId));
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new DrizzleStorageError("Failed to reset persistence state", {
|
|
338
|
+
cause: error,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|