@apibara/indexer 2.0.0-beta.4 → 2.0.0-beta.41
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 +278 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +267 -0
- package/dist/internal/index.cjs +10 -0
- package/dist/internal/index.d.cts +3 -0
- package/dist/internal/index.d.mts +3 -0
- package/dist/internal/index.d.ts +3 -0
- package/dist/internal/index.mjs +8 -0
- package/dist/internal/plugins.cjs +38 -0
- package/dist/internal/plugins.d.cts +13 -0
- package/dist/internal/plugins.d.mts +13 -0
- package/dist/internal/plugins.d.ts +13 -0
- package/dist/internal/plugins.mjs +34 -0
- package/dist/internal/testing.cjs +118 -0
- package/dist/internal/testing.d.cts +42 -0
- package/dist/internal/testing.d.mts +42 -0
- package/dist/internal/testing.d.ts +42 -0
- package/dist/internal/testing.mjs +113 -0
- package/dist/plugins/index.cjs +43 -0
- package/dist/plugins/index.d.cts +18 -0
- package/dist/plugins/index.d.mts +18 -0
- package/dist/plugins/index.d.ts +18 -0
- package/dist/plugins/index.mjs +38 -0
- package/dist/shared/indexer.077335f3.cjs +15 -0
- package/dist/shared/indexer.2416906c.cjs +29 -0
- package/dist/shared/indexer.601ceab0.cjs +7 -0
- package/dist/shared/indexer.9b21ddd2.mjs +5 -0
- package/dist/shared/indexer.a55ad619.mjs +12 -0
- package/dist/shared/indexer.fedcd831.d.cts +100 -0
- package/dist/shared/indexer.fedcd831.d.mts +100 -0
- package/dist/shared/indexer.fedcd831.d.ts +100 -0
- package/dist/shared/indexer.ff25c953.mjs +26 -0
- package/dist/testing/index.cjs +66 -0
- package/dist/testing/index.d.cts +12 -0
- package/dist/testing/index.d.mts +12 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.mjs +60 -0
- package/dist/vcr/index.cjs +92 -0
- package/dist/vcr/index.d.cts +27 -0
- package/dist/vcr/index.d.mts +27 -0
- package/dist/vcr/index.d.ts +27 -0
- package/dist/vcr/index.mjs +78 -0
- package/package.json +43 -41
- package/src/compose.test.ts +76 -0
- package/src/compose.ts +71 -0
- package/src/context.ts +14 -8
- package/src/index.ts +0 -5
- package/src/indexer.test.ts +125 -186
- package/src/indexer.ts +274 -151
- package/src/internal/index.ts +6 -0
- package/src/internal/plugins.ts +1 -0
- package/src/internal/testing.ts +148 -0
- package/src/plugins/config.ts +4 -4
- package/src/plugins/context.ts +40 -0
- package/src/plugins/index.ts +8 -1
- package/src/plugins/logger.ts +30 -0
- package/src/plugins/persistence.ts +24 -187
- package/src/testing/index.ts +58 -3
- package/src/vcr/record.ts +6 -4
- package/src/vcr/replay.ts +8 -18
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useKVStore.ts +0 -12
- package/src/hooks/useSink.ts +0 -13
- package/src/plugins/kv.test.ts +0 -120
- package/src/plugins/kv.ts +0 -132
- package/src/plugins/persistence.test.ts +0 -151
- package/src/sink.ts +0 -36
- package/src/sinks/csv.test.ts +0 -65
- package/src/sinks/csv.ts +0 -159
- package/src/sinks/drizzle/Int8Range.ts +0 -52
- package/src/sinks/drizzle/delete.ts +0 -42
- package/src/sinks/drizzle/drizzle.test.ts +0 -239
- package/src/sinks/drizzle/drizzle.ts +0 -115
- package/src/sinks/drizzle/index.ts +0 -6
- package/src/sinks/drizzle/insert.ts +0 -39
- package/src/sinks/drizzle/select.ts +0 -44
- package/src/sinks/drizzle/transaction.ts +0 -49
- package/src/sinks/drizzle/update.ts +0 -47
- package/src/sinks/drizzle/utils.ts +0 -36
- package/src/sinks/sqlite.test.ts +0 -99
- package/src/sinks/sqlite.ts +0 -170
- package/src/testing/helper.ts +0 -13
- package/src/testing/indexer.ts +0 -35
- package/src/testing/setup.ts +0 -59
- package/src/testing/vcr.ts +0 -54
package/src/plugins/kv.test.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import Database, { type Database as SqliteDatabase } from "better-sqlite3";
|
|
2
|
-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
3
|
-
import { KVStore } from "./kv";
|
|
4
|
-
|
|
5
|
-
type ValueType = { data: bigint };
|
|
6
|
-
|
|
7
|
-
type DatabaseRowType = {
|
|
8
|
-
from_block: number;
|
|
9
|
-
k: string;
|
|
10
|
-
to_block: number;
|
|
11
|
-
v: unknown;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
describe("KVStore", () => {
|
|
15
|
-
let db: SqliteDatabase;
|
|
16
|
-
let store: KVStore;
|
|
17
|
-
const key = "test_key";
|
|
18
|
-
|
|
19
|
-
beforeAll(() => {
|
|
20
|
-
db = new Database(":memory:");
|
|
21
|
-
KVStore.initialize(db);
|
|
22
|
-
store = new KVStore(db, "finalized", { orderKey: 5_000_000n });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterAll(async () => {
|
|
26
|
-
db.close();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("should begin transaction", () => {
|
|
30
|
-
store.beginTransaction();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("should put and get a value", async () => {
|
|
34
|
-
const value = { data: 0n };
|
|
35
|
-
|
|
36
|
-
store.put<ValueType>(key, value);
|
|
37
|
-
const result = store.get<ValueType>(key);
|
|
38
|
-
|
|
39
|
-
expect(result).toEqual(value);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("should commit transaction", async () => {
|
|
43
|
-
store.commitTransaction();
|
|
44
|
-
|
|
45
|
-
const value = { data: 0n };
|
|
46
|
-
|
|
47
|
-
const result = store.get<ValueType>(key);
|
|
48
|
-
|
|
49
|
-
expect(result).toEqual(value);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("should return undefined for non-existing key", async () => {
|
|
53
|
-
const result = store.get<ValueType>("non_existent_key");
|
|
54
|
-
expect(result).toBeUndefined();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("should begin transaction", async () => {
|
|
58
|
-
store.beginTransaction();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("should update an existing value", async () => {
|
|
62
|
-
store = new KVStore(db, "finalized", { orderKey: 5_000_020n });
|
|
63
|
-
|
|
64
|
-
const value = { data: 50n };
|
|
65
|
-
|
|
66
|
-
store.put<ValueType>(key, value);
|
|
67
|
-
const result = store.get<ValueType>(key);
|
|
68
|
-
|
|
69
|
-
expect(result).toEqual(value);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("should delete a value", async () => {
|
|
73
|
-
store.del(key);
|
|
74
|
-
const result = store.get<ValueType>(key);
|
|
75
|
-
|
|
76
|
-
expect(result).toBeUndefined();
|
|
77
|
-
|
|
78
|
-
const rows = db
|
|
79
|
-
.prepare<string, DatabaseRowType>(
|
|
80
|
-
`
|
|
81
|
-
SELECT from_block, to_block, k, v
|
|
82
|
-
FROM kvs
|
|
83
|
-
WHERE k = ?
|
|
84
|
-
`,
|
|
85
|
-
)
|
|
86
|
-
.all(key);
|
|
87
|
-
|
|
88
|
-
// Check that the old is correctly marked with to_block
|
|
89
|
-
expect(rows[0].to_block).toBe(Number(5_000_020n));
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("should rollback transaction", async () => {
|
|
93
|
-
await store.rollbackTransaction();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("should revert the changes to last commit", async () => {
|
|
97
|
-
const rows = db
|
|
98
|
-
.prepare(
|
|
99
|
-
`
|
|
100
|
-
SELECT from_block, to_block, k, v
|
|
101
|
-
FROM kvs
|
|
102
|
-
WHERE k = ?
|
|
103
|
-
`,
|
|
104
|
-
)
|
|
105
|
-
.all([key]);
|
|
106
|
-
|
|
107
|
-
expect(rows).toMatchInlineSnapshot(`
|
|
108
|
-
[
|
|
109
|
-
{
|
|
110
|
-
"from_block": 5000000,
|
|
111
|
-
"k": "test_key",
|
|
112
|
-
"to_block": null,
|
|
113
|
-
"v": "{
|
|
114
|
-
"data": "0n"
|
|
115
|
-
}",
|
|
116
|
-
},
|
|
117
|
-
]
|
|
118
|
-
`);
|
|
119
|
-
});
|
|
120
|
-
});
|
package/src/plugins/kv.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import type { Cursor, DataFinality } from "@apibara/protocol";
|
|
3
|
-
import type { Database as SqliteDatabase, Statement } from "better-sqlite3";
|
|
4
|
-
import { useIndexerContext } from "../context";
|
|
5
|
-
import { deserialize, serialize } from "../vcr";
|
|
6
|
-
import { defineIndexerPlugin } from "./config";
|
|
7
|
-
|
|
8
|
-
export function kv<TFilter, TBlock, TTxnParams>({
|
|
9
|
-
database,
|
|
10
|
-
}: { database: SqliteDatabase }) {
|
|
11
|
-
return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
|
|
12
|
-
indexer.hooks.hook("run:before", () => {
|
|
13
|
-
KVStore.initialize(database);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
indexer.hooks.hook("handler:before", ({ finality, endCursor }) => {
|
|
17
|
-
const ctx = useIndexerContext();
|
|
18
|
-
|
|
19
|
-
assert(endCursor, new Error("endCursor cannot be undefined"));
|
|
20
|
-
|
|
21
|
-
ctx.kv = new KVStore(database, finality, endCursor);
|
|
22
|
-
|
|
23
|
-
ctx.kv.beginTransaction();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
indexer.hooks.hook("handler:after", () => {
|
|
27
|
-
const ctx = useIndexerContext();
|
|
28
|
-
|
|
29
|
-
ctx.kv.commitTransaction();
|
|
30
|
-
|
|
31
|
-
ctx.kv = null;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
indexer.hooks.hook("handler:exception", () => {
|
|
35
|
-
const ctx = useIndexerContext();
|
|
36
|
-
|
|
37
|
-
ctx.kv.rollbackTransaction();
|
|
38
|
-
|
|
39
|
-
ctx.kv = null;
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class KVStore {
|
|
45
|
-
/** Sqlite Queries Prepare Statements */
|
|
46
|
-
private _beginTxnQuery: Statement;
|
|
47
|
-
private _commitTxnQuery: Statement;
|
|
48
|
-
private _rollbackTxnQuery: Statement;
|
|
49
|
-
private _getQuery: Statement<string, { v: string }>;
|
|
50
|
-
private _updateToBlockQuery: Statement<[number, string]>;
|
|
51
|
-
private _insertIntoKvsQuery: Statement<[number, string, string]>;
|
|
52
|
-
private _delQuery: Statement<[number, string]>;
|
|
53
|
-
|
|
54
|
-
constructor(
|
|
55
|
-
private _db: SqliteDatabase,
|
|
56
|
-
private _finality: DataFinality,
|
|
57
|
-
private _endCursor: Cursor,
|
|
58
|
-
) {
|
|
59
|
-
this._beginTxnQuery = this._db.prepare(statements.beginTxn);
|
|
60
|
-
this._commitTxnQuery = this._db.prepare(statements.commitTxn);
|
|
61
|
-
this._rollbackTxnQuery = this._db.prepare(statements.rollbackTxn);
|
|
62
|
-
this._getQuery = this._db.prepare(statements.get);
|
|
63
|
-
this._updateToBlockQuery = this._db.prepare(statements.updateToBlock);
|
|
64
|
-
this._insertIntoKvsQuery = this._db.prepare(statements.insertIntoKvs);
|
|
65
|
-
this._delQuery = this._db.prepare(statements.del);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
static initialize(db: SqliteDatabase) {
|
|
69
|
-
db.prepare(statements.createTable).run();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
beginTransaction() {
|
|
73
|
-
this._beginTxnQuery.run();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
commitTransaction() {
|
|
77
|
-
this._commitTxnQuery.run();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
rollbackTransaction() {
|
|
81
|
-
this._rollbackTxnQuery.run();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
get<T>(key: string): T {
|
|
85
|
-
const row = this._getQuery.get(key);
|
|
86
|
-
|
|
87
|
-
return row ? deserialize(row.v) : undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
put<T>(key: string, value: T) {
|
|
91
|
-
this._updateToBlockQuery.run(Number(this._endCursor.orderKey), key);
|
|
92
|
-
|
|
93
|
-
this._insertIntoKvsQuery.run(
|
|
94
|
-
Number(this._endCursor.orderKey),
|
|
95
|
-
key,
|
|
96
|
-
serialize(value as Record<string, unknown>),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
del(key: string) {
|
|
101
|
-
this._delQuery.run(Number(this._endCursor.orderKey), key);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const statements = {
|
|
106
|
-
beginTxn: "BEGIN TRANSACTION",
|
|
107
|
-
commitTxn: "COMMIT TRANSACTION",
|
|
108
|
-
rollbackTxn: "ROLLBACK TRANSACTION",
|
|
109
|
-
createTable: `
|
|
110
|
-
CREATE TABLE IF NOT EXISTS kvs (
|
|
111
|
-
from_block INTEGER NOT NULL,
|
|
112
|
-
to_block INTEGER,
|
|
113
|
-
k TEXT NOT NULL,
|
|
114
|
-
v BLOB NOT NULL,
|
|
115
|
-
PRIMARY KEY (from_block, k)
|
|
116
|
-
);`,
|
|
117
|
-
get: `
|
|
118
|
-
SELECT v
|
|
119
|
-
FROM kvs
|
|
120
|
-
WHERE k = ? AND to_block IS NULL`,
|
|
121
|
-
updateToBlock: `
|
|
122
|
-
UPDATE kvs
|
|
123
|
-
SET to_block = ?
|
|
124
|
-
WHERE k = ? AND to_block IS NULL`,
|
|
125
|
-
insertIntoKvs: `
|
|
126
|
-
INSERT INTO kvs (from_block, to_block, k, v)
|
|
127
|
-
VALUES (?, NULL, ?, ?)`,
|
|
128
|
-
del: `
|
|
129
|
-
UPDATE kvs
|
|
130
|
-
SET to_block = ?
|
|
131
|
-
WHERE k = ? AND to_block IS NULL`,
|
|
132
|
-
};
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import type { Cursor } from "@apibara/protocol";
|
|
2
|
-
import {
|
|
3
|
-
type MockBlock,
|
|
4
|
-
MockClient,
|
|
5
|
-
type MockFilter,
|
|
6
|
-
} from "@apibara/protocol/testing";
|
|
7
|
-
import Database from "better-sqlite3";
|
|
8
|
-
import { klona } from "klona/full";
|
|
9
|
-
import { describe, expect, it } from "vitest";
|
|
10
|
-
import { run } from "../indexer";
|
|
11
|
-
import { generateMockMessages } from "../testing";
|
|
12
|
-
import { getMockIndexer } from "../testing/indexer";
|
|
13
|
-
import { SqlitePersistence, sqlitePersistence } from "./persistence";
|
|
14
|
-
|
|
15
|
-
describe("Persistence", () => {
|
|
16
|
-
const initDB = () => {
|
|
17
|
-
const db = new Database(":memory:");
|
|
18
|
-
SqlitePersistence.initialize(db);
|
|
19
|
-
return db;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
it("should handle storing and updating a cursor & filter", () => {
|
|
23
|
-
const db = initDB();
|
|
24
|
-
const store = new SqlitePersistence<MockFilter>(db);
|
|
25
|
-
|
|
26
|
-
// Assert there's no data
|
|
27
|
-
let latest = store.get();
|
|
28
|
-
|
|
29
|
-
expect(latest.cursor).toBeUndefined();
|
|
30
|
-
expect(latest.filter).toBeUndefined();
|
|
31
|
-
|
|
32
|
-
// Insert value
|
|
33
|
-
const cursor: Cursor = {
|
|
34
|
-
orderKey: 5_000_000n,
|
|
35
|
-
};
|
|
36
|
-
const filter: MockFilter = {
|
|
37
|
-
filter: "X",
|
|
38
|
-
};
|
|
39
|
-
store.put({ cursor, filter });
|
|
40
|
-
|
|
41
|
-
// Check that value was stored
|
|
42
|
-
latest = store.get();
|
|
43
|
-
|
|
44
|
-
expect(latest.cursor).toEqual({
|
|
45
|
-
orderKey: 5_000_000n,
|
|
46
|
-
uniqueKey: null,
|
|
47
|
-
});
|
|
48
|
-
expect(latest.filter).toEqual({
|
|
49
|
-
filter: "X",
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Update value
|
|
53
|
-
const updatedCursor: Cursor = {
|
|
54
|
-
orderKey: 5_000_010n,
|
|
55
|
-
uniqueKey: "0x1234567890",
|
|
56
|
-
};
|
|
57
|
-
const updatedFilter: MockFilter = {
|
|
58
|
-
filter: "Y",
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
store.put({ cursor: updatedCursor, filter: updatedFilter });
|
|
62
|
-
|
|
63
|
-
// Check that value was updated
|
|
64
|
-
latest = store.get();
|
|
65
|
-
|
|
66
|
-
expect(latest.cursor).toEqual({
|
|
67
|
-
orderKey: 5_000_010n,
|
|
68
|
-
uniqueKey: "0x1234567890",
|
|
69
|
-
});
|
|
70
|
-
expect(latest.filter).toEqual({
|
|
71
|
-
filter: "Y",
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
db.close();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("should handle storing and deleting a cursor & filter", () => {
|
|
78
|
-
const db = initDB();
|
|
79
|
-
const store = new SqlitePersistence(db);
|
|
80
|
-
|
|
81
|
-
// Assert there's no data
|
|
82
|
-
let latest = store.get();
|
|
83
|
-
expect(latest.cursor).toBeUndefined();
|
|
84
|
-
expect(latest.filter).toBeUndefined();
|
|
85
|
-
|
|
86
|
-
// Insert value
|
|
87
|
-
const cursor: Cursor = {
|
|
88
|
-
orderKey: 5_000_000n,
|
|
89
|
-
};
|
|
90
|
-
const filter: MockFilter = {
|
|
91
|
-
filter: "X",
|
|
92
|
-
};
|
|
93
|
-
store.put({ cursor, filter });
|
|
94
|
-
|
|
95
|
-
// Check that value was stored
|
|
96
|
-
latest = store.get();
|
|
97
|
-
expect(latest.cursor).toEqual({
|
|
98
|
-
orderKey: 5_000_000n,
|
|
99
|
-
uniqueKey: null,
|
|
100
|
-
});
|
|
101
|
-
expect(latest.filter).toEqual({
|
|
102
|
-
filter: "X",
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Delete value
|
|
106
|
-
store.del();
|
|
107
|
-
|
|
108
|
-
// Check there's no data
|
|
109
|
-
latest = store.get();
|
|
110
|
-
expect(latest.cursor).toBeUndefined();
|
|
111
|
-
expect(latest.filter).toBeUndefined();
|
|
112
|
-
|
|
113
|
-
db.close();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("should work with indexer and store cursor of last message", async () => {
|
|
117
|
-
const client = new MockClient<MockFilter, MockBlock>((request, options) => {
|
|
118
|
-
return messages;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const db = new Database(":memory:");
|
|
122
|
-
|
|
123
|
-
// create mock indexer with persistence plugin
|
|
124
|
-
const indexer = klona(
|
|
125
|
-
getMockIndexer({
|
|
126
|
-
plugins: [
|
|
127
|
-
sqlitePersistence({
|
|
128
|
-
database: db,
|
|
129
|
-
}),
|
|
130
|
-
],
|
|
131
|
-
}),
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
await run(client, indexer);
|
|
135
|
-
|
|
136
|
-
const store = new SqlitePersistence<MockFilter>(db);
|
|
137
|
-
|
|
138
|
-
const latest = store.get();
|
|
139
|
-
|
|
140
|
-
expect(latest.cursor).toMatchInlineSnapshot(`
|
|
141
|
-
{
|
|
142
|
-
"orderKey": 5000009n,
|
|
143
|
-
"uniqueKey": null,
|
|
144
|
-
}
|
|
145
|
-
`);
|
|
146
|
-
|
|
147
|
-
db.close();
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const messages = generateMockMessages();
|
package/src/sink.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { Cursor, DataFinality } from "@apibara/protocol";
|
|
2
|
-
import consola from "consola";
|
|
3
|
-
|
|
4
|
-
export type SinkData = Record<string, unknown>;
|
|
5
|
-
|
|
6
|
-
export type SinkCursorParams = {
|
|
7
|
-
cursor?: Cursor;
|
|
8
|
-
endCursor?: Cursor;
|
|
9
|
-
finality: DataFinality;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export abstract class Sink<TTxnParams = unknown> {
|
|
13
|
-
abstract transaction(
|
|
14
|
-
{ cursor, endCursor, finality }: SinkCursorParams,
|
|
15
|
-
cb: (params: TTxnParams) => Promise<void>,
|
|
16
|
-
): Promise<void>;
|
|
17
|
-
|
|
18
|
-
abstract invalidate(cursor?: Cursor): Promise<void>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class DefaultSink extends Sink<unknown> {
|
|
22
|
-
async transaction(
|
|
23
|
-
{ cursor, endCursor, finality }: SinkCursorParams,
|
|
24
|
-
cb: (params: unknown) => Promise<void>,
|
|
25
|
-
): Promise<void> {
|
|
26
|
-
await cb({});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async invalidate(cursor?: Cursor) {
|
|
30
|
-
consola.info(`Invalidating cursor ${cursor?.orderKey}`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function defaultSink() {
|
|
35
|
-
return new DefaultSink();
|
|
36
|
-
}
|
package/src/sinks/csv.test.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import {
|
|
3
|
-
type MockBlock,
|
|
4
|
-
MockClient,
|
|
5
|
-
type MockFilter,
|
|
6
|
-
} from "@apibara/protocol/testing";
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
-
import { useSink } from "../hooks";
|
|
9
|
-
import { run } from "../indexer";
|
|
10
|
-
import {} from "../plugins/persistence";
|
|
11
|
-
import { generateMockMessages } from "../testing";
|
|
12
|
-
import { getMockIndexer } from "../testing/indexer";
|
|
13
|
-
import { csv } from "./csv";
|
|
14
|
-
|
|
15
|
-
describe("Run Test", () => {
|
|
16
|
-
async function cleanup() {
|
|
17
|
-
try {
|
|
18
|
-
await fs.unlink("test.csv");
|
|
19
|
-
} catch {}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
beforeEach(async () => {
|
|
23
|
-
await cleanup();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(async () => {
|
|
27
|
-
await cleanup();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("should store in csv file via csv sink", async () => {
|
|
31
|
-
const client = new MockClient<MockFilter, MockBlock>((request, options) => {
|
|
32
|
-
return generateMockMessages();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const sink = csv({ filepath: "test.csv" });
|
|
36
|
-
await run(
|
|
37
|
-
client,
|
|
38
|
-
getMockIndexer({
|
|
39
|
-
sink,
|
|
40
|
-
override: {
|
|
41
|
-
transform: async ({ context, endCursor, block: { data } }) => {
|
|
42
|
-
const { writer } = useSink({ context });
|
|
43
|
-
writer.insert([{ data }]);
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
}),
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const csvData = await fs.readFile("test.csv", "utf-8");
|
|
50
|
-
|
|
51
|
-
expect(csvData).toMatchInlineSnapshot(`
|
|
52
|
-
"5000000,5000000
|
|
53
|
-
5000001,5000001
|
|
54
|
-
5000002,5000002
|
|
55
|
-
5000003,5000003
|
|
56
|
-
5000004,5000004
|
|
57
|
-
5000005,5000005
|
|
58
|
-
5000006,5000006
|
|
59
|
-
5000007,5000007
|
|
60
|
-
5000008,5000008
|
|
61
|
-
5000009,5000009
|
|
62
|
-
"
|
|
63
|
-
`);
|
|
64
|
-
});
|
|
65
|
-
});
|
package/src/sinks/csv.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import type { Cursor } from "@apibara/protocol";
|
|
3
|
-
import { type Options, type Stringifier, stringify } from "csv-stringify";
|
|
4
|
-
import { Sink, type SinkCursorParams, type SinkData } from "../sink";
|
|
5
|
-
|
|
6
|
-
export type CsvArgs = {
|
|
7
|
-
/**
|
|
8
|
-
* csv-stringy options
|
|
9
|
-
* @reference https://csv.js.org/stringify/options/
|
|
10
|
-
*/
|
|
11
|
-
csvOptions?: Options;
|
|
12
|
-
/**
|
|
13
|
-
* filepath for your csv file
|
|
14
|
-
*/
|
|
15
|
-
filepath: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type CsvSinkOptions = {
|
|
19
|
-
/**
|
|
20
|
-
* An optional column name used to store the cursor value. If specified,
|
|
21
|
-
* the value of this column must match the `endCursor.orderKey` for each row.
|
|
22
|
-
*/
|
|
23
|
-
cursorColumn?: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
type TxnContext = {
|
|
27
|
-
buffer: SinkData[];
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type TxnParams = {
|
|
31
|
-
writer: {
|
|
32
|
-
insert: (data: SinkData[]) => void;
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const transactionHelper = (context: TxnContext) => {
|
|
37
|
-
return {
|
|
38
|
-
insert: (data: SinkData[]) => {
|
|
39
|
-
context.buffer.push(...data);
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* A sink that writes data to a CSV file.
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
*
|
|
49
|
-
* ```ts
|
|
50
|
-
* const sink = csv({
|
|
51
|
-
* filepath: "./data.csv",
|
|
52
|
-
* csvOptions: {
|
|
53
|
-
* header: true,
|
|
54
|
-
* },
|
|
55
|
-
* });
|
|
56
|
-
*
|
|
57
|
-
* ...
|
|
58
|
-
* async transform({context, endCursor}){
|
|
59
|
-
* const { writer } = useSink(context);
|
|
60
|
-
* const insertHelper = writer(endCursor);
|
|
61
|
-
*
|
|
62
|
-
* insertHelper.insert([
|
|
63
|
-
* { id: 1, name: "John" },
|
|
64
|
-
* { id: 2, name: "Jane" },
|
|
65
|
-
* ]);
|
|
66
|
-
* }
|
|
67
|
-
*
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
export class CsvSink extends Sink {
|
|
71
|
-
constructor(
|
|
72
|
-
private _stringifier: Stringifier,
|
|
73
|
-
private _config: CsvSinkOptions,
|
|
74
|
-
) {
|
|
75
|
-
super();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private async write({
|
|
79
|
-
data,
|
|
80
|
-
endCursor,
|
|
81
|
-
}: { data: SinkData[]; endCursor?: Cursor }) {
|
|
82
|
-
// adds a "_cursor" property if "cursorColumn" is not specified by user
|
|
83
|
-
data = this.processCursorColumn(data, endCursor);
|
|
84
|
-
// Insert the data into csv
|
|
85
|
-
await this.insertToCSV(data);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async transaction(
|
|
89
|
-
{ cursor, endCursor, finality }: SinkCursorParams,
|
|
90
|
-
cb: (params: TxnParams) => Promise<void>,
|
|
91
|
-
) {
|
|
92
|
-
const context: TxnContext = {
|
|
93
|
-
buffer: [],
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const writer = transactionHelper(context);
|
|
97
|
-
|
|
98
|
-
await cb({ writer });
|
|
99
|
-
await this.write({ data: context.buffer, endCursor });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async invalidate(cursor?: Cursor) {
|
|
103
|
-
// TODO: Implement
|
|
104
|
-
throw new Error("Not implemented");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private async insertToCSV(data: SinkData[]) {
|
|
108
|
-
if (data.length === 0) return;
|
|
109
|
-
|
|
110
|
-
return await new Promise<void>((resolve, reject) => {
|
|
111
|
-
for (const row of data) {
|
|
112
|
-
this._stringifier.write(row, (err) => {
|
|
113
|
-
if (err) throw new Error(err.message);
|
|
114
|
-
|
|
115
|
-
// resolve when all rows are inserted into csv
|
|
116
|
-
if (row === data[data.length - 1]) {
|
|
117
|
-
resolve();
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private processCursorColumn(
|
|
125
|
-
data: SinkData[],
|
|
126
|
-
endCursor?: Cursor,
|
|
127
|
-
): SinkData[] {
|
|
128
|
-
const { cursorColumn } = this._config;
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
cursorColumn &&
|
|
132
|
-
data.some(
|
|
133
|
-
(row) => Number(row[cursorColumn]) !== Number(endCursor?.orderKey),
|
|
134
|
-
)
|
|
135
|
-
) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`Mismatch of ${cursorColumn} and Cursor ${Number(endCursor?.orderKey)}`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (cursorColumn) {
|
|
142
|
-
return data;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return data.map((row) => ({
|
|
146
|
-
...row,
|
|
147
|
-
_cursor: Number(endCursor?.orderKey),
|
|
148
|
-
}));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export const csv = (args: CsvArgs & CsvSinkOptions) => {
|
|
153
|
-
const { csvOptions, filepath, ...sinkOptions } = args;
|
|
154
|
-
const stringifier = stringify({ ...csvOptions });
|
|
155
|
-
|
|
156
|
-
const writeStream = fs.createWriteStream(filepath, { flags: "a" });
|
|
157
|
-
stringifier.pipe(writeStream);
|
|
158
|
-
return new CsvSink(stringifier, sinkOptions);
|
|
159
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { customType } from "drizzle-orm/pg-core";
|
|
2
|
-
import type { Range } from "postgres-range";
|
|
3
|
-
import {
|
|
4
|
-
parse as rangeParse,
|
|
5
|
-
serialize as rangeSerialize,
|
|
6
|
-
} from "postgres-range";
|
|
7
|
-
|
|
8
|
-
type Comparable = string | number;
|
|
9
|
-
|
|
10
|
-
type RangeBound<T extends Comparable> =
|
|
11
|
-
| T
|
|
12
|
-
| {
|
|
13
|
-
value: T;
|
|
14
|
-
inclusive: boolean;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class Int8Range {
|
|
18
|
-
constructor(public readonly range: Range<number>) {}
|
|
19
|
-
|
|
20
|
-
get start(): RangeBound<number> | null {
|
|
21
|
-
return this.range.lower != null
|
|
22
|
-
? {
|
|
23
|
-
value: this.range.lower,
|
|
24
|
-
inclusive: this.range.isLowerBoundClosed(),
|
|
25
|
-
}
|
|
26
|
-
: null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
get end(): RangeBound<number> | null {
|
|
30
|
-
return this.range.upper != null
|
|
31
|
-
? {
|
|
32
|
-
value: this.range.upper,
|
|
33
|
-
inclusive: this.range.isUpperBoundClosed(),
|
|
34
|
-
}
|
|
35
|
-
: null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const int8range = customType<{
|
|
40
|
-
data: Int8Range;
|
|
41
|
-
}>({
|
|
42
|
-
dataType: () => "int8range",
|
|
43
|
-
fromDriver: (value: unknown): Int8Range => {
|
|
44
|
-
if (typeof value !== "string") {
|
|
45
|
-
throw new Error("Expected string");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const parsed = rangeParse(value, (val) => Number.parseInt(val, 10));
|
|
49
|
-
return new Int8Range(parsed);
|
|
50
|
-
},
|
|
51
|
-
toDriver: (value: Int8Range): string => rangeSerialize(value.range),
|
|
52
|
-
});
|