@apibara/indexer 2.0.0-beta.0 → 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 (66) hide show
  1. package/package.json +30 -19
  2. package/src/context.ts +8 -3
  3. package/src/hooks/index.ts +1 -0
  4. package/src/hooks/useSink.ts +13 -0
  5. package/src/index.ts +1 -0
  6. package/src/indexer.test.ts +70 -41
  7. package/src/indexer.ts +168 -168
  8. package/src/plugins/config.ts +4 -4
  9. package/src/plugins/kv.ts +2 -2
  10. package/src/plugins/persistence.test.ts +10 -6
  11. package/src/plugins/persistence.ts +3 -3
  12. package/src/sink.ts +21 -24
  13. package/src/sinks/csv.test.ts +15 -3
  14. package/src/sinks/csv.ts +68 -7
  15. package/src/sinks/drizzle/Int8Range.ts +52 -0
  16. package/src/sinks/drizzle/delete.ts +42 -0
  17. package/src/sinks/drizzle/drizzle.test.ts +239 -0
  18. package/src/sinks/drizzle/drizzle.ts +115 -0
  19. package/src/sinks/drizzle/index.ts +6 -0
  20. package/src/sinks/drizzle/insert.ts +39 -0
  21. package/src/sinks/drizzle/select.ts +44 -0
  22. package/src/sinks/drizzle/transaction.ts +49 -0
  23. package/src/sinks/drizzle/update.ts +47 -0
  24. package/src/sinks/drizzle/utils.ts +36 -0
  25. package/src/sinks/sqlite.test.ts +13 -1
  26. package/src/sinks/sqlite.ts +65 -5
  27. package/src/testing/indexer.ts +15 -8
  28. package/src/testing/setup.ts +5 -5
  29. package/src/testing/vcr.ts +42 -4
  30. package/src/vcr/record.ts +2 -2
  31. package/src/vcr/replay.ts +3 -3
  32. package/.turbo/turbo-build.log +0 -37
  33. package/CHANGELOG.md +0 -83
  34. package/LICENSE.txt +0 -202
  35. package/build.config.ts +0 -16
  36. package/dist/index.cjs +0 -34
  37. package/dist/index.d.cts +0 -21
  38. package/dist/index.d.mts +0 -21
  39. package/dist/index.d.ts +0 -21
  40. package/dist/index.mjs +0 -19
  41. package/dist/shared/indexer.371c0482.mjs +0 -15
  42. package/dist/shared/indexer.3852a4d3.d.ts +0 -91
  43. package/dist/shared/indexer.50aa7ab0.mjs +0 -268
  44. package/dist/shared/indexer.7c118fb5.d.cts +0 -28
  45. package/dist/shared/indexer.7c118fb5.d.mts +0 -28
  46. package/dist/shared/indexer.7c118fb5.d.ts +0 -28
  47. package/dist/shared/indexer.a27bcb35.d.cts +0 -91
  48. package/dist/shared/indexer.c8ef02ea.cjs +0 -289
  49. package/dist/shared/indexer.e05aedca.cjs +0 -19
  50. package/dist/shared/indexer.f7dd57e5.d.mts +0 -91
  51. package/dist/sinks/csv.cjs +0 -66
  52. package/dist/sinks/csv.d.cts +0 -34
  53. package/dist/sinks/csv.d.mts +0 -34
  54. package/dist/sinks/csv.d.ts +0 -34
  55. package/dist/sinks/csv.mjs +0 -59
  56. package/dist/sinks/sqlite.cjs +0 -71
  57. package/dist/sinks/sqlite.d.cts +0 -41
  58. package/dist/sinks/sqlite.d.mts +0 -41
  59. package/dist/sinks/sqlite.d.ts +0 -41
  60. package/dist/sinks/sqlite.mjs +0 -68
  61. package/dist/testing/index.cjs +0 -63
  62. package/dist/testing/index.d.cts +0 -29
  63. package/dist/testing/index.d.mts +0 -29
  64. package/dist/testing/index.d.ts +0 -29
  65. package/dist/testing/index.mjs +0 -59
  66. package/tsconfig.json +0 -11
package/src/sinks/csv.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import type { Cursor } from "@apibara/protocol";
3
3
  import { type Options, type Stringifier, stringify } from "csv-stringify";
4
- import { Sink, type SinkData, type SinkWriteArgs } from "../sink";
4
+ import { Sink, type SinkCursorParams, type SinkData } from "../sink";
5
5
 
6
6
  export type CsvArgs = {
7
7
  /**
@@ -23,6 +23,50 @@ export type CsvSinkOptions = {
23
23
  cursorColumn?: string;
24
24
  };
25
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
+ */
26
70
  export class CsvSink extends Sink {
27
71
  constructor(
28
72
  private _stringifier: Stringifier,
@@ -31,14 +75,33 @@ export class CsvSink extends Sink {
31
75
  super();
32
76
  }
33
77
 
34
- async write({ data, endCursor, finality }: SinkWriteArgs) {
35
- await this.callHook("write", { data });
78
+ private async write({
79
+ data,
80
+ endCursor,
81
+ }: { data: SinkData[]; endCursor?: Cursor }) {
36
82
  // adds a "_cursor" property if "cursorColumn" is not specified by user
37
83
  data = this.processCursorColumn(data, endCursor);
38
84
  // Insert the data into csv
39
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
+ }
40
101
 
41
- await this.callHook("flush", { endCursor, finality });
102
+ async invalidate(cursor?: Cursor) {
103
+ // TODO: Implement
104
+ throw new Error("Not implemented");
42
105
  }
43
106
 
44
107
  private async insertToCSV(data: SinkData[]) {
@@ -86,9 +149,7 @@ export class CsvSink extends Sink {
86
149
  }
87
150
  }
88
151
 
89
- export const csv = <TData extends Record<string, unknown>>(
90
- args: CsvArgs & CsvSinkOptions,
91
- ) => {
152
+ export const csv = (args: CsvArgs & CsvSinkOptions) => {
92
153
  const { csvOptions, filepath, ...sinkOptions } = args;
93
154
  const stringifier = stringify({ ...csvOptions });
94
155
 
@@ -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
+ }
@@ -0,0 +1,239 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import {
3
+ type MockBlock,
4
+ MockClient,
5
+ type MockFilter,
6
+ } from "@apibara/protocol/testing";
7
+ import { asc, eq, sql } from "drizzle-orm";
8
+ import { drizzle } from "drizzle-orm/node-postgres";
9
+ import { serial, text } from "drizzle-orm/pg-core";
10
+ import { Client } from "pg";
11
+ import { beforeAll, beforeEach, describe, expect, it } from "vitest";
12
+ import {
13
+ type Int8Range,
14
+ drizzle as drizzleSink,
15
+ getDrizzleCursor,
16
+ pgTable,
17
+ } from ".";
18
+ import { useSink } from "../../hooks";
19
+ import { run } from "../../indexer";
20
+ import { generateMockMessages } from "../../testing";
21
+ import { getMockIndexer } from "../../testing/indexer";
22
+
23
+ const testTable = pgTable("test_table", {
24
+ id: serial("id").primaryKey(),
25
+ data: text("data"),
26
+ });
27
+
28
+ const client = new Client({
29
+ connectionString: "postgres://postgres:postgres@localhost:5432/postgres",
30
+ });
31
+
32
+ await client.connect();
33
+
34
+ const db = drizzle(client);
35
+
36
+ describe("Drizzle Test", () => {
37
+ beforeAll(async () => {
38
+ // drop test_table if exists
39
+ await db.execute(sql`DROP TABLE IF EXISTS test_table`);
40
+ // create test_table with db
41
+ await db.execute(
42
+ sql`CREATE TABLE test_table (id SERIAL PRIMARY KEY, data TEXT, _cursor INT8RANGE)`,
43
+ );
44
+ });
45
+
46
+ beforeEach(async () => {
47
+ await db.delete(testTable).execute();
48
+ });
49
+
50
+ it("should insert data", async () => {
51
+ const client = new MockClient<MockFilter, MockBlock>((request, options) => {
52
+ return generateMockMessages(5);
53
+ });
54
+
55
+ const sink = drizzleSink({ database: db, tables: [testTable] });
56
+
57
+ const indexer = getMockIndexer({
58
+ sink,
59
+ override: {
60
+ transform: async ({ context, endCursor, block: { data } }) => {
61
+ const { db } = useSink({ context });
62
+ // Insert a new row into the test_table
63
+ // The id is set to the current cursor's orderKey
64
+ // The data is set to the block data
65
+ await db
66
+ .insert(testTable)
67
+ .values([{ id: Number(endCursor?.orderKey), data }]);
68
+ },
69
+ },
70
+ });
71
+
72
+ await run(client, indexer);
73
+
74
+ const result = await db.select().from(testTable).orderBy(asc(testTable.id));
75
+
76
+ expect(result).toHaveLength(5);
77
+ expect(result[0].data).toBe("5000000");
78
+ expect(result[2].data).toBe("5000002");
79
+ });
80
+
81
+ it("should update data", async () => {
82
+ const client = new MockClient<MockFilter, MockBlock>((request, options) => {
83
+ return generateMockMessages(5);
84
+ });
85
+
86
+ const sink = drizzleSink({ database: db, tables: [testTable] });
87
+
88
+ const indexer = getMockIndexer({
89
+ sink,
90
+ override: {
91
+ transform: async ({ context, endCursor, block: { data } }) => {
92
+ const { db } = useSink({ context });
93
+
94
+ // insert data for each message in db
95
+ await db
96
+ .insert(testTable)
97
+ .values([{ id: Number(endCursor?.orderKey), data }]);
98
+
99
+ // update data for id 5000002 when orderKey is 5000004
100
+ // this is to test if the update query is working
101
+ if (endCursor?.orderKey === 5000004n) {
102
+ await db
103
+ .update(testTable)
104
+ .set({ data: "0000000" })
105
+ .where(eq(testTable.id, 5000002));
106
+ }
107
+ },
108
+ },
109
+ });
110
+
111
+ await run(client, indexer);
112
+
113
+ const result = await db.select().from(testTable).orderBy(asc(testTable.id));
114
+
115
+ expect(result).toHaveLength(5);
116
+ expect(result[2].data).toBe("0000000");
117
+ });
118
+
119
+ it("should delete data", async () => {
120
+ const client = new MockClient<MockFilter, MockBlock>((request, options) => {
121
+ return generateMockMessages(5);
122
+ });
123
+
124
+ const sink = drizzleSink({ database: db, tables: [testTable] });
125
+
126
+ const indexer = getMockIndexer({
127
+ sink,
128
+ override: {
129
+ transform: async ({ context, endCursor, block: { data } }) => {
130
+ const { db } = useSink({ context });
131
+
132
+ // insert data for each message in db
133
+ await db
134
+ .insert(testTable)
135
+ .values([{ id: Number(endCursor?.orderKey), data }]);
136
+
137
+ // delete data for id 5000002 when orderKey is 5000004
138
+ // this is to test if the delete query is working
139
+ if (endCursor?.orderKey === 5000004n) {
140
+ await db.delete(testTable).where(eq(testTable.id, 5000002));
141
+ }
142
+ },
143
+ },
144
+ });
145
+
146
+ await run(client, indexer);
147
+
148
+ const result = await db.select().from(testTable).orderBy(asc(testTable.id));
149
+
150
+ expect(result).toHaveLength(5);
151
+
152
+ // as when you run delete query on a data, it isnt literally deleted from the db,
153
+ // instead, we just update the upper bound of that row to the current cursor
154
+ // check if the cursor upper bound has been set correctly
155
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
156
+ expect(((result[2] as any)._cursor as Int8Range).range.upper).toBe(5000004);
157
+ });
158
+
159
+ it("should select data", async () => {
160
+ const client = new MockClient<MockFilter, MockBlock>((request, options) => {
161
+ return generateMockMessages(5);
162
+ });
163
+
164
+ const sink = drizzleSink({ database: db, tables: [testTable] });
165
+
166
+ let result: (typeof testTable.$inferSelect)[] = [];
167
+
168
+ const indexer = getMockIndexer({
169
+ sink,
170
+ override: {
171
+ transform: async ({ context, endCursor, block: { data } }) => {
172
+ const { db } = useSink({ context });
173
+
174
+ // insert data for each message in db
175
+ await db
176
+ .insert(testTable)
177
+ .values([{ id: Number(endCursor?.orderKey), data }]);
178
+
179
+ // delete data for id 5000002 when orderKey is 5000004
180
+ // this will update the upper bound of the row with id 5000002 from infinity to 5000004
181
+ // so when we select all rows, row with id 5000002 will not be included
182
+ // as when we run select query it should only return rows with upper bound infinity
183
+ if (endCursor?.orderKey === 5000003n) {
184
+ await db.delete(testTable).where(eq(testTable.id, 5000002));
185
+ }
186
+
187
+ // when on last message of mock stream, select all rows from db
188
+ if (endCursor?.orderKey === 5000004n) {
189
+ result = await db
190
+ .select()
191
+ .from(testTable)
192
+ .orderBy(asc(testTable.id));
193
+ }
194
+ },
195
+ },
196
+ });
197
+
198
+ await run(client, indexer);
199
+
200
+ expect(result).toHaveLength(4);
201
+ expect(result.find((r) => r.id === 5000002)).toBeUndefined();
202
+ // check if all rows are still in db
203
+ const allRows = await db.select().from(testTable);
204
+ expect(allRows).toHaveLength(5);
205
+ });
206
+
207
+ it("should invalidate data correctly", async () => {
208
+ const sink = drizzleSink({ database: db, tables: [testTable] });
209
+
210
+ // Insert some test data
211
+ await db.insert(testTable).values(
212
+ // @ts-ignore
213
+ [
214
+ { id: 1, data: "data1", _cursor: getDrizzleCursor([1n, 5n]) },
215
+ { id: 2, data: "data2", _cursor: getDrizzleCursor([2n, 5n]) },
216
+ { id: 3, data: "data3", _cursor: getDrizzleCursor(3n) },
217
+ { id: 4, data: "data4", _cursor: getDrizzleCursor(4n) },
218
+ { id: 5, data: "data5", _cursor: getDrizzleCursor(5n) },
219
+ ],
220
+ );
221
+
222
+ // Create a cursor at position 3
223
+ const cursor: Cursor = { orderKey: 3n };
224
+
225
+ // Invalidate data
226
+ await sink.invalidate(cursor);
227
+
228
+ // Check the results
229
+ const result = await db.select().from(testTable).orderBy(asc(testTable.id));
230
+
231
+ expect(result).toHaveLength(3);
232
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
233
+ expect(((result[0] as any)._cursor as Int8Range).range.upper).toBe(null);
234
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
235
+ expect(((result[1] as any)._cursor as Int8Range).range.upper).toBe(null);
236
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
237
+ expect(((result[2] as any)._cursor as Int8Range).range.upper).toBe(null);
238
+ });
239
+ });
@@ -0,0 +1,115 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import {
3
+ type ExtractTablesWithRelations,
4
+ type TablesRelationalConfig,
5
+ gt,
6
+ sql,
7
+ } from "drizzle-orm";
8
+ import type {
9
+ AnyPgTable,
10
+ PgDatabase,
11
+ PgQueryResultHKT,
12
+ PgTableWithColumns,
13
+ TableConfig,
14
+ } from "drizzle-orm/pg-core";
15
+ import { Sink, type SinkCursorParams } from "../../sink";
16
+ import { DrizzleSinkTransaction } from "./transaction";
17
+
18
+ export type DrizzleSinkTables<
19
+ TTableConfig extends Record<string, TableConfig>,
20
+ > = {
21
+ [K in keyof TTableConfig]: PgTableWithColumns<TTableConfig[K]>;
22
+ };
23
+
24
+ export type DrizzleSinkOptions<
25
+ TQueryResult extends PgQueryResultHKT,
26
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
27
+ TSchema extends
28
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
29
+ > = {
30
+ /**
31
+ * Database instance of drizzle-orm
32
+ */
33
+ database: PgDatabase<TQueryResult, TFullSchema, TSchema>;
34
+ tables: AnyPgTable[];
35
+ };
36
+
37
+ /**
38
+ * A sink that writes data to a PostgreSQL database using Drizzle ORM.
39
+ *
40
+ * @example
41
+ *
42
+ * ```ts
43
+ * const sink = drizzle({
44
+ * database: db,
45
+ * });
46
+ *
47
+ * ...
48
+ * async transform({context, endCursor}){
49
+ * const { transaction } = useSink(context);
50
+ * const db = transaction(endCursor);
51
+ *
52
+ * db.insert(users).values([
53
+ * { id: 1, name: "John" },
54
+ * { id: 2, name: "Jane" },
55
+ * ]);
56
+ * }
57
+ *
58
+ * ```
59
+ */
60
+ export class DrizzleSink<
61
+ TQueryResult extends PgQueryResultHKT,
62
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
63
+ TSchema extends
64
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
65
+ > extends Sink {
66
+ private _db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
67
+ private _tables: AnyPgTable[];
68
+ constructor(options: DrizzleSinkOptions<TQueryResult, TFullSchema, TSchema>) {
69
+ super();
70
+ const { database, tables } = options;
71
+ this._db = database;
72
+ this._tables = tables;
73
+ }
74
+
75
+ async transaction(
76
+ { cursor, endCursor, finality }: SinkCursorParams,
77
+ cb: (params: {
78
+ db: DrizzleSinkTransaction<TQueryResult, TFullSchema, TSchema>;
79
+ }) => Promise<void>,
80
+ ): Promise<void> {
81
+ await this._db.transaction(async (db) => {
82
+ await cb({ db: new DrizzleSinkTransaction(db, endCursor) });
83
+ });
84
+ }
85
+
86
+ async invalidate(cursor?: Cursor) {
87
+ await this._db.transaction(async (db) => {
88
+ for (const table of this._tables) {
89
+ // delete all rows whose lowerbound of "_cursor" (int8range) column is greater than the invalidate cursor
90
+ await db
91
+ .delete(table)
92
+ .where(gt(sql`lower(_cursor)`, sql`${Number(cursor?.orderKey)}`))
93
+ .returning();
94
+ // and for rows whose upperbound of "_cursor" (int8range) column is greater than the invalidate cursor, set the upperbound to infinity
95
+ await db
96
+ .update(table)
97
+ .set({
98
+ _cursor: sql`int8range(lower(_cursor), NULL, '[)')`,
99
+ })
100
+ .where(gt(sql`upper(_cursor)`, sql`${Number(cursor?.orderKey)}`));
101
+ }
102
+ });
103
+ }
104
+ }
105
+
106
+ export const drizzle = <
107
+ TQueryResult extends PgQueryResultHKT,
108
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
109
+ TSchema extends
110
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
111
+ >(
112
+ args: DrizzleSinkOptions<TQueryResult, TFullSchema, TSchema>,
113
+ ) => {
114
+ return new DrizzleSink(args);
115
+ };
@@ -0,0 +1,6 @@
1
+ export * from "./drizzle";
2
+ export * from "./Int8Range";
3
+ export * from "./utils";
4
+ export * from "./transaction";
5
+ export * from "./update";
6
+ export * from "./delete";
@@ -0,0 +1,39 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import type {
3
+ ExtractTablesWithRelations,
4
+ TablesRelationalConfig,
5
+ } from "drizzle-orm";
6
+ import type {
7
+ PgInsertValue,
8
+ PgQueryResultHKT,
9
+ PgTable,
10
+ PgTransaction,
11
+ } from "drizzle-orm/pg-core";
12
+ import { getDrizzleCursor } from "./utils";
13
+
14
+ export class DrizzleSinkInsert<
15
+ TTable extends PgTable,
16
+ TQueryResult extends PgQueryResultHKT,
17
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
18
+ TSchema extends
19
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
20
+ > {
21
+ constructor(
22
+ private db: PgTransaction<TQueryResult, TFullSchema, TSchema>,
23
+ private table: TTable,
24
+ private endCursor?: Cursor,
25
+ ) {}
26
+
27
+ values(values: PgInsertValue<TTable> | PgInsertValue<TTable>[]) {
28
+ const originalInsert = this.db.insert(this.table);
29
+ const cursoredValues = (Array.isArray(values) ? values : [values]).map(
30
+ (v) => {
31
+ return {
32
+ ...v,
33
+ _cursor: getDrizzleCursor(this.endCursor?.orderKey),
34
+ };
35
+ },
36
+ );
37
+ return originalInsert.values(cursoredValues as PgInsertValue<TTable>[]);
38
+ }
39
+ }
@@ -0,0 +1,44 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import {
3
+ type ExtractTablesWithRelations,
4
+ type SQL,
5
+ type Subquery,
6
+ type TablesRelationalConfig,
7
+ sql,
8
+ } from "drizzle-orm";
9
+ import type {
10
+ PgQueryResultHKT,
11
+ PgTable,
12
+ PgTransaction,
13
+ SelectedFields,
14
+ } from "drizzle-orm/pg-core";
15
+ import type { PgViewBase } from "drizzle-orm/pg-core/view-base";
16
+
17
+ export class DrizzleSinkSelect<
18
+ TSelection extends SelectedFields,
19
+ TQueryResult extends PgQueryResultHKT,
20
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
21
+ TSchema extends
22
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
23
+ > {
24
+ constructor(
25
+ private db: PgTransaction<TQueryResult, TFullSchema, TSchema>,
26
+ private fields?: TSelection,
27
+ private endCursor?: Cursor,
28
+ ) {}
29
+
30
+ from<TFrom extends PgTable | Subquery | PgViewBase | SQL>(source: TFrom) {
31
+ if (this.fields) {
32
+ const originalFrom = this.db.select(this.fields).from(source);
33
+ return {
34
+ ...originalFrom,
35
+ where: (where?: SQL) => {
36
+ const combinedWhere = sql`${where ? sql`${where} AND ` : sql``}upper_inf(_cursor)`;
37
+ return originalFrom.where(combinedWhere);
38
+ },
39
+ };
40
+ }
41
+
42
+ return this.db.select().from(source).where(sql`upper_inf(_cursor)`);
43
+ }
44
+ }
@@ -0,0 +1,49 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import type {
3
+ ExtractTablesWithRelations,
4
+ TablesRelationalConfig,
5
+ } from "drizzle-orm";
6
+ import type {
7
+ PgQueryResultHKT,
8
+ PgSelectBuilder,
9
+ PgTable,
10
+ PgTransaction,
11
+ SelectedFields,
12
+ } from "drizzle-orm/pg-core";
13
+ import { DrizzleSinkDelete } from "./delete";
14
+ import { DrizzleSinkInsert } from "./insert";
15
+ import { DrizzleSinkSelect } from "./select";
16
+ import { DrizzleSinkUpdate } from "./update";
17
+
18
+ export class DrizzleSinkTransaction<
19
+ TQueryResult extends PgQueryResultHKT,
20
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
21
+ TSchema extends
22
+ TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
23
+ > {
24
+ constructor(
25
+ private db: PgTransaction<TQueryResult, TFullSchema, TSchema>,
26
+ private endCursor?: Cursor,
27
+ ) {}
28
+
29
+ insert<TTable extends PgTable>(table: TTable) {
30
+ return new DrizzleSinkInsert(this.db, table, this.endCursor);
31
+ }
32
+
33
+ update<TTable extends PgTable>(table: TTable) {
34
+ return new DrizzleSinkUpdate(this.db, table, this.endCursor);
35
+ }
36
+
37
+ delete<TTable extends PgTable>(table: TTable) {
38
+ return new DrizzleSinkDelete(this.db, table, this.endCursor);
39
+ }
40
+
41
+ // @ts-ignore
42
+ select(): PgSelectBuilder<undefined>;
43
+ select<TSelection extends SelectedFields>(
44
+ fields: TSelection,
45
+ ): PgSelectBuilder<TSelection>;
46
+ select(fields?: SelectedFields) {
47
+ return new DrizzleSinkSelect(this.db, fields, this.endCursor);
48
+ }
49
+ }