@apibara/indexer 2.0.0-beta.9 → 2.1.0-beta.2
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 +271 -39
- package/dist/index.d.cts +1 -16
- package/dist/index.d.mts +1 -16
- package/dist/index.d.ts +1 -16
- package/dist/index.mjs +262 -25
- 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 +39 -3
- package/dist/plugins/index.d.cts +16 -2
- package/dist/plugins/index.d.mts +16 -2
- package/dist/plugins/index.d.ts +16 -2
- package/dist/plugins/index.mjs +36 -3
- 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 +52 -50
- package/dist/testing/index.d.cts +8 -36
- package/dist/testing/index.d.mts +8 -36
- package/dist/testing/index.d.ts +8 -36
- package/dist/testing/index.mjs +47 -47
- package/dist/vcr/index.cjs +84 -17
- package/dist/vcr/index.d.cts +16 -7
- package/dist/vcr/index.d.mts +16 -7
- package/dist/vcr/index.d.ts +16 -7
- package/dist/vcr/index.mjs +75 -11
- package/package.json +22 -42
- 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 +278 -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 +5 -3
- package/src/vcr/replay.ts +8 -18
- package/dist/plugins/kv.cjs +0 -131
- package/dist/plugins/kv.d.cts +0 -32
- package/dist/plugins/kv.d.mts +0 -32
- package/dist/plugins/kv.d.ts +0 -32
- package/dist/plugins/kv.mjs +0 -124
- package/dist/plugins/persistence.cjs +0 -182
- package/dist/plugins/persistence.d.cts +0 -50
- package/dist/plugins/persistence.d.mts +0 -50
- package/dist/plugins/persistence.d.ts +0 -50
- package/dist/plugins/persistence.mjs +0 -179
- package/dist/shared/indexer.2c23c9cd.mjs +0 -35
- package/dist/shared/indexer.318d3617.cjs +0 -47
- package/dist/shared/indexer.36530330.mjs +0 -249
- package/dist/shared/indexer.500fd281.d.cts +0 -23
- package/dist/shared/indexer.541d43eb.cjs +0 -266
- package/dist/shared/indexer.93d6b2eb.mjs +0 -17
- package/dist/shared/indexer.a8b7ab1f.cjs +0 -25
- package/dist/shared/indexer.b9c8f0d8.d.cts +0 -19
- package/dist/shared/indexer.b9c8f0d8.d.mts +0 -19
- package/dist/shared/indexer.b9c8f0d8.d.ts +0 -19
- package/dist/shared/indexer.c7ed6b83.d.cts +0 -82
- package/dist/shared/indexer.e1856641.d.mts +0 -23
- package/dist/shared/indexer.e4f2430f.d.ts +0 -23
- package/dist/shared/indexer.e8bd138d.d.mts +0 -82
- package/dist/shared/indexer.f761abcd.d.ts +0 -82
- package/dist/sinks/csv.cjs +0 -85
- package/dist/sinks/csv.d.cts +0 -66
- package/dist/sinks/csv.d.mts +0 -66
- package/dist/sinks/csv.d.ts +0 -66
- package/dist/sinks/csv.mjs +0 -78
- package/dist/sinks/drizzle/index.cjs +0 -212
- package/dist/sinks/drizzle/index.d.cts +0 -153
- package/dist/sinks/drizzle/index.d.mts +0 -153
- package/dist/sinks/drizzle/index.d.ts +0 -153
- package/dist/sinks/drizzle/index.mjs +0 -198
- package/dist/sinks/sqlite.cjs +0 -90
- package/dist/sinks/sqlite.d.cts +0 -71
- package/dist/sinks/sqlite.d.mts +0 -71
- package/dist/sinks/sqlite.d.ts +0 -71
- package/dist/sinks/sqlite.mjs +0 -87
- 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 -42
- 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 -99
- 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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { type Finalize, type Invalidate, isCursor } from "@apibara/protocol";
|
|
2
|
+
import {
|
|
3
|
+
type MockBlock,
|
|
4
|
+
type MockFilter,
|
|
5
|
+
MockStream,
|
|
6
|
+
type MockStreamResponse,
|
|
7
|
+
} from "@apibara/protocol/testing";
|
|
8
|
+
|
|
9
|
+
import { useIndexerContext } from "../context";
|
|
10
|
+
import { type IndexerConfig, createIndexer, defineIndexer } from "../indexer";
|
|
11
|
+
import { defineIndexerPlugin } from "../plugins";
|
|
12
|
+
import { type InternalContext, internalContext } from "./plugins";
|
|
13
|
+
|
|
14
|
+
export type MockMessagesOptions = {
|
|
15
|
+
invalidate?: {
|
|
16
|
+
invalidateFromIndex: number;
|
|
17
|
+
invalidateTriggerIndex: number;
|
|
18
|
+
};
|
|
19
|
+
finalize?: {
|
|
20
|
+
finalizeToIndex: number;
|
|
21
|
+
finalizeTriggerIndex: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function generateMockMessages(
|
|
26
|
+
count = 10,
|
|
27
|
+
options?: MockMessagesOptions,
|
|
28
|
+
): MockStreamResponse[] {
|
|
29
|
+
const invalidateAt = options?.invalidate;
|
|
30
|
+
const finalizeAt = options?.finalize;
|
|
31
|
+
const messages: MockStreamResponse[] = [];
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < count; i++) {
|
|
34
|
+
if (invalidateAt && i === invalidateAt.invalidateTriggerIndex) {
|
|
35
|
+
messages.push({
|
|
36
|
+
_tag: "invalidate",
|
|
37
|
+
invalidate: {
|
|
38
|
+
cursor: {
|
|
39
|
+
orderKey: BigInt(5_000_000 + invalidateAt.invalidateFromIndex),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
} as Invalidate);
|
|
43
|
+
} else if (finalizeAt && i === finalizeAt.finalizeTriggerIndex) {
|
|
44
|
+
messages.push({
|
|
45
|
+
_tag: "finalize",
|
|
46
|
+
finalize: {
|
|
47
|
+
cursor: {
|
|
48
|
+
orderKey: BigInt(5_000_000 + finalizeAt.finalizeToIndex),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
} as Finalize);
|
|
52
|
+
} else {
|
|
53
|
+
messages.push({
|
|
54
|
+
_tag: "data",
|
|
55
|
+
data: {
|
|
56
|
+
cursor: { orderKey: BigInt(5_000_000 + i - 1) },
|
|
57
|
+
finality: "accepted",
|
|
58
|
+
data: [{ data: `${5_000_000 + i}` }],
|
|
59
|
+
endCursor: { orderKey: BigInt(5_000_000 + i) },
|
|
60
|
+
production: "backfill",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return messages;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type MockIndexerParams = {
|
|
70
|
+
internalContext?: InternalContext;
|
|
71
|
+
override?: Partial<IndexerConfig<MockFilter, MockBlock>>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function getMockIndexer(params?: MockIndexerParams) {
|
|
75
|
+
const { internalContext: contextParams, override } = params ?? {};
|
|
76
|
+
const { plugins, ...rest } = override ?? {};
|
|
77
|
+
|
|
78
|
+
return createIndexer(
|
|
79
|
+
defineIndexer(MockStream)({
|
|
80
|
+
streamUrl: "https://sepolia.ethereum.a5a.ch",
|
|
81
|
+
finality: "accepted",
|
|
82
|
+
filter: {},
|
|
83
|
+
async transform() {},
|
|
84
|
+
plugins: [
|
|
85
|
+
internalContext(
|
|
86
|
+
contextParams ??
|
|
87
|
+
({
|
|
88
|
+
availableIndexers: ["testing"],
|
|
89
|
+
indexerName: "testing",
|
|
90
|
+
} as InternalContext),
|
|
91
|
+
),
|
|
92
|
+
...(plugins ?? []),
|
|
93
|
+
],
|
|
94
|
+
...(rest ?? {}),
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type MockRet = {
|
|
100
|
+
data: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A mock sink used for testing. The indexer function can write to the output array.
|
|
105
|
+
* The indexer context is optionally written to the metadata object.
|
|
106
|
+
*/
|
|
107
|
+
export function mockSink<TFilter, TBlock>({
|
|
108
|
+
output,
|
|
109
|
+
metadata,
|
|
110
|
+
}: { output: unknown[]; metadata?: Record<string, unknown> }) {
|
|
111
|
+
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
112
|
+
indexer.hooks.hook("connect:before", ({ request }) => {
|
|
113
|
+
if (metadata?.lastCursor && isCursor(metadata.lastCursor)) {
|
|
114
|
+
request.startingCursor = metadata.lastCursor;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (metadata?.lastFilter) {
|
|
118
|
+
request.filter[1] = metadata.lastFilter as TFilter;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
|
|
123
|
+
if (request.filter[1]) {
|
|
124
|
+
if (metadata) {
|
|
125
|
+
metadata.lastCursor = endCursor;
|
|
126
|
+
metadata.lastFilter = request.filter[1];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
indexer.hooks.hook("handler:middleware", ({ use }) => {
|
|
132
|
+
use(async (context, next) => {
|
|
133
|
+
context.output = output;
|
|
134
|
+
await next();
|
|
135
|
+
context.output = null;
|
|
136
|
+
|
|
137
|
+
if (metadata) {
|
|
138
|
+
metadata.lastCursor = context.endCursor;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function useMockSink(): { output: unknown[] } {
|
|
146
|
+
const context = useIndexerContext();
|
|
147
|
+
return { output: context.output };
|
|
148
|
+
}
|
package/src/plugins/config.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Indexer } from "../indexer";
|
|
2
2
|
|
|
3
|
-
export type IndexerPlugin<TFilter, TBlock
|
|
4
|
-
indexer: Indexer<TFilter, TBlock
|
|
3
|
+
export type IndexerPlugin<TFilter, TBlock> = (
|
|
4
|
+
indexer: Indexer<TFilter, TBlock>,
|
|
5
5
|
) => void;
|
|
6
6
|
|
|
7
|
-
export function defineIndexerPlugin<TFilter, TBlock
|
|
8
|
-
def: IndexerPlugin<TFilter, TBlock
|
|
7
|
+
export function defineIndexerPlugin<TFilter, TBlock>(
|
|
8
|
+
def: IndexerPlugin<TFilter, TBlock>,
|
|
9
9
|
) {
|
|
10
10
|
return def;
|
|
11
11
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useIndexerContext } from "../context";
|
|
2
|
+
import { defineIndexerPlugin } from "./config";
|
|
3
|
+
|
|
4
|
+
export const INTERNAL_CONTEXT_PROPERTY = "_internal";
|
|
5
|
+
|
|
6
|
+
export function internalContext<TFilter, TBlock, TTxnParams>(
|
|
7
|
+
values: Record<string, unknown>,
|
|
8
|
+
) {
|
|
9
|
+
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
10
|
+
indexer.hooks.hook("run:before", () => {
|
|
11
|
+
try {
|
|
12
|
+
const ctx = useIndexerContext();
|
|
13
|
+
ctx[INTERNAL_CONTEXT_PROPERTY] = {
|
|
14
|
+
...(ctx[INTERNAL_CONTEXT_PROPERTY] || {}),
|
|
15
|
+
...values,
|
|
16
|
+
};
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error("Failed to set internal context", {
|
|
19
|
+
cause: error,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type InternalContext = {
|
|
27
|
+
indexerName: string;
|
|
28
|
+
availableIndexers: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function useInternalContext(): InternalContext {
|
|
32
|
+
const ctx = useIndexerContext();
|
|
33
|
+
|
|
34
|
+
if (ctx[INTERNAL_CONTEXT_PROPERTY] === undefined) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Internal context is not available, possibly 'internalContext' plugin is missing!",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return ctx[INTERNAL_CONTEXT_PROPERTY];
|
|
40
|
+
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ConsolaInstance, type ConsolaReporter, consola } from "consola";
|
|
2
|
+
import { useIndexerContext } from "../context";
|
|
3
|
+
import { defineIndexerPlugin } from "./config";
|
|
4
|
+
|
|
5
|
+
export type { ConsolaReporter, ConsolaInstance } from "consola";
|
|
6
|
+
|
|
7
|
+
export function logger<TFilter, TBlock, TTxnParams>({
|
|
8
|
+
logger,
|
|
9
|
+
}: { logger?: ConsolaReporter } = {}) {
|
|
10
|
+
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
11
|
+
indexer.hooks.hook("run:before", () => {
|
|
12
|
+
const ctx = useIndexerContext();
|
|
13
|
+
|
|
14
|
+
if (logger) {
|
|
15
|
+
ctx.logger = consola.create({ reporters: [logger] });
|
|
16
|
+
} else {
|
|
17
|
+
ctx.logger = consola.create({});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useLogger(): ConsolaInstance {
|
|
24
|
+
const ctx = useIndexerContext();
|
|
25
|
+
|
|
26
|
+
if (!ctx?.logger)
|
|
27
|
+
throw new Error("Logger plugin is not available in context");
|
|
28
|
+
|
|
29
|
+
return ctx.logger;
|
|
30
|
+
}
|
|
@@ -1,202 +1,39 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { Database as SqliteDatabase, Statement } from "better-sqlite3";
|
|
3
|
-
import { deserialize, serialize } from "../vcr";
|
|
4
|
-
import { defineIndexerPlugin } from "./config";
|
|
1
|
+
import { type Cursor, isCursor } from "@apibara/protocol";
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
database,
|
|
8
|
-
}: { database: SqliteDatabase }) {
|
|
9
|
-
return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
|
|
10
|
-
let store: SqlitePersistence<TFilter>;
|
|
3
|
+
import { defineIndexerPlugin } from "./config";
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A plugin that persists the last cursor and filter to memory.
|
|
7
|
+
*/
|
|
8
|
+
export function inMemoryPersistence<TFilter, TBlock>() {
|
|
9
|
+
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
10
|
+
let lastCursor: Cursor | undefined;
|
|
11
|
+
let lastFilter: TFilter | undefined;
|
|
17
12
|
|
|
18
13
|
indexer.hooks.hook("connect:before", ({ request }) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (cursor) {
|
|
22
|
-
request.startingCursor = cursor;
|
|
14
|
+
if (lastCursor) {
|
|
15
|
+
request.startingCursor = lastCursor;
|
|
23
16
|
}
|
|
24
17
|
|
|
25
|
-
if (
|
|
26
|
-
request.filter[1] =
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
indexer.hooks.hook("transaction:commit", ({ endCursor }) => {
|
|
31
|
-
if (endCursor) {
|
|
32
|
-
store.put({ cursor: endCursor });
|
|
18
|
+
if (lastFilter) {
|
|
19
|
+
request.filter[1] = lastFilter;
|
|
33
20
|
}
|
|
34
21
|
});
|
|
35
22
|
|
|
36
23
|
indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
|
|
37
24
|
if (request.filter[1]) {
|
|
38
|
-
|
|
25
|
+
lastCursor = endCursor;
|
|
26
|
+
lastFilter = request.filter[1];
|
|
39
27
|
}
|
|
40
28
|
});
|
|
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
29
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
}
|
|
30
|
+
indexer.hooks.hook("handler:middleware", ({ use }) => {
|
|
31
|
+
use(async (context, next) => {
|
|
32
|
+
await next();
|
|
33
|
+
if (context.endCursor && isCursor(context.endCursor)) {
|
|
34
|
+
lastCursor = context.endCursor;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
139
39
|
}
|
|
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/testing/index.ts
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { createClient } from "@apibara/protocol";
|
|
2
|
+
import ci from "ci-info";
|
|
3
|
+
import { type IndexerWithStreamConfig, createIndexer } from "../indexer";
|
|
4
|
+
import { type InternalContext, internalContext } from "../plugins/context";
|
|
5
|
+
import { logger } from "../plugins/logger";
|
|
6
|
+
import type { CassetteOptions, VcrConfig } from "../vcr/config";
|
|
7
|
+
import { isCassetteAvailable } from "../vcr/helper";
|
|
8
|
+
import { record } from "../vcr/record";
|
|
9
|
+
import { replay } from "../vcr/replay";
|
|
10
|
+
|
|
11
|
+
export function createVcr() {
|
|
12
|
+
return {
|
|
13
|
+
async run<TFilter, TBlock>(
|
|
14
|
+
cassetteName: string,
|
|
15
|
+
indexerConfig: IndexerWithStreamConfig<TFilter, TBlock>,
|
|
16
|
+
range: { fromBlock: bigint; toBlock: bigint },
|
|
17
|
+
) {
|
|
18
|
+
const vcrConfig: VcrConfig = {
|
|
19
|
+
cassetteDir: "cassettes",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const cassetteOptions: CassetteOptions = {
|
|
23
|
+
name: cassetteName,
|
|
24
|
+
startingCursor: {
|
|
25
|
+
orderKey: range.fromBlock,
|
|
26
|
+
},
|
|
27
|
+
endingCursor: {
|
|
28
|
+
orderKey: range.toBlock,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
indexerConfig.plugins = [
|
|
33
|
+
internalContext({
|
|
34
|
+
indexerName: cassetteName,
|
|
35
|
+
availableIndexers: [cassetteName],
|
|
36
|
+
} as InternalContext),
|
|
37
|
+
logger(),
|
|
38
|
+
...(indexerConfig.plugins ?? []),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const indexer = createIndexer(indexerConfig);
|
|
42
|
+
|
|
43
|
+
if (!isCassetteAvailable(vcrConfig, cassetteName)) {
|
|
44
|
+
if (ci.isCI) {
|
|
45
|
+
throw new Error("Cannot record cassette in CI");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const client = createClient(
|
|
49
|
+
indexer.streamConfig,
|
|
50
|
+
indexer.options.streamUrl,
|
|
51
|
+
);
|
|
52
|
+
await record(vcrConfig, client, indexer, cassetteOptions);
|
|
53
|
+
} else {
|
|
54
|
+
await replay(vcrConfig, indexer, cassetteName);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/vcr/record.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { Client, StreamDataResponse } from "@apibara/protocol";
|
|
4
|
-
import { klona } from "klona/full";
|
|
5
4
|
import { type Indexer, run } from "../indexer";
|
|
6
5
|
import type { CassetteOptions, VcrConfig } from "./config";
|
|
7
6
|
import { serialize } from "./helper";
|
|
@@ -14,10 +13,9 @@ export type CassetteDataType<TFilter, TBlock> = {
|
|
|
14
13
|
export async function record<TFilter, TBlock, TTxnParams>(
|
|
15
14
|
vcrConfig: VcrConfig,
|
|
16
15
|
client: Client<TFilter, TBlock>,
|
|
17
|
-
|
|
16
|
+
indexer: Indexer<TFilter, TBlock>,
|
|
18
17
|
cassetteOptions: CassetteOptions,
|
|
19
18
|
) {
|
|
20
|
-
const indexer = klona(indexerArg);
|
|
21
19
|
const messages: StreamDataResponse<TBlock>[] = [];
|
|
22
20
|
|
|
23
21
|
indexer.hooks.addHooks({
|
|
@@ -33,10 +31,14 @@ export async function record<TFilter, TBlock, TTxnParams>(
|
|
|
33
31
|
filter: indexer.options.filter,
|
|
34
32
|
messages: messages,
|
|
35
33
|
};
|
|
34
|
+
|
|
35
|
+
await fs.mkdir(vcrConfig.cassetteDir, { recursive: true });
|
|
36
|
+
|
|
36
37
|
const filePath = path.join(
|
|
37
38
|
vcrConfig.cassetteDir,
|
|
38
39
|
`${cassetteOptions.name}.json`,
|
|
39
40
|
);
|
|
41
|
+
|
|
40
42
|
await fs.writeFile(filePath, serialize(output), { flag: "w" });
|
|
41
43
|
},
|
|
42
44
|
});
|
package/src/vcr/replay.ts
CHANGED
|
@@ -1,34 +1,21 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import type { Client
|
|
4
|
+
import type { Client } from "@apibara/protocol";
|
|
5
5
|
import { MockClient } from "@apibara/protocol/testing";
|
|
6
6
|
import { type Indexer, run } from "../indexer";
|
|
7
|
-
import type { SinkData } from "../sink";
|
|
8
|
-
import { vcr } from "../testing/vcr";
|
|
9
7
|
import { type CassetteDataType, deserialize } from "../vcr";
|
|
10
8
|
import type { VcrConfig } from "./config";
|
|
11
9
|
|
|
12
10
|
export async function replay<TFilter, TBlock, TTxnParams>(
|
|
13
11
|
vcrConfig: VcrConfig,
|
|
14
|
-
indexer: Indexer<TFilter, TBlock
|
|
12
|
+
indexer: Indexer<TFilter, TBlock>,
|
|
15
13
|
cassetteName: string,
|
|
16
|
-
)
|
|
14
|
+
) {
|
|
17
15
|
const client = loadCassette<TFilter, TBlock>(vcrConfig, cassetteName);
|
|
18
|
-
|
|
19
|
-
const sink = vcr();
|
|
20
|
-
|
|
21
16
|
await run(client, indexer);
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
outputs: sink.result,
|
|
25
|
-
};
|
|
26
17
|
}
|
|
27
18
|
|
|
28
|
-
export type VcrReplayResult = {
|
|
29
|
-
outputs: Array<{ endCursor?: Cursor; data: SinkData[] }>;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
19
|
export function loadCassette<TFilter, TBlock>(
|
|
33
20
|
vcrConfig: VcrConfig,
|
|
34
21
|
cassetteName: string,
|
|
@@ -41,11 +28,14 @@ export function loadCassette<TFilter, TBlock>(
|
|
|
41
28
|
const { filter, messages } = cassetteData;
|
|
42
29
|
|
|
43
30
|
return new MockClient<TFilter, TBlock>((request, options) => {
|
|
31
|
+
// Notice that the request filter is an array of filters,
|
|
32
|
+
// so we need to wrap the indexer filter in an array.
|
|
44
33
|
assert.deepStrictEqual(
|
|
45
34
|
request.filter,
|
|
46
|
-
filter,
|
|
47
|
-
"
|
|
35
|
+
[filter],
|
|
36
|
+
"Indexer and cassette filter mismatch. Hint: delete the cassette and run again.",
|
|
48
37
|
);
|
|
38
|
+
|
|
49
39
|
return messages;
|
|
50
40
|
});
|
|
51
41
|
}
|