@thru/indexer 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +477 -0
- package/dist/index.cjs +1208 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1064 -0
- package/dist/index.d.ts +1064 -0
- package/dist/index.mjs +1185 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# @thru/indexer
|
|
2
|
+
|
|
3
|
+
A reusable blockchain indexing framework for building backends that index Thru chain data.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Event Streams** - Index historical, immutable event data
|
|
8
|
+
- **Account Streams** - Track current on-chain account state with slot-aware upserts
|
|
9
|
+
- **Type-Safe Schema Builder** - Fluent API with full TypeScript inference
|
|
10
|
+
- **Auto-Generated REST API** - Hono + OpenAPI routes with pagination
|
|
11
|
+
- **Resumable Indexing** - Checkpoint-based recovery after restarts
|
|
12
|
+
- **Drizzle ORM** - PostgreSQL with type-safe queries and migrations
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @thru/indexer @thru/replay @thru/helpers postgres drizzle-orm hono @hono/zod-openapi
|
|
18
|
+
pnpm add -D drizzle-kit tsx typescript
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Define an Event Stream
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// src/streams/transfers.ts
|
|
27
|
+
import { create } from "@bufbuild/protobuf";
|
|
28
|
+
import { decodeAddress, encodeAddress, encodeSignature } from "@thru/helpers";
|
|
29
|
+
import { defineEventStream, t } from "@thru/indexer";
|
|
30
|
+
import { FilterSchema, FilterParamValueSchema, type Event } from "@thru/replay";
|
|
31
|
+
import { TokenEvent } from "../abi/token";
|
|
32
|
+
|
|
33
|
+
const TOKEN_PROGRAM = "taAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqq";
|
|
34
|
+
|
|
35
|
+
const transfers = defineEventStream({
|
|
36
|
+
name: "transfers",
|
|
37
|
+
description: "Token transfer events",
|
|
38
|
+
|
|
39
|
+
schema: {
|
|
40
|
+
id: t.text().primaryKey(),
|
|
41
|
+
slot: t.bigint().notNull().index(),
|
|
42
|
+
txnSignature: t.text().notNull(),
|
|
43
|
+
source: t.text().notNull().index(),
|
|
44
|
+
dest: t.text().notNull().index(),
|
|
45
|
+
amount: t.bigint().notNull(),
|
|
46
|
+
indexedAt: t.timestamp().notNull().defaultNow(),
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Lazy filter for drizzle-kit compatibility
|
|
50
|
+
filterFactory: () => {
|
|
51
|
+
const programBytes = new Uint8Array(decodeAddress(TOKEN_PROGRAM));
|
|
52
|
+
return create(FilterSchema, {
|
|
53
|
+
expression: "event.program.value == params.address",
|
|
54
|
+
params: {
|
|
55
|
+
address: create(FilterParamValueSchema, {
|
|
56
|
+
kind: { case: "bytesValue", value: programBytes },
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Parse raw event into table row (return null to skip)
|
|
63
|
+
parse: (event: Event) => {
|
|
64
|
+
const payload = event.payload;
|
|
65
|
+
if (!payload || payload[0] !== 2) return null;
|
|
66
|
+
|
|
67
|
+
const tokenEvent = TokenEvent.from_array(payload);
|
|
68
|
+
const transfer = tokenEvent?.payload()?.asTransfer();
|
|
69
|
+
if (!transfer) return null;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: event.eventId,
|
|
73
|
+
slot: event.slot!,
|
|
74
|
+
txnSignature: encodeSignature(event.transactionSignature?.value ?? new Uint8Array()),
|
|
75
|
+
source: encodeAddress(new Uint8Array(transfer.source.get_bytes())),
|
|
76
|
+
dest: encodeAddress(new Uint8Array(transfer.dest.get_bytes())),
|
|
77
|
+
amount: transfer.amount,
|
|
78
|
+
indexedAt: new Date(),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
api: { filters: ["source", "dest"] },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Export table for Drizzle migrations
|
|
86
|
+
export const transferEvents = transfers.table;
|
|
87
|
+
export default transfers;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Define an Account Stream
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// src/account-streams/token-accounts.ts
|
|
94
|
+
import { decodeAddress, encodeAddress } from "@thru/helpers";
|
|
95
|
+
import { defineAccountStream, t } from "@thru/indexer";
|
|
96
|
+
import { TokenAccount } from "../abi/token";
|
|
97
|
+
|
|
98
|
+
const TOKEN_PROGRAM = "taAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqq";
|
|
99
|
+
|
|
100
|
+
const tokenAccounts = defineAccountStream({
|
|
101
|
+
name: "token-accounts",
|
|
102
|
+
description: "Token account balances",
|
|
103
|
+
|
|
104
|
+
ownerProgramFactory: () => new Uint8Array(decodeAddress(TOKEN_PROGRAM)),
|
|
105
|
+
expectedSize: 73,
|
|
106
|
+
|
|
107
|
+
schema: {
|
|
108
|
+
address: t.text().primaryKey(),
|
|
109
|
+
mint: t.text().notNull().index(),
|
|
110
|
+
owner: t.text().notNull().index(),
|
|
111
|
+
amount: t.bigint().notNull(),
|
|
112
|
+
slot: t.bigint().notNull(),
|
|
113
|
+
seq: t.bigint().notNull(),
|
|
114
|
+
updatedAt: t.timestamp().notNull().defaultNow(),
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
parse: (account) => {
|
|
118
|
+
if (account.data.length !== 73) return null;
|
|
119
|
+
|
|
120
|
+
const parsed = TokenAccount.from_array(account.data);
|
|
121
|
+
if (!parsed) return null;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
address: encodeAddress(account.address),
|
|
125
|
+
mint: encodeAddress(new Uint8Array(parsed.mint.get_bytes())),
|
|
126
|
+
owner: encodeAddress(new Uint8Array(parsed.owner.get_bytes())),
|
|
127
|
+
amount: parsed.amount,
|
|
128
|
+
slot: account.slot,
|
|
129
|
+
seq: account.seq,
|
|
130
|
+
updatedAt: new Date(),
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
api: { filters: ["mint", "owner"], idField: "address" },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export const tokenAccountsTable = tokenAccounts.table;
|
|
138
|
+
export default tokenAccounts;
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 3. Set Up Database Schema
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// src/db/schema.ts
|
|
145
|
+
export { checkpointTable } from "@thru/indexer";
|
|
146
|
+
export { transferEvents } from "../streams/transfers";
|
|
147
|
+
export { tokenAccountsTable } from "../account-streams/token-accounts";
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// drizzle.config.ts
|
|
152
|
+
import { defineConfig } from "drizzle-kit";
|
|
153
|
+
|
|
154
|
+
export default defineConfig({
|
|
155
|
+
schema: "./src/db/schema.ts",
|
|
156
|
+
out: "./drizzle",
|
|
157
|
+
dialect: "postgresql",
|
|
158
|
+
dbCredentials: {
|
|
159
|
+
url: process.env.DATABASE_URL!,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// src/db/index.ts
|
|
166
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
167
|
+
import postgres from "postgres";
|
|
168
|
+
|
|
169
|
+
const client = postgres(process.env.DATABASE_URL!);
|
|
170
|
+
export const db = drizzle(client);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 4. Create Indexer
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// src/indexer.ts
|
|
177
|
+
import { ChainClient } from "@thru/replay";
|
|
178
|
+
import { Indexer } from "@thru/indexer";
|
|
179
|
+
import { db } from "./db";
|
|
180
|
+
import transfers from "./streams/transfers";
|
|
181
|
+
import tokenAccounts from "./account-streams/token-accounts";
|
|
182
|
+
|
|
183
|
+
const indexer = new Indexer({
|
|
184
|
+
db,
|
|
185
|
+
clientFactory: () => new ChainClient({ baseUrl: process.env.CHAIN_RPC_URL! }),
|
|
186
|
+
eventStreams: [transfers],
|
|
187
|
+
accountStreams: [tokenAccounts],
|
|
188
|
+
defaultStartSlot: 0n,
|
|
189
|
+
safetyMargin: 64,
|
|
190
|
+
pageSize: 512,
|
|
191
|
+
logLevel: "info",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
process.on("SIGINT", () => indexer.stop());
|
|
195
|
+
process.on("SIGTERM", () => indexer.stop());
|
|
196
|
+
|
|
197
|
+
indexer.start().then((result) => {
|
|
198
|
+
console.log("Indexer finished:", result);
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 5. Create API Server
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// src/api.ts
|
|
206
|
+
import { serve } from "@hono/node-server";
|
|
207
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
208
|
+
import { mountStreamRoutes } from "@thru/indexer";
|
|
209
|
+
import { db } from "./db";
|
|
210
|
+
import transfers from "./streams/transfers";
|
|
211
|
+
import tokenAccounts from "./account-streams/token-accounts";
|
|
212
|
+
|
|
213
|
+
const app = new OpenAPIHono();
|
|
214
|
+
|
|
215
|
+
mountStreamRoutes(app, {
|
|
216
|
+
db,
|
|
217
|
+
basePath: "/api/v1",
|
|
218
|
+
eventStreams: [transfers],
|
|
219
|
+
accountStreams: [tokenAccounts],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
serve({ fetch: app.fetch, port: 3000 }, (info) => {
|
|
223
|
+
console.log(`API server running on http://localhost:${info.port}`);
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 6. Run
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Generate and apply migrations
|
|
231
|
+
pnpm drizzle-kit generate
|
|
232
|
+
pnpm drizzle-kit push
|
|
233
|
+
|
|
234
|
+
# Start indexer
|
|
235
|
+
pnpm tsx src/indexer.ts
|
|
236
|
+
|
|
237
|
+
# Start API (separate terminal)
|
|
238
|
+
pnpm tsx src/api.ts
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## API Reference
|
|
242
|
+
|
|
243
|
+
### Schema Builder
|
|
244
|
+
|
|
245
|
+
The `t` object provides a fluent API for defining columns:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { t } from "@thru/indexer";
|
|
249
|
+
|
|
250
|
+
const schema = {
|
|
251
|
+
id: t.text().primaryKey(),
|
|
252
|
+
slot: t.bigint().notNull().index(),
|
|
253
|
+
name: t.text(), // nullable by default
|
|
254
|
+
count: t.integer().notNull(),
|
|
255
|
+
active: t.boolean().notNull().default(true),
|
|
256
|
+
createdAt: t.timestamp().notNull().defaultNow(),
|
|
257
|
+
mintId: t.text().notNull().references(mintsTable, "id"),
|
|
258
|
+
};
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Column Types:**
|
|
262
|
+
- `t.text()` - VARCHAR/TEXT
|
|
263
|
+
- `t.bigint()` - BIGINT (for slots, amounts)
|
|
264
|
+
- `t.integer()` - INTEGER
|
|
265
|
+
- `t.boolean()` - BOOLEAN
|
|
266
|
+
- `t.timestamp()` - TIMESTAMP WITH TIME ZONE
|
|
267
|
+
|
|
268
|
+
**Modifiers:**
|
|
269
|
+
- `.notNull()` - NOT NULL constraint
|
|
270
|
+
- `.primaryKey()` - Primary key (implies NOT NULL)
|
|
271
|
+
- `.index()` - Create index
|
|
272
|
+
- `.unique()` - Unique constraint
|
|
273
|
+
- `.default(value)` - Default value
|
|
274
|
+
- `.defaultNow()` - Default to current timestamp
|
|
275
|
+
- `.references(table, column)` - Foreign key
|
|
276
|
+
|
|
277
|
+
### Event Stream Options
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
defineEventStream({
|
|
281
|
+
name: string; // Unique stream name
|
|
282
|
+
description?: string; // Human-readable description
|
|
283
|
+
schema: { ... }; // Column definitions
|
|
284
|
+
filter?: Filter; // Direct CEL filter
|
|
285
|
+
filterFactory?: () => Filter; // Lazy filter (for drizzle-kit)
|
|
286
|
+
parse: (event: Event) => Row | null;
|
|
287
|
+
api?: {
|
|
288
|
+
filters?: string[]; // Filterable columns
|
|
289
|
+
};
|
|
290
|
+
filterBatch?: (events, ctx) => Promise<events>; // Pre-commit filter
|
|
291
|
+
onCommit?: (batch, ctx) => Promise<void>; // Post-commit hook
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Account Stream Options
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
defineAccountStream({
|
|
299
|
+
name: string;
|
|
300
|
+
description?: string;
|
|
301
|
+
ownerProgram?: Uint8Array; // Direct program address
|
|
302
|
+
ownerProgramFactory?: () => Uint8Array; // Lazy (for drizzle-kit)
|
|
303
|
+
expectedSize?: number; // Filter by data size
|
|
304
|
+
dataSizes?: number[]; // Multiple valid sizes
|
|
305
|
+
schema: { ... };
|
|
306
|
+
parse: (account: AccountState) => Row | null;
|
|
307
|
+
api?: {
|
|
308
|
+
filters?: string[];
|
|
309
|
+
idField?: string; // Primary key field name
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Indexer Options
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
new Indexer({
|
|
318
|
+
db: DatabaseClient; // Drizzle database client
|
|
319
|
+
clientFactory: () => ChainClient; // Factory for RPC connections
|
|
320
|
+
eventStreams?: EventStream[];
|
|
321
|
+
accountStreams?: AccountStream[];
|
|
322
|
+
defaultStartSlot?: bigint; // Start slot if no checkpoint
|
|
323
|
+
safetyMargin?: number; // Slots behind tip (default: 64)
|
|
324
|
+
pageSize?: number; // Events per page (default: 512)
|
|
325
|
+
logLevel?: "debug" | "info" | "warn" | "error";
|
|
326
|
+
validateParse?: boolean; // Validate parse output with Zod (dev mode)
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Hooks
|
|
331
|
+
|
|
332
|
+
**`filterBatch`** - Filter events before database commit:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
filterBatch: async (events, { db }) => {
|
|
336
|
+
// Only keep transfers involving registered users
|
|
337
|
+
const users = await db.select().from(usersTable);
|
|
338
|
+
const userAddresses = new Set(users.map(u => u.address));
|
|
339
|
+
|
|
340
|
+
return events.filter(e =>
|
|
341
|
+
userAddresses.has(e.source) || userAddresses.has(e.dest)
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**`onCommit`** - Side effects after commit:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
onCommit: async (batch, { db }) => {
|
|
350
|
+
// Queue notifications for transfer recipients
|
|
351
|
+
await queueNotifications(db, batch.events);
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Migrations
|
|
356
|
+
|
|
357
|
+
The library uses Drizzle Kit for migrations. Tables are automatically created from stream schemas.
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
# Generate migration from schema changes
|
|
361
|
+
pnpm drizzle-kit generate
|
|
362
|
+
|
|
363
|
+
# Apply migrations
|
|
364
|
+
pnpm drizzle-kit migrate
|
|
365
|
+
|
|
366
|
+
# Push schema directly (development)
|
|
367
|
+
pnpm drizzle-kit push
|
|
368
|
+
|
|
369
|
+
# Open Drizzle Studio
|
|
370
|
+
pnpm drizzle-kit studio
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Why `filterFactory` / `ownerProgramFactory`?
|
|
374
|
+
|
|
375
|
+
Drizzle Kit imports your schema files to generate migrations. If those files load config at import time, it fails:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// Breaks drizzle-kit (config not available at import time)
|
|
379
|
+
filter: create(FilterSchema, {
|
|
380
|
+
params: { address: decodeAddress(loadConfig().TOKEN_PROGRAM) }
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// Works (lazy loading, only called at runtime)
|
|
384
|
+
filterFactory: () => {
|
|
385
|
+
const config = loadConfig();
|
|
386
|
+
return create(FilterSchema, { ... });
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Schema Helper
|
|
391
|
+
|
|
392
|
+
Use `getSchemaExports()` to collect all tables for your Drizzle schema file:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// db/schema.ts
|
|
396
|
+
import { getSchemaExports } from "@thru/indexer";
|
|
397
|
+
import transfers from "../streams/transfers";
|
|
398
|
+
import tokenAccounts from "../account-streams/token-accounts";
|
|
399
|
+
|
|
400
|
+
// Export all tables for Drizzle migrations
|
|
401
|
+
export const { checkpointTable, transfersTable, tokenAccountsTable } = getSchemaExports({
|
|
402
|
+
eventStreams: [transfers],
|
|
403
|
+
accountStreams: [tokenAccounts],
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Runtime Validation
|
|
408
|
+
|
|
409
|
+
Enable `validateParse` to validate parse function output at runtime using Zod schemas. This is useful during development to catch type mismatches early:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
const indexer = new Indexer({
|
|
413
|
+
db,
|
|
414
|
+
clientFactory: () => new ChainClient({ baseUrl: RPC_URL }),
|
|
415
|
+
eventStreams: [transfers],
|
|
416
|
+
validateParse: process.env.NODE_ENV !== "production", // Enable in dev
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
When validation fails, the indexer logs detailed error messages:
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
[transfers] Stream "transfers" parse returned invalid data:
|
|
424
|
+
- amount: Expected bigint, received number
|
|
425
|
+
- source: Required
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Exports
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// Schema
|
|
432
|
+
export { t, columnBuilder } from "@thru/indexer";
|
|
433
|
+
export type { ColumnDef, SchemaDefinition, InferRow, InferInsert } from "@thru/indexer";
|
|
434
|
+
|
|
435
|
+
// Validation (for development)
|
|
436
|
+
export { generateZodSchema, validateParsedData } from "@thru/indexer";
|
|
437
|
+
|
|
438
|
+
// Streams
|
|
439
|
+
export { defineEventStream, defineAccountStream } from "@thru/indexer";
|
|
440
|
+
export type { EventStream, AccountStream } from "@thru/indexer";
|
|
441
|
+
|
|
442
|
+
// Checkpoint
|
|
443
|
+
export { checkpointTable, getCheckpoint, updateCheckpoint, getSchemaExports } from "@thru/indexer";
|
|
444
|
+
|
|
445
|
+
// API
|
|
446
|
+
export { mountStreamRoutes, generateSchemas } from "@thru/indexer";
|
|
447
|
+
export { paginate, parseCursor, paginationQuerySchema } from "@thru/indexer";
|
|
448
|
+
|
|
449
|
+
// Runtime
|
|
450
|
+
export { Indexer } from "@thru/indexer";
|
|
451
|
+
export type { IndexerConfig, IndexerResult } from "@thru/indexer";
|
|
452
|
+
|
|
453
|
+
// Types
|
|
454
|
+
export type { ApiConfig, StreamBatch, HookContext } from "@thru/indexer";
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Example Project Structure
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
my-indexer/
|
|
461
|
+
├── src/
|
|
462
|
+
│ ├── abi/ # ABI type definitions
|
|
463
|
+
│ │ └── token.ts
|
|
464
|
+
│ ├── streams/ # Event stream definitions
|
|
465
|
+
│ │ └── transfers.ts
|
|
466
|
+
│ ├── account-streams/ # Account stream definitions
|
|
467
|
+
│ │ └── token-accounts.ts
|
|
468
|
+
│ ├── db/
|
|
469
|
+
│ │ ├── index.ts # Database client
|
|
470
|
+
│ │ └── schema.ts # Drizzle schema exports
|
|
471
|
+
│ ├── indexer.ts # Indexer entry point
|
|
472
|
+
│ └── api.ts # API entry point
|
|
473
|
+
├── drizzle/ # Generated migrations
|
|
474
|
+
├── drizzle.config.ts
|
|
475
|
+
├── package.json
|
|
476
|
+
└── tsconfig.json
|
|
477
|
+
```
|