apibara 2.0.0-beta.9 → 2.1.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 (83) hide show
  1. package/dist/chunks/add.mjs +44 -0
  2. package/dist/chunks/build.mjs +3 -3
  3. package/dist/chunks/dev.mjs +22 -18
  4. package/dist/chunks/init.mjs +37 -0
  5. package/dist/chunks/prepare.mjs +0 -2
  6. package/dist/chunks/start.mjs +56 -0
  7. package/dist/cli/index.mjs +5 -1
  8. package/dist/config/index.d.mts +1 -1
  9. package/dist/config/index.d.ts +1 -1
  10. package/dist/core/index.mjs +61 -97
  11. package/dist/create/index.d.mts +17 -0
  12. package/dist/create/index.d.ts +17 -0
  13. package/dist/create/index.mjs +981 -0
  14. package/dist/rollup/index.d.mts +2 -1
  15. package/dist/rollup/index.d.ts +2 -1
  16. package/dist/rollup/index.mjs +130 -167
  17. package/dist/runtime/dev.d.ts +3 -0
  18. package/dist/runtime/dev.mjs +55 -0
  19. package/dist/runtime/index.d.ts +2 -0
  20. package/dist/runtime/index.mjs +2 -0
  21. package/dist/runtime/internal/app.d.ts +2 -0
  22. package/dist/runtime/internal/app.mjs +56 -0
  23. package/dist/runtime/internal/logger.d.ts +14 -0
  24. package/dist/runtime/internal/logger.mjs +45 -0
  25. package/dist/runtime/start.d.ts +3 -0
  26. package/dist/runtime/start.mjs +41 -0
  27. package/dist/types/index.d.mts +22 -19
  28. package/dist/types/index.d.ts +22 -19
  29. package/package.json +35 -13
  30. package/runtime-meta.d.ts +2 -0
  31. package/runtime-meta.mjs +7 -0
  32. package/src/cli/commands/add.ts +44 -0
  33. package/src/cli/commands/build.ts +5 -3
  34. package/src/cli/commands/dev.ts +28 -18
  35. package/src/cli/commands/init.ts +36 -0
  36. package/src/cli/commands/prepare.ts +0 -2
  37. package/src/cli/commands/start.ts +61 -0
  38. package/src/cli/index.ts +3 -0
  39. package/src/config/index.ts +5 -4
  40. package/src/core/apibara.ts +4 -2
  41. package/src/core/build/build.ts +2 -0
  42. package/src/core/build/dev.ts +1 -0
  43. package/src/core/build/error.ts +0 -1
  44. package/src/core/build/prepare.ts +5 -2
  45. package/src/core/build/prod.ts +10 -6
  46. package/src/core/build/types.ts +4 -95
  47. package/src/core/config/defaults.ts +1 -4
  48. package/src/core/config/loader.ts +1 -0
  49. package/src/core/config/resolvers/runtime-config.resolver.ts +1 -1
  50. package/src/core/config/update.ts +2 -3
  51. package/src/core/path.ts +11 -0
  52. package/src/core/scan.ts +40 -0
  53. package/src/create/add.ts +238 -0
  54. package/src/create/colors.ts +15 -0
  55. package/src/create/constants.ts +98 -0
  56. package/src/create/index.ts +2 -0
  57. package/src/create/init.ts +175 -0
  58. package/src/create/templates.ts +468 -0
  59. package/src/create/types.ts +34 -0
  60. package/src/create/utils.ts +422 -0
  61. package/src/rollup/config.ts +67 -189
  62. package/src/rollup/index.ts +1 -0
  63. package/src/rollup/plugins/config.ts +12 -0
  64. package/src/rollup/plugins/esm-shim.ts +69 -0
  65. package/src/rollup/plugins/indexers.ts +17 -0
  66. package/src/runtime/dev.ts +64 -0
  67. package/src/runtime/index.ts +2 -0
  68. package/src/runtime/internal/app.ts +78 -0
  69. package/src/runtime/internal/logger.ts +70 -0
  70. package/src/runtime/start.ts +48 -0
  71. package/src/types/apibara.ts +8 -0
  72. package/src/types/config.ts +28 -27
  73. package/src/types/hooks.ts +1 -0
  74. package/src/types/virtual/config.d.ts +3 -0
  75. package/src/types/virtual/indexers.d.ts +10 -0
  76. package/dist/internal/citty/index.d.mts +0 -1
  77. package/dist/internal/citty/index.d.ts +0 -1
  78. package/dist/internal/citty/index.mjs +0 -1
  79. package/dist/internal/consola/index.d.mts +0 -2
  80. package/dist/internal/consola/index.d.ts +0 -2
  81. package/dist/internal/consola/index.mjs +0 -1
  82. package/src/internal/citty/index.ts +0 -1
  83. package/src/internal/consola/index.ts +0 -1
@@ -0,0 +1,981 @@
1
+ import path, { basename } from 'node:path';
2
+ import consola$1, { consola } from 'consola';
3
+ import prompts from 'prompts';
4
+ import colors from 'picocolors';
5
+ import fs from 'node:fs';
6
+ import { Project, SyntaxKind } from 'ts-morph';
7
+ import * as prettier from 'prettier';
8
+
9
+ const {
10
+ blue,
11
+ blueBright,
12
+ cyan,
13
+ gray,
14
+ green,
15
+ greenBright,
16
+ magenta,
17
+ red,
18
+ redBright,
19
+ reset,
20
+ yellow
21
+ } = colors;
22
+
23
+ const chains = [
24
+ {
25
+ name: "starknet",
26
+ display: "Starknet",
27
+ color: blue,
28
+ networks: [
29
+ { name: "mainnet", display: "Mainnet", color: blue },
30
+ { name: "sepolia", display: "Sepolia", color: yellow }
31
+ ]
32
+ },
33
+ {
34
+ name: "ethereum",
35
+ display: "Ethereum",
36
+ color: green,
37
+ networks: [
38
+ { name: "mainnet", display: "Mainnet", color: blue },
39
+ { name: "sepolia", display: "Sepolia", color: yellow }
40
+ ]
41
+ },
42
+ {
43
+ name: "beaconchain",
44
+ display: "Beacon Chain",
45
+ color: yellow,
46
+ networks: [{ name: "mainnet", display: "Mainnet", color: yellow }]
47
+ }
48
+ ];
49
+ const networks = [
50
+ { name: "mainnet", display: "Mainnet", color: blue },
51
+ { name: "sepolia", display: "Sepolia", color: green },
52
+ { name: "other", display: "Other", color: red }
53
+ ];
54
+ const storages = [
55
+ { name: "postgres", display: "Postgres", color: green },
56
+ { name: "none", display: "None", color: red }
57
+ ];
58
+ const packageVersions = {
59
+ // Required Dependencies
60
+ apibara: "^2.1.0-beta.2",
61
+ "@apibara/indexer": "^2.1.0-beta.2",
62
+ "@apibara/protocol": "^2.1.0-beta.2",
63
+ // Chain Dependencies
64
+ "@apibara/evm": "^2.1.0-beta.2",
65
+ "@apibara/beaconchain": "^2.1.0-beta.2",
66
+ "@apibara/starknet": "^2.1.0-beta.2",
67
+ // Storage Dependencies
68
+ "@apibara/plugin-drizzle": "^2.1.0-beta.2",
69
+ "@apibara/plugin-mongo": "^2.1.0-beta.2",
70
+ "@apibara/plugin-sqlite": "^2.1.0-beta.2",
71
+ // Postgres Dependencies
72
+ "@electric-sql/pglite": "^0.2.17",
73
+ "drizzle-orm": "^0.37.0",
74
+ pg: "^8.13.1",
75
+ "@types/pg": "^8.11.10",
76
+ "drizzle-kit": "^0.29.0",
77
+ // Typescript Dependencies
78
+ typescript: "^5.6.2",
79
+ "@rollup/plugin-typescript": "^11.1.6",
80
+ "@types/node": "^20.5.2"
81
+ };
82
+ const dnaUrls = {
83
+ ethereum: "https://ethereum.preview.apibara.org",
84
+ ethereumSepolia: "https://ethereum-sepolia.preview.apibara.org",
85
+ beaconchain: "https://beaconchain.preview.apibara.org",
86
+ starknet: "https://starknet.preview.apibara.org",
87
+ starknetSepolia: "https://starknet-sepolia.preview.apibara.org"
88
+ };
89
+
90
+ function isEmpty(path2) {
91
+ const files = fs.readdirSync(path2);
92
+ return files.length === 0 || files.length === 1 && files[0] === ".git";
93
+ }
94
+ function emptyDir(dir) {
95
+ if (!fs.existsSync(dir)) {
96
+ return;
97
+ }
98
+ for (const file of fs.readdirSync(dir)) {
99
+ if (file === ".git") {
100
+ continue;
101
+ }
102
+ fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
103
+ }
104
+ }
105
+ function validateLanguage(language, throwError = false) {
106
+ if (!language) {
107
+ return false;
108
+ }
109
+ if (language === "typescript" || language === "ts" || language === "javascript" || language === "js") {
110
+ return true;
111
+ }
112
+ if (throwError) {
113
+ throw new Error(
114
+ `Invalid language ${cyan("(--language | -l)")}: ${red(language)}. Options: ${blue("typescript, ts")} or ${yellow("javascript, js")} | default: ${cyan("typescript")}`
115
+ );
116
+ }
117
+ return false;
118
+ }
119
+ function getLanguageFromAlias(alias) {
120
+ if (alias === "ts" || alias === "typescript") {
121
+ return "typescript";
122
+ }
123
+ if (alias === "js" || alias === "javascript") {
124
+ return "javascript";
125
+ }
126
+ throw new Error(
127
+ `Invalid language ${cyan("(--language | -l)")}: ${red(alias)}. Options: ${blue("typescript, ts")} or ${yellow("javascript, js")}`
128
+ );
129
+ }
130
+ function validateIndexerId(indexerId, throwError = false) {
131
+ if (!indexerId) {
132
+ return false;
133
+ }
134
+ if (!/^[a-z0-9-]+$/.test(indexerId)) {
135
+ if (throwError) {
136
+ throw new Error(
137
+ `Invalid indexer ID ${cyan("(--indexer-id)")}: ${red(indexerId)}. Indexer ID must contain only lowercase letters, numbers, and hyphens.`
138
+ );
139
+ }
140
+ return false;
141
+ }
142
+ return true;
143
+ }
144
+ function validateChain(chain, throwError = false) {
145
+ if (!chain) {
146
+ return false;
147
+ }
148
+ if (chain) {
149
+ if (chain === "starknet" || chain === "ethereum" || chain === "beaconchain")
150
+ return true;
151
+ if (throwError) {
152
+ throw new Error(
153
+ `Invalid chain ${cyan("(--chain)")}: ${red(chain)}. Chain must be one of ${blue("starknet, ethereum, beaconchain")}.`
154
+ );
155
+ }
156
+ return false;
157
+ }
158
+ return true;
159
+ }
160
+ function validateNetwork(chain, network, throwError = false) {
161
+ if (!network) {
162
+ return false;
163
+ }
164
+ if (network === "other") {
165
+ return true;
166
+ }
167
+ if (chain) {
168
+ if (chain === "starknet") {
169
+ if (network === "mainnet" || network === "sepolia") {
170
+ return true;
171
+ }
172
+ if (throwError) {
173
+ throw new Error(
174
+ `Invalid network ${cyan("(--network)")}: ${red(network)}. For chain ${blue("starknet")}, network must be one of ${blue("mainnet, sepolia, other")}.`
175
+ );
176
+ }
177
+ return false;
178
+ }
179
+ if (chain === "ethereum") {
180
+ if (network === "mainnet" || network === "goerli") {
181
+ return true;
182
+ }
183
+ if (throwError) {
184
+ throw new Error(
185
+ `Invalid network ${cyan("(--network)")}: ${red(network)}. For chain ${blue("ethereum")}, network must be one of ${blue("mainnet, goerli, other")}.`
186
+ );
187
+ }
188
+ return false;
189
+ }
190
+ if (chain === "beaconchain") {
191
+ if (network === "mainnet") {
192
+ return true;
193
+ }
194
+ if (throwError) {
195
+ throw new Error(
196
+ `Invalid network ${cyan("(--network)")}: ${red(network)}. For chain ${blue("beaconchain")}, network must be ${blue("mainnet, other")}.`
197
+ );
198
+ }
199
+ return false;
200
+ }
201
+ }
202
+ if (networks.find((n) => n.name === network)) {
203
+ return true;
204
+ }
205
+ if (throwError) {
206
+ throw new Error(
207
+ `Invalid network ${cyan("(--network)")}: ${red(network)}. Network must be one of ${blue("mainnet, sepolia, goerli, other")}.`
208
+ );
209
+ }
210
+ return false;
211
+ }
212
+ function validateStorage(storage, throwError = false) {
213
+ if (!storage) {
214
+ return false;
215
+ }
216
+ if (storage === "postgres" || storage === "none") {
217
+ return true;
218
+ }
219
+ if (throwError) {
220
+ throw new Error(
221
+ `Invalid storage ${cyan("(--storage)")}: ${red(storage)}. Storage must be one of ${blue("postgres, none")}.`
222
+ );
223
+ }
224
+ return false;
225
+ }
226
+ function validateDnaUrl(dnaUrl, throwError = false) {
227
+ if (!dnaUrl) {
228
+ return false;
229
+ }
230
+ if (!dnaUrl.startsWith("https://") && !dnaUrl.startsWith("http://")) {
231
+ if (throwError) {
232
+ throw new Error(
233
+ `Invalid DNA URL ${cyan("(--dna-url)")}: ${red(dnaUrl)}. DNA URL must start with ${blue("https:// or http://")}.`
234
+ );
235
+ }
236
+ return false;
237
+ }
238
+ return true;
239
+ }
240
+ function hasApibaraConfig(cwd) {
241
+ const configPathJS = path.join(cwd, "apibara.config.js");
242
+ const configPathTS = path.join(cwd, "apibara.config.ts");
243
+ return fs.existsSync(configPathJS) || fs.existsSync(configPathTS);
244
+ }
245
+ function getApibaraConfigLanguage(cwd) {
246
+ const configPathJS = path.join(cwd, "apibara.config.js");
247
+ const configPathTS = path.join(cwd, "apibara.config.ts");
248
+ if (fs.existsSync(configPathJS)) {
249
+ return "javascript";
250
+ }
251
+ if (fs.existsSync(configPathTS)) {
252
+ return "typescript";
253
+ }
254
+ throw new Error(red("\u2716") + " No apibara.config found");
255
+ }
256
+ function getDnaUrl(chain, network) {
257
+ if (chain === "ethereum") {
258
+ if (network === "mainnet") {
259
+ return dnaUrls.ethereum;
260
+ }
261
+ if (network === "sepolia") {
262
+ return dnaUrls.ethereumSepolia;
263
+ }
264
+ }
265
+ if (chain === "beaconchain") {
266
+ if (network === "mainnet") {
267
+ return dnaUrls.beaconchain;
268
+ }
269
+ }
270
+ if (chain === "starknet") {
271
+ if (network === "mainnet") {
272
+ return dnaUrls.starknet;
273
+ }
274
+ if (network === "sepolia") {
275
+ return dnaUrls.starknetSepolia;
276
+ }
277
+ }
278
+ throw new Error(red("\u2716") + " Invalid chain or network");
279
+ }
280
+ function convertKebabToCamelCase(_str) {
281
+ let str = _str;
282
+ if (!str || typeof str !== "string") {
283
+ return "";
284
+ }
285
+ if (/^[a-z][a-zA-Z0-9]*$/.test(str)) {
286
+ return str;
287
+ }
288
+ str = str.trim().replace(/^-+|-+$/g, "");
289
+ if (!str) {
290
+ return "";
291
+ }
292
+ return str.replace(/[-_]+/g, "-").split("-").filter(Boolean).map((word, index) => {
293
+ const _word = word.toLowerCase();
294
+ if (index > 0) {
295
+ return _word.charAt(0).toUpperCase() + _word.slice(1);
296
+ }
297
+ return _word;
298
+ }).join("");
299
+ }
300
+ async function checkFileExists(path2, options) {
301
+ const { askPrompt = false, fileName, allowIgnore = false } = options ?? {};
302
+ if (!fs.existsSync(path2)) {
303
+ return {
304
+ exists: false,
305
+ overwrite: false
306
+ };
307
+ }
308
+ if (askPrompt) {
309
+ const { overwrite } = await prompts({
310
+ type: "select",
311
+ name: "overwrite",
312
+ message: `${fileName ?? basename(path2)} already exists. Please choose how to proceed:`,
313
+ initial: 0,
314
+ choices: [
315
+ ...allowIgnore ? [
316
+ {
317
+ title: "Keep original file",
318
+ value: "ignore"
319
+ }
320
+ ] : [],
321
+ {
322
+ title: "Cancel operation",
323
+ value: "no"
324
+ },
325
+ {
326
+ title: "Overwrite file",
327
+ value: "yes"
328
+ }
329
+ ]
330
+ });
331
+ if (overwrite === "no") {
332
+ cancelOperation();
333
+ }
334
+ if (overwrite === "ignore") {
335
+ return {
336
+ exists: true,
337
+ overwrite: false
338
+ };
339
+ }
340
+ return {
341
+ exists: true,
342
+ overwrite: true
343
+ };
344
+ }
345
+ return {
346
+ exists: true,
347
+ overwrite: false
348
+ };
349
+ }
350
+ function cancelOperation(message) {
351
+ throw new Error(red("\u2716") + (message ?? " Operation cancelled"));
352
+ }
353
+ function getPackageManager() {
354
+ const userAgent = process.env.npm_config_user_agent;
355
+ const pkgInfo = pkgFromUserAgent(userAgent);
356
+ if (pkgInfo) {
357
+ return pkgInfo;
358
+ }
359
+ return {
360
+ name: "npm"
361
+ };
362
+ }
363
+ function pkgFromUserAgent(userAgent) {
364
+ if (!userAgent)
365
+ return void 0;
366
+ const pkgSpec = userAgent.split(" ")[0];
367
+ const pkgSpecArr = pkgSpec.split("/");
368
+ return {
369
+ name: pkgSpecArr[0],
370
+ version: pkgSpecArr[1]
371
+ };
372
+ }
373
+ async function formatFile(path2) {
374
+ const file = fs.readFileSync(path2, "utf8");
375
+ const formatted = await prettier.format(file, {
376
+ filepath: path2,
377
+ tabWidth: 2
378
+ });
379
+ fs.writeFileSync(path2, formatted);
380
+ }
381
+
382
+ function generatePackageJson(isTypeScript) {
383
+ return {
384
+ name: "apibara-app",
385
+ version: "0.1.0",
386
+ private: true,
387
+ type: "module",
388
+ scripts: {
389
+ prepare: "apibara prepare",
390
+ dev: "apibara dev",
391
+ start: "apibara start",
392
+ build: "apibara build",
393
+ ...isTypeScript && { typecheck: "tsc --noEmit" }
394
+ },
395
+ dependencies: {
396
+ "@apibara/indexer": packageVersions["@apibara/indexer"],
397
+ "@apibara/protocol": packageVersions["@apibara/protocol"],
398
+ apibara: packageVersions.apibara
399
+ },
400
+ devDependencies: {
401
+ ...isTypeScript && {
402
+ "@rollup/plugin-typescript": packageVersions["@rollup/plugin-typescript"],
403
+ "@types/node": packageVersions["@types/node"],
404
+ typescript: packageVersions.typescript
405
+ }
406
+ }
407
+ };
408
+ }
409
+ function generateTsConfig() {
410
+ return {
411
+ $schema: "https://json.schemastore.org/tsconfig",
412
+ display: "Default",
413
+ compilerOptions: {
414
+ forceConsistentCasingInFileNames: true,
415
+ target: "ES2022",
416
+ lib: ["ESNext"],
417
+ module: "ESNext",
418
+ moduleResolution: "bundler",
419
+ skipLibCheck: true,
420
+ types: ["node"],
421
+ noEmit: true,
422
+ strict: true,
423
+ baseUrl: "."
424
+ },
425
+ include: [".", "./.apibara/types"],
426
+ exclude: ["node_modules"]
427
+ };
428
+ }
429
+ function generateApibaraConfig(isTypeScript) {
430
+ return `${isTypeScript ? 'import typescript from "@rollup/plugin-typescript";\nimport type { Plugin } from "apibara/rollup";\n' : ""}import { defineConfig } from "apibara/config";
431
+
432
+ export default defineConfig({
433
+ runtimeConfig: {},${isTypeScript ? `
434
+ rollupConfig: {
435
+ plugins: [typescript()${isTypeScript ? " as Plugin" : ""}],
436
+ },` : ""}
437
+ });
438
+ `;
439
+ }
440
+ function generateIndexer({
441
+ indexerId,
442
+ storage,
443
+ chain,
444
+ language
445
+ }) {
446
+ return `import { defineIndexer } from "@apibara/indexer";
447
+ import { useLogger } from "@apibara/indexer/plugins";
448
+ ${storage === "postgres" ? `import { drizzleStorage } from "@apibara/plugin-drizzle";` : ""}
449
+ ${chain === "ethereum" ? `import { EvmStream } from "@apibara/evm";` : chain === "beaconchain" ? `import { BeaconChainStream } from "@apibara/beaconchain";` : chain === "starknet" ? `import { StarknetStream } from "@apibara/starknet";` : ""}
450
+ ${language === "typescript" ? `import type { ApibaraRuntimeConfig } from "apibara/types";` : ""}
451
+ ${storage === "postgres" ? `import { getDrizzlePgDatabase } from "../lib/db";` : ""}
452
+
453
+
454
+ export default function (runtimeConfig${language === "typescript" ? ": ApibaraRuntimeConfig" : ""}) {
455
+ const indexerId = "${indexerId}";
456
+ const { startingBlock, streamUrl${storage === "postgres" ? ", postgresConnectionString" : ""} } = runtimeConfig[indexerId];
457
+ ${storage === "postgres" ? "const { db } = getDrizzlePgDatabase(postgresConnectionString);" : ""}
458
+
459
+ return defineIndexer(${chain === "ethereum" ? "EvmStream" : chain === "beaconchain" ? "BeaconChainStream" : chain === "starknet" ? "StarknetStream" : ""})({
460
+ streamUrl,
461
+ finality: "accepted",
462
+ startingBlock: BigInt(startingBlock),
463
+ filter: {
464
+ header: "always",
465
+ },
466
+ plugins: [${storage === "postgres" ? "drizzleStorage({ db, persistState: true })" : ""}],
467
+ async transform({ endCursor, finality }) {
468
+ const logger = useLogger();
469
+
470
+ logger.info(
471
+ "Transforming block | orderKey: ",
472
+ endCursor?.orderKey,
473
+ " | finality: ",
474
+ finality
475
+ );
476
+
477
+ ${storage === "postgres" ? `// Example snippet to insert data into db using drizzle with postgres
478
+ // const { db } = useDrizzleStorage();
479
+ // const { logs } = block;
480
+ // for (const log of logs) {
481
+ // await db.insert(exampleTable).values({
482
+ // number: Number(endCursor?.orderKey),
483
+ // hash: log.transactionHash,
484
+ // });
485
+ // }` : ""}
486
+ },
487
+ });
488
+ }
489
+ `;
490
+ }
491
+ async function createIndexerFile(options) {
492
+ const indexerFilePath = path.join(
493
+ options.cwd,
494
+ "indexers",
495
+ `${options.indexerFileId}.indexer.${options.language === "typescript" ? "ts" : "js"}`
496
+ );
497
+ const { exists, overwrite } = await checkFileExists(indexerFilePath, {
498
+ askPrompt: true
499
+ });
500
+ if (exists && !overwrite)
501
+ return;
502
+ const indexerContent = generateIndexer(options);
503
+ fs.mkdirSync(path.dirname(indexerFilePath), { recursive: true });
504
+ fs.writeFileSync(indexerFilePath, indexerContent);
505
+ await formatFile(indexerFilePath);
506
+ }
507
+ async function updatePackageJson({
508
+ cwd,
509
+ chain,
510
+ storage,
511
+ language
512
+ }) {
513
+ const packageJsonPath = path.join(cwd, "package.json");
514
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
515
+ if (chain === "ethereum") {
516
+ packageJson.dependencies["@apibara/evm"] = packageVersions["@apibara/evm"];
517
+ } else if (chain === "beaconchain") {
518
+ packageJson.dependencies["@apibara/beaconchain"] = packageVersions["@apibara/beaconchain"];
519
+ } else if (chain === "starknet") {
520
+ packageJson.dependencies["@apibara/starknet"] = packageVersions["@apibara/starknet"];
521
+ }
522
+ if (storage === "postgres") {
523
+ packageJson.scripts["drizzle:generate"] = "drizzle-kit generate";
524
+ packageJson.scripts["drizzle:migrate"] = "drizzle-kit migrate";
525
+ packageJson.dependencies["@apibara/plugin-drizzle"] = packageVersions["@apibara/plugin-drizzle"];
526
+ packageJson.dependencies["drizzle-orm"] = packageVersions["drizzle-orm"];
527
+ packageJson.dependencies["@electric-sql/pglite"] = packageVersions["@electric-sql/pglite"];
528
+ packageJson.dependencies["drizzle-kit"] = packageVersions["drizzle-kit"];
529
+ packageJson.dependencies["pg"] = packageVersions["pg"];
530
+ if (language === "typescript") {
531
+ packageJson.devDependencies["@types/pg"] = packageVersions["@types/pg"];
532
+ }
533
+ }
534
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
535
+ await formatFile(packageJsonPath);
536
+ }
537
+ async function updateApibaraConfigFile({
538
+ indexerId,
539
+ cwd,
540
+ chain,
541
+ storage,
542
+ language,
543
+ network,
544
+ dnaUrl
545
+ }) {
546
+ const pathToConfig = path.join(
547
+ cwd,
548
+ `apibara.config.${language === "typescript" ? "ts" : "js"}`
549
+ );
550
+ const runtimeConfigString = `{
551
+ startingBlock: 0,
552
+ streamUrl: "${dnaUrl ?? getDnaUrl(chain, network)}"${storage === "postgres" ? `,
553
+ postgresConnectionString: process.env["POSTGRES_CONNECTION_STRING"] ?? "memory://${indexerId}"` : ""}}`;
554
+ const project = new Project();
555
+ const sourceFile = project.addSourceFileAtPath(pathToConfig);
556
+ const defineConfigCall = sourceFile.getFirstDescendantByKind(
557
+ SyntaxKind.CallExpression
558
+ );
559
+ if (!defineConfigCall)
560
+ return;
561
+ const configObjectExpression = defineConfigCall.getArguments()[0];
562
+ const runtimeConfigObject = configObjectExpression.getProperty("runtimeConfig");
563
+ if (!runtimeConfigObject) {
564
+ configObjectExpression.addPropertyAssignment({
565
+ name: "runtimeConfig",
566
+ initializer: `{
567
+ "${indexerId}": ${runtimeConfigString}
568
+ }`
569
+ });
570
+ } else {
571
+ const runtimeConfigProp = runtimeConfigObject.asKindOrThrow(
572
+ SyntaxKind.PropertyAssignment
573
+ );
574
+ const runtimeConfigObj = runtimeConfigProp.getInitializerOrThrow().asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
575
+ runtimeConfigObj.addPropertyAssignment({
576
+ name: `"${indexerId}"`,
577
+ initializer: runtimeConfigString
578
+ });
579
+ }
580
+ sourceFile.saveSync();
581
+ await formatFile(pathToConfig);
582
+ }
583
+ async function createDrizzleStorageFiles(options) {
584
+ const { cwd, language, storage } = options;
585
+ if (storage !== "postgres")
586
+ return;
587
+ const fileExtension = language === "typescript" ? "ts" : "js";
588
+ const drizzleConfigFileName = `drizzle.config.${fileExtension}`;
589
+ const drizzleConfigPath = path.join(cwd, drizzleConfigFileName);
590
+ const { exists, overwrite } = await checkFileExists(drizzleConfigPath, {
591
+ askPrompt: true,
592
+ allowIgnore: true
593
+ });
594
+ if (!exists || overwrite) {
595
+ const drizzleConfigContent = `${language === "typescript" ? 'import type { Config } from "drizzle-kit";' : ""}
596
+
597
+ export default {
598
+ schema: "./lib/schema.ts",
599
+ out: "./drizzle",
600
+ dialect: "postgresql",
601
+ dbCredentials: {
602
+ url: process.env["POSTGRES_CONNECTION_STRING"] ?? "",
603
+ },
604
+ }${language === "typescript" ? " satisfies Config" : ""};`;
605
+ fs.writeFileSync(drizzleConfigPath, drizzleConfigContent);
606
+ await formatFile(drizzleConfigPath);
607
+ consola.success(`Created ${cyan(drizzleConfigFileName)}`);
608
+ }
609
+ const schemaFileName = `schema.${fileExtension}`;
610
+ const schemaPath = path.join(cwd, "lib", schemaFileName);
611
+ const { exists: schemaExists, overwrite: schemaOverwrite } = await checkFileExists(schemaPath, {
612
+ askPrompt: true,
613
+ allowIgnore: true,
614
+ fileName: `lib/${schemaFileName}`
615
+ });
616
+ if (!schemaExists || schemaOverwrite) {
617
+ const schemaContent = `// --- Add your pg table schemas here ----
618
+
619
+ // import { bigint, pgTable, text, uuid } from "drizzle-orm/pg-core";
620
+
621
+ // export const exampleTable = pgTable("example_table", {
622
+ // id: uuid("id").primaryKey().defaultRandom(),
623
+ // number: bigint("number", { mode: "number" }),
624
+ // hash: text("hash"),
625
+ // });
626
+
627
+ export {};
628
+ `;
629
+ fs.mkdirSync(path.dirname(schemaPath), { recursive: true });
630
+ fs.writeFileSync(schemaPath, schemaContent);
631
+ await formatFile(schemaPath);
632
+ consola.success(`Created ${cyan("lib/schema.ts")}`);
633
+ }
634
+ const dbFileName = `db.${fileExtension}`;
635
+ const dbPath = path.join(cwd, "lib", dbFileName);
636
+ const { exists: dbExists, overwrite: dbOverwrite } = await checkFileExists(
637
+ dbPath,
638
+ {
639
+ askPrompt: true,
640
+ fileName: `lib/${dbFileName}`,
641
+ allowIgnore: true
642
+ }
643
+ );
644
+ if (!dbExists || dbOverwrite) {
645
+ const dbContent = `import * as schema from "./schema";
646
+ import { drizzle as nodePgDrizzle } from "drizzle-orm/node-postgres";
647
+ import { drizzle as pgLiteDrizzle } from "drizzle-orm/pglite";
648
+ import pg from "pg";
649
+
650
+
651
+ export function getDrizzlePgDatabase(connectionString${language === "typescript" ? ": string" : ""}) {
652
+ // Create pglite instance
653
+ if (connectionString.includes("memory")) {
654
+ return {
655
+ db: pgLiteDrizzle({
656
+ schema,
657
+ connection: {
658
+ dataDir: connectionString,
659
+ },
660
+ }),
661
+ };
662
+ }
663
+
664
+ // Create node-postgres instance
665
+ const pool = new pg.Pool({
666
+ connectionString,
667
+ });
668
+
669
+ return { db: nodePgDrizzle(pool, { schema }) };
670
+ }`;
671
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
672
+ fs.writeFileSync(dbPath, dbContent);
673
+ await formatFile(dbPath);
674
+ consola.success(`Created ${cyan(`lib/${dbFileName}`)}`);
675
+ }
676
+ console.log("\n");
677
+ if (!schemaExists || schemaOverwrite) {
678
+ consola.info(
679
+ `Make sure to export your pgTables in ${cyan(`lib/${schemaFileName}`)}`
680
+ );
681
+ console.log();
682
+ consola.info(`${magenta("Example:")}
683
+
684
+ ${yellow(`
685
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
686
+ \u2502 lib/schema.ts \u2502
687
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
688
+
689
+ import { bigint, pgTable, text, uuid } from "drizzle-orm/pg-core";
690
+
691
+ export const exampleTable = pgTable("example_table", {
692
+ id: uuid("id").primaryKey().defaultRandom(),
693
+ number: bigint("number", { mode: "number" }),
694
+ hash: text("hash"),
695
+ });`)}`);
696
+ console.log("\n");
697
+ }
698
+ consola.info(
699
+ `Run ${green(`${options.packageManager} run drizzle:generate`)} & ${green(`${options.packageManager} run drizzle:migrate`)} to generate and apply migrations.`
700
+ );
701
+ }
702
+ async function createStorageRelatedFiles(options) {
703
+ const { storage } = options;
704
+ if (storage === "postgres") {
705
+ await createDrizzleStorageFiles(options);
706
+ }
707
+ }
708
+
709
+ async function initializeProject({
710
+ argTargetDir,
711
+ argLanguage,
712
+ argNoCreateIndexer
713
+ }) {
714
+ const cwd = process.cwd();
715
+ validateLanguage(argLanguage, true);
716
+ console.log();
717
+ const result = await prompts(
718
+ [
719
+ {
720
+ type: () => argTargetDir && (!fs.existsSync(argTargetDir) || isEmpty(argTargetDir)) ? null : "select",
721
+ name: "overwrite",
722
+ message: () => (argTargetDir === "." ? "Current directory" : `Target directory "${argTargetDir}"`) + " is not empty. Please choose how to proceed:",
723
+ initial: 0,
724
+ choices: [
725
+ {
726
+ title: "Cancel operation",
727
+ value: "no"
728
+ },
729
+ {
730
+ title: "Remove existing files and continue",
731
+ value: "yes"
732
+ },
733
+ {
734
+ title: "Ignore files and continue",
735
+ value: "ignore"
736
+ }
737
+ ],
738
+ hint: "\nCurrent Working Directory: " + cwd
739
+ },
740
+ {
741
+ type: (_, { overwrite: overwrite2 }) => {
742
+ if (overwrite2 === "no") {
743
+ cancelOperation();
744
+ }
745
+ return null;
746
+ },
747
+ name: "overwriteChecker"
748
+ },
749
+ {
750
+ type: argLanguage ? null : "select",
751
+ name: "prompt_language",
752
+ message: "Select a language:",
753
+ choices: [
754
+ {
755
+ title: "Typescript",
756
+ value: "typescript"
757
+ },
758
+ {
759
+ title: "Javascript",
760
+ value: "javascript"
761
+ }
762
+ ]
763
+ }
764
+ ],
765
+ {
766
+ onCancel: () => {
767
+ cancelOperation();
768
+ }
769
+ }
770
+ );
771
+ const { overwrite, prompt_language } = result;
772
+ const root = path.join(cwd, argTargetDir);
773
+ if (overwrite === "yes") {
774
+ emptyDir(root);
775
+ } else if (!fs.existsSync(root)) {
776
+ fs.mkdirSync(root, { recursive: true });
777
+ }
778
+ const lang = argLanguage ? getLanguageFromAlias(argLanguage) : prompt_language;
779
+ const isTs = lang === "typescript";
780
+ const configExt = isTs ? "ts" : "js";
781
+ console.log("\n");
782
+ consola$1.info(`Initializing project in ${argTargetDir}
783
+
784
+ `);
785
+ const packageJsonPath = path.join(root, "package.json");
786
+ const packageJson = generatePackageJson(isTs);
787
+ fs.writeFileSync(
788
+ packageJsonPath,
789
+ JSON.stringify(packageJson, null, 2) + "\n"
790
+ );
791
+ await formatFile(packageJsonPath);
792
+ consola$1.success("Created ", cyan("package.json"));
793
+ if (isTs) {
794
+ const tsConfigPath = path.join(root, "tsconfig.json");
795
+ const tsConfig = generateTsConfig();
796
+ fs.writeFileSync(tsConfigPath, JSON.stringify(tsConfig, null, 2) + "\n");
797
+ await formatFile(tsConfigPath);
798
+ consola$1.success("Created ", cyan("tsconfig.json"));
799
+ }
800
+ const apibaraConfigPath = path.join(root, `apibara.config.${configExt}`);
801
+ const apibaraConfig = generateApibaraConfig(isTs);
802
+ fs.writeFileSync(apibaraConfigPath, apibaraConfig);
803
+ await formatFile(apibaraConfigPath);
804
+ consola$1.success("Created ", cyan(`apibara.config.${configExt}`));
805
+ const indexersDir = path.join(root, "indexers");
806
+ if (!fs.existsSync(indexersDir)) {
807
+ fs.mkdirSync(indexersDir, { recursive: true });
808
+ consola$1.success(`Created ${cyan("indexers")} directory`);
809
+ }
810
+ console.log("\n");
811
+ consola$1.ready(green("Project initialized successfully"));
812
+ console.log();
813
+ if (!argNoCreateIndexer) {
814
+ consola$1.info("Let's create an indexer\n");
815
+ await addIndexer({});
816
+ } else {
817
+ const pkgManager = getPackageManager();
818
+ consola$1.info(
819
+ "Run ",
820
+ green(`${pkgManager.name} run install`),
821
+ " to install all dependencies"
822
+ );
823
+ }
824
+ }
825
+
826
+ async function addIndexer({
827
+ argIndexerId,
828
+ argChain,
829
+ argNetwork,
830
+ argStorage,
831
+ argDnaUrl
832
+ }) {
833
+ const configExists = hasApibaraConfig(process.cwd());
834
+ if (!configExists) {
835
+ consola$1.error("No apibara.config found in the current directory.");
836
+ const prompt_initialize = await prompts({
837
+ type: "confirm",
838
+ name: "prompt_initialize",
839
+ message: reset(
840
+ "Do you want to initialize a apibara project here before adding an indexer?"
841
+ )
842
+ });
843
+ if (prompt_initialize.prompt_initialize) {
844
+ await initializeProject({
845
+ argTargetDir: process.cwd(),
846
+ argNoCreateIndexer: true
847
+ });
848
+ } else {
849
+ consola$1.info(
850
+ `Initialize a project with ${cyan("apibara init")} before adding an indexer`
851
+ );
852
+ throw new Error(
853
+ red("\u2716") + " Operation cancelled: No apibara.config found"
854
+ );
855
+ }
856
+ }
857
+ const language = getApibaraConfigLanguage(process.cwd());
858
+ validateIndexerId(argIndexerId, true);
859
+ validateChain(argChain, true);
860
+ validateNetwork(argChain, argNetwork, true);
861
+ validateStorage(argStorage, true);
862
+ validateDnaUrl(argDnaUrl, true);
863
+ const result = await prompts(
864
+ [
865
+ {
866
+ type: argIndexerId ? null : "text",
867
+ name: "prompt_indexerId",
868
+ message: reset("Indexer ID:"),
869
+ initial: argIndexerId ?? "my-indexer",
870
+ validate: (id) => validateIndexerId(id) ? checkFileExists(
871
+ path.join(
872
+ process.cwd(),
873
+ "indexers",
874
+ `${id}.indexer.${language === "typescript" ? "ts" : "js"}`
875
+ )
876
+ ).then(
877
+ ({ exists }) => exists ? `Indexer ${cyan(`${id}.indexer.${language === "typescript" ? "ts" : "js"}`)} already exists` : true
878
+ ) : "Invalid indexer ID, it cannot be empty and must be in kebab-case format"
879
+ },
880
+ {
881
+ type: argChain ? null : "select",
882
+ name: "prompt_chain",
883
+ message: reset("Select a chain:"),
884
+ choices: chains.map((chain) => ({
885
+ title: chain.color(chain.display),
886
+ value: chain
887
+ }))
888
+ },
889
+ {
890
+ type: argNetwork ? null : "select",
891
+ name: "prompt_network",
892
+ message: reset("Select a network:"),
893
+ choices: (chain) => [
894
+ ...(chain?.networks ?? chains.find((c) => c.name === argChain)?.networks ?? []).map((network) => ({
895
+ title: network.color(network.display),
896
+ value: network
897
+ })),
898
+ {
899
+ title: cyan("Other"),
900
+ value: {
901
+ color: cyan,
902
+ display: "Other",
903
+ name: "other"
904
+ }
905
+ }
906
+ ]
907
+ },
908
+ {
909
+ type: (network) => {
910
+ if (network || argNetwork) {
911
+ return network?.name === "other" || argNetwork === "other" ? "text" : null;
912
+ }
913
+ return null;
914
+ },
915
+ name: "prompt_dnaUrl",
916
+ message: reset("Enter a DNA URL:"),
917
+ validate: (url) => validateDnaUrl(url) || "Provide a valid DNA Url"
918
+ },
919
+ {
920
+ type: argStorage ? null : "select",
921
+ name: "prompt_storage",
922
+ message: reset("Select a storage:"),
923
+ choices: storages.map((storage) => ({
924
+ title: storage.color(storage.display),
925
+ value: storage
926
+ }))
927
+ }
928
+ ],
929
+ {
930
+ onCancel: () => {
931
+ cancelOperation();
932
+ }
933
+ }
934
+ );
935
+ const {
936
+ prompt_indexerId,
937
+ prompt_chain,
938
+ prompt_network,
939
+ prompt_storage,
940
+ prompt_dnaUrl
941
+ } = result;
942
+ if (!argIndexerId && !prompt_indexerId) {
943
+ throw new Error(red("\u2716") + " Indexer ID is required");
944
+ }
945
+ if (!argChain && !prompt_chain) {
946
+ throw new Error(red("\u2716") + " Chain is required");
947
+ }
948
+ if (!argNetwork && !prompt_network) {
949
+ throw new Error(red("\u2716") + " Network is required");
950
+ }
951
+ const indexerFileId = argIndexerId ?? prompt_indexerId;
952
+ const pkgManager = getPackageManager();
953
+ const options = {
954
+ cwd: process.cwd(),
955
+ indexerFileId,
956
+ indexerId: convertKebabToCamelCase(indexerFileId),
957
+ chain: argChain ?? prompt_chain?.name,
958
+ network: argNetwork ?? prompt_network?.name,
959
+ storage: argStorage ?? prompt_storage?.name,
960
+ dnaUrl: argDnaUrl ?? prompt_dnaUrl,
961
+ language,
962
+ packageManager: pkgManager.name
963
+ };
964
+ await updateApibaraConfigFile(options);
965
+ consola$1.success(
966
+ `Updated ${cyan("apibara.config." + (language === "typescript" ? "ts" : "js"))}`
967
+ );
968
+ await updatePackageJson(options);
969
+ consola$1.success(`Updated ${cyan("package.json")}`);
970
+ await createIndexerFile(options);
971
+ consola$1.success(
972
+ `Created ${cyan(`${indexerFileId}.indexer.${language === "typescript" ? "ts" : "js"}`)}`
973
+ );
974
+ await createStorageRelatedFiles(options);
975
+ console.log();
976
+ consola$1.info(
977
+ `Before running the indexer, run ${cyan(`${options.packageManager} run install`)}${language === "typescript" ? " & " + cyan(`${options.packageManager} run prepare`) : ""}`
978
+ );
979
+ }
980
+
981
+ export { addIndexer, initializeProject };