@apibara/plugin-sqlite 2.0.0-beta.27

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/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # `@apibara/plugin-sqlite`
2
+
3
+ TODO
4
+
5
+ ## Installation
6
+
7
+ TODO
package/dist/index.cjs ADDED
@@ -0,0 +1,274 @@
1
+ 'use strict';
2
+
3
+ const indexer = require('@apibara/indexer');
4
+ const plugins = require('@apibara/indexer/plugins');
5
+ const protocol = require('@apibara/protocol');
6
+
7
+ class SqliteStorageError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "SqliteStorageError";
11
+ }
12
+ }
13
+ async function withTransaction(db, cb) {
14
+ db.prepare("BEGIN TRANSACTION").run();
15
+ try {
16
+ await cb(db);
17
+ } catch (error) {
18
+ db.prepare("ROLLBACK TRANSACTION").run();
19
+ throw error;
20
+ }
21
+ db.prepare("COMMIT TRANSACTION").run();
22
+ }
23
+ function assertInTransaction(db) {
24
+ if (!db.inTransaction) {
25
+ throw new SqliteStorageError("Database is not in transaction");
26
+ }
27
+ }
28
+ function deserialize(str) {
29
+ return JSON.parse(
30
+ str,
31
+ (_, value) => typeof value === "string" && value.match(/^\d+n$/) ? BigInt(value.slice(0, -1)) : value
32
+ );
33
+ }
34
+ function serialize(obj) {
35
+ return JSON.stringify(
36
+ obj,
37
+ (_, value) => typeof value === "bigint" ? `${value.toString()}n` : value,
38
+ " "
39
+ );
40
+ }
41
+
42
+ function initializeKeyValueStore(db) {
43
+ assertInTransaction(db);
44
+ db.exec(statements$1.createTable);
45
+ }
46
+ class KeyValueStore {
47
+ constructor(db, endCursor, finality, serialize, deserialize) {
48
+ this.db = db;
49
+ this.endCursor = endCursor;
50
+ this.finality = finality;
51
+ this.serialize = serialize;
52
+ this.deserialize = deserialize;
53
+ assertInTransaction(db);
54
+ }
55
+ get(key) {
56
+ const row = this.db.prepare(statements$1.get).get(key);
57
+ return row ? this.deserialize(row.v) : void 0;
58
+ }
59
+ put(key, value) {
60
+ this.db.prepare(statements$1.updateToBlock).run(Number(this.endCursor.orderKey), key);
61
+ this.db.prepare(statements$1.insertIntoKvs).run(
62
+ Number(this.endCursor.orderKey),
63
+ key,
64
+ this.serialize(value)
65
+ );
66
+ }
67
+ del(key) {
68
+ this.db.prepare(statements$1.del).run(Number(this.endCursor.orderKey), key);
69
+ }
70
+ }
71
+ const statements$1 = {
72
+ createTable: `
73
+ CREATE TABLE IF NOT EXISTS kvs (
74
+ from_block INTEGER NOT NULL,
75
+ to_block INTEGER,
76
+ k TEXT NOT NULL,
77
+ v BLOB NOT NULL,
78
+ PRIMARY KEY (from_block, k)
79
+ );`,
80
+ get: `
81
+ SELECT v
82
+ FROM kvs
83
+ WHERE k = ? AND to_block IS NULL`,
84
+ updateToBlock: `
85
+ UPDATE kvs
86
+ SET to_block = ?
87
+ WHERE k = ? AND to_block IS NULL`,
88
+ insertIntoKvs: `
89
+ INSERT INTO kvs (from_block, to_block, k, v)
90
+ VALUES (?, NULL, ?, ?)`,
91
+ del: `
92
+ UPDATE kvs
93
+ SET to_block = ?
94
+ WHERE k = ? AND to_block IS NULL`
95
+ };
96
+
97
+ const DEFAULT_INDEXER_ID = "default";
98
+ function initializePersistentState(db) {
99
+ assertInTransaction(db);
100
+ db.exec(statements.createCheckpointsTable);
101
+ db.exec(statements.createFiltersTable);
102
+ }
103
+ function persistState(db, endCursor, filter) {
104
+ assertInTransaction(db);
105
+ db.prepare(statements.putCheckpoint).run(
106
+ DEFAULT_INDEXER_ID,
107
+ Number(endCursor.orderKey),
108
+ endCursor.uniqueKey
109
+ );
110
+ if (filter) {
111
+ db.prepare(statements.updateFilterToBlock).run(
112
+ Number(endCursor.orderKey),
113
+ DEFAULT_INDEXER_ID
114
+ );
115
+ db.prepare(statements.insertFilter).run(
116
+ DEFAULT_INDEXER_ID,
117
+ serialize(filter),
118
+ Number(endCursor.orderKey)
119
+ );
120
+ }
121
+ }
122
+ function getState(db) {
123
+ assertInTransaction(db);
124
+ const storedCursor = db.prepare(
125
+ statements.getCheckpoint
126
+ ).get(DEFAULT_INDEXER_ID);
127
+ const storedFilter = db.prepare(statements.getFilter).get(DEFAULT_INDEXER_ID);
128
+ let cursor;
129
+ let filter;
130
+ if (storedCursor?.order_key) {
131
+ cursor = {
132
+ orderKey: BigInt(storedCursor.order_key),
133
+ uniqueKey: storedCursor.unique_key
134
+ };
135
+ }
136
+ if (storedFilter) {
137
+ filter = deserialize(storedFilter.filter);
138
+ }
139
+ return { cursor, filter };
140
+ }
141
+ const statements = {
142
+ createCheckpointsTable: `
143
+ CREATE TABLE IF NOT EXISTS checkpoints (
144
+ id TEXT NOT NULL PRIMARY KEY,
145
+ order_key INTEGER,
146
+ unique_key TEXT
147
+ );`,
148
+ createFiltersTable: `
149
+ CREATE TABLE IF NOT EXISTS filters (
150
+ id TEXT NOT NULL,
151
+ filter BLOB NOT NULL,
152
+ from_block INTEGER NOT NULL,
153
+ to_block INTEGER,
154
+ PRIMARY KEY (id, from_block)
155
+ );`,
156
+ getCheckpoint: `
157
+ SELECT *
158
+ FROM checkpoints
159
+ WHERE id = ?`,
160
+ putCheckpoint: `
161
+ INSERT INTO checkpoints (id, order_key, unique_key)
162
+ VALUES (?, ?, ?)
163
+ ON CONFLICT(id) DO UPDATE SET
164
+ order_key = excluded.order_key,
165
+ unique_key = excluded.unique_key`,
166
+ delCheckpoint: `
167
+ DELETE FROM checkpoints
168
+ WHERE id = ?`,
169
+ getFilter: `
170
+ SELECT *
171
+ FROM filters
172
+ WHERE id = ? AND to_block IS NULL`,
173
+ updateFilterToBlock: `
174
+ UPDATE filters
175
+ SET to_block = ?
176
+ WHERE id = ? AND to_block IS NULL`,
177
+ insertFilter: `
178
+ INSERT INTO filters (id, filter, from_block)
179
+ VALUES (?, ?, ?)
180
+ ON CONFLICT(id, from_block) DO UPDATE SET
181
+ filter = excluded.filter,
182
+ from_block = excluded.from_block`,
183
+ delFilter: `
184
+ DELETE FROM filters
185
+ WHERE id = ?`
186
+ };
187
+
188
+ const KV_PROPERTY = "_kv_sqlite";
189
+ function useSqliteKeyValueStore() {
190
+ const kv = indexer.useIndexerContext()[KV_PROPERTY];
191
+ if (!kv) {
192
+ throw new SqliteStorageError(
193
+ "SQLite key-value store is not available. Did you forget to enable it?"
194
+ );
195
+ }
196
+ return kv;
197
+ }
198
+ function sqliteStorage({
199
+ database,
200
+ persistState: enablePersistState = true,
201
+ keyValueStore: enableKeyValueStore = true,
202
+ serialize: serializeFn = serialize,
203
+ deserialize: deserializeFn = deserialize
204
+ }) {
205
+ return plugins.defineIndexerPlugin((indexer) => {
206
+ indexer.hooks.hook("run:before", async () => {
207
+ await withTransaction(database, async (db) => {
208
+ if (enablePersistState) {
209
+ initializePersistentState(db);
210
+ }
211
+ if (enableKeyValueStore) {
212
+ initializeKeyValueStore(db);
213
+ }
214
+ });
215
+ });
216
+ indexer.hooks.hook("connect:before", async ({ request }) => {
217
+ if (!enablePersistState) {
218
+ return;
219
+ }
220
+ return await withTransaction(database, async (db) => {
221
+ const { cursor, filter } = getState(db);
222
+ if (cursor) {
223
+ request.startingCursor = cursor;
224
+ }
225
+ if (filter) {
226
+ request.filter[1] = filter;
227
+ }
228
+ });
229
+ });
230
+ indexer.hooks.hook("handler:middleware", ({ use }) => {
231
+ use(async (ctx, next) => {
232
+ if (!ctx.finality) {
233
+ throw new SqliteStorageError("finality is undefined");
234
+ }
235
+ if (!ctx.endCursor || !protocol.isCursor(ctx.endCursor)) {
236
+ throw new SqliteStorageError(
237
+ "endCursor is undefined or not a cursor"
238
+ );
239
+ }
240
+ await withTransaction(database, async (db) => {
241
+ if (enableKeyValueStore) {
242
+ ctx[KV_PROPERTY] = new KeyValueStore(
243
+ db,
244
+ ctx.endCursor,
245
+ ctx.finality,
246
+ serializeFn,
247
+ deserializeFn
248
+ );
249
+ }
250
+ await next();
251
+ if (enablePersistState) {
252
+ persistState(db, ctx.endCursor);
253
+ }
254
+ if (enableKeyValueStore) {
255
+ delete ctx[KV_PROPERTY];
256
+ }
257
+ });
258
+ });
259
+ });
260
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
261
+ if (!enablePersistState) {
262
+ return;
263
+ }
264
+ assertInTransaction(database);
265
+ if (endCursor && request.filter[1]) {
266
+ persistState(database, endCursor, request.filter[1]);
267
+ }
268
+ });
269
+ });
270
+ }
271
+
272
+ exports.KeyValueStore = KeyValueStore;
273
+ exports.sqliteStorage = sqliteStorage;
274
+ exports.useSqliteKeyValueStore = useSqliteKeyValueStore;
@@ -0,0 +1,40 @@
1
+ import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
2
+ import { Database } from 'better-sqlite3';
3
+ import { Cursor, DataFinality } from '@apibara/protocol';
4
+
5
+ type SerializeFn = <T>(value: T) => string;
6
+ type DeserializeFn = <T>(value: string) => T;
7
+
8
+ declare class KeyValueStore {
9
+ private readonly db;
10
+ private readonly endCursor;
11
+ private readonly finality;
12
+ private readonly serialize;
13
+ private readonly deserialize;
14
+ constructor(db: Database, endCursor: Cursor, finality: DataFinality, serialize: SerializeFn, deserialize: DeserializeFn);
15
+ get<T>(key: string): T | undefined;
16
+ put<T>(key: string, value: T): void;
17
+ del(key: string): void;
18
+ }
19
+
20
+ declare function useSqliteKeyValueStore(): KeyValueStore;
21
+ type SqliteStorageOptions = {
22
+ database: Database;
23
+ keyValueStore?: boolean;
24
+ persistState?: boolean;
25
+ serialize?: SerializeFn;
26
+ deserialize?: DeserializeFn;
27
+ };
28
+ /**
29
+ * Creates a plugin that uses SQLite as the storage layer.
30
+ *
31
+ * Supports storing the indexer's state and provides a simple Key-Value store.
32
+ * @param options.database - The SQLite database instance.
33
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
34
+ * @param options.keyValueStore - Whether to enable the Key-Value store. Defaults to true.
35
+ * @param options.serialize - A function to serialize the value to the KV.
36
+ * @param options.deserialize - A function to deserialize the value from the KV.
37
+ */
38
+ declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
39
+
40
+ export { KeyValueStore, type SqliteStorageOptions, sqliteStorage, useSqliteKeyValueStore };
@@ -0,0 +1,40 @@
1
+ import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
2
+ import { Database } from 'better-sqlite3';
3
+ import { Cursor, DataFinality } from '@apibara/protocol';
4
+
5
+ type SerializeFn = <T>(value: T) => string;
6
+ type DeserializeFn = <T>(value: string) => T;
7
+
8
+ declare class KeyValueStore {
9
+ private readonly db;
10
+ private readonly endCursor;
11
+ private readonly finality;
12
+ private readonly serialize;
13
+ private readonly deserialize;
14
+ constructor(db: Database, endCursor: Cursor, finality: DataFinality, serialize: SerializeFn, deserialize: DeserializeFn);
15
+ get<T>(key: string): T | undefined;
16
+ put<T>(key: string, value: T): void;
17
+ del(key: string): void;
18
+ }
19
+
20
+ declare function useSqliteKeyValueStore(): KeyValueStore;
21
+ type SqliteStorageOptions = {
22
+ database: Database;
23
+ keyValueStore?: boolean;
24
+ persistState?: boolean;
25
+ serialize?: SerializeFn;
26
+ deserialize?: DeserializeFn;
27
+ };
28
+ /**
29
+ * Creates a plugin that uses SQLite as the storage layer.
30
+ *
31
+ * Supports storing the indexer's state and provides a simple Key-Value store.
32
+ * @param options.database - The SQLite database instance.
33
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
34
+ * @param options.keyValueStore - Whether to enable the Key-Value store. Defaults to true.
35
+ * @param options.serialize - A function to serialize the value to the KV.
36
+ * @param options.deserialize - A function to deserialize the value from the KV.
37
+ */
38
+ declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
39
+
40
+ export { KeyValueStore, type SqliteStorageOptions, sqliteStorage, useSqliteKeyValueStore };
@@ -0,0 +1,40 @@
1
+ import * as _apibara_indexer_plugins from '@apibara/indexer/plugins';
2
+ import { Database } from 'better-sqlite3';
3
+ import { Cursor, DataFinality } from '@apibara/protocol';
4
+
5
+ type SerializeFn = <T>(value: T) => string;
6
+ type DeserializeFn = <T>(value: string) => T;
7
+
8
+ declare class KeyValueStore {
9
+ private readonly db;
10
+ private readonly endCursor;
11
+ private readonly finality;
12
+ private readonly serialize;
13
+ private readonly deserialize;
14
+ constructor(db: Database, endCursor: Cursor, finality: DataFinality, serialize: SerializeFn, deserialize: DeserializeFn);
15
+ get<T>(key: string): T | undefined;
16
+ put<T>(key: string, value: T): void;
17
+ del(key: string): void;
18
+ }
19
+
20
+ declare function useSqliteKeyValueStore(): KeyValueStore;
21
+ type SqliteStorageOptions = {
22
+ database: Database;
23
+ keyValueStore?: boolean;
24
+ persistState?: boolean;
25
+ serialize?: SerializeFn;
26
+ deserialize?: DeserializeFn;
27
+ };
28
+ /**
29
+ * Creates a plugin that uses SQLite as the storage layer.
30
+ *
31
+ * Supports storing the indexer's state and provides a simple Key-Value store.
32
+ * @param options.database - The SQLite database instance.
33
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
34
+ * @param options.keyValueStore - Whether to enable the Key-Value store. Defaults to true.
35
+ * @param options.serialize - A function to serialize the value to the KV.
36
+ * @param options.deserialize - A function to deserialize the value from the KV.
37
+ */
38
+ declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
39
+
40
+ export { KeyValueStore, type SqliteStorageOptions, sqliteStorage, useSqliteKeyValueStore };
package/dist/index.mjs ADDED
@@ -0,0 +1,270 @@
1
+ import { useIndexerContext } from '@apibara/indexer';
2
+ import { defineIndexerPlugin } from '@apibara/indexer/plugins';
3
+ import { isCursor } from '@apibara/protocol';
4
+
5
+ class SqliteStorageError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "SqliteStorageError";
9
+ }
10
+ }
11
+ async function withTransaction(db, cb) {
12
+ db.prepare("BEGIN TRANSACTION").run();
13
+ try {
14
+ await cb(db);
15
+ } catch (error) {
16
+ db.prepare("ROLLBACK TRANSACTION").run();
17
+ throw error;
18
+ }
19
+ db.prepare("COMMIT TRANSACTION").run();
20
+ }
21
+ function assertInTransaction(db) {
22
+ if (!db.inTransaction) {
23
+ throw new SqliteStorageError("Database is not in transaction");
24
+ }
25
+ }
26
+ function deserialize(str) {
27
+ return JSON.parse(
28
+ str,
29
+ (_, value) => typeof value === "string" && value.match(/^\d+n$/) ? BigInt(value.slice(0, -1)) : value
30
+ );
31
+ }
32
+ function serialize(obj) {
33
+ return JSON.stringify(
34
+ obj,
35
+ (_, value) => typeof value === "bigint" ? `${value.toString()}n` : value,
36
+ " "
37
+ );
38
+ }
39
+
40
+ function initializeKeyValueStore(db) {
41
+ assertInTransaction(db);
42
+ db.exec(statements$1.createTable);
43
+ }
44
+ class KeyValueStore {
45
+ constructor(db, endCursor, finality, serialize, deserialize) {
46
+ this.db = db;
47
+ this.endCursor = endCursor;
48
+ this.finality = finality;
49
+ this.serialize = serialize;
50
+ this.deserialize = deserialize;
51
+ assertInTransaction(db);
52
+ }
53
+ get(key) {
54
+ const row = this.db.prepare(statements$1.get).get(key);
55
+ return row ? this.deserialize(row.v) : void 0;
56
+ }
57
+ put(key, value) {
58
+ this.db.prepare(statements$1.updateToBlock).run(Number(this.endCursor.orderKey), key);
59
+ this.db.prepare(statements$1.insertIntoKvs).run(
60
+ Number(this.endCursor.orderKey),
61
+ key,
62
+ this.serialize(value)
63
+ );
64
+ }
65
+ del(key) {
66
+ this.db.prepare(statements$1.del).run(Number(this.endCursor.orderKey), key);
67
+ }
68
+ }
69
+ const statements$1 = {
70
+ createTable: `
71
+ CREATE TABLE IF NOT EXISTS kvs (
72
+ from_block INTEGER NOT NULL,
73
+ to_block INTEGER,
74
+ k TEXT NOT NULL,
75
+ v BLOB NOT NULL,
76
+ PRIMARY KEY (from_block, k)
77
+ );`,
78
+ get: `
79
+ SELECT v
80
+ FROM kvs
81
+ WHERE k = ? AND to_block IS NULL`,
82
+ updateToBlock: `
83
+ UPDATE kvs
84
+ SET to_block = ?
85
+ WHERE k = ? AND to_block IS NULL`,
86
+ insertIntoKvs: `
87
+ INSERT INTO kvs (from_block, to_block, k, v)
88
+ VALUES (?, NULL, ?, ?)`,
89
+ del: `
90
+ UPDATE kvs
91
+ SET to_block = ?
92
+ WHERE k = ? AND to_block IS NULL`
93
+ };
94
+
95
+ const DEFAULT_INDEXER_ID = "default";
96
+ function initializePersistentState(db) {
97
+ assertInTransaction(db);
98
+ db.exec(statements.createCheckpointsTable);
99
+ db.exec(statements.createFiltersTable);
100
+ }
101
+ function persistState(db, endCursor, filter) {
102
+ assertInTransaction(db);
103
+ db.prepare(statements.putCheckpoint).run(
104
+ DEFAULT_INDEXER_ID,
105
+ Number(endCursor.orderKey),
106
+ endCursor.uniqueKey
107
+ );
108
+ if (filter) {
109
+ db.prepare(statements.updateFilterToBlock).run(
110
+ Number(endCursor.orderKey),
111
+ DEFAULT_INDEXER_ID
112
+ );
113
+ db.prepare(statements.insertFilter).run(
114
+ DEFAULT_INDEXER_ID,
115
+ serialize(filter),
116
+ Number(endCursor.orderKey)
117
+ );
118
+ }
119
+ }
120
+ function getState(db) {
121
+ assertInTransaction(db);
122
+ const storedCursor = db.prepare(
123
+ statements.getCheckpoint
124
+ ).get(DEFAULT_INDEXER_ID);
125
+ const storedFilter = db.prepare(statements.getFilter).get(DEFAULT_INDEXER_ID);
126
+ let cursor;
127
+ let filter;
128
+ if (storedCursor?.order_key) {
129
+ cursor = {
130
+ orderKey: BigInt(storedCursor.order_key),
131
+ uniqueKey: storedCursor.unique_key
132
+ };
133
+ }
134
+ if (storedFilter) {
135
+ filter = deserialize(storedFilter.filter);
136
+ }
137
+ return { cursor, filter };
138
+ }
139
+ const statements = {
140
+ createCheckpointsTable: `
141
+ CREATE TABLE IF NOT EXISTS checkpoints (
142
+ id TEXT NOT NULL PRIMARY KEY,
143
+ order_key INTEGER,
144
+ unique_key TEXT
145
+ );`,
146
+ createFiltersTable: `
147
+ CREATE TABLE IF NOT EXISTS filters (
148
+ id TEXT NOT NULL,
149
+ filter BLOB NOT NULL,
150
+ from_block INTEGER NOT NULL,
151
+ to_block INTEGER,
152
+ PRIMARY KEY (id, from_block)
153
+ );`,
154
+ getCheckpoint: `
155
+ SELECT *
156
+ FROM checkpoints
157
+ WHERE id = ?`,
158
+ putCheckpoint: `
159
+ INSERT INTO checkpoints (id, order_key, unique_key)
160
+ VALUES (?, ?, ?)
161
+ ON CONFLICT(id) DO UPDATE SET
162
+ order_key = excluded.order_key,
163
+ unique_key = excluded.unique_key`,
164
+ delCheckpoint: `
165
+ DELETE FROM checkpoints
166
+ WHERE id = ?`,
167
+ getFilter: `
168
+ SELECT *
169
+ FROM filters
170
+ WHERE id = ? AND to_block IS NULL`,
171
+ updateFilterToBlock: `
172
+ UPDATE filters
173
+ SET to_block = ?
174
+ WHERE id = ? AND to_block IS NULL`,
175
+ insertFilter: `
176
+ INSERT INTO filters (id, filter, from_block)
177
+ VALUES (?, ?, ?)
178
+ ON CONFLICT(id, from_block) DO UPDATE SET
179
+ filter = excluded.filter,
180
+ from_block = excluded.from_block`,
181
+ delFilter: `
182
+ DELETE FROM filters
183
+ WHERE id = ?`
184
+ };
185
+
186
+ const KV_PROPERTY = "_kv_sqlite";
187
+ function useSqliteKeyValueStore() {
188
+ const kv = useIndexerContext()[KV_PROPERTY];
189
+ if (!kv) {
190
+ throw new SqliteStorageError(
191
+ "SQLite key-value store is not available. Did you forget to enable it?"
192
+ );
193
+ }
194
+ return kv;
195
+ }
196
+ function sqliteStorage({
197
+ database,
198
+ persistState: enablePersistState = true,
199
+ keyValueStore: enableKeyValueStore = true,
200
+ serialize: serializeFn = serialize,
201
+ deserialize: deserializeFn = deserialize
202
+ }) {
203
+ return defineIndexerPlugin((indexer) => {
204
+ indexer.hooks.hook("run:before", async () => {
205
+ await withTransaction(database, async (db) => {
206
+ if (enablePersistState) {
207
+ initializePersistentState(db);
208
+ }
209
+ if (enableKeyValueStore) {
210
+ initializeKeyValueStore(db);
211
+ }
212
+ });
213
+ });
214
+ indexer.hooks.hook("connect:before", async ({ request }) => {
215
+ if (!enablePersistState) {
216
+ return;
217
+ }
218
+ return await withTransaction(database, async (db) => {
219
+ const { cursor, filter } = getState(db);
220
+ if (cursor) {
221
+ request.startingCursor = cursor;
222
+ }
223
+ if (filter) {
224
+ request.filter[1] = filter;
225
+ }
226
+ });
227
+ });
228
+ indexer.hooks.hook("handler:middleware", ({ use }) => {
229
+ use(async (ctx, next) => {
230
+ if (!ctx.finality) {
231
+ throw new SqliteStorageError("finality is undefined");
232
+ }
233
+ if (!ctx.endCursor || !isCursor(ctx.endCursor)) {
234
+ throw new SqliteStorageError(
235
+ "endCursor is undefined or not a cursor"
236
+ );
237
+ }
238
+ await withTransaction(database, async (db) => {
239
+ if (enableKeyValueStore) {
240
+ ctx[KV_PROPERTY] = new KeyValueStore(
241
+ db,
242
+ ctx.endCursor,
243
+ ctx.finality,
244
+ serializeFn,
245
+ deserializeFn
246
+ );
247
+ }
248
+ await next();
249
+ if (enablePersistState) {
250
+ persistState(db, ctx.endCursor);
251
+ }
252
+ if (enableKeyValueStore) {
253
+ delete ctx[KV_PROPERTY];
254
+ }
255
+ });
256
+ });
257
+ });
258
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
259
+ if (!enablePersistState) {
260
+ return;
261
+ }
262
+ assertInTransaction(database);
263
+ if (endCursor && request.filter[1]) {
264
+ persistState(database, endCursor, request.filter[1]);
265
+ }
266
+ });
267
+ });
268
+ }
269
+
270
+ export { KeyValueStore, sqliteStorage, useSqliteKeyValueStore };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@apibara/plugin-sqlite",
3
+ "version": "2.0.0-beta.27",
4
+ "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "src",
8
+ "README.md"
9
+ ],
10
+ "main": "./dist/index.mjs",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.cjs",
17
+ "default": "./dist/index.mjs"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "unbuild",
22
+ "typecheck": "tsc --noEmit",
23
+ "lint": "biome check .",
24
+ "lint:fix": "pnpm lint --write",
25
+ "test": "vitest",
26
+ "test:ci": "vitest run"
27
+ },
28
+ "devDependencies": {
29
+ "@types/better-sqlite3": "^7.6.11",
30
+ "@types/node": "^20.14.0",
31
+ "unbuild": "^2.0.0",
32
+ "vitest": "^1.6.0"
33
+ },
34
+ "peerDependencies": {
35
+ "better-sqlite3": "^9.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@apibara/indexer": "2.0.0-beta.28",
39
+ "@apibara/protocol": "2.0.0-beta.28"
40
+ }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,144 @@
1
+ import { useIndexerContext } from "@apibara/indexer";
2
+ import { defineIndexerPlugin } from "@apibara/indexer/plugins";
3
+ import { isCursor } from "@apibara/protocol";
4
+ import type { Database as SqliteDatabase } from "better-sqlite3";
5
+
6
+ import { KeyValueStore, initializeKeyValueStore } from "./kv";
7
+ import {
8
+ getState,
9
+ initializePersistentState,
10
+ persistState,
11
+ } from "./persistence";
12
+ import {
13
+ type DeserializeFn,
14
+ type SerializeFn,
15
+ SqliteStorageError,
16
+ assertInTransaction,
17
+ deserialize,
18
+ serialize,
19
+ withTransaction,
20
+ } from "./utils";
21
+
22
+ const KV_PROPERTY = "_kv_sqlite" as const;
23
+
24
+ export { KeyValueStore } from "./kv";
25
+
26
+ export function useSqliteKeyValueStore(): KeyValueStore {
27
+ const kv = useIndexerContext()[KV_PROPERTY] as KeyValueStore | undefined;
28
+ if (!kv) {
29
+ throw new SqliteStorageError(
30
+ "SQLite key-value store is not available. Did you forget to enable it?",
31
+ );
32
+ }
33
+
34
+ return kv;
35
+ }
36
+
37
+ export type SqliteStorageOptions = {
38
+ database: SqliteDatabase;
39
+ keyValueStore?: boolean;
40
+ persistState?: boolean;
41
+
42
+ serialize?: SerializeFn;
43
+ deserialize?: DeserializeFn;
44
+ };
45
+
46
+ /**
47
+ * Creates a plugin that uses SQLite as the storage layer.
48
+ *
49
+ * Supports storing the indexer's state and provides a simple Key-Value store.
50
+ * @param options.database - The SQLite database instance.
51
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
52
+ * @param options.keyValueStore - Whether to enable the Key-Value store. Defaults to true.
53
+ * @param options.serialize - A function to serialize the value to the KV.
54
+ * @param options.deserialize - A function to deserialize the value from the KV.
55
+ */
56
+ export function sqliteStorage<TFilter, TBlock>({
57
+ database,
58
+ persistState: enablePersistState = true,
59
+ keyValueStore: enableKeyValueStore = true,
60
+ serialize: serializeFn = serialize,
61
+ deserialize: deserializeFn = deserialize,
62
+ }: SqliteStorageOptions) {
63
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
64
+ indexer.hooks.hook("run:before", async () => {
65
+ await withTransaction(database, async (db) => {
66
+ if (enablePersistState) {
67
+ initializePersistentState(db);
68
+ }
69
+
70
+ if (enableKeyValueStore) {
71
+ initializeKeyValueStore(db);
72
+ }
73
+ });
74
+ });
75
+
76
+ indexer.hooks.hook("connect:before", async ({ request }) => {
77
+ if (!enablePersistState) {
78
+ return;
79
+ }
80
+
81
+ return await withTransaction(database, async (db) => {
82
+ const { cursor, filter } = getState<TFilter>(db);
83
+
84
+ if (cursor) {
85
+ request.startingCursor = cursor;
86
+ }
87
+
88
+ if (filter) {
89
+ request.filter[1] = filter;
90
+ }
91
+ });
92
+ });
93
+
94
+ indexer.hooks.hook("handler:middleware", ({ use }) => {
95
+ use(async (ctx, next) => {
96
+ if (!ctx.finality) {
97
+ throw new SqliteStorageError("finality is undefined");
98
+ }
99
+
100
+ if (!ctx.endCursor || !isCursor(ctx.endCursor)) {
101
+ throw new SqliteStorageError(
102
+ "endCursor is undefined or not a cursor",
103
+ );
104
+ }
105
+
106
+ await withTransaction(database, async (db) => {
107
+ if (enableKeyValueStore) {
108
+ ctx[KV_PROPERTY] = new KeyValueStore(
109
+ db,
110
+ ctx.endCursor,
111
+ ctx.finality,
112
+ serializeFn,
113
+ deserializeFn,
114
+ );
115
+ }
116
+
117
+ await next();
118
+
119
+ if (enablePersistState) {
120
+ persistState(db, ctx.endCursor);
121
+ }
122
+
123
+ if (enableKeyValueStore) {
124
+ delete ctx[KV_PROPERTY];
125
+ }
126
+ });
127
+ });
128
+ });
129
+
130
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
131
+ if (!enablePersistState) {
132
+ return;
133
+ }
134
+
135
+ // The connect factory hook is called while indexing a block, so the database should be in a transaction
136
+ // created by the middleware.
137
+ assertInTransaction(database);
138
+
139
+ if (endCursor && request.filter[1]) {
140
+ persistState(database, endCursor, request.filter[1]);
141
+ }
142
+ });
143
+ });
144
+ }
package/src/kv.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { Cursor, DataFinality } from "@apibara/protocol";
2
+ import type { Database } from "better-sqlite3";
3
+
4
+ import {
5
+ type DeserializeFn,
6
+ type SerializeFn,
7
+ assertInTransaction,
8
+ } from "./utils";
9
+
10
+ export function initializeKeyValueStore(db: Database) {
11
+ assertInTransaction(db);
12
+ db.exec(statements.createTable);
13
+ }
14
+
15
+ export class KeyValueStore {
16
+ constructor(
17
+ private readonly db: Database,
18
+ private readonly endCursor: Cursor,
19
+ private readonly finality: DataFinality,
20
+ private readonly serialize: SerializeFn,
21
+ private readonly deserialize: DeserializeFn,
22
+ ) {
23
+ assertInTransaction(db);
24
+ }
25
+
26
+ get<T>(key: string): T | undefined {
27
+ const row = this.db.prepare<string, KeyValueRow>(statements.get).get(key);
28
+
29
+ return row ? this.deserialize(row.v) : undefined;
30
+ }
31
+
32
+ put<T>(key: string, value: T) {
33
+ this.db
34
+ .prepare<[number, string], KeyValueRow>(statements.updateToBlock)
35
+ .run(Number(this.endCursor.orderKey), key);
36
+
37
+ this.db
38
+ .prepare<[number, string, string], KeyValueRow>(statements.insertIntoKvs)
39
+ .run(
40
+ Number(this.endCursor.orderKey),
41
+ key,
42
+ this.serialize(value as Record<string, unknown>),
43
+ );
44
+ }
45
+
46
+ del(key: string) {
47
+ this.db
48
+ .prepare<[number, string], KeyValueRow>(statements.del)
49
+ .run(Number(this.endCursor.orderKey), key);
50
+ }
51
+ }
52
+
53
+ type KeyValueRow = {
54
+ from_block: number;
55
+ to_block: number;
56
+ k: string;
57
+ v: string;
58
+ };
59
+
60
+ const statements = {
61
+ createTable: `
62
+ CREATE TABLE IF NOT EXISTS kvs (
63
+ from_block INTEGER NOT NULL,
64
+ to_block INTEGER,
65
+ k TEXT NOT NULL,
66
+ v BLOB NOT NULL,
67
+ PRIMARY KEY (from_block, k)
68
+ );`,
69
+ get: `
70
+ SELECT v
71
+ FROM kvs
72
+ WHERE k = ? AND to_block IS NULL`,
73
+ updateToBlock: `
74
+ UPDATE kvs
75
+ SET to_block = ?
76
+ WHERE k = ? AND to_block IS NULL`,
77
+ insertIntoKvs: `
78
+ INSERT INTO kvs (from_block, to_block, k, v)
79
+ VALUES (?, NULL, ?, ?)`,
80
+ del: `
81
+ UPDATE kvs
82
+ SET to_block = ?
83
+ WHERE k = ? AND to_block IS NULL`,
84
+ };
@@ -0,0 +1,113 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import type { Database } from "better-sqlite3";
3
+
4
+ import { assertInTransaction, deserialize, serialize } from "./utils";
5
+
6
+ const DEFAULT_INDEXER_ID = "default";
7
+
8
+ export function initializePersistentState(db: Database) {
9
+ assertInTransaction(db);
10
+ db.exec(statements.createCheckpointsTable);
11
+ db.exec(statements.createFiltersTable);
12
+ }
13
+
14
+ export function persistState<TFilter>(
15
+ db: Database,
16
+ endCursor: Cursor,
17
+ filter?: TFilter,
18
+ ) {
19
+ assertInTransaction(db);
20
+
21
+ db.prepare(statements.putCheckpoint).run(
22
+ DEFAULT_INDEXER_ID,
23
+ Number(endCursor.orderKey),
24
+ endCursor.uniqueKey,
25
+ );
26
+
27
+ if (filter) {
28
+ db.prepare(statements.updateFilterToBlock).run(
29
+ Number(endCursor.orderKey),
30
+ DEFAULT_INDEXER_ID,
31
+ );
32
+ db.prepare(statements.insertFilter).run(
33
+ DEFAULT_INDEXER_ID,
34
+ serialize(filter as Record<string, unknown>),
35
+ Number(endCursor.orderKey),
36
+ );
37
+ }
38
+ }
39
+
40
+ export function getState<TFilter>(db: Database) {
41
+ assertInTransaction(db);
42
+ const storedCursor = db
43
+ .prepare<string, { order_key?: number; unique_key?: string }>(
44
+ statements.getCheckpoint,
45
+ )
46
+ .get(DEFAULT_INDEXER_ID);
47
+ const storedFilter = db
48
+ .prepare<string, { filter: string }>(statements.getFilter)
49
+ .get(DEFAULT_INDEXER_ID);
50
+
51
+ let cursor: Cursor | undefined;
52
+ let filter: TFilter | undefined;
53
+
54
+ if (storedCursor?.order_key) {
55
+ cursor = {
56
+ orderKey: BigInt(storedCursor.order_key),
57
+ uniqueKey: storedCursor.unique_key as `0x${string}`,
58
+ };
59
+ }
60
+
61
+ if (storedFilter) {
62
+ filter = deserialize(storedFilter.filter) as TFilter;
63
+ }
64
+
65
+ return { cursor, filter };
66
+ }
67
+
68
+ const statements = {
69
+ createCheckpointsTable: `
70
+ CREATE TABLE IF NOT EXISTS checkpoints (
71
+ id TEXT NOT NULL PRIMARY KEY,
72
+ order_key INTEGER,
73
+ unique_key TEXT
74
+ );`,
75
+ createFiltersTable: `
76
+ CREATE TABLE IF NOT EXISTS filters (
77
+ id TEXT NOT NULL,
78
+ filter BLOB NOT NULL,
79
+ from_block INTEGER NOT NULL,
80
+ to_block INTEGER,
81
+ PRIMARY KEY (id, from_block)
82
+ );`,
83
+ getCheckpoint: `
84
+ SELECT *
85
+ FROM checkpoints
86
+ WHERE id = ?`,
87
+ putCheckpoint: `
88
+ INSERT INTO checkpoints (id, order_key, unique_key)
89
+ VALUES (?, ?, ?)
90
+ ON CONFLICT(id) DO UPDATE SET
91
+ order_key = excluded.order_key,
92
+ unique_key = excluded.unique_key`,
93
+ delCheckpoint: `
94
+ DELETE FROM checkpoints
95
+ WHERE id = ?`,
96
+ getFilter: `
97
+ SELECT *
98
+ FROM filters
99
+ WHERE id = ? AND to_block IS NULL`,
100
+ updateFilterToBlock: `
101
+ UPDATE filters
102
+ SET to_block = ?
103
+ WHERE id = ? AND to_block IS NULL`,
104
+ insertFilter: `
105
+ INSERT INTO filters (id, filter, from_block)
106
+ VALUES (?, ?, ?)
107
+ ON CONFLICT(id, from_block) DO UPDATE SET
108
+ filter = excluded.filter,
109
+ from_block = excluded.from_block`,
110
+ delFilter: `
111
+ DELETE FROM filters
112
+ WHERE id = ?`,
113
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { Database } from "better-sqlite3";
2
+
3
+ export type SerializeFn = <T>(value: T) => string;
4
+ export type DeserializeFn = <T>(value: string) => T;
5
+
6
+ export class SqliteStorageError extends Error {
7
+ constructor(message: string) {
8
+ super(message);
9
+ this.name = "SqliteStorageError";
10
+ }
11
+ }
12
+
13
+ export async function withTransaction(
14
+ db: Database,
15
+ cb: (db: Database) => Promise<void>,
16
+ ) {
17
+ db.prepare("BEGIN TRANSACTION").run();
18
+ try {
19
+ await cb(db);
20
+ } catch (error) {
21
+ db.prepare("ROLLBACK TRANSACTION").run();
22
+ throw error;
23
+ }
24
+ db.prepare("COMMIT TRANSACTION").run();
25
+ }
26
+
27
+ export function assertInTransaction(db: Database) {
28
+ if (!db.inTransaction) {
29
+ throw new SqliteStorageError("Database is not in transaction");
30
+ }
31
+ }
32
+
33
+ export function deserialize<T>(str: string): T {
34
+ return JSON.parse(str, (_, value) =>
35
+ typeof value === "string" && value.match(/^\d+n$/)
36
+ ? BigInt(value.slice(0, -1))
37
+ : value,
38
+ ) as T;
39
+ }
40
+
41
+ export function serialize<T>(obj: T): string {
42
+ return JSON.stringify(
43
+ obj,
44
+ (_, value) => (typeof value === "bigint" ? `${value.toString()}n` : value),
45
+ "\t",
46
+ );
47
+ }