@apibara/indexer 0.4.1 → 2.0.0-beta.3

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.
Files changed (112) hide show
  1. package/README.md +2 -9
  2. package/package.json +66 -45
  3. package/src/context.ts +19 -0
  4. package/src/hooks/index.ts +2 -0
  5. package/src/hooks/useKVStore.ts +12 -0
  6. package/src/hooks/useSink.ts +13 -0
  7. package/src/index.ts +7 -1
  8. package/src/indexer.test.ts +491 -0
  9. package/src/indexer.ts +322 -0
  10. package/src/otel.ts +3 -0
  11. package/src/plugins/config.ts +11 -0
  12. package/src/plugins/kv.test.ts +120 -0
  13. package/src/plugins/kv.ts +132 -0
  14. package/src/plugins/persistence.test.ts +151 -0
  15. package/src/plugins/persistence.ts +202 -0
  16. package/src/sink.ts +36 -0
  17. package/src/sinks/csv.test.ts +65 -0
  18. package/src/sinks/csv.ts +159 -0
  19. package/src/sinks/drizzle/Int8Range.ts +52 -0
  20. package/src/sinks/drizzle/delete.ts +42 -0
  21. package/src/sinks/drizzle/drizzle.test.ts +239 -0
  22. package/src/sinks/drizzle/drizzle.ts +115 -0
  23. package/src/sinks/drizzle/index.ts +6 -0
  24. package/src/sinks/drizzle/insert.ts +39 -0
  25. package/src/sinks/drizzle/select.ts +44 -0
  26. package/src/sinks/drizzle/transaction.ts +49 -0
  27. package/src/sinks/drizzle/update.ts +47 -0
  28. package/src/sinks/drizzle/utils.ts +36 -0
  29. package/src/sinks/sqlite.test.ts +99 -0
  30. package/src/sinks/sqlite.ts +170 -0
  31. package/src/testing/helper.ts +13 -0
  32. package/src/testing/index.ts +3 -0
  33. package/src/testing/indexer.ts +35 -0
  34. package/src/testing/setup.ts +59 -0
  35. package/src/testing/vcr.ts +54 -0
  36. package/src/vcr/config.ts +11 -0
  37. package/src/vcr/helper.ts +27 -0
  38. package/src/vcr/index.ts +4 -0
  39. package/src/vcr/record.ts +45 -0
  40. package/src/vcr/replay.ts +51 -0
  41. package/LICENSE.txt +0 -202
  42. package/dist/config.d.ts +0 -35
  43. package/dist/config.js +0 -2
  44. package/dist/config.js.map +0 -1
  45. package/dist/config.test-d.d.ts +0 -1
  46. package/dist/config.test-d.js +0 -38
  47. package/dist/config.test-d.js.map +0 -1
  48. package/dist/index.js +0 -2
  49. package/dist/index.js.map +0 -1
  50. package/dist/sink/console.d.ts +0 -10
  51. package/dist/sink/console.js +0 -2
  52. package/dist/sink/console.js.map +0 -1
  53. package/dist/sink/console.test-d.d.ts +0 -1
  54. package/dist/sink/console.test-d.js +0 -12
  55. package/dist/sink/console.test-d.js.map +0 -1
  56. package/dist/sink/mongo.d.ts +0 -14
  57. package/dist/sink/mongo.js +0 -2
  58. package/dist/sink/mongo.js.map +0 -1
  59. package/dist/sink/parquet.d.ts +0 -9
  60. package/dist/sink/parquet.js +0 -2
  61. package/dist/sink/parquet.js.map +0 -1
  62. package/dist/sink/postgres.d.ts +0 -10
  63. package/dist/sink/postgres.js +0 -2
  64. package/dist/sink/postgres.js.map +0 -1
  65. package/dist/sink/webhook.d.ts +0 -12
  66. package/dist/sink/webhook.js +0 -2
  67. package/dist/sink/webhook.js.map +0 -1
  68. package/dist/starknet/block.d.ts +0 -409
  69. package/dist/starknet/block.js +0 -2
  70. package/dist/starknet/block.js.map +0 -1
  71. package/dist/starknet/block.test-d.d.ts +0 -1
  72. package/dist/starknet/block.test-d.js +0 -95
  73. package/dist/starknet/block.test-d.js.map +0 -1
  74. package/dist/starknet/felt.d.ts +0 -11
  75. package/dist/starknet/felt.js +0 -25
  76. package/dist/starknet/felt.js.map +0 -1
  77. package/dist/starknet/felt.test.d.ts +0 -1
  78. package/dist/starknet/felt.test.js +0 -16
  79. package/dist/starknet/felt.test.js.map +0 -1
  80. package/dist/starknet/filter.d.ts +0 -175
  81. package/dist/starknet/filter.js +0 -2
  82. package/dist/starknet/filter.js.map +0 -1
  83. package/dist/starknet/filter.test-d.d.ts +0 -1
  84. package/dist/starknet/filter.test-d.js +0 -166
  85. package/dist/starknet/filter.test-d.js.map +0 -1
  86. package/dist/starknet/index.d.ts +0 -10
  87. package/dist/starknet/index.js +0 -5
  88. package/dist/starknet/index.js.map +0 -1
  89. package/dist/starknet/parser.d.ts +0 -16
  90. package/dist/starknet/parser.js +0 -49
  91. package/dist/starknet/parser.js.map +0 -1
  92. package/dist/starknet/parser.test.d.ts +0 -1
  93. package/dist/starknet/parser.test.js +0 -50
  94. package/dist/starknet/parser.test.js.map +0 -1
  95. package/src/config.test-d.ts +0 -55
  96. package/src/config.ts +0 -47
  97. package/src/sink/console.test-d.ts +0 -14
  98. package/src/sink/console.ts +0 -11
  99. package/src/sink/mongo.ts +0 -14
  100. package/src/sink/parquet.ts +0 -9
  101. package/src/sink/postgres.ts +0 -10
  102. package/src/sink/webhook.ts +0 -12
  103. package/src/starknet/block.test-d.ts +0 -112
  104. package/src/starknet/block.ts +0 -469
  105. package/src/starknet/felt.test.ts +0 -25
  106. package/src/starknet/felt.ts +0 -30
  107. package/src/starknet/filter.test-d.ts +0 -197
  108. package/src/starknet/filter.ts +0 -205
  109. package/src/starknet/index.ts +0 -12
  110. package/src/starknet/parser.test.ts +0 -67
  111. package/src/starknet/parser.ts +0 -69
  112. /package/{dist/index.d.ts → src/plugins/index.ts} +0 -0
@@ -0,0 +1,151 @@
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();
@@ -0,0 +1,202 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import type { Database as SqliteDatabase, Statement } from "better-sqlite3";
3
+ import { deserialize, serialize } from "../vcr";
4
+ import { defineIndexerPlugin } from "./config";
5
+
6
+ export function sqlitePersistence<TFilter, TBlock, TTxnParams>({
7
+ database,
8
+ }: { database: SqliteDatabase }) {
9
+ return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
10
+ let store: SqlitePersistence<TFilter>;
11
+
12
+ indexer.hooks.hook("run:before", () => {
13
+ SqlitePersistence.initialize(database);
14
+
15
+ store = new SqlitePersistence(database);
16
+ });
17
+
18
+ indexer.hooks.hook("connect:before", ({ request }) => {
19
+ const { cursor, filter } = store.get();
20
+
21
+ if (cursor) {
22
+ request.startingCursor = cursor;
23
+ }
24
+
25
+ if (filter) {
26
+ request.filter[1] = filter;
27
+ }
28
+ });
29
+
30
+ indexer.hooks.hook("transaction:commit", ({ endCursor }) => {
31
+ if (endCursor) {
32
+ store.put({ cursor: endCursor });
33
+ }
34
+ });
35
+
36
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
37
+ if (request.filter[1]) {
38
+ store.put({ cursor: endCursor, filter: request.filter[1] });
39
+ }
40
+ });
41
+ });
42
+ }
43
+
44
+ export class SqlitePersistence<TFilter> {
45
+ /** Sqlite Queries Prepare Statements */
46
+ private _getCheckpointQuery: Statement<string, CheckpointRow>;
47
+ private _putCheckpointQuery: Statement<
48
+ [string, number, `0x${string}` | undefined]
49
+ >;
50
+ private _delCheckpointQuery: Statement<string>;
51
+ private _getFilterQuery: Statement<string, FilterRow>;
52
+ private _updateFilterToBlockQuery: Statement<[number, string]>;
53
+ private _insertFilterQuery: Statement<[string, string, number]>;
54
+ private _delFilterQuery: Statement<string>;
55
+
56
+ constructor(private _db: SqliteDatabase) {
57
+ this._getCheckpointQuery = this._db.prepare(statements.getCheckpoint);
58
+ this._putCheckpointQuery = this._db.prepare(statements.putCheckpoint);
59
+ this._delCheckpointQuery = this._db.prepare(statements.delCheckpoint);
60
+ this._getFilterQuery = this._db.prepare(statements.getFilter);
61
+ this._updateFilterToBlockQuery = this._db.prepare(
62
+ statements.updateFilterToBlock,
63
+ );
64
+ this._insertFilterQuery = this._db.prepare(statements.insertFilter);
65
+ this._delFilterQuery = this._db.prepare(statements.delFilter);
66
+ }
67
+
68
+ static initialize(db: SqliteDatabase) {
69
+ db.prepare(statements.createCheckpointsTable).run();
70
+ db.prepare(statements.createFiltersTable).run();
71
+ }
72
+
73
+ public get(): { cursor?: Cursor; filter?: TFilter } {
74
+ const cursor = this._getCheckpoint();
75
+ const filter = this._getFilter();
76
+
77
+ return { cursor, filter };
78
+ }
79
+
80
+ public put({ cursor, filter }: { cursor?: Cursor; filter?: TFilter }) {
81
+ if (cursor) {
82
+ this._putCheckpoint(cursor);
83
+
84
+ if (filter) {
85
+ this._putFilter(filter, cursor);
86
+ }
87
+ }
88
+ }
89
+
90
+ public del() {
91
+ this._delCheckpoint();
92
+ this._delFilter();
93
+ }
94
+
95
+ // --- CHECKPOINTS TABLE METHODS ---
96
+
97
+ private _getCheckpoint(): Cursor | undefined {
98
+ const row = this._getCheckpointQuery.get("default");
99
+
100
+ if (!row) return undefined;
101
+
102
+ return { orderKey: BigInt(row.order_key), uniqueKey: row.unique_key };
103
+ }
104
+
105
+ private _putCheckpoint(cursor: Cursor) {
106
+ this._putCheckpointQuery.run(
107
+ "default",
108
+ Number(cursor.orderKey),
109
+ cursor.uniqueKey,
110
+ );
111
+ }
112
+
113
+ private _delCheckpoint() {
114
+ this._delCheckpointQuery.run("default");
115
+ }
116
+
117
+ // --- FILTERS TABLE METHODS ---
118
+
119
+ private _getFilter(): TFilter | undefined {
120
+ const row = this._getFilterQuery.get("default");
121
+
122
+ if (!row) return undefined;
123
+
124
+ return deserialize(row.filter) as TFilter;
125
+ }
126
+
127
+ private _putFilter(filter: TFilter, endCursor: Cursor) {
128
+ this._updateFilterToBlockQuery.run(Number(endCursor.orderKey), "default");
129
+ this._insertFilterQuery.run(
130
+ "default",
131
+ serialize(filter as Record<string, unknown>),
132
+ Number(endCursor.orderKey),
133
+ );
134
+ }
135
+
136
+ private _delFilter() {
137
+ this._delFilterQuery.run("default");
138
+ }
139
+ }
140
+
141
+ const statements = {
142
+ beginTxn: "BEGIN TRANSACTION",
143
+ commitTxn: "COMMIT TRANSACTION",
144
+ rollbackTxn: "ROLLBACK TRANSACTION",
145
+ createCheckpointsTable: `
146
+ CREATE TABLE IF NOT EXISTS checkpoints (
147
+ id TEXT NOT NULL PRIMARY KEY,
148
+ order_key INTEGER NOT NULL,
149
+ unique_key TEXT
150
+ );`,
151
+ createFiltersTable: `
152
+ CREATE TABLE IF NOT EXISTS filters (
153
+ id TEXT NOT NULL,
154
+ filter BLOB NOT NULL,
155
+ from_block INTEGER NOT NULL,
156
+ to_block INTEGER,
157
+ PRIMARY KEY (id, from_block)
158
+ );`,
159
+ getCheckpoint: `
160
+ SELECT *
161
+ FROM checkpoints
162
+ WHERE id = ?`,
163
+ putCheckpoint: `
164
+ INSERT INTO checkpoints (id, order_key, unique_key)
165
+ VALUES (?, ?, ?)
166
+ ON CONFLICT(id) DO UPDATE SET
167
+ order_key = excluded.order_key,
168
+ unique_key = excluded.unique_key`,
169
+ delCheckpoint: `
170
+ DELETE FROM checkpoints
171
+ WHERE id = ?`,
172
+ getFilter: `
173
+ SELECT *
174
+ FROM filters
175
+ WHERE id = ? AND to_block IS NULL`,
176
+ updateFilterToBlock: `
177
+ UPDATE filters
178
+ SET to_block = ?
179
+ WHERE id = ? AND to_block IS NULL`,
180
+ insertFilter: `
181
+ INSERT INTO filters (id, filter, from_block)
182
+ VALUES (?, ?, ?)
183
+ ON CONFLICT(id, from_block) DO UPDATE SET
184
+ filter = excluded.filter,
185
+ from_block = excluded.from_block`,
186
+ delFilter: `
187
+ DELETE FROM filters
188
+ WHERE id = ?`,
189
+ };
190
+
191
+ export type CheckpointRow = {
192
+ id: string;
193
+ order_key: number;
194
+ unique_key?: `0x${string}`;
195
+ };
196
+
197
+ export type FilterRow = {
198
+ id: string;
199
+ filter: string;
200
+ from_block: number;
201
+ to_block?: number;
202
+ };
package/src/sink.ts ADDED
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,65 @@
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
+ });
@@ -0,0 +1,159 @@
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
+ };
@@ -0,0 +1,52 @@
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
+ });
@@ -0,0 +1,42 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import {
3
+ type ExtractTablesWithRelations,
4
+ type SQL,
5
+ type TablesRelationalConfig,
6
+ sql,
7
+ } from "drizzle-orm";
8
+ import type {
9
+ PgQueryResultHKT,
10
+ PgTable,
11
+ PgTransaction,
12
+ PgUpdateBase,
13
+ } from "drizzle-orm/pg-core";
14
+
15
+ export class DrizzleSinkDelete<
16
+ TTable extends PgTable,
17
+ TQueryResult extends PgQueryResultHKT,
18
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
19
+ TSchema extends
20
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
21
+ > {
22
+ constructor(
23
+ private db: PgTransaction<TQueryResult, TFullSchema, TSchema>,
24
+ private table: TTable,
25
+ private endCursor?: Cursor,
26
+ ) {}
27
+
28
+ //@ts-ignore
29
+ where(where: SQL): Omit<
30
+ // for type safety used PgUpdateBase instead of PgDeleteBase
31
+ PgUpdateBase<TTable, TQueryResult, undefined, false, "where">,
32
+ "where"
33
+ > {
34
+ return this.db
35
+ .update(this.table)
36
+ .set({
37
+ // @ts-ignore
38
+ _cursor: sql`int8range(lower(_cursor), ${Number(this.endCursor?.orderKey!)}, '[)')`,
39
+ })
40
+ .where(where);
41
+ }
42
+ }