@beignet/cli 0.0.26 → 0.0.28

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 (97) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +59 -15
  3. package/dist/check.d.ts +61 -0
  4. package/dist/check.d.ts.map +1 -0
  5. package/dist/check.js +164 -0
  6. package/dist/check.js.map +1 -0
  7. package/dist/choices.d.ts +8 -0
  8. package/dist/choices.d.ts.map +1 -1
  9. package/dist/choices.js +14 -0
  10. package/dist/choices.js.map +1 -1
  11. package/dist/db.d.ts +8 -0
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +4 -4
  14. package/dist/db.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +156 -21
  17. package/dist/index.js.map +1 -1
  18. package/dist/inspect.d.ts.map +1 -1
  19. package/dist/inspect.js +121 -3
  20. package/dist/inspect.js.map +1 -1
  21. package/dist/make/inbox.d.ts +63 -0
  22. package/dist/make/inbox.d.ts.map +1 -0
  23. package/dist/make/inbox.js +1747 -0
  24. package/dist/make/inbox.js.map +1 -0
  25. package/dist/make/payments.d.ts +61 -0
  26. package/dist/make/payments.d.ts.map +1 -0
  27. package/dist/make/payments.js +1900 -0
  28. package/dist/make/payments.js.map +1 -0
  29. package/dist/make/shared.d.ts +592 -0
  30. package/dist/make/shared.d.ts.map +1 -0
  31. package/dist/make/shared.js +2233 -0
  32. package/dist/make/shared.js.map +1 -0
  33. package/dist/make/tenancy.d.ts +101 -0
  34. package/dist/make/tenancy.d.ts.map +1 -0
  35. package/dist/make/tenancy.js +4409 -0
  36. package/dist/make/tenancy.js.map +1 -0
  37. package/dist/make.d.ts +8 -44
  38. package/dist/make.d.ts.map +1 -1
  39. package/dist/make.js +57 -3689
  40. package/dist/make.js.map +1 -1
  41. package/dist/mcp.d.ts.map +1 -1
  42. package/dist/mcp.js +45 -4
  43. package/dist/mcp.js.map +1 -1
  44. package/dist/provider-add.d.ts +19 -0
  45. package/dist/provider-add.d.ts.map +1 -0
  46. package/dist/provider-add.js +691 -0
  47. package/dist/provider-add.js.map +1 -0
  48. package/dist/provider-audit.d.ts.map +1 -1
  49. package/dist/provider-audit.js +77 -1
  50. package/dist/provider-audit.js.map +1 -1
  51. package/dist/registry-edits.d.ts +9 -0
  52. package/dist/registry-edits.d.ts.map +1 -1
  53. package/dist/registry-edits.js +32 -0
  54. package/dist/registry-edits.js.map +1 -1
  55. package/dist/task.d.ts +2 -0
  56. package/dist/task.d.ts.map +1 -1
  57. package/dist/task.js +2 -0
  58. package/dist/task.js.map +1 -1
  59. package/dist/templates/agents.d.ts.map +1 -1
  60. package/dist/templates/agents.js +29 -11
  61. package/dist/templates/agents.js.map +1 -1
  62. package/dist/templates/base.d.ts.map +1 -1
  63. package/dist/templates/base.js +42 -12
  64. package/dist/templates/base.js.map +1 -1
  65. package/dist/templates/db/sqlite.d.ts.map +1 -1
  66. package/dist/templates/db/sqlite.js +7 -1
  67. package/dist/templates/db/sqlite.js.map +1 -1
  68. package/dist/templates/server.d.ts.map +1 -1
  69. package/dist/templates/server.js +44 -37
  70. package/dist/templates/server.js.map +1 -1
  71. package/dist/templates/shared.d.ts +5 -0
  72. package/dist/templates/shared.d.ts.map +1 -1
  73. package/dist/templates/shared.js +5 -0
  74. package/dist/templates/shared.js.map +1 -1
  75. package/package.json +2 -2
  76. package/skills/app-structure/SKILL.md +1 -1
  77. package/src/check.ts +246 -0
  78. package/src/choices.ts +28 -0
  79. package/src/db.ts +4 -4
  80. package/src/index.ts +232 -21
  81. package/src/inspect.ts +168 -2
  82. package/src/make/inbox.ts +2015 -0
  83. package/src/make/payments.ts +2182 -0
  84. package/src/make/shared.ts +3638 -0
  85. package/src/make/tenancy.ts +4809 -0
  86. package/src/make.ts +388 -5355
  87. package/src/mcp.ts +66 -3
  88. package/src/provider-add.ts +926 -0
  89. package/src/provider-audit.ts +95 -3
  90. package/src/registry-edits.ts +47 -0
  91. package/src/task.ts +8 -10
  92. package/src/templates/agents.ts +29 -11
  93. package/src/templates/base.ts +43 -12
  94. package/src/templates/db/sqlite.ts +7 -1
  95. package/src/templates/server.ts +44 -37
  96. package/src/templates/shared.ts +5 -0
  97. package/src/test-helpers/generated-app.ts +168 -0
package/dist/make.js CHANGED
@@ -1,44 +1,14 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { clientIndexPath, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
5
- import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, } from "./registry-edits.js";
4
+ import { clientIndexPath, loadBeignetConfig, resolveConfig, } from "./config.js";
5
+ import { adapterFilePath, addNamedImport, addNamedTypeImport, aliasModule, appendDeferredPortKey, appendPickStringLiteralMember, applyPortProviderWiring, assertAdapterApp, assertFeatureArtifactApp, assertFeatureUiApp, assertGeneratedIdentifiers, assertPersistenceArtifactApp, assertPolicyApp, assertPortApp, assertServerRuntimeApp, assertStandardApp, assertTestApp, assertUseCaseApp, beignetDependencyVersion, constantCase, defineUploadsInitializerInfo, detectResourceDatabase, detectResourcePersistence, drizzleDialects, drizzleRepositoriesPath, drizzleResourceRepositoryFilePath, drizzleSchemaIndexPath, eventBusPortWiring, eventFilePath, factoryFilePath, featureArtifactDir, featureArtifactIndexFile, featureArtifactNames, featureResourceNames, featureUiClientQueriesFilePath, featureUiComponentFilePath, featureUiIndexFilePath, featureUiNames, fileExists, hasTopLevelObjectProperty, identifierNames, infrastructureDir, insertTypeProperty, jobFilePath, listenerFilePath, mergeMakeResultInto, mergeMakeResults, missingCapabilityBuilderFiles, notificationFilePath, notificationPortWirings, planGeneratedFile, planGeneratedFiles, portFilePath, portNames, providersFilePath, readOptionalFile, relativeModule, resourceContractFilePath, resourceFeatureDir, resourceNames, resourcePortFilePath, resourceSharedErrorsPath, scheduleFilePath, seedEntrypointPath, seedFilePath, seedNames, singularize, storagePortWiring, tanstackReactQueryDependencyVersion, taskFilePath, typeBodySource, uniqueStrings, updateDrizzleRepositories, updateDrizzleSchemaIndex, updateFeatureArtifactIndex, updateInfrastructureAdapterWiring, updateInfrastructureDeferredPorts, updateInfrastructureGatePolicies, updateInfrastructurePortStub, updateInfrastructurePorts, updateOpenApiRoute, updatePackageDependencies, updatePackageJson, updatePackageScripts, updatePortIndex, updatePortsIndex, updateSeedEntrypoint, updateServerRoutes, updateServerTransactionPorts, updateSharedErrors, uploadFilePath, useCaseFeatureDir, useCaseFilePath, usesFeatureOwnedPolicies, usesFeatureOwnedTests, wireListenersProviderSource, wirePortProviders, writeGeneratedFile, writePlannedGeneratedFile, } from "./make/shared.js";
6
+ import { appendToArrayExpression, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, } from "./registry-edits.js";
6
7
  export { makeFeatureAddonChoices, makeFeatureRecipeChoices, } from "./choices.js";
7
- const drizzleDialects = {
8
- sqlite: {
9
- subpath: "sqlite",
10
- dbTypeName: "DrizzleSqliteDatabase",
11
- columnModule: "drizzle-orm/sqlite-core",
12
- tableFunction: "sqliteTable",
13
- schemaImports: ["integer", "sqliteTable", "text"],
14
- supportsReturning: true,
15
- idColumn: (columnName) => `text("${columnName}")`,
16
- timestampColumn: (columnName) => `text("${columnName}")`,
17
- integerColumn: (columnName) => `integer("${columnName}")`,
18
- },
19
- postgres: {
20
- subpath: "postgres",
21
- dbTypeName: "DrizzlePostgresDatabase",
22
- columnModule: "drizzle-orm/pg-core",
23
- tableFunction: "pgTable",
24
- schemaImports: ["integer", "pgTable", "text"],
25
- supportsReturning: true,
26
- idColumn: (columnName) => `text("${columnName}")`,
27
- timestampColumn: (columnName) => `text("${columnName}")`,
28
- integerColumn: (columnName) => `integer("${columnName}")`,
29
- },
30
- mysql: {
31
- subpath: "mysql",
32
- dbTypeName: "DrizzleMysqlDatabase",
33
- columnModule: "drizzle-orm/mysql-core",
34
- tableFunction: "mysqlTable",
35
- schemaImports: ["int", "mysqlTable", "text", "varchar"],
36
- supportsReturning: false,
37
- idColumn: (columnName) => `varchar("${columnName}", { length: 36 })`,
38
- timestampColumn: (columnName) => `varchar("${columnName}", { length: 32 })`,
39
- integerColumn: (columnName) => `int("${columnName}")`,
40
- },
41
- };
8
+ export * from "./make/inbox.js";
9
+ export * from "./make/payments.js";
10
+ export * from "./make/shared.js";
11
+ export * from "./make/tenancy.js";
42
12
  export async function makeResource(options) {
43
13
  return makeResourceSlice(options, "resource");
44
14
  }
@@ -61,56 +31,6 @@ export async function makeFeature(options) {
61
31
  await makeFeatureSlice({ ...featureOptions, dryRun: true }, addons);
62
32
  return makeFeatureSlice(featureOptions, addons);
63
33
  }
64
- export async function makePayments(options) {
65
- const targetDir = path.resolve(options.cwd ?? process.cwd());
66
- const config = options.config
67
- ? resolveConfig(options.config)
68
- : await loadBeignetConfig(targetDir);
69
- await assertStandardApp(targetDir, config);
70
- const persistence = await detectResourcePersistence(targetDir, config);
71
- if (persistence !== "drizzle") {
72
- throw new Error("beignet make payments expects a Drizzle-backed Beignet app because billing state must be durable. Add a database provider first, then run make payments again.");
73
- }
74
- const database = await detectResourceDatabase(targetDir, config);
75
- const generatedFiles = paymentsFiles(config, database);
76
- const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
77
- force: Boolean(options.force),
78
- });
79
- const billingNames = resourceNames("billing");
80
- const plannedBillingUpdates = await updatePaymentsWiring(targetDir, billingNames, config, { dryRun: true });
81
- const plannedPaymentsUpdates = (await wirePortProviders(targetDir, config, [paymentsPortWiring("make payments")], { dryRun: true })).updatedFiles;
82
- const createdFiles = plannedFiles
83
- .filter((file) => file.result === "created")
84
- .map((file) => file.path);
85
- const updatedFiles = plannedFiles
86
- .filter((file) => file.result === "updated")
87
- .map((file) => file.path);
88
- const skippedFiles = plannedFiles
89
- .filter((file) => file.result === "skipped")
90
- .map((file) => file.path);
91
- if (options.dryRun) {
92
- updatedFiles.push(...plannedBillingUpdates, ...plannedPaymentsUpdates);
93
- }
94
- else {
95
- for (const file of plannedFiles) {
96
- await writePlannedGeneratedFile(targetDir, file);
97
- }
98
- updatedFiles.push(...(await updatePaymentsWiring(targetDir, billingNames, config, {
99
- dryRun: false,
100
- })));
101
- updatedFiles.push(...(await wirePortProviders(targetDir, config, [paymentsPortWiring("make payments")], { dryRun: false })).updatedFiles);
102
- }
103
- return {
104
- schemaVersion: 1,
105
- name: "payments",
106
- targetDir,
107
- dryRun: Boolean(options.dryRun),
108
- files: generatedFiles.map((file) => file.path),
109
- createdFiles,
110
- updatedFiles: uniqueStrings(updatedFiles),
111
- skippedFiles,
112
- };
113
- }
114
34
  async function makeFeatureResource(options) {
115
35
  try {
116
36
  return await makeResourceSlice(options, "feature");
@@ -339,33 +259,6 @@ const featureAddonOrder = [
339
259
  "ui",
340
260
  "upload",
341
261
  ];
342
- function mergeMakeResults(left, right) {
343
- return {
344
- ...left,
345
- files: uniqueStrings([...left.files, ...right.files]),
346
- createdFiles: uniqueStrings([...left.createdFiles, ...right.createdFiles]),
347
- updatedFiles: uniqueStrings([...left.updatedFiles, ...right.updatedFiles]),
348
- skippedFiles: uniqueStrings([...left.skippedFiles, ...right.skippedFiles]),
349
- };
350
- }
351
- function mergeMakeResultInto(target, source) {
352
- target.files = uniqueStrings([...target.files, ...source.files]);
353
- target.createdFiles = uniqueStrings([
354
- ...target.createdFiles,
355
- ...source.createdFiles,
356
- ]);
357
- target.updatedFiles = uniqueStrings([
358
- ...target.updatedFiles,
359
- ...source.updatedFiles,
360
- ]);
361
- target.skippedFiles = uniqueStrings([
362
- ...target.skippedFiles,
363
- ...source.skippedFiles,
364
- ]);
365
- }
366
- function uniqueStrings(values) {
367
- return [...new Set(values)];
368
- }
369
262
  export async function makeContract(options) {
370
263
  const targetDir = path.resolve(options.cwd ?? process.cwd());
371
264
  const names = resourceNames(options.name);
@@ -898,63 +791,19 @@ async function updateListenerProviderWiring(targetDir, names, config, options) {
898
791
  throw new Error(`Could not find the generated providers file ${providersFile}. Register listeners manually with registerListeners(...), or restore the generated providers file before running make listener.`);
899
792
  }
900
793
  const listenerIndex = featureArtifactIndexFile("listener", names, config);
901
- const providerName = `${names.featureSingularCamel}ListenersProvider`;
902
- let next = original;
903
- next = addNamedImport(next, "registerListeners", "@beignet/core/events");
904
- next = addNamedImport(next, "createServiceActor", "@beignet/core/ports");
905
- next = addNamedImport(next, "createProvider", "@beignet/core/providers");
906
- next = addNamedTypeImport(next, "AppContext", aliasModule(config.paths.appContext));
907
- next = addNamedTypeImport(next, "AppServiceContextInput", aliasModule(path.join(path.dirname(config.paths.server), "context.ts")));
908
- next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
909
- const listenerImport = `import { ${listenerIndex.registryName} } from "${aliasModule(listenerIndex.path)}";`;
910
- if (!next.includes(listenerImport)) {
911
- next = insertAfterImports(next, listenerImport);
912
- }
913
- if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
914
- const providerDefinition = `const ${providerName} = createProvider<
915
- \tPick<AppPorts, "eventBus" | "logger">,
916
- \tAppContext,
917
- \tAppServiceContextInput
918
- >()({
919
- \tname: "${names.feature.kebab}-listeners",
920
- \tsetup({ ports, createServiceContext }) {
921
- \t\tconst unregister = registerListeners(ports.eventBus, ${listenerIndex.registryName}, {
922
- \t\t\tctx: () =>
923
- \t\t\t\tcreateServiceContext({
924
- \t\t\t\t\tactor: createServiceActor("beignet-listener"),
925
- \t\t\t\t}),
926
- \t\t\tonError(error, listener) {
927
- \t\t\t\tports.logger.error("Event listener failed", {
928
- \t\t\t\t\terror,
929
- \t\t\t\t\tlistenerName: listener.name,
930
- \t\t\t\t});
931
- \t\t\t},
932
- \t\t});
933
-
934
- \t\treturn {
935
- \t\t\tstop() {
936
- \t\t\t\tunregister();
937
- \t\t\t},
938
- \t\t};
939
- \t},
940
- });
941
- `;
942
- const withProvider = next.replace(/\nexport const providers = \[/, `\n${providerDefinition}\nexport const providers = [`);
943
- if (withProvider === next) {
944
- throw new Error(`Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`);
945
- }
946
- next = withProvider;
947
- }
948
- const appended = appendToNamedArray(next, "providers", providerName);
949
- if (appended.kind === "missing") {
794
+ const result = wireListenersProviderSource(original, {
795
+ registryName: listenerIndex.registryName,
796
+ featureKebab: names.feature.kebab,
797
+ listenerModule: aliasModule(listenerIndex.path),
798
+ config,
799
+ });
800
+ if (result.kind === "missing") {
950
801
  throw new Error(`Could not find the exported providers array in ${providersFile}. Register ${listenerIndex.registryName} manually with registerListeners(...), or restore the generated providers file before running make listener.`);
951
802
  }
952
- if (appended.kind === "updated")
953
- next = appended.source;
954
- if (next === original)
803
+ if (result.kind === "unchanged")
955
804
  return "skipped";
956
805
  if (!options.dryRun)
957
- await writeFile(filePath, next);
806
+ await writeFile(filePath, result.source);
958
807
  return "updated";
959
808
  }
960
809
  export async function makeSchedule(options) {
@@ -1315,171 +1164,6 @@ async function applyOutboxDrainWiring(result, targetDir, plan, options) {
1315
1164
  ...plan.cronSecret.skippedFiles,
1316
1165
  ]);
1317
1166
  }
1318
- function eventBusPortWiring(command) {
1319
- return {
1320
- command,
1321
- portKey: "eventBus",
1322
- portType: "EventBusPort",
1323
- portTypeModule: "@beignet/core/ports",
1324
- providerFactory: "createInMemoryEventBusProvider",
1325
- providerModule: "@beignet/provider-event-bus-memory",
1326
- dependency: "@beignet/provider-event-bus-memory",
1327
- };
1328
- }
1329
- function notificationPortWirings(command) {
1330
- return [
1331
- {
1332
- command,
1333
- portKey: "mailer",
1334
- portType: "MailerPort",
1335
- portTypeModule: "@beignet/core/mail",
1336
- providerFactory: "createMemoryMailerProvider",
1337
- providerModule: "@beignet/core/mail",
1338
- },
1339
- {
1340
- command,
1341
- portKey: "notifications",
1342
- portType: "NotificationPort",
1343
- portTypeModule: "@beignet/core/notifications",
1344
- providerFactory: "createInlineNotificationsProvider",
1345
- providerModule: "@beignet/core/notifications",
1346
- },
1347
- ];
1348
- }
1349
- function paymentsPortWiring(command) {
1350
- return {
1351
- command,
1352
- portKey: "payments",
1353
- portType: "PaymentsPort",
1354
- portTypeModule: "@beignet/core/payments",
1355
- providerFactory: "createMemoryPaymentsProvider",
1356
- providerModule: "@beignet/core/payments",
1357
- };
1358
- }
1359
- function storagePortWiring(command) {
1360
- return {
1361
- command,
1362
- portKey: "storage",
1363
- portType: "StoragePort",
1364
- portTypeModule: "@beignet/core/ports",
1365
- providerFactory: "createLocalStorageProvider",
1366
- providerModule: "@beignet/provider-storage-local",
1367
- dependency: "@beignet/provider-storage-local",
1368
- };
1369
- }
1370
- function providersFilePath(config) {
1371
- return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
1372
- }
1373
- /**
1374
- * Wire provider-backed app ports a generator depends on.
1375
- *
1376
- * Each wiring is independent and skipped silently when its port key already
1377
- * appears in the ports file, so apps that wired their own adapter (or scaffolds
1378
- * whose integrations already contribute the port) are left untouched. All
1379
- * anchors are computed before any file is written; an anchor miss aborts with a
1380
- * copy-pasteable manual instruction so the generator never leaves partial
1381
- * wiring behind. Callers validate anchors with `dryRun: true` before writing
1382
- * artifact files.
1383
- */
1384
- async function wirePortProviders(targetDir, config, wirings, options) {
1385
- const portsFile = config.paths.ports;
1386
- const infrastructureFile = config.paths.infrastructurePorts;
1387
- const providersFile = providersFilePath(config);
1388
- const manualFor = (wiring) => `Wire the ${wiring.portKey} port manually: add ${wiring.portKey}: ${wiring.portType}; to AppPorts in ${portsFile}, defer "${wiring.portKey}" in ${infrastructureFile}, and register ${wiring.providerFactory}() from ${wiring.providerModule} in ${providersFile}.`;
1389
- const portsOriginal = await readOptionalFile(path.join(targetDir, portsFile));
1390
- if (portsOriginal === undefined) {
1391
- if (wirings.length === 0)
1392
- return { updatedFiles: [] };
1393
- throw new Error(`Could not find the generated ports file ${portsFile}. ${manualFor(wirings[0])}`);
1394
- }
1395
- // A port key that already appears in the ports file means the app wired its
1396
- // own adapter or an integration already contributes the port.
1397
- const active = wirings.filter((wiring) => !new RegExp(`\\b${wiring.portKey}\\b`).test(portsOriginal));
1398
- if (active.length === 0)
1399
- return { updatedFiles: [] };
1400
- let portsNext = portsOriginal;
1401
- for (const wiring of active) {
1402
- portsNext = addNamedTypeImport(portsNext, wiring.portType, wiring.portTypeModule);
1403
- portsNext = insertTypeProperty(portsNext, "AppPorts", `${wiring.portKey}: ${wiring.portType};`, `Could not find AppPorts in ${portsFile}. ${manualFor(wiring)} Or restore the generated ports file before running beignet ${wiring.command}.`);
1404
- }
1405
- const infrastructureOriginal = await readOptionalFile(path.join(targetDir, infrastructureFile));
1406
- if (infrastructureOriginal === undefined) {
1407
- throw new Error(`Could not find the generated infrastructure ports file ${infrastructureFile}. ${manualFor(active[0])}`);
1408
- }
1409
- let infrastructureNext = infrastructureOriginal;
1410
- for (const wiring of active) {
1411
- const appended = appendDeferredPortKey(infrastructureNext, wiring.portKey);
1412
- if (appended === infrastructureNext &&
1413
- !new RegExp(`["']${wiring.portKey}["']`).test(infrastructureNext)) {
1414
- throw new Error(`Could not find the deferred port list in ${infrastructureFile}. ${manualFor(wiring)} Or restore the generated infrastructure ports file before running beignet ${wiring.command}.`);
1415
- }
1416
- infrastructureNext = appended;
1417
- }
1418
- const providersOriginal = await readOptionalFile(path.join(targetDir, providersFile));
1419
- if (providersOriginal === undefined) {
1420
- throw new Error(`Could not find the generated providers file ${providersFile}. ${manualFor(active[0])}`);
1421
- }
1422
- let providersNext = providersOriginal;
1423
- for (const wiring of active) {
1424
- if (new RegExp(`\\b${wiring.providerFactory}\\b`).test(providersNext)) {
1425
- continue;
1426
- }
1427
- const appended = appendToNamedArray(providersNext, "providers", `${wiring.providerFactory}()`, `import { ${wiring.providerFactory} } from "${wiring.providerModule}";`);
1428
- if (appended.kind === "missing") {
1429
- throw new Error(`Could not find the exported providers array in ${providersFile}. ${manualFor(wiring)} Or restore the generated providers file before running beignet ${wiring.command}.`);
1430
- }
1431
- if (appended.kind === "updated")
1432
- providersNext = appended.source;
1433
- }
1434
- const dependencies = active.filter((wiring) => wiring.dependency !== undefined);
1435
- let packageJsonNext;
1436
- if (dependencies.length > 0) {
1437
- const packageJsonOriginal = await readFile(path.join(targetDir, "package.json"), "utf8");
1438
- const packageJson = JSON.parse(packageJsonOriginal);
1439
- packageJson.dependencies = packageJson.dependencies ?? {};
1440
- for (const wiring of dependencies) {
1441
- packageJson.dependencies[wiring.dependency] ??=
1442
- beignetDependencyVersion(packageJson);
1443
- }
1444
- const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
1445
- if (next !== packageJsonOriginal)
1446
- packageJsonNext = next;
1447
- }
1448
- const plannedFiles = [];
1449
- if (portsNext !== portsOriginal) {
1450
- plannedFiles.push({ path: portsFile, content: portsNext });
1451
- }
1452
- if (infrastructureNext !== infrastructureOriginal) {
1453
- plannedFiles.push({
1454
- path: infrastructureFile,
1455
- content: infrastructureNext,
1456
- });
1457
- }
1458
- if (providersNext !== providersOriginal) {
1459
- plannedFiles.push({ path: providersFile, content: providersNext });
1460
- }
1461
- if (packageJsonNext !== undefined) {
1462
- plannedFiles.push({ path: "package.json", content: packageJsonNext });
1463
- }
1464
- if (!options.dryRun) {
1465
- for (const file of plannedFiles) {
1466
- await writeFile(path.join(targetDir, file.path), file.content);
1467
- }
1468
- }
1469
- return { updatedFiles: plannedFiles.map((file) => file.path) };
1470
- }
1471
- /**
1472
- * Record port provider wiring updates on a make result.
1473
- */
1474
- async function applyPortProviderWiring(result, targetDir, config, wirings, options) {
1475
- const wired = await wirePortProviders(targetDir, config, wirings, options);
1476
- for (const file of wired.updatedFiles) {
1477
- if (!result.updatedFiles.includes(file))
1478
- result.updatedFiles.push(file);
1479
- if (!result.files.includes(file))
1480
- result.files.push(file);
1481
- }
1482
- }
1483
1167
  export async function makeUpload(options) {
1484
1168
  const targetDir = path.resolve(options.cwd ?? process.cwd());
1485
1169
  const names = uploadNames(options.name);
@@ -1626,247 +1310,6 @@ async function makeFeatureUi(options) {
1626
1310
  skippedFiles,
1627
1311
  };
1628
1312
  }
1629
- async function assertStandardApp(targetDir, config) {
1630
- const requiredFiles = [
1631
- config.paths.appContext,
1632
- config.paths.infrastructurePorts,
1633
- config.paths.ports,
1634
- config.paths.server,
1635
- config.paths.useCaseBuilder,
1636
- ];
1637
- for (const file of requiredFiles) {
1638
- try {
1639
- await stat(path.join(targetDir, file));
1640
- }
1641
- catch {
1642
- throw new Error(`beignet make resource expects a standard Beignet app. Missing ${file}.`);
1643
- }
1644
- }
1645
- }
1646
- async function assertUseCaseApp(targetDir, config) {
1647
- try {
1648
- await stat(path.join(targetDir, config.paths.useCaseBuilder));
1649
- }
1650
- catch {
1651
- throw new Error(`beignet make use-case expects a standard Beignet app. Missing ${config.paths.useCaseBuilder}.`);
1652
- }
1653
- }
1654
- async function assertTestApp(targetDir, names, config) {
1655
- const requiredFiles = [
1656
- config.paths.appContext,
1657
- config.paths.infrastructurePorts,
1658
- useCaseFilePath(names, config),
1659
- ];
1660
- for (const file of requiredFiles) {
1661
- try {
1662
- await stat(path.join(targetDir, file));
1663
- }
1664
- catch {
1665
- throw new Error(`beignet make test expects a standard Beignet app with the target use case. Missing ${file}.`);
1666
- }
1667
- }
1668
- }
1669
- async function assertPortApp(targetDir, config) {
1670
- const requiredFiles = [config.paths.ports, config.paths.infrastructurePorts];
1671
- for (const file of requiredFiles) {
1672
- try {
1673
- await stat(path.join(targetDir, file));
1674
- }
1675
- catch {
1676
- throw new Error(`beignet make port expects a standard Beignet app. Missing ${file}.`);
1677
- }
1678
- }
1679
- }
1680
- async function assertAdapterApp(targetDir, names, config) {
1681
- const requiredFiles = [
1682
- config.paths.infrastructurePorts,
1683
- portFilePath(names, config),
1684
- ];
1685
- for (const file of requiredFiles) {
1686
- try {
1687
- await stat(path.join(targetDir, file));
1688
- }
1689
- catch {
1690
- throw new Error(`beignet make adapter expects an existing port in a standard Beignet app. Missing ${file}.`);
1691
- }
1692
- }
1693
- const infrastructurePorts = await readFile(path.join(targetDir, config.paths.infrastructurePorts), "utf8");
1694
- if (!infrastructurePorts.includes(portStubEntry(names)) &&
1695
- !infrastructurePorts.includes(adapterEntry(names))) {
1696
- throw new Error(`Could not find the generated ${names.interfaceName} infrastructure stub in ${config.paths.infrastructurePorts}. Wire ${names.adapterFactoryName}() manually or restore the generated stub before running make adapter.`);
1697
- }
1698
- }
1699
- async function assertPolicyApp(targetDir, config) {
1700
- try {
1701
- await stat(path.join(targetDir, config.paths.appContext));
1702
- }
1703
- catch {
1704
- throw new Error(`beignet make policy expects a standard Beignet app. Missing ${config.paths.appContext}.`);
1705
- }
1706
- }
1707
- async function assertFeatureArtifactApp(targetDir, config, artifact, options = {}) {
1708
- const requiredFiles = [
1709
- config.paths.appContext,
1710
- ...(options.requireServer ? [config.paths.server] : []),
1711
- ];
1712
- for (const file of requiredFiles) {
1713
- try {
1714
- await stat(path.join(targetDir, file));
1715
- }
1716
- catch {
1717
- throw new Error(`beignet make ${artifact} expects a standard Beignet app. Missing ${file}.`);
1718
- }
1719
- }
1720
- }
1721
- async function assertFeatureUiApp(targetDir, config) {
1722
- const clientPath = clientIndexPath(config);
1723
- const requiredFiles = [config.paths.appContext, clientPath];
1724
- for (const file of requiredFiles) {
1725
- try {
1726
- await stat(path.join(targetDir, file));
1727
- }
1728
- catch {
1729
- throw new Error(`beignet make feature --with ui expects a standard Beignet app with React Query client helpers. Missing ${file}.`);
1730
- }
1731
- }
1732
- }
1733
- async function assertServerRuntimeApp(targetDir, config, artifact) {
1734
- const requiredFiles = [
1735
- config.paths.appContext,
1736
- config.paths.server,
1737
- config.paths.infrastructurePorts,
1738
- ];
1739
- for (const file of requiredFiles) {
1740
- try {
1741
- await stat(path.join(targetDir, file));
1742
- }
1743
- catch {
1744
- throw new Error(`beignet make ${artifact} expects a standard Beignet app with a server runtime. Missing ${file}.`);
1745
- }
1746
- }
1747
- }
1748
- async function assertPersistenceArtifactApp(targetDir, names, config, artifact) {
1749
- const requiredFiles = [
1750
- config.paths.appContext,
1751
- config.paths.ports,
1752
- resourcePortFilePath(featureResourceNames(names), config),
1753
- ];
1754
- for (const file of requiredFiles) {
1755
- try {
1756
- await stat(path.join(targetDir, file));
1757
- }
1758
- catch {
1759
- throw new Error(`beignet make ${artifact} expects a feature with a repository port. Missing ${file}. Run beignet make feature ${names.feature.kebab} first, or add the repository port before generating ${artifact}s.`);
1760
- }
1761
- }
1762
- }
1763
- async function writeGeneratedFile(targetDir, file, options) {
1764
- const plannedFile = await planGeneratedFile(targetDir, file, {
1765
- force: options.force,
1766
- });
1767
- if (!options.dryRun) {
1768
- await writePlannedGeneratedFile(targetDir, plannedFile);
1769
- }
1770
- return plannedFile.result;
1771
- }
1772
- async function planGeneratedFiles(targetDir, files, options) {
1773
- const plannedFiles = [];
1774
- for (const file of files) {
1775
- plannedFiles.push(await planGeneratedFile(targetDir, file, options));
1776
- }
1777
- return plannedFiles;
1778
- }
1779
- async function planGeneratedFile(targetDir, file, options) {
1780
- const destination = path.join(targetDir, file.path);
1781
- const existing = await readOptionalFile(destination);
1782
- if (existing === file.content) {
1783
- return { ...file, result: "skipped" };
1784
- }
1785
- if (existing !== undefined && !options.force) {
1786
- throw new Error(`File ${file.path} already exists with different content. Pass --force to overwrite it.`);
1787
- }
1788
- return { ...file, result: existing === undefined ? "created" : "updated" };
1789
- }
1790
- async function writePlannedGeneratedFile(targetDir, file) {
1791
- if (file.result === "skipped")
1792
- return;
1793
- const destination = path.join(targetDir, file.path);
1794
- await mkdir(path.dirname(destination), { recursive: true });
1795
- await writeFile(destination, file.content);
1796
- }
1797
- async function fileExists(filePath) {
1798
- try {
1799
- await stat(filePath);
1800
- return true;
1801
- }
1802
- catch {
1803
- return false;
1804
- }
1805
- }
1806
- async function readOptionalFile(filePath) {
1807
- try {
1808
- return await readFile(filePath, "utf8");
1809
- }
1810
- catch {
1811
- return undefined;
1812
- }
1813
- }
1814
- const capabilityBuilders = {
1815
- listeners: {
1816
- factoryName: "createListeners",
1817
- defineName: "defineListener",
1818
- module: "@beignet/core/events",
1819
- },
1820
- jobs: {
1821
- factoryName: "createJobs",
1822
- defineName: "defineJob",
1823
- module: "@beignet/core/jobs",
1824
- },
1825
- schedules: {
1826
- factoryName: "createSchedules",
1827
- defineName: "defineSchedule",
1828
- module: "@beignet/core/schedules",
1829
- },
1830
- notifications: {
1831
- factoryName: "createNotifications",
1832
- defineName: "defineNotification",
1833
- module: "@beignet/core/notifications",
1834
- },
1835
- tasks: {
1836
- factoryName: "createTasks",
1837
- defineName: "defineTask",
1838
- module: "@beignet/core/tasks",
1839
- },
1840
- };
1841
- function capabilityBuilderPath(capability, config) {
1842
- if (capability === "listeners")
1843
- return config.paths.listenersBuilder;
1844
- if (capability === "jobs")
1845
- return config.paths.jobsBuilder;
1846
- if (capability === "schedules")
1847
- return config.paths.schedulesBuilder;
1848
- if (capability === "notifications")
1849
- return config.paths.notificationsBuilder;
1850
- return config.paths.tasksBuilder;
1851
- }
1852
- function capabilityBuilderFile(capability, config) {
1853
- const builder = capabilityBuilders[capability];
1854
- return {
1855
- path: capabilityBuilderPath(capability, config),
1856
- content: `import { ${builder.factoryName} } from "${builder.module}";
1857
- import type { AppContext } from "${aliasModule(config.paths.appContext)}";
1858
-
1859
- export const { ${builder.defineName} } = ${builder.factoryName}<AppContext>();
1860
- `,
1861
- };
1862
- }
1863
- async function missingCapabilityBuilderFiles(targetDir, capability, config) {
1864
- const file = capabilityBuilderFile(capability, config);
1865
- if (await fileExists(path.join(targetDir, file.path))) {
1866
- return [];
1867
- }
1868
- return [file];
1869
- }
1870
1313
  async function makeFeatureArtifact(options) {
1871
1314
  const plannedFiles = await planGeneratedFiles(options.targetDir, options.files, {
1872
1315
  force: options.force,
@@ -1908,44 +1351,6 @@ async function makeFeatureArtifact(options) {
1908
1351
  skippedFiles,
1909
1352
  };
1910
1353
  }
1911
- async function updatePackageDependencies(targetDir, dependencies, options) {
1912
- const filePath = path.join(targetDir, "package.json");
1913
- const original = await readFile(filePath, "utf8");
1914
- const packageJson = JSON.parse(original);
1915
- packageJson.dependencies = packageJson.dependencies ?? {};
1916
- for (const [name, version] of Object.entries(dependencies)) {
1917
- packageJson.dependencies[name] ??= version(packageJson);
1918
- }
1919
- const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
1920
- if (next === original)
1921
- return false;
1922
- if (!options.dryRun)
1923
- await writeFile(filePath, next);
1924
- return true;
1925
- }
1926
- async function updatePackageScripts(targetDir, scripts, options) {
1927
- const filePath = path.join(targetDir, "package.json");
1928
- const original = await readFile(filePath, "utf8");
1929
- const packageJson = JSON.parse(original);
1930
- packageJson.scripts = packageJson.scripts ?? {};
1931
- for (const [name, command] of Object.entries(scripts)) {
1932
- packageJson.scripts[name] ??= command;
1933
- }
1934
- const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
1935
- if (next === original)
1936
- return false;
1937
- if (!options.dryRun)
1938
- await writeFile(filePath, next);
1939
- return true;
1940
- }
1941
- function beignetDependencyVersion(packageJson) {
1942
- return (packageJson.dependencies?.["@beignet/core"] ??
1943
- packageJson.dependencies?.["@beignet/next"] ??
1944
- "*");
1945
- }
1946
- function tanstackReactQueryDependencyVersion(packageJson) {
1947
- return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
1948
- }
1949
1354
  async function updateResourceWiring(targetDir, names, config, persistenceOrOptions, maybeOptions) {
1950
1355
  const persistence = typeof persistenceOrOptions === "string"
1951
1356
  ? persistenceOrOptions
@@ -2009,1256 +1414,9 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
2009
1414
  updated.add(routesFile);
2010
1415
  return [...updated];
2011
1416
  }
2012
- async function updatePaymentsWiring(targetDir, billingNames, config, options) {
2013
- const updated = new Set();
2014
- if (await updatePackageJson(targetDir, options)) {
2015
- updated.add("package.json");
2016
- }
2017
- if (await updatePortsIndex(targetDir, billingNames, config, {
2018
- ...options,
2019
- generationOptions: {
2020
- auth: false,
2021
- tenant: false,
2022
- events: false,
2023
- softDelete: false,
2024
- },
2025
- })) {
2026
- updated.add(config.paths.ports);
2027
- }
2028
- if (await updatePaymentsEntitlementsPort(targetDir, config, options)) {
2029
- updated.add(config.paths.ports);
2030
- updated.add(config.paths.infrastructurePorts);
2031
- }
2032
- if (await updateInfrastructureDeferredPorts(targetDir, billingNames, config, options)) {
2033
- updated.add(config.paths.infrastructurePorts);
2034
- }
2035
- if (await updateBillingDrizzleSchemaIndex(targetDir, config, options)) {
2036
- updated.add(drizzleSchemaIndexPath(config));
2037
- }
2038
- if (await updateDrizzleRepositories(targetDir, billingNames, config, options)) {
2039
- updated.add(drizzleRepositoriesPath(config));
2040
- }
2041
- if (await updateDatabaseProviderPick(targetDir, config, options)) {
2042
- updated.add(path.join(infrastructureDir(config), "db/provider.ts"));
2043
- }
2044
- if (await updateDatabaseProviderEntitlements(targetDir, config, options)) {
2045
- updated.add(path.join(infrastructureDir(config), "db/provider.ts"));
2046
- }
2047
- if (await updateBillingTransactionPorts(targetDir, config, options)) {
2048
- updated.add(path.join(infrastructureDir(config), "db/transaction-ports.ts"));
2049
- }
2050
- if (await updateDatabaseResetTables(targetDir, config, options)) {
2051
- updated.add(path.join(infrastructureDir(config), "db/reset.ts"));
2052
- }
2053
- if (await updateBillingSharedErrors(targetDir, config, options)) {
2054
- updated.add(resourceSharedErrorsPath(config));
2055
- }
2056
- if (await updateBillingEnv(targetDir, options)) {
2057
- updated.add("lib/env.ts");
2058
- }
2059
- if (await updateBillingEnvExample(targetDir, options)) {
2060
- updated.add(".env.example");
2061
- }
2062
- const routesFile = await updateServerRoutes(targetDir, billingNames, config, options);
2063
- if (routesFile)
2064
- updated.add(routesFile);
2065
- return [...updated];
2066
- }
2067
- async function updatePackageJson(targetDir, options) {
2068
- const filePath = path.join(targetDir, "package.json");
2069
- const original = await readFile(filePath, "utf8");
2070
- const packageJson = JSON.parse(original);
2071
- packageJson.scripts = packageJson.scripts ?? {};
2072
- packageJson.scripts.test ??= "bun test";
2073
- if (options.webTestingDependency) {
2074
- packageJson.devDependencies = packageJson.devDependencies ?? {};
2075
- packageJson.devDependencies["@beignet/web"] ??=
2076
- packageJson.dependencies?.["@beignet/core"] ??
2077
- packageJson.dependencies?.["@beignet/next"] ??
2078
- "*";
2079
- }
2080
- const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
2081
- if (!options.dryRun)
2082
- await writeFile(filePath, next);
2083
- return next !== original;
2084
- }
2085
- async function updatePortsIndex(targetDir, names, config, options) {
2086
- const filePath = path.join(targetDir, config.paths.ports);
2087
- const original = await readFile(filePath, "utf8");
2088
- let next = original;
2089
- const repositoryPath = resourcePortFilePath(names, config);
2090
- const importLine = `import type { ${names.singularPascal}Repository } from "${relativeModule(config.paths.ports, repositoryPath)}";`;
2091
- if (!next.includes(importLine)) {
2092
- next = insertAfterImports(next, importLine);
2093
- }
2094
- if (options.generationOptions?.auth) {
2095
- const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
2096
- const policyImportLine = `import type { AuthorizationContext as ${names.singularPascal}AuthorizationContext, ${names.singularCamel}Policy } from "${relativeModule(config.paths.ports, policyPath)}";`;
2097
- if (!next.includes(policyImportLine)) {
2098
- next = insertAfterImports(next, policyImportLine);
2099
- }
2100
- next = appendUnionTypeMember(next, "AppAuthorizationContext", `${names.singularPascal}AuthorizationContext`);
2101
- next = appendTupleTypeMember(next, "AppGate", `typeof ${names.singularCamel}Policy`);
2102
- next = appendGatePolicyMember(next, `typeof ${names.singularCamel}Policy`);
2103
- }
2104
- const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
2105
- if (!next.includes(entry.trim())) {
2106
- next = insertTypeProperty(next, "AppPorts", entry.trim(), `Could not find AppPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make resource.`);
2107
- }
2108
- const transactionPorts = typeBodySource(next, "AppTransactionPorts");
2109
- if (transactionPorts !== undefined &&
2110
- !transactionPorts.includes(entry.trim())) {
2111
- next = replaceTransactionPortsRequired(next, entry, `Could not find AppTransactionPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make resource.`);
2112
- }
2113
- if (next === original)
2114
- return false;
2115
- if (!options.dryRun)
2116
- await writeFile(filePath, next);
2117
- return true;
2118
- }
2119
- async function updatePortIndex(targetDir, names, config, options) {
2120
- const filePath = path.join(targetDir, config.paths.ports);
2121
- const original = await readFile(filePath, "utf8");
2122
- let next = original;
2123
- const portPath = path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
2124
- const importLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.ports, portPath)}";`;
2125
- if (!next.includes(importLine)) {
2126
- next = insertAfterImports(next, importLine);
2127
- }
2128
- const entry = `\t${names.camel}: ${names.interfaceName};`;
2129
- if (!next.includes(entry.trim())) {
2130
- next = insertTypeProperty(next, "AppPorts", entry.trim(), `Could not find AppPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make port.`);
2131
- }
2132
- if (next === original)
2133
- return false;
2134
- if (!options.dryRun)
2135
- await writeFile(filePath, next);
2136
- return true;
2137
- }
2138
- async function updatePaymentsEntitlementsPort(targetDir, config, options) {
2139
- let changed = false;
2140
- const portsFilePath = path.join(targetDir, config.paths.ports);
2141
- const portsOriginal = await readFile(portsFilePath, "utf8");
2142
- let portsNext = addNamedTypeImport(portsOriginal, "EntitlementsPort", "@beignet/core/entitlements");
2143
- if (!/\bentitlements\s*:/.test(portsNext)) {
2144
- portsNext = insertTypeProperty(portsNext, "AppPorts", "entitlements: EntitlementsPort;", `Could not find AppPorts in ${config.paths.ports}. Add entitlements: EntitlementsPort; manually, or restore the generated ports file before running make payments.`);
2145
- }
2146
- if (portsNext !== portsOriginal) {
2147
- changed = true;
2148
- if (!options.dryRun)
2149
- await writeFile(portsFilePath, portsNext);
2150
- }
2151
- const infrastructureFilePath = path.join(targetDir, config.paths.infrastructurePorts);
2152
- const infrastructureOriginal = await readFile(infrastructureFilePath, "utf8");
2153
- const infrastructureNext = appendDeferredPortKey(infrastructureOriginal, "entitlements");
2154
- if (infrastructureNext === infrastructureOriginal &&
2155
- !/["']entitlements["']/.test(infrastructureOriginal)) {
2156
- throw new Error(`Could not find the deferred port list in ${config.paths.infrastructurePorts}. Add "entitlements" manually, or restore the generated infrastructure ports file before running make payments.`);
2157
- }
2158
- if (infrastructureNext !== infrastructureOriginal) {
2159
- changed = true;
2160
- if (!options.dryRun) {
2161
- await writeFile(infrastructureFilePath, infrastructureNext);
2162
- }
2163
- }
2164
- return changed;
2165
- }
2166
- async function updateInfrastructurePortStub(targetDir, names, config, options) {
2167
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
2168
- const original = await readFile(filePath, "utf8");
2169
- let next = original;
2170
- const portPath = portFilePath(names, config);
2171
- const importLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.infrastructurePorts, portPath)}";`;
2172
- if (!next.includes(importLine)) {
2173
- next = insertAfterImports(next, importLine);
2174
- }
2175
- const entry = portStubEntry(names);
2176
- const definePortsBody = definePortsBodySource(next);
2177
- if (definePortsBody === undefined ||
2178
- !hasTopLevelObjectProperty(definePortsBody, names.camel)) {
2179
- next = insertDefinePortsProperty(next, entry.trim(), `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add the ${names.interfaceName} stub manually, or restore the generated infrastructure ports file before running make port.`);
2180
- }
2181
- if (next === original)
2182
- return false;
2183
- if (!options.dryRun)
2184
- await writeFile(filePath, next);
2185
- return true;
2186
- }
2187
- async function updateInfrastructureAdapterWiring(targetDir, names, config, options) {
2188
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
2189
- const original = await readFile(filePath, "utf8");
2190
- let next = original;
2191
- const adapterPath = adapterFilePath(names, config);
2192
- const adapterImportLine = `import { ${names.adapterFactoryName} } from "${relativeModule(config.paths.infrastructurePorts, adapterPath)}";`;
2193
- if (!next.includes(adapterImportLine)) {
2194
- next = insertAfterImports(next, adapterImportLine);
2195
- }
2196
- const portPath = portFilePath(names, config);
2197
- const portImportLine = `import type { ${names.interfaceName} } from "${relativeModule(config.paths.infrastructurePorts, portPath)}";`;
2198
- if (next.includes(portImportLine)) {
2199
- next = next.replace(`${portImportLine}\n`, "");
2200
- }
2201
- const adapter = adapterEntry(names);
2202
- const stub = portStubEntry(names);
2203
- if (next.includes(stub)) {
2204
- next = next.replace(stub, adapter);
2205
- }
2206
- else if (!next.includes(adapter)) {
2207
- throw new Error(`Could not find the generated ${names.interfaceName} infrastructure stub in ${config.paths.infrastructurePorts}. Wire ${names.adapterFactoryName}() manually or restore the generated stub before running make adapter.`);
2208
- }
2209
- if (next === original)
2210
- return false;
2211
- if (!options.dryRun)
2212
- await writeFile(filePath, next);
2213
- return true;
2214
- }
2215
- async function updateInfrastructurePorts(targetDir, names, config, options) {
2216
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
2217
- const original = await readFile(filePath, "utf8");
2218
- let next = original;
2219
- const repositoryPath = path.join(path.dirname(config.paths.infrastructurePorts), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
2220
- const importLine = `import { createInMemory${names.singularPascal}Repository } from "${relativeModule(config.paths.infrastructurePorts, repositoryPath)}";`;
2221
- if (!next.includes(importLine)) {
2222
- next = insertAfterImports(next, importLine);
2223
- }
2224
- const repositoryConst = `const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository();`;
2225
- if (!next.includes(repositoryConst)) {
2226
- next = replaceRequired(next, /(\nexport const appPorts = definePorts(?:<[^>]+>)?\((?:\)\()?\{)/, `\n${repositoryConst}\n$1`, `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`);
2227
- }
2228
- const entry = `\t${names.pluralCamel},`;
2229
- const definePortsBody = definePortsBodySource(next);
2230
- if (definePortsBody === undefined ||
2231
- !hasTopLevelObjectProperty(definePortsBody, names.pluralCamel)) {
2232
- next = insertDefinePortsProperty(next, entry.trim(), `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${entry.trim()} to appPorts manually, or restore the generated infrastructure ports file before running make resource.`);
2233
- }
2234
- const transactionEntry = `\t\t${names.pluralCamel},`;
2235
- const uowBody = noopUnitOfWorkBodySource(next);
2236
- if (uowBody !== undefined &&
2237
- !new RegExp(`\\b${names.pluralCamel}\\b\\s*,`).test(uowBody)) {
2238
- next = insertNoopUnitOfWorkProperty(next, transactionEntry.trim(), `Could not find the generated Unit of Work port list in ${config.paths.infrastructurePorts}. Add ${transactionEntry.trim()} manually, or restore the generated infrastructure ports file before running make resource.`);
2239
- }
2240
- if (next === original)
2241
- return false;
2242
- if (!options.dryRun)
2243
- await writeFile(filePath, next);
2244
- return true;
2245
- }
2246
- async function updateInfrastructureGatePolicies(targetDir, names, config, options) {
2247
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
2248
- const original = await readFile(filePath, "utf8");
2249
- let next = original;
2250
- const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
2251
- const importLine = `import { ${names.singularCamel}Policy } from "${relativeModule(config.paths.infrastructurePorts, policyPath)}";`;
2252
- if (!next.includes(importLine)) {
2253
- next = insertAfterImports(next, importLine);
2254
- }
2255
- next = appendCreateGatePolicy(next, `${names.singularCamel}Policy`, `Could not find createGate({ policies: [...] }) in ${config.paths.infrastructurePorts}. Add ${names.singularCamel}Policy to the gate policies manually, or restore the generated infrastructure ports file before running make resource --auth.`);
2256
- if (next === original)
2257
- return false;
2258
- if (!options.dryRun)
2259
- await writeFile(filePath, next);
2260
- return true;
2261
- }
2262
- async function updateDrizzleSchemaIndex(targetDir, names, config, options) {
2263
- const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
2264
- if (!(await fileExists(filePath)))
2265
- return false;
2266
- const original = await readFile(filePath, "utf8");
2267
- const exportLine = `export { ${names.pluralCamel} } from "./${names.pluralKebab}";`;
2268
- if (original.includes(exportLine))
2269
- return false;
2270
- const next = `${original.trimEnd()}\n${exportLine}\n`;
2271
- if (!options.dryRun)
2272
- await writeFile(filePath, next);
2273
- return true;
2274
- }
2275
- async function updateBillingDrizzleSchemaIndex(targetDir, config, options) {
2276
- const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
2277
- if (!(await fileExists(filePath)))
2278
- return false;
2279
- const original = await readFile(filePath, "utf8");
2280
- const exportLine = 'export { billingAccounts } from "./billing";';
2281
- if (original.includes(exportLine))
2282
- return false;
2283
- const next = `${original.trimEnd()}\n${exportLine}\n`;
2284
- if (!options.dryRun)
2285
- await writeFile(filePath, next);
2286
- return true;
2287
- }
2288
- async function updateDrizzleRepositories(targetDir, names, config, options) {
2289
- const filePath = path.join(targetDir, drizzleRepositoriesPath(config));
2290
- if (!(await fileExists(filePath)))
2291
- return false;
2292
- const original = await readFile(filePath, "utf8");
2293
- let next = original;
2294
- const repositoryPath = drizzleResourceRepositoryFilePath(names, config);
2295
- const importLine = `import { createDrizzle${names.singularPascal}Repository } from "${relativeModule(drizzleRepositoriesPath(config), repositoryPath)}";`;
2296
- const repositoriesBody = createRepositoriesReturnBodySource(next);
2297
- const entry = `\t\t${names.pluralCamel}: createDrizzle${names.singularPascal}Repository(db),`;
2298
- if (repositoriesBody === undefined) {
2299
- throw new Error(`Could not find return { in createRepositories in ${drizzleRepositoriesPath(config)}. Add ${entry.trim()} manually, or restore the generated repositories file before running make resource.`);
2300
- }
2301
- if (!next.includes(importLine)) {
2302
- next = insertAfterImports(next, importLine);
2303
- }
2304
- if (!hasTopLevelObjectProperty(repositoriesBody, names.pluralCamel)) {
2305
- next = insertCreateRepositoriesProperty(next, entry.trim(), `Could not find return { in createRepositories in ${drizzleRepositoriesPath(config)}. Add ${entry.trim()} manually, or restore the generated repositories file before running make resource.`);
2306
- }
2307
- if (next === original)
2308
- return false;
2309
- if (!options.dryRun)
2310
- await writeFile(filePath, next);
2311
- return true;
2312
- }
2313
- async function updateDatabaseProviderPick(targetDir, config, options) {
2314
- const file = path.join(infrastructureDir(config), "db/provider.ts");
2315
- const filePath = path.join(targetDir, file);
2316
- if (!(await fileExists(filePath)))
2317
- return false;
2318
- const original = await readFile(filePath, "utf8");
2319
- const next = appendPickStringLiteralMember(original, "AppPorts", "billing");
2320
- if (next === original)
2321
- return false;
2322
- if (!options.dryRun)
2323
- await writeFile(filePath, next);
2324
- return true;
2325
- }
2326
- async function updateDatabaseProviderEntitlements(targetDir, config, options) {
2327
- const file = path.join(infrastructureDir(config), "db/provider.ts");
2328
- const filePath = path.join(targetDir, file);
2329
- if (!(await fileExists(filePath)))
2330
- return false;
2331
- const original = await readFile(filePath, "utf8");
2332
- let next = original;
2333
- const importLine = 'import { createBillingEntitlements } from "@/features/billing/entitlements";';
2334
- if (!next.includes(importLine)) {
2335
- next = insertAfterImports(next, importLine);
2336
- }
2337
- next = appendPickStringLiteralMember(next, "AppPorts", "entitlements");
2338
- if (!/\bconst\s+entitlements\s*=/.test(next)) {
2339
- const withEntitlementsConst = next.replace(/(\n\s*const repositories = createRepositories\(dbPort\.(?:drizzle|db)\);)/, `$1\n\t\tconst entitlements = createBillingEntitlements(repositories.billing);`);
2340
- if (withEntitlementsConst === next) {
2341
- throw new Error(`Could not find createRepositories(dbPort.drizzle) in ${file}. Add const entitlements = createBillingEntitlements(repositories.billing) manually, or restore the generated database provider before running make payments.`);
2342
- }
2343
- next = withEntitlementsConst;
2344
- }
2345
- if (!/\bentitlements\s*,/.test(next)) {
2346
- const withEntitlementsPort = next.replace(/(\n\s*\.\.\.repositories,\n)/, `$1\t\t\tentitlements,\n`);
2347
- if (withEntitlementsPort === next) {
2348
- throw new Error(`Could not find the repository spread in ${file}. Add entitlements to providedPorts manually, or restore the generated database provider before running make payments.`);
2349
- }
2350
- next = withEntitlementsPort;
2351
- }
2352
- if (next === original)
2353
- return false;
2354
- if (!options.dryRun)
2355
- await writeFile(filePath, next);
2356
- return true;
2357
- }
2358
- async function updateBillingTransactionPorts(targetDir, config, options) {
2359
- const file = path.join(infrastructureDir(config), "db/transaction-ports.ts");
2360
- const filePath = path.join(targetDir, file);
2361
- if (!(await fileExists(filePath)))
2362
- return false;
2363
- const original = await readFile(filePath, "utf8");
2364
- if (/Pick<AppTransactionPorts,\s*["']billing["']>/.test(original)) {
2365
- return false;
2366
- }
2367
- const repositoryProperty = /repositories:\s*([^;\n]+);/.exec(original);
2368
- if (!repositoryProperty) {
2369
- throw new Error(`Could not find repositories: ... in ${file}. Add billing to the transaction repository dependency manually, or restore the generated transaction ports file before running make payments.`);
2370
- }
2371
- const repositoryType = repositoryProperty[1]?.trim();
2372
- if (!repositoryType) {
2373
- throw new Error(`Could not update repositories in ${file}. Add billing to the transaction repository dependency manually, or restore the generated transaction ports file before running make payments.`);
2374
- }
2375
- const next = original.replace(repositoryProperty[0], `repositories: ${repositoryType} & Pick<AppTransactionPorts, "billing">;`);
2376
- if (next === original)
2377
- return false;
2378
- if (!options.dryRun)
2379
- await writeFile(filePath, next);
2380
- return true;
2381
- }
2382
- async function updateDatabaseResetTables(targetDir, config, options) {
2383
- const file = path.join(infrastructureDir(config), "db/reset.ts");
2384
- const filePath = path.join(targetDir, file);
2385
- if (!(await fileExists(filePath)))
2386
- return false;
2387
- const original = await readFile(filePath, "utf8");
2388
- if (/["']billing_accounts["']/.test(original))
2389
- return false;
2390
- const tables = arrayInitializerInfo(original, "tables");
2391
- if (!tables)
2392
- return false;
2393
- const nextArray = appendToArrayExpression(tables.text, [
2394
- '"billing_accounts"',
2395
- ]);
2396
- const next = `${original.slice(0, tables.start)}${nextArray}${original.slice(tables.end)}`;
2397
- if (next === original)
2398
- return false;
2399
- if (!options.dryRun)
2400
- await writeFile(filePath, next);
2401
- return true;
2402
- }
2403
- async function updateInfrastructureDeferredPorts(targetDir, names, config, options) {
2404
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
2405
- const original = await readFile(filePath, "utf8");
2406
- // Drizzle resources are provided by the app database provider at startup,
2407
- // so deferred-form ports files defer the new repository key. Identity-form
2408
- // ports files are left untouched.
2409
- const next = appendDeferredPortKey(original, names.pluralCamel);
2410
- if (next === original)
2411
- return false;
2412
- if (!options.dryRun)
2413
- await writeFile(filePath, next);
2414
- return true;
2415
- }
2416
- async function updateServerTransactionPorts(targetDir, names, config, options) {
2417
- const filePath = path.join(targetDir, config.paths.server);
2418
- const original = await readFile(filePath, "utf8");
2419
- let next = original;
2420
- const transactionPortsBody = drizzleTransactionPortsBodySource(next);
2421
- if (transactionPortsBody === undefined ||
2422
- hasTopLevelObjectProperty(transactionPortsBody, names.pluralCamel)) {
2423
- return false;
2424
- }
2425
- if (!/\bappPorts\b/.test(next)) {
2426
- throw new Error(`Could not find appPorts in ${config.paths.server}. Add ${names.pluralCamel}: appPorts.${names.pluralCamel} to createTransactionPorts manually, or restore the generated server file before running make resource.`);
2427
- }
2428
- const entry = `\t\t\t\t\t\t${names.pluralCamel}: appPorts.${names.pluralCamel},`;
2429
- next = insertDrizzleTransactionPortsProperty(next, entry.trim(), `Could not find createTransactionPorts({ in ${config.paths.server}. Add ${entry.trim()} manually, or restore the generated server file before running make resource.`);
2430
- if (next === original)
2431
- return false;
2432
- if (!options.dryRun)
2433
- await writeFile(filePath, next);
2434
- return true;
2435
- }
2436
- async function updateOpenApiRoute(targetDir, names, config, options) {
2437
- const filePath = path.join(targetDir, config.paths.openapiRoute);
2438
- if (!(await fileExists(filePath)))
2439
- return false;
2440
- const original = await readFile(filePath, "utf8");
2441
- const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
2442
- if (!firstArg?.text.startsWith("["))
2443
- return false;
2444
- const contractNames = resourceContractNames(names, options.mode);
2445
- const listedContracts = identifiersFromArrayExpression(firstArg.text);
2446
- const missingContracts = contractNames.filter((contractName) => !listedContracts.has(contractName));
2447
- if (missingContracts.length === 0)
2448
- return false;
2449
- const importLine = `import { ${contractNames.join(", ")} } from "${aliasModule(resourceContractFilePath(names, config))}";`;
2450
- const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
2451
- let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(firstArg.end)}`;
2452
- if (!next.includes(importLine)) {
2453
- next = insertAfterImports(next, importLine);
2454
- }
2455
- if (next === original)
2456
- return false;
2457
- if (!options.dryRun)
2458
- await writeFile(filePath, next);
2459
- return true;
2460
- }
2461
- async function updateSharedErrors(targetDir, names, config, options) {
2462
- if (options.mode !== "resource")
2463
- return false;
2464
- const filePath = path.join(targetDir, resourceSharedErrorsPath(config));
2465
- if (!(await fileExists(filePath)))
2466
- return false;
2467
- const original = await readFile(filePath, "utf8");
2468
- const notFoundErrorName = `${names.singularPascal}NotFound`;
2469
- const conflictErrorName = `${names.singularPascal}Conflict`;
2470
- const missingErrorNames = [notFoundErrorName, conflictErrorName].filter((errorName) => !new RegExp(`\\b${errorName}\\b`).test(original));
2471
- if (missingErrorNames.length === 0)
2472
- return false;
2473
- const expectedErrorNames = missingErrorNames.join(" and ");
2474
- const defineErrorsStart = original.indexOf("defineErrors(");
2475
- if (defineErrorsStart === -1) {
2476
- throw new Error(`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
2477
- }
2478
- const openBrace = original.indexOf("{", defineErrorsStart);
2479
- if (openBrace === -1) {
2480
- throw new Error(`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
2481
- }
2482
- const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
2483
- if (closeBrace === -1) {
2484
- throw new Error(`Could not update ${resourceSharedErrorsPath(config)} because the error catalog object is malformed. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
2485
- }
2486
- const entries = [];
2487
- if (missingErrorNames.includes(notFoundErrorName)) {
2488
- entries.push(`${notFoundErrorName}: {
2489
- \t\tcode: "${constantCase(names.singularKebab)}_NOT_FOUND",
2490
- \t\tstatus: 404,
2491
- \t\tmessage: "${names.singularPascal} not found",
2492
- \t},`);
2493
- }
2494
- if (missingErrorNames.includes(conflictErrorName)) {
2495
- entries.push(`${conflictErrorName}: {
2496
- \t\tcode: "${constantCase(names.singularKebab)}_CONFLICT",
2497
- \t\tstatus: 409,
2498
- \t\tmessage: "${names.singularPascal} was changed by another request",
2499
- \t},`);
2500
- }
2501
- const next = insertBeforeClosingBrace(original, openBrace, closeBrace, entries.join("\n"));
2502
- if (next === original)
2503
- return false;
2504
- if (!options.dryRun)
2505
- await writeFile(filePath, next);
2506
- return true;
2507
- }
2508
- async function updateBillingSharedErrors(targetDir, config, options) {
2509
- const file = resourceSharedErrorsPath(config);
2510
- const filePath = path.join(targetDir, file);
2511
- if (!(await fileExists(filePath)))
2512
- return false;
2513
- const original = await readFile(filePath, "utf8");
2514
- const missingEntries = [
2515
- {
2516
- name: "BillingAccountNotFound",
2517
- entry: `BillingAccountNotFound: {
2518
- \t\tcode: "BILLING_ACCOUNT_NOT_FOUND",
2519
- \t\tstatus: 404,
2520
- \t\tmessage: "Billing account not found",
2521
- \t},`,
2522
- },
2523
- {
2524
- name: "IdempotencyConflict",
2525
- entry: `IdempotencyConflict: {
2526
- \t\tcode: "IDEMPOTENCY_CONFLICT",
2527
- \t\tstatus: 409,
2528
- \t\tmessage: "Idempotency key was already used with a different request",
2529
- \t},`,
2530
- },
2531
- {
2532
- name: "IdempotencyInProgress",
2533
- entry: `IdempotencyInProgress: {
2534
- \t\tcode: "IDEMPOTENCY_IN_PROGRESS",
2535
- \t\tstatus: 409,
2536
- \t\tmessage: "Idempotency key is already being processed",
2537
- \t},`,
2538
- },
2539
- {
2540
- name: "BillingCheckoutUnavailable",
2541
- entry: `BillingCheckoutUnavailable: {
2542
- \t\tcode: "BILLING_CHECKOUT_UNAVAILABLE",
2543
- \t\tstatus: 502,
2544
- \t\tmessage: "Billing checkout is unavailable",
2545
- \t},`,
2546
- },
2547
- ].filter(({ name }) => !new RegExp(`\\b${name}\\b`).test(original));
2548
- if (missingEntries.length === 0)
2549
- return false;
2550
- const defineErrorsStart = original.indexOf("defineErrors(");
2551
- if (defineErrorsStart === -1) {
2552
- throw new Error(`Could not find defineErrors({ in ${file}. Add BillingAccountNotFound and BillingCheckoutUnavailable manually, or restore the generated shared error catalog before running make payments.`);
2553
- }
2554
- const openBrace = original.indexOf("{", defineErrorsStart);
2555
- if (openBrace === -1) {
2556
- throw new Error(`Could not find defineErrors({ in ${file}. Add BillingAccountNotFound and BillingCheckoutUnavailable manually, or restore the generated shared error catalog before running make payments.`);
2557
- }
2558
- const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
2559
- if (closeBrace === -1) {
2560
- throw new Error(`Could not update ${file} because the error catalog object is malformed. Add BillingAccountNotFound and BillingCheckoutUnavailable manually, or restore the generated shared error catalog before running make payments.`);
2561
- }
2562
- const next = insertBeforeClosingBrace(original, openBrace, closeBrace, missingEntries.map(({ entry }) => entry).join("\n"));
2563
- if (next === original)
2564
- return false;
2565
- if (!options.dryRun)
2566
- await writeFile(filePath, next);
2567
- return true;
2568
- }
2569
- async function updateBillingEnv(targetDir, options) {
2570
- const file = "lib/env.ts";
2571
- const filePath = path.join(targetDir, file);
2572
- const original = await readOptionalFile(filePath);
2573
- if (original === undefined) {
2574
- throw new Error(`Could not find ${file}. Add BILLING_TEAM_PRICE_ID to your env schema manually, or restore the generated env file before running make payments.`);
2575
- }
2576
- if (/\bBILLING_TEAM_PRICE_ID\b/.test(original))
2577
- return false;
2578
- const match = /\n([\t ]*)LOG_LEVEL:/.exec(original);
2579
- if (!match) {
2580
- throw new Error(`Could not find LOG_LEVEL in ${file}. Add BILLING_TEAM_PRICE_ID: z.string().min(1).default("price_team_example") to the createEnv server block manually, or restore the generated env file before running make payments.`);
2581
- }
2582
- const entry = `\n${match[1]}BILLING_TEAM_PRICE_ID: z.string().min(1).default("price_team_example"),`;
2583
- const next = `${original.slice(0, match.index)}${entry}${original.slice(match.index)}`;
2584
- if (!options.dryRun)
2585
- await writeFile(filePath, next);
2586
- return true;
2587
- }
2588
- async function updateBillingEnvExample(targetDir, options) {
2589
- const file = ".env.example";
2590
- const filePath = path.join(targetDir, file);
2591
- const original = await readOptionalFile(filePath);
2592
- if (original === undefined)
2593
- return false;
2594
- if (/^BILLING_TEAM_PRICE_ID=/m.test(original))
2595
- return false;
2596
- const match = /\nLOG_LEVEL=/.exec(original);
2597
- const next = match
2598
- ? `${original.slice(0, match.index)}\nBILLING_TEAM_PRICE_ID=price_team_example${original.slice(match.index)}`
2599
- : `${original.trimEnd()}\nBILLING_TEAM_PRICE_ID=price_team_example\n`;
2600
- if (!options.dryRun)
2601
- await writeFile(filePath, next);
2602
- return true;
2603
- }
2604
- async function updateServerRoutes(targetDir, names, config, options) {
2605
- const filePath = path.join(targetDir, config.paths.server);
2606
- const original = await readFile(filePath, "utf8");
2607
- const routeRegistryPath = routeRegistryFileFromServerSource(original, config.paths.server);
2608
- if (routeRegistryPath) {
2609
- await assertImportedRoutesFileExists(targetDir, routeRegistryPath);
2610
- return updateDefineRoutesFile(targetDir, routeRegistryPath, names, config, options);
2611
- }
2612
- return updateDefineRoutesFile(targetDir, config.paths.server, names, config, options);
2613
- }
2614
- async function updateDefineRoutesFile(targetDir, file, names, config, options) {
2615
- const filePath = path.join(targetDir, file);
2616
- const original = await readFile(filePath, "utf8");
2617
- let next = original;
2618
- const routeGroupName = `${names.singularCamel}Routes`;
2619
- const routeGroupPath = path.join(config.paths.features, names.pluralKebab, "routes.ts");
2620
- const importLine = `import { ${routeGroupName} } from "${relativeModule(file, routeGroupPath)}";`;
2621
- if (!next.includes(importLine)) {
2622
- next = insertAfterImports(next, importLine);
2623
- }
2624
- if (!routeGroupIsRegistered(next, routeGroupName)) {
2625
- const firstArg = firstDefineRoutesArgInfo(next);
2626
- if (!firstArg?.text.startsWith("[")) {
2627
- throw new Error(`Could not find a central defineRoutes([...]) registration in ${file}. Import ${routeGroupName} and add it to defineRoutes manually.`);
2628
- }
2629
- const nextArray = appendToArrayExpression(firstArg.text, [routeGroupName]);
2630
- next = `${next.slice(0, firstArg.start)}${nextArray}${next.slice(firstArg.end)}`;
2631
- }
2632
- if (!routeGroupIsRegistered(next, routeGroupName)) {
2633
- throw new Error(`Could not find a central defineRoutes([...]) registration in ${file}. Import ${routeGroupName} and add it to defineRoutes manually.`);
2634
- }
2635
- if (next === original)
2636
- return undefined;
2637
- if (!options.dryRun)
2638
- await writeFile(filePath, next);
2639
- return file;
2640
- }
2641
- function routeGroupIsRegistered(source, routeGroupName) {
2642
- const firstArg = firstDefineRoutesArgInfo(source);
2643
- if (!firstArg?.text.startsWith("["))
2644
- return false;
2645
- return identifiersFromArrayExpression(firstArg.text).has(routeGroupName);
2646
- }
2647
- function routeRegistryFileFromServerSource(source, serverFile) {
2648
- const serverOptions = firstCreateNextServerArgInfo(source);
2649
- if (!serverOptions)
2650
- return undefined;
2651
- const routeIdentifier = routeOptionIdentifier(serverOptions.text);
2652
- if (!routeIdentifier)
2653
- return undefined;
2654
- const imported = parseNamedImportSources(source).get(routeIdentifier);
2655
- if (!imported)
2656
- return undefined;
2657
- return resolveImportFile(imported.sourcePath, serverFile);
2658
- }
2659
- function routeOptionIdentifier(source) {
2660
- const explicitMatch = /(?:^|[,{])\s*routes\s*:\s*([A-Za-z_$][\w$]*)/m.exec(source);
2661
- if (explicitMatch)
2662
- return explicitMatch[1];
2663
- if (/(?:^|[,{])\s*routes\s*(?:[,}])/m.test(source)) {
2664
- return "routes";
2665
- }
2666
- return undefined;
2667
- }
2668
- function parseNamedImportSources(source) {
2669
- const imports = new Map();
2670
- const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
2671
- for (const match of source.matchAll(importRegex)) {
2672
- for (const member of match[1].split(",")) {
2673
- const parsed = parseImportMember(member);
2674
- if (!parsed)
2675
- continue;
2676
- imports.set(parsed.localName, {
2677
- importedName: parsed.importedName,
2678
- sourcePath: match[2],
2679
- });
2680
- }
2681
- }
2682
- return imports;
2683
- }
2684
- function parseImportMember(member) {
2685
- const trimmed = member.trim();
2686
- if (!trimmed)
2687
- return undefined;
2688
- const aliasMatch = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(trimmed);
2689
- if (aliasMatch) {
2690
- return {
2691
- importedName: aliasMatch[1],
2692
- localName: aliasMatch[2],
2693
- };
2694
- }
2695
- if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
2696
- return {
2697
- importedName: trimmed,
2698
- localName: trimmed,
2699
- };
2700
- }
2701
- return undefined;
2702
- }
2703
- function resolveImportFile(sourcePath, importerFile) {
2704
- if (sourcePath.startsWith("@/")) {
2705
- return `${sourcePath.slice(2)}.ts`;
2706
- }
2707
- if (sourcePath.startsWith(".")) {
2708
- return `${normalizePath(path.join(path.dirname(importerFile), sourcePath))}.ts`;
2709
- }
2710
- return undefined;
2711
- }
2712
- async function assertImportedRoutesFileExists(targetDir, file) {
2713
- try {
2714
- await stat(path.join(targetDir, file));
2715
- }
2716
- catch {
2717
- throw new Error(`Could not find the route registry ${file}. Restore the imported routes file or add the new route group manually.`);
2718
- }
2719
- }
2720
- function firstDefineRoutesArgInfo(source) {
2721
- const match = /defineRoutes(?:<[^>]+>)?\(/.exec(source);
2722
- if (!match)
2723
- return undefined;
2724
- return firstCallArgInfo(source, match.index + match[0].length);
2725
- }
2726
- function firstCreateNextServerArgInfo(source) {
2727
- const match = /createNextServer(?:<[^>]+>)?\(/.exec(source);
2728
- if (!match)
2729
- return undefined;
2730
- return firstCallArgInfo(source, match.index + match[0].length);
2731
- }
2732
- function firstCreateOpenAPIHandlerArgInfo(source) {
2733
- const start = source.indexOf("createOpenAPIHandler(");
2734
- if (start === -1)
2735
- return undefined;
2736
- return firstCallArgInfo(source, start + "createOpenAPIHandler(".length);
2737
- }
2738
- function firstCallArgInfo(source, argStart) {
2739
- let depth = 0;
2740
- let inString;
2741
- let escaped = false;
2742
- for (let index = argStart; index < source.length; index++) {
2743
- const char = source[index];
2744
- if (inString) {
2745
- if (escaped) {
2746
- escaped = false;
2747
- }
2748
- else if (char === "\\") {
2749
- escaped = true;
2750
- }
2751
- else if (char === inString) {
2752
- inString = undefined;
2753
- }
2754
- continue;
2755
- }
2756
- if (char === '"' || char === "'" || char === "`") {
2757
- inString = char;
2758
- continue;
2759
- }
2760
- if (char === "[" || char === "(" || char === "{") {
2761
- depth++;
2762
- continue;
2763
- }
2764
- if (char === "]" || char === ")" || char === "}") {
2765
- if (depth === 0) {
2766
- return trimmedSlice(source, argStart, index);
2767
- }
2768
- depth--;
2769
- continue;
2770
- }
2771
- if (char === "," && depth === 0) {
2772
- return trimmedSlice(source, argStart, index);
2773
- }
2774
- }
2775
- return undefined;
2776
- }
2777
- function trimmedSlice(source, start, end) {
2778
- let trimmedStart = start;
2779
- let trimmedEnd = end;
2780
- while (trimmedStart < trimmedEnd && /\s/.test(source[trimmedStart])) {
2781
- trimmedStart++;
2782
- }
2783
- while (trimmedEnd > trimmedStart && /\s/.test(source[trimmedEnd - 1])) {
2784
- trimmedEnd--;
2785
- }
2786
- return {
2787
- text: source.slice(trimmedStart, trimmedEnd),
2788
- start: trimmedStart,
2789
- end: trimmedEnd,
2790
- };
2791
- }
2792
- function replaceRequired(source, searchValue, replaceValue, message) {
2793
- const next = source.replace(searchValue, replaceValue);
2794
- if (next === source)
2795
- throw new Error(message);
2796
- return next;
2797
- }
2798
- function insertTypeProperty(source, typeName, property, message) {
2799
- const match = new RegExp(`export\\s+type\\s+${typeName}\\b`).exec(source);
2800
- if (!match)
2801
- throw new Error(message);
2802
- const openBrace = source.indexOf("{", match.index);
2803
- if (openBrace === -1)
2804
- throw new Error(message);
2805
- const closeBrace = matchingBraceIndex(source, openBrace);
2806
- if (closeBrace === -1)
2807
- throw new Error(message);
2808
- return insertBeforeClosingBrace(source, openBrace, closeBrace, property);
2809
- }
2810
- function typeBodySource(source, typeName) {
2811
- const match = new RegExp(`export\\s+type\\s+${typeName}\\b`).exec(source);
2812
- if (!match)
2813
- return undefined;
2814
- const openBrace = source.indexOf("{", match.index);
2815
- if (openBrace === -1)
2816
- return undefined;
2817
- const closeBrace = matchingBraceIndex(source, openBrace);
2818
- if (closeBrace === -1)
2819
- return undefined;
2820
- return source.slice(openBrace + 1, closeBrace);
2821
- }
2822
- function typeHasTopLevelProperty(source, typeName, propertyName) {
2823
- const body = typeBodySource(source, typeName);
2824
- return body !== undefined && hasTopLevelObjectProperty(body, propertyName);
2825
- }
2826
- function appendUnionTypeMember(source, typeName, member) {
2827
- const match = new RegExp(`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`).exec(source);
2828
- if (!match)
2829
- return source;
2830
- const declaration = match[0];
2831
- if (new RegExp(`\\b${member}\\b`).test(declaration))
2832
- return source;
2833
- const nextDeclaration = declaration.replace(/;\s*$/, ` | ${member};`);
2834
- return `${source.slice(0, match.index)}${nextDeclaration}${source.slice(match.index + declaration.length)}`;
2835
- }
2836
- function appendPickStringLiteralMember(source, typeName, member) {
2837
- const match = new RegExp(`Pick<\\s*${typeName}\\s*,([\\s\\S]*?)>`).exec(source);
2838
- if (!match)
2839
- return source;
2840
- const literal = `"${member}"`;
2841
- if (match[1].includes(literal))
2842
- return source;
2843
- const union = match[1];
2844
- const indent = union.match(/\n([\t ]*)\|/)?.[1] ?? "\t";
2845
- const nextUnion = union.includes("\n")
2846
- ? `${union.replace(/\s*$/, "")}\n${indent}| ${literal}\n`
2847
- : `${union.trimEnd()} | ${literal}`;
2848
- return `${source.slice(0, match.index)}Pick<${typeName},${nextUnion}>${source.slice(match.index + match[0].length)}`;
2849
- }
2850
- function appendTupleTypeMember(source, typeName, member) {
2851
- const match = new RegExp(`export\\s+type\\s+${typeName}\\s*=\\s*`).exec(source);
2852
- if (!match)
2853
- return source;
2854
- const tupleStart = source.indexOf("[", match.index);
2855
- if (tupleStart === -1)
2856
- return source;
2857
- const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
2858
- if (tupleEnd === -1)
2859
- return source;
2860
- const tuple = source.slice(tupleStart, tupleEnd + 1);
2861
- if (tuple.includes(member))
2862
- return source;
2863
- const nextTuple = appendToArrayExpression(tuple, [member]);
2864
- return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
2865
- }
2866
- function appendGatePolicyMember(source, member) {
2867
- const gateMatch = /\bGatePort\s*</.exec(source);
2868
- if (!gateMatch)
2869
- return source;
2870
- const tupleStart = source.indexOf("[", gateMatch.index);
2871
- if (tupleStart === -1)
2872
- return source;
2873
- const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
2874
- if (tupleEnd === -1)
2875
- return source;
2876
- const tuple = source.slice(tupleStart, tupleEnd + 1);
2877
- if (tuple.includes(member))
2878
- return source;
2879
- const nextTuple = appendToArrayExpression(tuple, [member]);
2880
- return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
2881
- }
2882
- function appendCreateGatePolicy(source, policy, failureMessage) {
2883
- const policyMatch = /\bpolicies\s*:\s*\[/.exec(source);
2884
- if (!policyMatch) {
2885
- throw new Error(failureMessage);
2886
- }
2887
- const arrayStart = source.indexOf("[", policyMatch.index);
2888
- if (arrayStart === -1) {
2889
- throw new Error(failureMessage);
2890
- }
2891
- const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
2892
- if (arrayEnd === -1) {
2893
- throw new Error(failureMessage);
2894
- }
2895
- const policies = source.slice(arrayStart, arrayEnd + 1);
2896
- if (policies.includes(policy))
2897
- return source;
2898
- const nextPolicies = appendToArrayExpression(policies, [policy]);
2899
- return `${source.slice(0, arrayStart)}${nextPolicies}${source.slice(arrayEnd + 1)}`;
2900
- }
2901
- function insertDefinePortsProperty(source, property, message) {
2902
- const body = definePortsBodyRange(source);
2903
- if (!body)
2904
- throw new Error(message);
2905
- return insertBeforeClosingBrace(source, body.openBrace, body.closeBrace, property);
2906
- }
2907
- function insertNoopUnitOfWorkProperty(source, property, message) {
2908
- const body = noopUnitOfWorkBodyRange(source);
2909
- if (!body)
2910
- throw new Error(message);
2911
- return insertBeforeClosingBrace(source, body.openBrace, body.closeBrace, property);
2912
- }
2913
- function insertDrizzleTransactionPortsProperty(source, property, message) {
2914
- const body = drizzleTransactionPortsBodyRange(source);
2915
- if (!body)
2916
- throw new Error(message);
2917
- return insertBeforeClosingBrace(source, body.openBrace, body.closeBrace, property);
2918
- }
2919
- function insertCreateRepositoriesProperty(source, property, message) {
2920
- const body = createRepositoriesReturnBodyRange(source);
2921
- if (!body)
2922
- throw new Error(message);
2923
- return insertBeforeClosingBrace(source, body.openBrace, body.closeBrace, property);
2924
- }
2925
- function noopUnitOfWorkBodySource(source) {
2926
- const body = noopUnitOfWorkBodyRange(source);
2927
- if (!body)
2928
- return undefined;
2929
- return source.slice(body.openBrace + 1, body.closeBrace);
2930
- }
2931
- function drizzleTransactionPortsBodySource(source) {
2932
- const body = drizzleTransactionPortsBodyRange(source);
2933
- if (!body)
2934
- return undefined;
2935
- return source.slice(body.openBrace + 1, body.closeBrace);
2936
- }
2937
- function createRepositoriesReturnBodySource(source) {
2938
- const body = createRepositoriesReturnBodyRange(source);
2939
- if (!body)
2940
- return undefined;
2941
- return source.slice(body.openBrace + 1, body.closeBrace);
2942
- }
2943
- function definePortsBodySource(source) {
2944
- const body = definePortsBodyRange(source);
2945
- if (!body)
2946
- return undefined;
2947
- return source.slice(body.openBrace + 1, body.closeBrace);
2948
- }
2949
- function definePortsBodyRange(source) {
2950
- const outer = definePortsOuterBodyRange(source);
2951
- if (!outer)
2952
- return undefined;
2953
- // The curried `definePorts<AppPorts>()({ bound, deferred })` form binds app
2954
- // ports inside `bound`; generated bindings belong there.
2955
- return (topLevelBoundObjectRange(source, outer.openBrace, outer.closeBrace) ?? outer);
2956
- }
2957
- function definePortsOuterBodyRange(source) {
2958
- const match = /definePorts(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?\{/.exec(source);
2959
- if (!match)
2960
- return undefined;
2961
- const openBrace = source.indexOf("{", match.index);
2962
- const closeBrace = matchingBraceIndex(source, openBrace);
2963
- if (closeBrace === -1)
2964
- return undefined;
2965
- return { openBrace, closeBrace };
2966
- }
2967
- function appendDeferredPortKey(source, portKey) {
2968
- const outer = definePortsOuterBodyRange(source);
2969
- if (!outer)
2970
- return source;
2971
- const body = source.slice(outer.openBrace + 1, outer.closeBrace);
2972
- if (!hasTopLevelObjectProperty(body, "deferred"))
2973
- return source;
2974
- const deferredMatch = /\bdeferred\s*:\s*\[/.exec(body);
2975
- if (!deferredMatch)
2976
- return source;
2977
- const arrayStart = source.indexOf("[", outer.openBrace + 1 + deferredMatch.index);
2978
- const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
2979
- if (arrayEnd === -1)
2980
- return source;
2981
- const tuple = source.slice(arrayStart, arrayEnd + 1);
2982
- if (new RegExp(`["']${portKey}["']`).test(tuple))
2983
- return source;
2984
- const nextTuple = appendToArrayExpression(tuple, [`"${portKey}"`]);
2985
- return `${source.slice(0, arrayStart)}${nextTuple}${source.slice(arrayEnd + 1)}`;
2986
- }
2987
- /**
2988
- * Add a name to an `import type { ... } from "<module>";` statement, creating
2989
- * the import when the module is not imported yet. Names are inserted in sorted
2990
- * position and single-line versus multiline formatting is preserved.
2991
- */
2992
- function addNamedTypeImport(source, name, moduleName) {
2993
- const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2994
- const importPattern = new RegExp(`import type \\{([^}]*)\\} from "${escapedModule}";`);
2995
- const match = importPattern.exec(source);
2996
- if (!match) {
2997
- return insertAfterImports(source, `import type { ${name} } from "${moduleName}";`);
2998
- }
2999
- const names = match[1]
3000
- .split(",")
3001
- .map((entry) => entry.trim())
3002
- .filter(Boolean);
3003
- if (names.includes(name))
3004
- return source;
3005
- const insertIndex = names.findIndex((existing) => existing.localeCompare(name) > 0);
3006
- if (insertIndex === -1)
3007
- names.push(name);
3008
- else
3009
- names.splice(insertIndex, 0, name);
3010
- const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
3011
- const replacement = match[1].includes("\n")
3012
- ? `import type {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
3013
- : `import type { ${names.join(", ")} } from "${moduleName}";`;
3014
- return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
3015
- }
3016
- function addNamedImport(source, name, moduleName) {
3017
- const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3018
- const importPattern = new RegExp(`import \\{([^}]*)\\} from "${escapedModule}";`);
3019
- const match = importPattern.exec(source);
3020
- if (!match) {
3021
- return insertAfterImports(source, `import { ${name} } from "${moduleName}";`);
3022
- }
3023
- const names = match[1]
3024
- .split(",")
3025
- .map((entry) => entry.trim())
3026
- .filter(Boolean);
3027
- if (names.includes(name))
3028
- return source;
3029
- const insertIndex = names.findIndex((existing) => existing.localeCompare(name) > 0);
3030
- if (insertIndex === -1)
3031
- names.push(name);
3032
- else
3033
- names.splice(insertIndex, 0, name);
3034
- const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
3035
- const replacement = match[1].includes("\n")
3036
- ? `import {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
3037
- : `import { ${names.join(", ")} } from "${moduleName}";`;
3038
- return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
3039
- }
3040
- function topLevelBoundObjectRange(source, openBrace, closeBrace) {
3041
- const body = source.slice(openBrace + 1, closeBrace);
3042
- if (!hasTopLevelObjectProperty(body, "bound"))
3043
- return undefined;
3044
- const boundMatch = /\bbound\s*:\s*\{/.exec(body);
3045
- if (!boundMatch)
3046
- return undefined;
3047
- const boundOpen = source.indexOf("{", openBrace + 1 + boundMatch.index);
3048
- const boundClose = matchingBraceIndex(source, boundOpen);
3049
- if (boundClose === -1)
3050
- return undefined;
3051
- return { openBrace: boundOpen, closeBrace: boundClose };
3052
- }
3053
- function hasTopLevelObjectProperty(source, propertyName) {
3054
- let depth = 0;
3055
- let inString;
3056
- let escaped = false;
3057
- let lineComment = false;
3058
- let blockComment = false;
3059
- for (let index = 0; index < source.length; index++) {
3060
- const char = source[index];
3061
- const next = source[index + 1];
3062
- if (lineComment) {
3063
- if (char === "\n")
3064
- lineComment = false;
3065
- continue;
3066
- }
3067
- if (blockComment) {
3068
- if (char === "*" && next === "/") {
3069
- blockComment = false;
3070
- index++;
3071
- }
3072
- continue;
3073
- }
3074
- if (inString) {
3075
- if (escaped) {
3076
- escaped = false;
3077
- }
3078
- else if (char === "\\") {
3079
- escaped = true;
3080
- }
3081
- else if (char === inString) {
3082
- inString = undefined;
3083
- }
3084
- continue;
3085
- }
3086
- if (char === "/" && next === "/") {
3087
- lineComment = true;
3088
- index++;
3089
- continue;
3090
- }
3091
- if (char === "/" && next === "*") {
3092
- blockComment = true;
3093
- index++;
3094
- continue;
3095
- }
3096
- if (char === '"' || char === "'" || char === "`") {
3097
- inString = char;
3098
- continue;
3099
- }
3100
- if (char === "{" || char === "[" || char === "(") {
3101
- depth++;
3102
- continue;
3103
- }
3104
- if (char === "}" || char === "]" || char === ")") {
3105
- depth = Math.max(0, depth - 1);
3106
- continue;
3107
- }
3108
- if (depth !== 0 || !identifierStartPattern.test(char)) {
3109
- continue;
3110
- }
3111
- const property = identifierAt(source, index);
3112
- if (property !== propertyName) {
3113
- index += property.length - 1;
3114
- continue;
3115
- }
3116
- let cursor = index + property.length;
3117
- while (/\s/.test(source[cursor] ?? ""))
3118
- cursor++;
3119
- if (source[cursor] === ":" || source[cursor] === ",")
3120
- return true;
3121
- index = cursor;
3122
- }
3123
- return false;
3124
- }
3125
- const identifierStartPattern = /[$A-Z_a-z]/;
3126
- const identifierPartPattern = /[$\w]/;
3127
- function identifierAt(source, index) {
3128
- let cursor = index;
3129
- while (identifierPartPattern.test(source[cursor] ?? "")) {
3130
- cursor++;
3131
- }
3132
- return source.slice(index, cursor);
3133
- }
3134
- function noopUnitOfWorkBodyRange(source) {
3135
- const match = /createNoopUnitOfWork\(\s*\(\)\s*=>\s*\(\s*\{/.exec(source);
3136
- if (!match)
3137
- return undefined;
3138
- const openBrace = source.indexOf("{", match.index);
3139
- const closeBrace = matchingBraceIndex(source, openBrace);
3140
- if (closeBrace === -1)
3141
- return undefined;
3142
- return { openBrace, closeBrace };
3143
- }
3144
- function drizzleTransactionPortsBodyRange(source) {
3145
- const match = /createTransactionPorts\s*:\s*\([^)]*\)\s*=>\s*\(\s*\{/.exec(source);
3146
- if (!match)
3147
- return undefined;
3148
- const openBrace = source.indexOf("{", match.index);
3149
- const closeBrace = matchingBraceIndex(source, openBrace);
3150
- if (closeBrace === -1)
3151
- return undefined;
3152
- return { openBrace, closeBrace };
3153
- }
3154
- function createRepositoriesReturnBodyRange(source) {
3155
- const functionBody = createRepositoriesFunctionBodyRange(source);
3156
- if (!functionBody)
3157
- return undefined;
3158
- const functionSource = source.slice(functionBody.openBrace + 1, functionBody.closeBrace);
3159
- const match = /return\s*\{\s*/.exec(functionSource);
3160
- if (!match)
3161
- return undefined;
3162
- const openBrace = source.indexOf("{", functionBody.openBrace + 1 + match.index);
3163
- if (openBrace === -1)
3164
- return undefined;
3165
- const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
3166
- if (closeBrace === -1 || closeBrace > functionBody.closeBrace) {
3167
- return undefined;
3168
- }
3169
- return { openBrace, closeBrace };
3170
- }
3171
- function createRepositoriesFunctionBodyRange(source) {
3172
- const match = /(?:export\s+)?function\s+createRepositories\b/.exec(source);
3173
- if (!match)
3174
- return undefined;
3175
- const openParen = source.indexOf("(", match.index);
3176
- if (openParen === -1)
3177
- return undefined;
3178
- const closeParen = matchingDelimiterIndex(source, openParen, "(", ")");
3179
- if (closeParen === -1)
3180
- return undefined;
3181
- const bodyMatch = /(?:\s*:[^{]+)?\s*\{/.exec(source.slice(closeParen + 1));
3182
- if (!bodyMatch)
3183
- return undefined;
3184
- const openBrace = closeParen + 1 + bodyMatch.index + bodyMatch[0].lastIndexOf("{");
3185
- const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
3186
- if (closeBrace === -1)
3187
- return undefined;
3188
- return { openBrace, closeBrace };
3189
- }
3190
- function insertBeforeClosingBrace(source, openBrace, closeBrace, property) {
3191
- const body = source.slice(openBrace + 1, closeBrace);
3192
- const propertyIndent = detectPropertyIndent(body);
3193
- const closingIndent = source.slice(0, closeBrace).match(/\n([\t ]*)$/)?.[1] ?? "";
3194
- const trimmedBody = body.replace(/\s*$/, "");
3195
- const nextBody = `${trimmedBody}\n${propertyIndent}${property}\n${closingIndent}`;
3196
- return `${source.slice(0, openBrace + 1)}${nextBody}${source.slice(closeBrace)}`;
3197
- }
3198
- function detectPropertyIndent(body) {
3199
- return (body
3200
- .split("\n")
3201
- .find((line) => line.trim())
3202
- ?.match(/^[\t ]*/)?.[0] ?? "\t");
3203
- }
3204
- function matchingBraceIndex(source, openBrace) {
3205
- return matchingDelimiterIndex(source, openBrace, "{", "}");
3206
- }
3207
- function replaceTransactionPortsRequired(source, entry, message) {
3208
- return insertTypeProperty(source, "AppTransactionPorts", entry.trim(), message);
3209
- }
3210
- function aliasModule(filePath) {
3211
- return `@/${modulePath(filePath)}`;
3212
- }
3213
- function relativeModule(fromFile, toFile) {
3214
- const fromDir = path.posix.dirname(normalizePath(fromFile));
3215
- const relative = path.posix.relative(fromDir, normalizePath(toFile));
3216
- const specifier = modulePath(relative);
3217
- if (specifier.startsWith("../"))
3218
- return specifier;
3219
- if (specifier === "..")
3220
- return specifier;
3221
- return specifier.startsWith(".") ? specifier : `./${specifier}`;
3222
- }
3223
- function modulePath(filePath) {
3224
- return directoryPath(filePath)
3225
- .replace(/\.(?:[cm]?[jt]sx?)$/, "")
3226
- .replace(/\/index$/, "");
3227
- }
3228
- function resourceNames(input) {
3229
- const pluralKebab = normalizeResourceName(input);
3230
- const singularKebab = singularize(pluralKebab);
3231
- const names = {
3232
- input,
3233
- pluralKebab,
3234
- singularKebab,
3235
- pluralCamel: camelCase(pluralKebab),
3236
- singularCamel: camelCase(singularKebab),
3237
- singularPascal: pascalCase(singularKebab),
3238
- pluralPascal: pascalCase(pluralKebab),
3239
- };
3240
- assertGeneratedIdentifiers(input, "Resource name", [
3241
- names.pluralCamel,
3242
- names.singularCamel,
3243
- names.singularPascal,
3244
- names.pluralPascal,
3245
- ]);
3246
- return names;
3247
- }
3248
- function featureUiNames(input) {
3249
- const names = resourceNames(input);
3250
- const resolvedNames = {
3251
- ...names,
3252
- componentExportName: `${names.pluralPascal}Panel`,
3253
- componentFileName: `${names.pluralKebab}-panel`,
3254
- };
3255
- assertGeneratedIdentifiers(input, "Feature UI name", [
3256
- resolvedNames.componentExportName,
3257
- ]);
3258
- return resolvedNames;
3259
- }
3260
- function featureResourceNames(names) {
3261
- return resourceNames(names.feature.kebab);
1417
+ function typeHasTopLevelProperty(source, typeName, propertyName) {
1418
+ const body = typeBodySource(source, typeName);
1419
+ return body !== undefined && hasTopLevelObjectProperty(body, propertyName);
3262
1420
  }
3263
1421
  function useCaseNames(input) {
3264
1422
  const parts = input
@@ -3289,32 +1447,6 @@ function useCaseNames(input) {
3289
1447
  kind: inferUseCaseKind(action.kebab),
3290
1448
  };
3291
1449
  }
3292
- function identifierNames(input, label) {
3293
- const kebab = normalizeName(input, label);
3294
- return {
3295
- input,
3296
- kebab,
3297
- camel: camelCase(kebab),
3298
- pascal: pascalCase(kebab),
3299
- };
3300
- }
3301
- function portNames(input) {
3302
- const names = identifierNames(input, "Port name");
3303
- const resolvedNames = {
3304
- ...names,
3305
- interfaceName: `${names.pascal}Port`,
3306
- inputName: `Execute${names.pascal}Input`,
3307
- adapterFactoryName: `create${names.pascal}Adapter`,
3308
- };
3309
- assertGeneratedIdentifiers(input, "Port name", [
3310
- resolvedNames.camel,
3311
- resolvedNames.pascal,
3312
- resolvedNames.interfaceName,
3313
- resolvedNames.inputName,
3314
- resolvedNames.adapterFactoryName,
3315
- ]);
3316
- return resolvedNames;
3317
- }
3318
1450
  function policyNames(input) {
3319
1451
  const names = identifierNames(input, "Policy name");
3320
1452
  const resolvedNames = {
@@ -3324,32 +1456,6 @@ function policyNames(input) {
3324
1456
  assertGeneratedIdentifiers(input, "Policy name", [resolvedNames.policyName]);
3325
1457
  return resolvedNames;
3326
1458
  }
3327
- function featureArtifactNames(input, label) {
3328
- const parts = artifactNameParts(input, label);
3329
- const feature = identifierNames(parts[0], `${label} feature`);
3330
- const artifact = identifierNames(parts[1], `${label} name`);
3331
- const featureSingularKebab = singularize(feature.kebab);
3332
- const names = {
3333
- input,
3334
- feature,
3335
- featureSingularKebab,
3336
- featureSingularCamel: camelCase(featureSingularKebab),
3337
- featureSingularPascal: pascalCase(featureSingularKebab),
3338
- artifact,
3339
- stableName: `${feature.kebab}.${artifact.kebab}`,
3340
- };
3341
- assertGeneratedIdentifiers(parts[0], `${label} feature`, [
3342
- names.feature.camel,
3343
- names.feature.pascal,
3344
- names.featureSingularCamel,
3345
- names.featureSingularPascal,
3346
- ]);
3347
- assertGeneratedIdentifiers(parts[1], `${label} name`, [
3348
- names.artifact.camel,
3349
- names.artifact.pascal,
3350
- ]);
3351
- return names;
3352
- }
3353
1459
  function eventNames(input) {
3354
1460
  const names = featureArtifactNames(normalizeEventReference(input), "Event");
3355
1461
  const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
@@ -3396,16 +1502,6 @@ function factoryNames(input) {
3396
1502
  factoryExportName,
3397
1503
  };
3398
1504
  }
3399
- function seedNames(input) {
3400
- const names = featureArtifactNames(input, "Seed");
3401
- const seedExportName = `${names.artifact.camel}Seed`;
3402
- assertGeneratedIdentifiers(input, "Seed name", [seedExportName]);
3403
- return {
3404
- ...names,
3405
- seedName: names.stableName,
3406
- seedExportName,
3407
- };
3408
- }
3409
1505
  function defaultFactoryNamesForSeed(names) {
3410
1506
  return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
3411
1507
  }
@@ -3469,127 +1565,12 @@ function normalizeEventReference(input) {
3469
1565
  return trimmed;
3470
1566
  return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
3471
1567
  }
3472
- function artifactNameParts(input, label) {
3473
- const parts = input
3474
- .trim()
3475
- .split("/")
3476
- .map((part) => part.trim())
3477
- .filter(Boolean);
3478
- if (parts.length !== 2) {
3479
- throw new Error(`${label} name must use feature/name format, for example posts/published.`);
3480
- }
3481
- return [parts[0], parts[1]];
3482
- }
3483
- function portStubEntry(names) {
3484
- return `\t${names.camel}: {\n\t\tasync execute() {\n\t\t\tthrow new Error("${names.interfaceName} is not implemented.");\n\t\t},\n\t} satisfies ${names.interfaceName},`;
3485
- }
3486
- function adapterEntry(names) {
3487
- return `\t${names.camel}: ${names.adapterFactoryName}(),`;
3488
- }
3489
- function normalizeResourceName(input) {
3490
- return normalizeName(input, "Resource name");
3491
- }
3492
- function normalizeName(input, label) {
3493
- const normalized = input
3494
- .trim()
3495
- .toLowerCase()
3496
- .replaceAll(/[^a-z0-9]+/g, "-")
3497
- .replaceAll(/^-+|-+$/g, "");
3498
- if (!normalized) {
3499
- throw new Error(`${label} is required.`);
3500
- }
3501
- return normalized;
3502
- }
3503
- function assertGeneratedIdentifiers(input, label, identifiers) {
3504
- for (const identifier of new Set(identifiers)) {
3505
- if (isValidGeneratedIdentifier(identifier)) {
3506
- continue;
3507
- }
3508
- throw new Error(`${label} "${input}" generates invalid JavaScript identifier "${identifier}". Use a name that starts with a letter and is not a reserved word.`);
3509
- }
3510
- }
3511
- function isValidGeneratedIdentifier(identifier) {
3512
- return (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier) &&
3513
- !reservedJavaScriptIdentifiers.has(identifier));
3514
- }
3515
- const reservedJavaScriptIdentifiers = new Set([
3516
- "arguments",
3517
- "await",
3518
- "break",
3519
- "case",
3520
- "catch",
3521
- "class",
3522
- "const",
3523
- "continue",
3524
- "debugger",
3525
- "default",
3526
- "delete",
3527
- "do",
3528
- "else",
3529
- "enum",
3530
- "eval",
3531
- "export",
3532
- "extends",
3533
- "false",
3534
- "finally",
3535
- "for",
3536
- "function",
3537
- "if",
3538
- "implements",
3539
- "import",
3540
- "in",
3541
- "instanceof",
3542
- "interface",
3543
- "let",
3544
- "new",
3545
- "null",
3546
- "package",
3547
- "private",
3548
- "protected",
3549
- "public",
3550
- "return",
3551
- "static",
3552
- "super",
3553
- "switch",
3554
- "this",
3555
- "throw",
3556
- "true",
3557
- "try",
3558
- "typeof",
3559
- "var",
3560
- "void",
3561
- "while",
3562
- "with",
3563
- "yield",
3564
- ]);
3565
1568
  function inferUseCaseKind(action) {
3566
1569
  if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
3567
1570
  return "query";
3568
1571
  }
3569
1572
  return "command";
3570
1573
  }
3571
- function singularize(value) {
3572
- if (value.endsWith("ies") && value.length > 3) {
3573
- return `${value.slice(0, -3)}y`;
3574
- }
3575
- if (value.endsWith("ses") && value.length > 3) {
3576
- return value.slice(0, -2);
3577
- }
3578
- if (value.endsWith("s") && value.length > 1) {
3579
- return value.slice(0, -1);
3580
- }
3581
- return value;
3582
- }
3583
- function camelCase(value) {
3584
- return value.replaceAll(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
3585
- }
3586
- function pascalCase(value) {
3587
- const camel = camelCase(value);
3588
- return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
3589
- }
3590
- function constantCase(value) {
3591
- return value.replaceAll("-", "_").toUpperCase();
3592
- }
3593
1574
  function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
3594
1575
  auth: false,
3595
1576
  tenant: false,
@@ -3685,54 +1666,6 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
3685
1666
  }
3686
1667
  return files;
3687
1668
  }
3688
- function paymentsFiles(config, database) {
3689
- return [
3690
- {
3691
- path: path.join(config.paths.features, "billing/schemas.ts"),
3692
- content: billingSchemasFile(),
3693
- },
3694
- {
3695
- path: path.join(config.paths.features, "billing/pricing.ts"),
3696
- content: billingPricingFile(),
3697
- },
3698
- {
3699
- path: path.join(config.paths.features, "billing/entitlements.ts"),
3700
- content: billingEntitlementsFile(),
3701
- },
3702
- {
3703
- path: path.join(config.paths.features, "billing/ports.ts"),
3704
- content: billingPortsFile(),
3705
- },
3706
- {
3707
- path: path.join(config.paths.features, "billing/contracts.ts"),
3708
- content: billingContractsFile(),
3709
- },
3710
- {
3711
- path: path.join(config.paths.features, "billing/use-cases/index.ts"),
3712
- content: billingUseCasesFile(config),
3713
- },
3714
- {
3715
- path: path.join(config.paths.features, "billing/routes.ts"),
3716
- content: billingRoutesFile(config),
3717
- },
3718
- {
3719
- path: path.join(config.paths.features, "billing/tests/billing.test.ts"),
3720
- content: billingTestFile(config),
3721
- },
3722
- {
3723
- path: path.join(infrastructureDir(config), "billing/drizzle-billing-repository.ts"),
3724
- content: billingDrizzleRepositoryFile(config, database),
3725
- },
3726
- {
3727
- path: path.join(infrastructureDir(config), "db/schema/billing.ts"),
3728
- content: billingDrizzleSchemaFile(database),
3729
- },
3730
- {
3731
- path: "app/api/webhooks/payments/route.ts",
3732
- content: billingWebhookRouteFile(),
3733
- },
3734
- ];
3735
- }
3736
1669
  function featureUiComponentFiles(names, config) {
3737
1670
  const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
3738
1671
  const componentPath = featureUiComponentFilePath(names, config);
@@ -3894,125 +1827,12 @@ function outboxFiles(config) {
3894
1827
  },
3895
1828
  ];
3896
1829
  }
3897
- function portFilePath(names, config) {
3898
- return path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
3899
- }
3900
- function adapterFilePath(names, config) {
3901
- return path.join(path.dirname(config.paths.infrastructurePorts), names.kebab, `${names.kebab}-adapter.ts`);
3902
- }
3903
- function infrastructureDir(config) {
3904
- return path.dirname(config.paths.infrastructurePorts);
3905
- }
3906
- function drizzleSchemaIndexPath(config) {
3907
- return path.join(infrastructureDir(config), "db", "schema", "index.ts");
3908
- }
3909
- function drizzleRepositoriesPath(config) {
3910
- return path.join(infrastructureDir(config), "db", "repositories.ts");
3911
- }
3912
1830
  function drizzleResourceSchemaFilePath(names, config) {
3913
1831
  return path.join(infrastructureDir(config), "db", "schema", `${names.pluralKebab}.ts`);
3914
1832
  }
3915
1833
  function outboxDrizzleSchemaFilePath(config) {
3916
1834
  return path.join(infrastructureDir(config), "db", "schema", "outbox-messages.ts");
3917
1835
  }
3918
- function drizzleResourceRepositoryFilePath(names, config) {
3919
- return path.join(infrastructureDir(config), names.pluralKebab, `drizzle-${names.singularKebab}-repository.ts`);
3920
- }
3921
- function resourceSharedErrorsPath(config) {
3922
- return path.join(config.paths.features, "shared", "errors.ts");
3923
- }
3924
- function resourceContractNames(names, mode) {
3925
- const contractNames = [
3926
- `list${names.pluralPascal}`,
3927
- `create${names.singularPascal}`,
3928
- ];
3929
- if (mode === "resource") {
3930
- contractNames.push(`get${names.singularPascal}`, `update${names.singularPascal}`, `delete${names.singularPascal}`);
3931
- }
3932
- return contractNames;
3933
- }
3934
- async function detectResourcePersistence(targetDir, config) {
3935
- return (await fileExists(path.join(targetDir, drizzleRepositoriesPath(config))))
3936
- ? "drizzle"
3937
- : "memory";
3938
- }
3939
- const databaseNames = ["sqlite", "postgres", "mysql"];
3940
- /**
3941
- * Detect the SQL dialect of a Drizzle app from its generated wiring.
3942
- *
3943
- * Precedence:
3944
- * 1. The provider import in the Drizzle repositories file
3945
- * (`@beignet/provider-db-drizzle/<dialect>`), resolved through the
3946
- * configured infrastructure paths so custom layouts keep working.
3947
- * 2. The provider registration tokens in `server/providers.ts`
3948
- * (`drizzle<Dialect>Provider` / `createDrizzle<Dialect>Provider`).
3949
- * 3. `"sqlite"`, the starter default.
3950
- *
3951
- * Self-healing by design: there is no config field to drift, the detector
3952
- * reads the same files the app boots from.
3953
- */
3954
- export async function detectResourceDatabase(targetDir, config) {
3955
- const repositories = await readOptionalFile(path.join(targetDir, drizzleRepositoriesPath(config)));
3956
- const providerImport = repositories?.match(/from\s+["']@beignet\/provider-db-drizzle\/(sqlite|postgres|mysql)["']/);
3957
- if (providerImport)
3958
- return providerImport[1];
3959
- const providers = await readOptionalFile(path.join(targetDir, providersFilePath(config)));
3960
- if (providers !== undefined) {
3961
- for (const database of databaseNames) {
3962
- const dialectPascal = database.charAt(0).toUpperCase() + database.slice(1);
3963
- const tokens = new RegExp(`\\b(?:drizzle${dialectPascal}Provider|createDrizzle${dialectPascal}Provider)\\b`);
3964
- if (tokens.test(providers))
3965
- return database;
3966
- }
3967
- }
3968
- return "sqlite";
3969
- }
3970
- function resourceFeatureDir(names, config) {
3971
- return path.join(config.paths.features, names.pluralKebab);
3972
- }
3973
- function featureUiDir(names, config) {
3974
- return path.join(resourceFeatureDir(names, config), "components");
3975
- }
3976
- function featureUiClientDir(names, config) {
3977
- return path.join(resourceFeatureDir(names, config), "client");
3978
- }
3979
- function featureUiClientQueriesFilePath(names, config) {
3980
- return path.join(featureUiClientDir(names, config), "queries.ts");
3981
- }
3982
- function featureUiComponentFilePath(names, config) {
3983
- return path.join(featureUiDir(names, config), `${names.componentFileName}.tsx`);
3984
- }
3985
- function featureUiIndexFilePath(names, config) {
3986
- return path.join(featureUiDir(names, config), "index.ts");
3987
- }
3988
- function usesFeatureOwnedContracts(config) {
3989
- return (directoryPath(config.paths.contracts) ===
3990
- directoryPath(config.paths.features));
3991
- }
3992
- function usesFeatureOwnedUseCases(config) {
3993
- return (directoryPath(config.paths.useCases) ===
3994
- directoryPath(config.paths.features));
3995
- }
3996
- function usesFeatureOwnedPolicies(config) {
3997
- return (directoryPath(config.paths.policies) ===
3998
- directoryPath(config.paths.features));
3999
- }
4000
- function usesFeatureOwnedTests(config) {
4001
- return (usesFeatureOwnedUseCases(config) &&
4002
- directoryPath(config.paths.tests) === "tests");
4003
- }
4004
- function resourceContractFilePath(names, config) {
4005
- if (usesFeatureOwnedContracts(config)) {
4006
- return path.join(resourceFeatureDir(names, config), "contracts.ts");
4007
- }
4008
- return path.join(config.paths.contracts, `${names.pluralKebab}.ts`);
4009
- }
4010
- function useCaseFeatureDir(feature, config) {
4011
- if (usesFeatureOwnedUseCases(config)) {
4012
- return path.join(config.paths.features, feature, "use-cases");
4013
- }
4014
- return path.join(config.paths.useCases, feature);
4015
- }
4016
1836
  function resourceUseCaseDir(names, config) {
4017
1837
  return useCaseFeatureDir(names.pluralKebab, config);
4018
1838
  }
@@ -4022,12 +1842,6 @@ function resourceUseCaseIndexPath(names, config) {
4022
1842
  function resourceSchemaFilePath(names, config) {
4023
1843
  return path.join(resourceFeatureDir(names, config), "schemas.ts");
4024
1844
  }
4025
- function resourcePortFilePath(names, config) {
4026
- if (usesFeatureOwnedContracts(config)) {
4027
- return path.join(resourceFeatureDir(names, config), "ports.ts");
4028
- }
4029
- return path.join(path.dirname(config.paths.ports), `${names.singularKebab}-repository.ts`);
4030
- }
4031
1845
  function resourceTestFilePath(names, config) {
4032
1846
  if (usesFeatureOwnedTests(config)) {
4033
1847
  return path.join(resourceFeatureDir(names, config), "tests", `${names.pluralKebab}.test.ts`);
@@ -4046,46 +1860,6 @@ function policyFilePath(names, config) {
4046
1860
  }
4047
1861
  return path.join(config.paths.policies, `${names.kebab}.ts`);
4048
1862
  }
4049
- function featureArtifactDir(names, kind, config) {
4050
- const featureDir = path.join(config.paths.features, names.feature.kebab);
4051
- if (kind === "events") {
4052
- return path.join(featureDir, "domain", "events");
4053
- }
4054
- if (kind === "factories") {
4055
- return path.join(featureDir, "tests", "factories");
4056
- }
4057
- return path.join(featureDir, kind);
4058
- }
4059
- function eventFilePath(names, config) {
4060
- return path.join(featureArtifactDir(names, "events", config), `${names.artifact.kebab}.ts`);
4061
- }
4062
- function jobFilePath(names, config) {
4063
- return path.join(featureArtifactDir(names, "jobs", config), `${names.artifact.kebab}.ts`);
4064
- }
4065
- function taskFilePath(names, config) {
4066
- return path.join(featureArtifactDir(names, "tasks", config), `${names.artifact.kebab}.ts`);
4067
- }
4068
- function factoryFilePath(names, config) {
4069
- return path.join(featureArtifactDir(names, "factories", config), `${names.artifact.kebab}.ts`);
4070
- }
4071
- function seedFilePath(names, config) {
4072
- return path.join(featureArtifactDir(names, "seeds", config), `${names.artifact.kebab}.ts`);
4073
- }
4074
- function notificationFilePath(names, config) {
4075
- return path.join(featureArtifactDir(names, "notifications", config), `${names.artifact.kebab}.ts`);
4076
- }
4077
- function listenerFilePath(names, config) {
4078
- return path.join(featureArtifactDir(names, "listeners", config), `${names.artifact.kebab}.ts`);
4079
- }
4080
- function scheduleFilePath(names, config) {
4081
- return path.join(featureArtifactDir(names, "schedules", config), `${names.artifact.kebab}.ts`);
4082
- }
4083
- function uploadFilePath(names, config) {
4084
- return path.join(featureArtifactDir(names, "uploads", config), `${names.artifact.kebab}.ts`);
4085
- }
4086
- function seedEntrypointPath(config) {
4087
- return path.join(infrastructureDir(config), "db", "seed.ts");
4088
- }
4089
1863
  function uploadRoutePath(config) {
4090
1864
  return path.join(config.paths.routes, "uploads", "[uploadName]", "[action]", "route.ts");
4091
1865
  }
@@ -4095,9 +1869,6 @@ function outboxDrainRoutePath(config) {
4095
1869
  function scheduleRouteFilePath(names, config) {
4096
1870
  return path.join(config.paths.routes, "cron", names.feature.kebab, names.artifact.kebab, "route.ts");
4097
1871
  }
4098
- function useCaseFilePath(names, config) {
4099
- return path.join(useCaseFeatureDir(names.feature.kebab, config), `${names.action.kebab}.ts`);
4100
- }
4101
1872
  function useCaseIndexFile(names, config) {
4102
1873
  const filePath = path.join(useCaseFeatureDir(names.feature.kebab, config), "index.ts");
4103
1874
  return {
@@ -4128,160 +1899,15 @@ async function updateFeatureUiIndex(targetDir, file, options) {
4128
1899
  const existing = await readOptionalFile(destination);
4129
1900
  if (existing === undefined) {
4130
1901
  if (!options.dryRun) {
4131
- await mkdir(path.dirname(destination), { recursive: true });
4132
- await writeFile(destination, file.content);
4133
- }
4134
- return "created";
4135
- }
4136
- if (existing.includes(file.content.trim())) {
4137
- return "skipped";
4138
- }
4139
- const next = `${existing.trimEnd()}\n${file.content}`;
4140
- if (!options.dryRun)
4141
- await writeFile(destination, next);
4142
- return "updated";
4143
- }
4144
- function featureArtifactIndexFile(kind, names, config) {
4145
- const artifactPath = kind === "event"
4146
- ? eventFilePath(names, config)
4147
- : kind === "job"
4148
- ? jobFilePath(names, config)
4149
- : kind === "task"
4150
- ? taskFilePath(names, config)
4151
- : kind === "factory"
4152
- ? factoryFilePath(names, config)
4153
- : kind === "seed"
4154
- ? seedFilePath(names, config)
4155
- : kind === "notification"
4156
- ? notificationFilePath(names, config)
4157
- : kind === "listener"
4158
- ? listenerFilePath(names, config)
4159
- : kind === "schedule"
4160
- ? scheduleFilePath(names, config)
4161
- : uploadFilePath(names, config);
4162
- const registrySuffix = kind === "event"
4163
- ? "Events"
4164
- : kind === "job"
4165
- ? "Jobs"
4166
- : kind === "task"
4167
- ? "Tasks"
4168
- : kind === "factory"
4169
- ? "Factories"
4170
- : kind === "seed"
4171
- ? "Seeds"
4172
- : kind === "notification"
4173
- ? "Notifications"
4174
- : kind === "listener"
4175
- ? "Listeners"
4176
- : kind === "schedule"
4177
- ? "Schedules"
4178
- : "Uploads";
4179
- const importName = kind === "event"
4180
- ? names.eventExportName
4181
- : kind === "job"
4182
- ? names.jobExportName
4183
- : kind === "task"
4184
- ? names.taskExportName
4185
- : kind === "factory"
4186
- ? names.factoryExportName
4187
- : kind === "seed"
4188
- ? names.seedExportName
4189
- : kind === "notification"
4190
- ? names.notificationExportName
4191
- : kind === "listener"
4192
- ? names.listenerExportName
4193
- : kind === "schedule"
4194
- ? names.scheduleExportName
4195
- : names.uploadExportName;
4196
- return {
4197
- path: path.join(path.dirname(artifactPath), "index.ts"),
4198
- kind,
4199
- importName,
4200
- artifactPath,
4201
- registryName: `${names.featureSingularCamel}${registrySuffix}`,
4202
- registryEntryName: names.artifact.camel,
4203
- };
4204
- }
4205
- async function updateFeatureArtifactIndex(targetDir, file, options) {
4206
- const destination = path.join(targetDir, file.path);
4207
- const existing = await readOptionalFile(destination);
4208
- const specifier = relativeModule(file.path, file.artifactPath);
4209
- const importLine = `import { ${file.importName} } from "${specifier}";`;
4210
- const defineUploadsImport = `import { defineUploads } from "@beignet/core/uploads";`;
4211
- const exportLine = `export { ${file.importName} } from "${specifier}";`;
4212
- if (existing === undefined) {
4213
- const content = file.kind === "upload"
4214
- ? `${defineUploadsImport}
4215
- ${importLine}
4216
-
4217
- ${exportLine}
4218
-
4219
- export const ${file.registryName} = defineUploads({
4220
- \t${file.registryEntryName}: ${file.importName},
4221
- });
4222
- `
4223
- : `${importLine}
4224
-
4225
- ${exportLine}
4226
-
4227
- export const ${file.registryName} = [${file.importName}] as const;
4228
- `;
4229
- if (!options.dryRun) {
4230
- await mkdir(path.dirname(destination), { recursive: true });
4231
- await writeFile(destination, content);
4232
- }
4233
- return "created";
4234
- }
4235
- let next = existing;
4236
- if (!next.includes(importLine)) {
4237
- next = insertAfterImports(next, importLine);
4238
- }
4239
- if (!next.includes(exportLine)) {
4240
- const registryExport = new RegExp(`export\\s+const\\s+${file.registryName}\\b`).exec(next);
4241
- next = registryExport
4242
- ? `${next.slice(0, registryExport.index).trimEnd()}\n${exportLine}\n\n${next.slice(registryExport.index)}`
4243
- : `${next.trimEnd()}\n${exportLine}\n`;
4244
- }
4245
- if (file.kind === "upload") {
4246
- const registry = defineUploadsInitializerInfo(next, file.registryName);
4247
- const arrayRegistry = arrayInitializerInfo(next, file.registryName);
4248
- if (registry) {
4249
- if (!next.includes(defineUploadsImport)) {
4250
- next = insertAfterImports(next, defineUploadsImport);
4251
- }
4252
- if (!registry.text.includes(`${file.registryEntryName}:`)) {
4253
- next = `${next.slice(0, registry.end)}\t${file.registryEntryName}: ${file.importName},\n${next.slice(registry.end)}`;
4254
- }
4255
- }
4256
- else if (arrayRegistry) {
4257
- if (!identifiersFromArrayExpression(arrayRegistry.text).has(file.importName)) {
4258
- const nextArray = appendToArrayExpression(arrayRegistry.text, [
4259
- file.importName,
4260
- ]);
4261
- next = `${next.slice(0, arrayRegistry.start)}${nextArray}${next.slice(arrayRegistry.end)}`;
4262
- }
4263
- }
4264
- else {
4265
- if (!next.includes(defineUploadsImport)) {
4266
- next = insertAfterImports(next, defineUploadsImport);
4267
- }
4268
- next = `${next.trimEnd()}\n\nexport const ${file.registryName} = defineUploads({\n\t${file.registryEntryName}: ${file.importName},\n});\n`;
4269
- }
4270
- }
4271
- else {
4272
- const registry = arrayInitializerInfo(next, file.registryName);
4273
- if (!registry) {
4274
- next = `${next.trimEnd()}\n\nexport const ${file.registryName} = [${file.importName}] as const;\n`;
4275
- }
4276
- else if (!identifiersFromArrayExpression(registry.text).has(file.importName)) {
4277
- const nextArray = appendToArrayExpression(registry.text, [
4278
- file.importName,
4279
- ]);
4280
- next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
1902
+ await mkdir(path.dirname(destination), { recursive: true });
1903
+ await writeFile(destination, file.content);
4281
1904
  }
1905
+ return "created";
4282
1906
  }
4283
- if (next === existing)
1907
+ if (existing.includes(file.content.trim())) {
4284
1908
  return "skipped";
1909
+ }
1910
+ const next = `${existing.trimEnd()}\n${file.content}`;
4285
1911
  if (!options.dryRun)
4286
1912
  await writeFile(destination, next);
4287
1913
  return "updated";
@@ -4292,10 +1918,10 @@ async function updateTaskRegistry(targetDir, names, config, options) {
4292
1918
  const featureTasksPath = path.join(featureArtifactDir(names, "tasks", config), "index.ts");
4293
1919
  const registryName = `${names.featureSingularCamel}Tasks`;
4294
1920
  const importLine = `import { ${registryName} } from "${relativeModule(config.paths.tasks, featureTasksPath)}";`;
4295
- const defineTasksImport = `import { defineTasks } from "@beignet/core/tasks";`;
1921
+ const defineTasksImport = `import { defineTasks, type TaskRunContextArgs } from "@beignet/core/tasks";`;
4296
1922
  const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
4297
1923
  const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
4298
- const serverImport = `import { server } from "${relativeModule(config.paths.tasks, config.paths.server)}";`;
1924
+ const serverImport = `import { getServer } from "${relativeModule(config.paths.tasks, config.paths.server)}";`;
4299
1925
  const registryEntry = `...${registryName}`;
4300
1926
  if (existing === undefined) {
4301
1927
  const content = `${defineTasksImport}
@@ -4308,13 +1934,20 @@ export const tasks = defineTasks([
4308
1934
  \t${registryEntry},
4309
1935
  ] as const);
4310
1936
 
4311
- export async function createTaskContext(): Promise<AppContext> {
1937
+ export async function createTaskContext(
1938
+ \targs?: TaskRunContextArgs,
1939
+ ): Promise<AppContext> {
1940
+ \tconst server = await getServer();
4312
1941
  \treturn server.createServiceContext({
1942
+ \t\t// \`--tenant\` flows through TaskRunContextArgs; the starter has no tenant
1943
+ \t\t// lookup, so the raw value scopes the service context directly.
1944
+ \t\ttenantId: args?.tenant,
4313
1945
  \t\tactor: createServiceActor("beignet-cli"),
4314
1946
  \t});
4315
1947
  }
4316
1948
 
4317
1949
  export async function stopTaskContext(): Promise<void> {
1950
+ \tconst server = await getServer();
4318
1951
  \tawait server.stop();
4319
1952
  }
4320
1953
  `;
@@ -4325,7 +1958,9 @@ export async function stopTaskContext(): Promise<void> {
4325
1958
  return "created";
4326
1959
  }
4327
1960
  let next = existing;
4328
- if (!next.includes(defineTasksImport)) {
1961
+ // Match any `defineTasks` import shape so registries that also import
1962
+ // `TaskRunContextArgs` (or predate it) are not given a duplicate import.
1963
+ if (!(next.includes("defineTasks") && next.includes('"@beignet/core/tasks"'))) {
4329
1964
  next = insertAfterImports(next, defineTasksImport);
4330
1965
  }
4331
1966
  if (!next.includes(importLine)) {
@@ -4353,7 +1988,7 @@ async function updateScheduleRegistry(targetDir, names, config, options) {
4353
1988
  const importLine = `import { ${registryName} } from "${aliasModule(featureSchedulesPath)}";`;
4354
1989
  const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
4355
1990
  const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
4356
- const serverImport = `import { server } from "${relativeModule(config.paths.schedules, config.paths.server)}";`;
1991
+ const serverImport = `import { getServer } from "${relativeModule(config.paths.schedules, config.paths.server)}";`;
4357
1992
  const registryEntry = `...${registryName}`;
4358
1993
  if (existing === undefined) {
4359
1994
  const content = `${serviceActorImport}
@@ -4364,12 +1999,14 @@ ${serverImport}
4364
1999
  export const schedules = [${registryEntry}] as const;
4365
2000
 
4366
2001
  export async function createScheduleContext(): Promise<AppContext> {
2002
+ \tconst server = await getServer();
4367
2003
  \treturn server.createServiceContext({
4368
2004
  \t\tactor: createServiceActor("beignet-schedule"),
4369
2005
  \t});
4370
2006
  }
4371
2007
 
4372
2008
  export async function stopScheduleContext(): Promise<void> {
2009
+ \tconst server = await getServer();
4373
2010
  \tawait server.stop();
4374
2011
  }
4375
2012
  `;
@@ -4397,39 +2034,6 @@ export async function stopScheduleContext(): Promise<void> {
4397
2034
  await writeFile(destination, next);
4398
2035
  return "updated";
4399
2036
  }
4400
- async function updateSeedEntrypoint(targetDir, names, config, options) {
4401
- const file = seedEntrypointPath(config);
4402
- const destination = path.join(targetDir, file);
4403
- const existing = await readOptionalFile(destination);
4404
- const registryName = `${names.featureSingularCamel}Seeds`;
4405
- const importLine = `import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "seeds", config), "index.ts"))}";`;
4406
- const registryEntry = `...${registryName}`;
4407
- if (existing === undefined) {
4408
- const content = seedEntrypointFile(names, config);
4409
- if (!options.dryRun) {
4410
- await mkdir(path.dirname(destination), { recursive: true });
4411
- await writeFile(destination, content);
4412
- }
4413
- return "created";
4414
- }
4415
- let next = existing;
4416
- if (!next.includes(importLine)) {
4417
- next = insertAfterImports(next, importLine);
4418
- }
4419
- const registry = arrayInitializerInfo(next, "seeds");
4420
- if (!registry) {
4421
- next = `${next.trimEnd()}\n\nconst seeds = [${registryEntry}] as const;\n`;
4422
- }
4423
- else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
4424
- const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
4425
- next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
4426
- }
4427
- if (next === existing)
4428
- return "skipped";
4429
- if (!options.dryRun)
4430
- await writeFile(destination, next);
4431
- return "updated";
4432
- }
4433
2037
  async function updateUploadRoute(targetDir, names, config, options) {
4434
2038
  const file = uploadRoutePath(config);
4435
2039
  const destination = path.join(targetDir, file);
@@ -4506,22 +2110,6 @@ async function applyOutboxRegistryUpdate(result, targetDir, names, kind, config,
4506
2110
  result.files.push(config.paths.outbox);
4507
2111
  }
4508
2112
  }
4509
- function defineUploadsInitializerInfo(source, constName) {
4510
- const match = new RegExp(`\\bconst\\s+${constName}\\s*=\\s*defineUploads\\s*\\(`).exec(source);
4511
- if (!match)
4512
- return undefined;
4513
- const openBrace = source.indexOf("{", match.index);
4514
- if (openBrace === -1)
4515
- return undefined;
4516
- const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
4517
- if (closeBrace === -1)
4518
- return undefined;
4519
- return {
4520
- text: source.slice(openBrace, closeBrace + 1),
4521
- start: openBrace,
4522
- end: closeBrace,
4523
- };
4524
- }
4525
2113
  function schemasFile(names, mode, options) {
4526
2114
  return `import { z } from "zod";
4527
2115
 
@@ -5924,26 +3512,6 @@ export const ${names.uploadExportName} = defineUpload<
5924
3512
  });
5925
3513
  `;
5926
3514
  }
5927
- function seedEntrypointFile(names, config) {
5928
- const registryName = `${names.featureSingularCamel}Seeds`;
5929
- return `import { createServiceActor } from "@beignet/core/ports";
5930
- import { runSeeds } from "@beignet/core/testing";
5931
- import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "seeds", config), "index.ts"))}";
5932
- import { server } from "${aliasModule(config.paths.server)}";
5933
-
5934
- const seeds = [...${registryName}] as const;
5935
-
5936
- const ctx = await server.createServiceContext({
5937
- \tactor: createServiceActor("beignet-seed"),
5938
- });
5939
-
5940
- try {
5941
- \tawait runSeeds({ ctx, seeds });
5942
- } finally {
5943
- \tawait server.stop();
5944
- }
5945
- `;
5946
- }
5947
3515
  function uploadRouteFile(names, config) {
5948
3516
  const registryName = `${names.featureSingularCamel}Uploads`;
5949
3517
  return `import {
@@ -5953,19 +3521,21 @@ function uploadRouteFile(names, config) {
5953
3521
  } from "@beignet/core/uploads";
5954
3522
  import { createUploadRoute } from "@beignet/next";
5955
3523
  import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "uploads", config), "index.ts"))}";
5956
- import { server } from "${aliasModule(config.paths.server)}";
3524
+ import { getServer } from "${aliasModule(config.paths.server)}";
5957
3525
 
5958
3526
  const uploadRegistry = defineUploads({
5959
3527
  \t${names.featureSingularCamel}: ${registryName},
5960
3528
  });
5961
3529
 
5962
- const uploadRouter = createUploadRouter({
5963
- \tuploads: uploadsFromRegistry(uploadRegistry),
5964
- \tctx: () => server.createContextFromNext(),
5965
- \tstorage: server.ports.storage,
5966
- });
3530
+ export const { POST } = createUploadRoute(async () => {
3531
+ \tconst server = await getServer();
5967
3532
 
5968
- export const { POST } = createUploadRoute(uploadRouter);
3533
+ \treturn createUploadRouter({
3534
+ \t\tuploads: uploadsFromRegistry(uploadRegistry),
3535
+ \t\tctx: () => server.createContextFromNext(),
3536
+ \t\tstorage: server.ports.storage,
3537
+ \t});
3538
+ });
5969
3539
  `;
5970
3540
  }
5971
3541
  function outboxRegistryFile(config, registration) {
@@ -5980,7 +3550,7 @@ function outboxRegistryFile(config, registration) {
5980
3550
  return `import { defineOutboxRegistry } from "@beignet/core/outbox";
5981
3551
  import { createServiceActor } from "@beignet/core/ports";
5982
3552
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
5983
- ${featureImport}import { server } from "${relativeModule(config.paths.outbox, config.paths.server)}";
3553
+ ${featureImport}import { getServer } from "${relativeModule(config.paths.outbox, config.paths.server)}";
5984
3554
 
5985
3555
  export const outboxRegistry = defineOutboxRegistry({
5986
3556
  \tevents: [${eventEntry}],
@@ -5988,12 +3558,14 @@ export const outboxRegistry = defineOutboxRegistry({
5988
3558
  });
5989
3559
 
5990
3560
  export async function createOutboxDrainContext(): Promise<AppContext> {
3561
+ \tconst server = await getServer();
5991
3562
  \treturn server.createServiceContext({
5992
3563
  \t\tactor: createServiceActor("beignet-outbox"),
5993
3564
  \t});
5994
3565
  }
5995
3566
 
5996
3567
  export async function stopOutboxDrainContext(): Promise<void> {
3568
+ \tconst server = await getServer();
5997
3569
  \tawait server.stop();
5998
3570
  }
5999
3571
  `;
@@ -6001,13 +3573,13 @@ export async function stopOutboxDrainContext(): Promise<void> {
6001
3573
  function outboxDrainRouteFile(config) {
6002
3574
  return `import { createOutboxDrainRoute } from "@beignet/next";
6003
3575
  import { env } from "@/lib/env";
6004
- import { server } from "${aliasModule(config.paths.server)}";
3576
+ import { getServer } from "${aliasModule(config.paths.server)}";
6005
3577
  import { outboxRegistry } from "${aliasModule(config.paths.outbox)}";
6006
3578
 
6007
3579
  export const runtime = "nodejs";
6008
3580
 
6009
3581
  export const { GET, POST } = createOutboxDrainRoute({
6010
- \tserver,
3582
+ \tserver: getServer,
6011
3583
  \tregistry: outboxRegistry,
6012
3584
  \tsecret: env.CRON_SECRET,
6013
3585
  \tbatchSize: 100,
@@ -6023,1210 +3595,6 @@ function outboxFeatureRegistry(names, kind, config) {
6023
3595
  importLine: `import { ${name} } from "${aliasModule(featureIndexPath)}";`,
6024
3596
  };
6025
3597
  }
6026
- function billingSchemasFile() {
6027
- return `import { z } from "zod";
6028
-
6029
- export const billingPlanSchema = z.enum(["team"]);
6030
-
6031
- export const billingStatusSchema = z.enum([
6032
- \t"inactive",
6033
- \t"trialing",
6034
- \t"active",
6035
- \t"past_due",
6036
- \t"canceled",
6037
- ]);
6038
-
6039
- export const createCheckoutSessionInputSchema = z.object({
6040
- \tplan: billingPlanSchema,
6041
- });
6042
-
6043
- export const createCheckoutSessionOutputSchema = z.object({
6044
- \tsessionId: z.string(),
6045
- \turl: z.string().url(),
6046
- });
6047
-
6048
- export const createBillingPortalSessionOutputSchema = z.object({
6049
- \turl: z.string().url(),
6050
- });
6051
-
6052
- export const billingStatusOutputSchema = z.object({
6053
- \ttenantId: z.string(),
6054
- \tplan: billingPlanSchema,
6055
- \tstatus: billingStatusSchema,
6056
- \tactive: z.boolean(),
6057
- \tprovider: z.string().optional(),
6058
- \tcustomerId: z.string().optional(),
6059
- \tsubscriptionId: z.string().optional(),
6060
- \tcurrentPeriodEnd: z.string().optional(),
6061
- \tcancelAtPeriodEnd: z.boolean().optional(),
6062
- \tcheckoutSessionId: z.string().optional(),
6063
- \tlastEventId: z.string().optional(),
6064
- \tupdatedAt: z.string().optional(),
6065
- });
6066
-
6067
- export type BillingPlan = z.infer<typeof billingPlanSchema>;
6068
- export type BillingStatus = z.infer<typeof billingStatusSchema>;
6069
- export type BillingStatusOutput = z.infer<typeof billingStatusOutputSchema>;
6070
- `;
6071
- }
6072
- function billingPricingFile() {
6073
- return `import type {
6074
- \tPaymentCheckoutLineItem,
6075
- \tPaymentCheckoutMode,
6076
- } from "@beignet/core/payments";
6077
- import { env } from "@/lib/env";
6078
- import type { BillingPlan } from "./schemas";
6079
-
6080
- export type BillingEntitlement = "todos.create";
6081
-
6082
- export type BillingPlanDefinition = {
6083
- \tid: BillingPlan;
6084
- \tlabel: string;
6085
- \tmode: PaymentCheckoutMode;
6086
- \tlineItems: readonly PaymentCheckoutLineItem[];
6087
- \tentitlements: readonly BillingEntitlement[];
6088
- };
6089
-
6090
- export const billingPlans = {
6091
- \tteam: {
6092
- \t\tid: "team",
6093
- \t\tlabel: "Team",
6094
- \t\tmode: "subscription",
6095
- \t\tlineItems: [{ priceId: env.BILLING_TEAM_PRICE_ID, quantity: 1 }],
6096
- \t\tentitlements: ["todos.create"],
6097
- \t},
6098
- } as const satisfies Record<BillingPlan, BillingPlanDefinition>;
6099
-
6100
- export function getBillingPlan(plan: BillingPlan): BillingPlanDefinition {
6101
- \treturn billingPlans[plan];
6102
- }
6103
-
6104
- export function billingPlanIncludesEntitlement(
6105
- \tplan: BillingPlan,
6106
- \tentitlement: string,
6107
- ): entitlement is BillingEntitlement {
6108
- \treturn getBillingPlan(plan).entitlements.includes(
6109
- \t\tentitlement as BillingEntitlement,
6110
- \t);
6111
- }
6112
- `;
6113
- }
6114
- function billingEntitlementsFile() {
6115
- return `import {
6116
- \tcreateEntitlements,
6117
- \tdenyEntitlement,
6118
- } from "@beignet/core/entitlements";
6119
- import { billingPlanIncludesEntitlement } from "./pricing";
6120
- import { isBillingAccountActive, type BillingRepository } from "./ports";
6121
-
6122
- export function createBillingEntitlements(billing: BillingRepository) {
6123
- \treturn createEntitlements({
6124
- \t\tasync inspect(input) {
6125
- \t\t\tif (input.subject.type !== "tenant") {
6126
- \t\t\t\treturn denyEntitlement({
6127
- \t\t\t\t\treason: "Billing entitlements require a tenant subject.",
6128
- \t\t\t\t\tcode: "ENTITLEMENT_SUBJECT_UNSUPPORTED",
6129
- \t\t\t\t\tdetails: { subject: input.subject },
6130
- \t\t\t\t});
6131
- \t\t\t}
6132
-
6133
- \t\t\tconst account = await billing.findByTenantId(input.subject.id);
6134
- \t\t\tif (!isBillingAccountActive(account)) {
6135
- \t\t\t\treturn denyEntitlement({
6136
- \t\t\t\t\treason: "An active billing plan is required for this action.",
6137
- \t\t\t\t\tcode: "BILLING_INACTIVE",
6138
- \t\t\t\t\tdetails: {
6139
- \t\t\t\t\t\tentitlement: input.entitlement,
6140
- \t\t\t\t\t\tstatus: account?.status ?? "inactive",
6141
- \t\t\t\t\t\tsubject: input.subject,
6142
- \t\t\t\t\t},
6143
- \t\t\t\t});
6144
- \t\t\t}
6145
-
6146
- \t\t\tif (!billingPlanIncludesEntitlement(account.plan, input.entitlement)) {
6147
- \t\t\t\treturn denyEntitlement({
6148
- \t\t\t\t\treason:
6149
- \t\t\t\t\t\t"The current billing plan does not include this entitlement.",
6150
- \t\t\t\t\tcode: "ENTITLEMENT_NOT_INCLUDED",
6151
- \t\t\t\t\tdetails: {
6152
- \t\t\t\t\t\tentitlement: input.entitlement,
6153
- \t\t\t\t\t\tplan: account.plan,
6154
- \t\t\t\t\t\tsubject: input.subject,
6155
- \t\t\t\t\t},
6156
- \t\t\t\t});
6157
- \t\t\t}
6158
-
6159
- \t\t\treturn true;
6160
- \t\t},
6161
- \t});
6162
- }
6163
- `;
6164
- }
6165
- function billingPortsFile() {
6166
- return `import type { BillingPlan, BillingStatus } from "./schemas";
6167
-
6168
- export type BillingAccount = {
6169
- \ttenantId: string;
6170
- \tplan: BillingPlan;
6171
- \tstatus: BillingStatus;
6172
- \tprovider: string;
6173
- \tcustomerId?: string;
6174
- \tsubscriptionId?: string;
6175
- \tcurrentPeriodEnd?: string;
6176
- \tcancelAtPeriodEnd?: boolean;
6177
- \tcheckoutSessionId?: string;
6178
- \tlastEventId?: string;
6179
- \tupdatedAt: string;
6180
- };
6181
-
6182
- export type SaveBillingAccountInput = BillingAccount;
6183
-
6184
- export interface BillingRepository {
6185
- \tfindByTenantId(tenantId: string): Promise<BillingAccount | null>;
6186
- \tfindByCustomerId(
6187
- \t\tcustomerId: string,
6188
- \t\tprovider: string,
6189
- \t): Promise<BillingAccount | null>;
6190
- \tsave(input: SaveBillingAccountInput): Promise<BillingAccount>;
6191
- }
6192
-
6193
- export function isBillingAccountActive(
6194
- \taccount: BillingAccount | null,
6195
- ): account is BillingAccount & { status: "active" | "trialing" } {
6196
- \treturn account?.status === "active" || account?.status === "trialing";
6197
- }
6198
- `;
6199
- }
6200
- function billingContractsFile() {
6201
- return `import { defineContractGroup } from "@beignet/core/contracts";
6202
- import { z } from "zod";
6203
- import {
6204
- \tbillingStatusOutputSchema,
6205
- \tcreateBillingPortalSessionOutputSchema,
6206
- \tcreateCheckoutSessionInputSchema,
6207
- \tcreateCheckoutSessionOutputSchema,
6208
- } from "@/features/billing/schemas";
6209
- import { errors } from "@/features/shared/errors";
6210
-
6211
- const billing = defineContractGroup()
6212
- \t.namespace("billing")
6213
- \t.prefix("/api/billing");
6214
- const checkoutHeadersSchema = z.object({
6215
- \t"idempotency-key": z.string().min(1),
6216
- });
6217
- const checkoutBilling = billing.headers(checkoutHeadersSchema);
6218
-
6219
- export const getBillingStatus = billing
6220
- \t.get("/")
6221
- \t.query(z.object({}))
6222
- \t.meta({ auth: "required" })
6223
- \t.errors({ Unauthorized: errors.Unauthorized })
6224
- \t.responses({
6225
- \t\t200: billingStatusOutputSchema,
6226
- \t})
6227
- \t.openapi({ summary: "Get billing status", tags: ["billing"] });
6228
-
6229
- export const createCheckoutSession = checkoutBilling
6230
- \t.post("/checkout")
6231
- \t.body(createCheckoutSessionInputSchema)
6232
- \t.meta({
6233
- \t\tauth: "required",
6234
- \t\tidempotency: {
6235
- \t\t\trequired: true,
6236
- \t\t\theader: "idempotency-key",
6237
- \t\t\tscope: "actor-tenant",
6238
- \t\t\tttlSec: 86_400,
6239
- \t\t},
6240
- \t})
6241
- \t.errors({
6242
- \t\tUnauthorized: errors.Unauthorized,
6243
- \t\tIdempotencyConflict: errors.IdempotencyConflict,
6244
- \t\tIdempotencyInProgress: errors.IdempotencyInProgress,
6245
- \t\tBillingCheckoutUnavailable: errors.BillingCheckoutUnavailable,
6246
- \t})
6247
- \t.responses({
6248
- \t\t201: createCheckoutSessionOutputSchema,
6249
- \t})
6250
- \t.openapi({ summary: "Create a checkout session", tags: ["billing"] });
6251
-
6252
- export const createBillingPortalSession = billing
6253
- \t.post("/portal")
6254
- \t.body(z.object({}))
6255
- \t.meta({ auth: "required" })
6256
- \t.errors({
6257
- \t\tUnauthorized: errors.Unauthorized,
6258
- \t\tBillingAccountNotFound: errors.BillingAccountNotFound,
6259
- \t})
6260
- \t.responses({
6261
- \t\t200: createBillingPortalSessionOutputSchema,
6262
- \t})
6263
- \t.openapi({ summary: "Create a billing portal session", tags: ["billing"] });
6264
-
6265
- export const billingContracts = [
6266
- \tgetBillingStatus,
6267
- \tcreateCheckoutSession,
6268
- \tcreateBillingPortalSession,
6269
- ];
6270
- `;
6271
- }
6272
- function billingUseCasesFile(config) {
6273
- return `import {
6274
- \tcreateIdempotencyFingerprint,
6275
- \trunIdempotently,
6276
- } from "@beignet/core/idempotency";
6277
- import type { PaymentWebhookEvent } from "@beignet/core/payments";
6278
- import { requireUser } from "@beignet/core/ports";
6279
- import { z } from "zod";
6280
- import type { AppContext } from "${aliasModule(config.paths.appContext)}";
6281
- import { appError } from "@/features/shared/errors";
6282
- import { env } from "@/lib/env";
6283
- import { useCase } from "@/lib/use-case";
6284
- import { getBillingPlan } from "../pricing";
6285
- import type { BillingAccount } from "../ports";
6286
- import { isBillingAccountActive } from "../ports";
6287
- import {
6288
- \ttype BillingPlan,
6289
- \ttype BillingStatus,
6290
- \tbillingStatusOutputSchema,
6291
- \tcreateBillingPortalSessionOutputSchema,
6292
- \tcreateCheckoutSessionInputSchema,
6293
- \tcreateCheckoutSessionOutputSchema,
6294
- } from "../schemas";
6295
-
6296
- type ProviderPayload = Record<string, unknown>;
6297
-
6298
- const paymentWebhookEventSchema = z.custom<PaymentWebhookEvent>(
6299
- \t(value): value is PaymentWebhookEvent =>
6300
- \t\ttypeof value === "object" &&
6301
- \t\tvalue !== null &&
6302
- \t\ttypeof (value as PaymentWebhookEvent).id === "string" &&
6303
- \t\ttypeof (value as PaymentWebhookEvent).type === "string" &&
6304
- \t\ttypeof (value as PaymentWebhookEvent).provider === "string",
6305
- );
6306
- const paymentWebhookOutputSchema = z.object({
6307
- \treceived: z.boolean(),
6308
- \taccountUpdated: z.boolean(),
6309
- });
6310
-
6311
- function nowIso(): string {
6312
- \treturn new Date().toISOString();
6313
- }
6314
-
6315
- function record(value: unknown): ProviderPayload {
6316
- \treturn value && typeof value === "object" && !Array.isArray(value)
6317
- \t\t? (value as ProviderPayload)
6318
- \t\t: {};
6319
- }
6320
-
6321
- function stringValue(value: unknown): string | undefined {
6322
- \treturn typeof value === "string" && value.length > 0 ? value : undefined;
6323
- }
6324
-
6325
- function providerId(value: unknown): string | undefined {
6326
- \tif (typeof value === "string") return stringValue(value);
6327
- \tconst payload = record(value);
6328
- \treturn stringValue(payload.id);
6329
- }
6330
-
6331
- function metadataValue(data: ProviderPayload, key: string): string | undefined {
6332
- \treturn stringValue(record(data.metadata)[key]);
6333
- }
6334
-
6335
- function tenantIdFromPayload(data: ProviderPayload): string | undefined {
6336
- \treturn (
6337
- \t\tstringValue(data.client_reference_id) ??
6338
- \t\tmetadataValue(data, "tenantId") ??
6339
- \t\tmetadataValue(data, "tenant_id")
6340
- \t);
6341
- }
6342
-
6343
- function planFromPayload(data: ProviderPayload): BillingPlan {
6344
- \tconst plan = metadataValue(data, "plan");
6345
- \treturn plan === "team" ? plan : "team";
6346
- }
6347
-
6348
- function statusFromPayload(
6349
- \tdata: ProviderPayload,
6350
- \tfallback: BillingStatus,
6351
- ): BillingStatus {
6352
- \tconst status = stringValue(data.status);
6353
- \tswitch (status) {
6354
- \t\tcase "active":
6355
- \t\tcase "trialing":
6356
- \t\tcase "past_due":
6357
- \t\tcase "canceled":
6358
- \t\tcase "inactive":
6359
- \t\t\treturn status;
6360
- \t\tcase "unpaid":
6361
- \t\t\treturn "past_due";
6362
- \t\tcase "incomplete":
6363
- \t\t\treturn "inactive";
6364
- \t\tcase "incomplete_expired":
6365
- \t\t\treturn "canceled";
6366
- \t\tcase "paused":
6367
- \t\t\treturn "inactive";
6368
- \t\tdefault:
6369
- \t\t\treturn fallback;
6370
- \t}
6371
- }
6372
-
6373
- function periodEndFromPayload(data: ProviderPayload): string | undefined {
6374
- \tconst value = data.current_period_end;
6375
- \treturn typeof value === "number"
6376
- \t\t? new Date(value * 1000).toISOString()
6377
- \t\t: stringValue(value);
6378
- }
6379
-
6380
- function cancelAtPeriodEndFromPayload(
6381
- \tdata: ProviderPayload,
6382
- ): boolean | undefined {
6383
- \treturn typeof data.cancel_at_period_end === "boolean"
6384
- \t\t? data.cancel_at_period_end
6385
- \t\t: undefined;
6386
- }
6387
-
6388
- function billingTenantId(ctx: AppContext): string {
6389
- \tconst user = requireUser(ctx);
6390
- \treturn ctx.tenant?.id ?? user.id;
6391
- }
6392
-
6393
- function billingStatusOutput(tenantId: string, account: BillingAccount | null) {
6394
- \treturn {
6395
- \t\ttenantId,
6396
- \t\tplan: account?.plan ?? "team",
6397
- \t\tstatus: account?.status ?? "inactive",
6398
- \t\tactive: isBillingAccountActive(account),
6399
- \t\t...(account?.provider ? { provider: account.provider } : {}),
6400
- \t\t...(account?.customerId ? { customerId: account.customerId } : {}),
6401
- \t\t...(account?.subscriptionId
6402
- \t\t\t? { subscriptionId: account.subscriptionId }
6403
- \t\t\t: {}),
6404
- \t\t...(account?.currentPeriodEnd
6405
- \t\t\t? { currentPeriodEnd: account.currentPeriodEnd }
6406
- \t\t\t: {}),
6407
- \t\t...(account?.cancelAtPeriodEnd !== undefined
6408
- \t\t\t? { cancelAtPeriodEnd: account.cancelAtPeriodEnd }
6409
- \t\t\t: {}),
6410
- \t\t...(account?.checkoutSessionId
6411
- \t\t\t? { checkoutSessionId: account.checkoutSessionId }
6412
- \t\t\t: {}),
6413
- \t\t...(account?.lastEventId ? { lastEventId: account.lastEventId } : {}),
6414
- \t\t...(account?.updatedAt ? { updatedAt: account.updatedAt } : {}),
6415
- \t};
6416
- }
6417
-
6418
- async function saveBillingAccount(args: {
6419
- \texisting: BillingAccount | null;
6420
- \ttenantId: string;
6421
- \tprovider: string;
6422
- \tplan?: BillingPlan;
6423
- \tstatus?: BillingStatus;
6424
- \tcustomerId?: string;
6425
- \tsubscriptionId?: string;
6426
- \tcurrentPeriodEnd?: string;
6427
- \tcancelAtPeriodEnd?: boolean;
6428
- \tcheckoutSessionId?: string;
6429
- \tlastEventId?: string;
6430
- \tsave: (account: BillingAccount) => Promise<BillingAccount>;
6431
- }) {
6432
- \treturn args.save({
6433
- \t\ttenantId: args.tenantId,
6434
- \t\tprovider: args.provider,
6435
- \t\tplan: args.plan ?? args.existing?.plan ?? "team",
6436
- \t\tstatus: args.status ?? args.existing?.status ?? "inactive",
6437
- \t\tcustomerId: args.customerId ?? args.existing?.customerId,
6438
- \t\tsubscriptionId: args.subscriptionId ?? args.existing?.subscriptionId,
6439
- \t\tcurrentPeriodEnd:
6440
- \t\t\targs.currentPeriodEnd ?? args.existing?.currentPeriodEnd,
6441
- \t\tcancelAtPeriodEnd:
6442
- \t\t\targs.cancelAtPeriodEnd ?? args.existing?.cancelAtPeriodEnd,
6443
- \t\tcheckoutSessionId:
6444
- \t\t\targs.checkoutSessionId ?? args.existing?.checkoutSessionId,
6445
- \t\tlastEventId: args.lastEventId ?? args.existing?.lastEventId,
6446
- \t\tupdatedAt: nowIso(),
6447
- \t});
6448
- }
6449
-
6450
- async function applyCheckoutCompleted(
6451
- \tevent: PaymentWebhookEvent,
6452
- \tsave: (account: BillingAccount) => Promise<BillingAccount>,
6453
- \tfindByTenantId: (tenantId: string) => Promise<BillingAccount | null>,
6454
- ) {
6455
- \tconst data = record(event.data);
6456
- \tconst tenantId = tenantIdFromPayload(data);
6457
- \tif (!tenantId) return null;
6458
-
6459
- \tconst existing = await findByTenantId(tenantId);
6460
- \treturn saveBillingAccount({
6461
- \t\texisting,
6462
- \t\ttenantId,
6463
- \t\tprovider: event.provider,
6464
- \t\tplan: planFromPayload(data),
6465
- \t\tstatus: "active",
6466
- \t\tcustomerId: providerId(data.customer),
6467
- \t\tsubscriptionId: providerId(data.subscription),
6468
- \t\tcheckoutSessionId: stringValue(data.id),
6469
- \t\tlastEventId: event.id,
6470
- \t\tsave,
6471
- \t});
6472
- }
6473
-
6474
- async function applySubscriptionEvent(
6475
- \tevent: PaymentWebhookEvent,
6476
- \tsave: (account: BillingAccount) => Promise<BillingAccount>,
6477
- \tfindByCustomerId: (
6478
- \t\tcustomerId: string,
6479
- \t\tprovider: string,
6480
- \t) => Promise<BillingAccount | null>,
6481
- \tfindByTenantId: (tenantId: string) => Promise<BillingAccount | null>,
6482
- ) {
6483
- \tconst data = record(event.data);
6484
- \tconst customerId = providerId(data.customer);
6485
- \tconst explicitTenantId = tenantIdFromPayload(data);
6486
- \tconst existing = explicitTenantId
6487
- \t\t? await findByTenantId(explicitTenantId)
6488
- \t\t: customerId
6489
- \t\t\t? await findByCustomerId(customerId, event.provider)
6490
- \t\t\t: null;
6491
- \tconst tenantId = explicitTenantId ?? existing?.tenantId;
6492
- \tif (!tenantId) return null;
6493
-
6494
- \treturn saveBillingAccount({
6495
- \t\texisting,
6496
- \t\ttenantId,
6497
- \t\tprovider: event.provider,
6498
- \t\tplan: planFromPayload(data),
6499
- \t\tstatus:
6500
- \t\t\tevent.type === "customer.subscription.deleted"
6501
- \t\t\t\t? "canceled"
6502
- \t\t\t\t: statusFromPayload(data, existing?.status ?? "active"),
6503
- \t\tcustomerId,
6504
- \t\tsubscriptionId: providerId(data.id) ?? existing?.subscriptionId,
6505
- \t\tcurrentPeriodEnd: periodEndFromPayload(data),
6506
- \t\tcancelAtPeriodEnd:
6507
- \t\t\tevent.type === "customer.subscription.deleted"
6508
- \t\t\t\t? false
6509
- \t\t\t\t: cancelAtPeriodEndFromPayload(data),
6510
- \t\tlastEventId: event.id,
6511
- \t\tsave,
6512
- \t});
6513
- }
6514
-
6515
- async function applyInvoiceSucceeded(
6516
- \tevent: PaymentWebhookEvent,
6517
- \tsave: (account: BillingAccount) => Promise<BillingAccount>,
6518
- \tfindByCustomerId: (
6519
- \t\tcustomerId: string,
6520
- \t\tprovider: string,
6521
- \t) => Promise<BillingAccount | null>,
6522
- ) {
6523
- \tconst data = record(event.data);
6524
- \tconst customerId = providerId(data.customer);
6525
- \tif (!customerId) return null;
6526
-
6527
- \tconst existing = await findByCustomerId(customerId, event.provider);
6528
- \tif (!existing) return null;
6529
- \tif (existing.status === "canceled") return null;
6530
-
6531
- \treturn saveBillingAccount({
6532
- \t\texisting,
6533
- \t\ttenantId: existing.tenantId,
6534
- \t\tprovider: event.provider,
6535
- \t\tstatus: "active",
6536
- \t\tcustomerId,
6537
- \t\tsubscriptionId: providerId(data.subscription),
6538
- \t\tcancelAtPeriodEnd: cancelAtPeriodEndFromPayload(data),
6539
- \t\tlastEventId: event.id,
6540
- \t\tsave,
6541
- \t});
6542
- }
6543
-
6544
- async function applyInvoiceFailed(
6545
- \tevent: PaymentWebhookEvent,
6546
- \tsave: (account: BillingAccount) => Promise<BillingAccount>,
6547
- \tfindByCustomerId: (
6548
- \t\tcustomerId: string,
6549
- \t\tprovider: string,
6550
- \t) => Promise<BillingAccount | null>,
6551
- ) {
6552
- \tconst data = record(event.data);
6553
- \tconst customerId = providerId(data.customer);
6554
- \tif (!customerId) return null;
6555
-
6556
- \tconst existing = await findByCustomerId(customerId, event.provider);
6557
- \tif (!existing) return null;
6558
- \tif (existing.status === "canceled") return null;
6559
-
6560
- \treturn saveBillingAccount({
6561
- \t\texisting,
6562
- \t\ttenantId: existing.tenantId,
6563
- \t\tprovider: event.provider,
6564
- \t\tstatus: "past_due",
6565
- \t\tcustomerId,
6566
- \t\tsubscriptionId: providerId(data.subscription),
6567
- \t\tlastEventId: event.id,
6568
- \t\tsave,
6569
- \t});
6570
- }
6571
-
6572
- export const getBillingStatusUseCase = useCase
6573
- \t.query("billing.getStatus")
6574
- \t.input(z.object({}))
6575
- \t.output(billingStatusOutputSchema)
6576
- \t.run(async ({ ctx }) => {
6577
- \t\tconst tenantId = billingTenantId(ctx);
6578
- \t\tconst account = await ctx.ports.billing.findByTenantId(tenantId);
6579
- \t\treturn billingStatusOutput(tenantId, account);
6580
- \t});
6581
-
6582
- export const createCheckoutSessionUseCase = useCase
6583
- \t.command("billing.createCheckoutSession")
6584
- \t.input(createCheckoutSessionInputSchema)
6585
- \t.output(createCheckoutSessionOutputSchema)
6586
- \t.run(async ({ ctx, input }) => {
6587
- \t\tconst tenantId = billingTenantId(ctx);
6588
- \t\tconst account = await ctx.ports.billing.findByTenantId(tenantId);
6589
- \t\tconst plan = getBillingPlan(input.plan);
6590
- \t\tconst session = await ctx.ports.payments.createCheckoutSession({
6591
- \t\t\tmode: plan.mode,
6592
- \t\t\tlineItems: plan.lineItems,
6593
- \t\t\tsuccessUrl: \`\${env.APP_URL}/billing?checkout=success\`,
6594
- \t\t\tcancelUrl: \`\${env.APP_URL}/billing?checkout=cancelled\`,
6595
- \t\t\tcustomerId: account?.customerId,
6596
- \t\t\tclientReferenceId: tenantId,
6597
- \t\t\tmetadata: {
6598
- \t\t\t\ttenantId,
6599
- \t\t\t\tplan: input.plan,
6600
- \t\t\t},
6601
- \t\t});
6602
-
6603
- \t\tif (!session.url) {
6604
- \t\t\tthrow appError("BillingCheckoutUnavailable", {
6605
- \t\t\t\tdetails: { provider: session.provider, sessionId: session.id },
6606
- \t\t\t});
6607
- \t\t}
6608
-
6609
- \t\tawait ctx.ports.billing.save({
6610
- \t\t\ttenantId,
6611
- \t\t\tprovider: session.provider,
6612
- \t\t\tplan: input.plan,
6613
- \t\t\tstatus: account?.status ?? "inactive",
6614
- \t\t\tcustomerId: session.customerId ?? account?.customerId,
6615
- \t\t\tsubscriptionId: account?.subscriptionId,
6616
- \t\t\tcurrentPeriodEnd: account?.currentPeriodEnd,
6617
- \t\t\tcancelAtPeriodEnd: account?.cancelAtPeriodEnd,
6618
- \t\t\tcheckoutSessionId: session.id,
6619
- \t\t\tlastEventId: account?.lastEventId,
6620
- \t\t\tupdatedAt: nowIso(),
6621
- \t\t});
6622
-
6623
- \t\treturn {
6624
- \t\t\tsessionId: session.id,
6625
- \t\t\turl: session.url,
6626
- \t\t};
6627
- \t});
6628
-
6629
- export const createBillingPortalSessionUseCase = useCase
6630
- \t.command("billing.createPortalSession")
6631
- \t.input(z.object({}))
6632
- \t.output(createBillingPortalSessionOutputSchema)
6633
- \t.run(async ({ ctx }) => {
6634
- \t\tconst tenantId = billingTenantId(ctx);
6635
- \t\tconst account = await ctx.ports.billing.findByTenantId(tenantId);
6636
-
6637
- \t\tif (!account?.customerId) {
6638
- \t\t\tthrow appError("BillingAccountNotFound", {
6639
- \t\t\t\tdetails: { tenantId },
6640
- \t\t\t});
6641
- \t\t}
6642
-
6643
- \t\tconst session = await ctx.ports.payments.createBillingPortalSession({
6644
- \t\t\tcustomerId: account.customerId,
6645
- \t\t\treturnUrl: \`\${env.APP_URL}/billing\`,
6646
- \t\t});
6647
-
6648
- \t\treturn { url: session.url };
6649
- \t});
6650
-
6651
- export const handlePaymentWebhookUseCase = useCase
6652
- \t.command("billing.handlePaymentWebhook")
6653
- \t.input(paymentWebhookEventSchema)
6654
- \t.output(paymentWebhookOutputSchema)
6655
- \t.run(async ({ ctx, input }) => {
6656
- \t\tconst fingerprint = await createIdempotencyFingerprint({
6657
- \t\t\ttype: input.type,
6658
- \t\t\tdata: input.data,
6659
- \t\t});
6660
-
6661
- \t\treturn runIdempotently(ctx.ports.idempotency, {
6662
- \t\t\tnamespace: "webhooks.payments",
6663
- \t\t\tkey: input.id,
6664
- \t\t\tscope: { provider: input.provider },
6665
- \t\t\tfingerprint,
6666
- \t\t\tttlSec: 60 * 60 * 24 * 30,
6667
- \t\t\trun: () =>
6668
- \t\t\t\tctx.ports.uow.transaction(async (tx) => {
6669
- \t\t\t\t\tlet account: BillingAccount | null = null;
6670
-
6671
- \t\t\t\t\tif (input.type === "checkout.session.completed") {
6672
- \t\t\t\t\t\taccount = await applyCheckoutCompleted(
6673
- \t\t\t\t\t\t\tinput,
6674
- \t\t\t\t\t\t\ttx.billing.save,
6675
- \t\t\t\t\t\t\ttx.billing.findByTenantId,
6676
- \t\t\t\t\t\t);
6677
- \t\t\t\t\t} else if (input.type.startsWith("customer.subscription.")) {
6678
- \t\t\t\t\t\taccount = await applySubscriptionEvent(
6679
- \t\t\t\t\t\t\tinput,
6680
- \t\t\t\t\t\t\ttx.billing.save,
6681
- \t\t\t\t\t\t\ttx.billing.findByCustomerId,
6682
- \t\t\t\t\t\t\ttx.billing.findByTenantId,
6683
- \t\t\t\t\t\t);
6684
- \t\t\t\t\t} else if (input.type === "invoice.payment_succeeded") {
6685
- \t\t\t\t\t\taccount = await applyInvoiceSucceeded(
6686
- \t\t\t\t\t\t\tinput,
6687
- \t\t\t\t\t\t\ttx.billing.save,
6688
- \t\t\t\t\t\t\ttx.billing.findByCustomerId,
6689
- \t\t\t\t\t\t);
6690
- \t\t\t\t\t} else if (input.type === "invoice.payment_failed") {
6691
- \t\t\t\t\t\taccount = await applyInvoiceFailed(
6692
- \t\t\t\t\t\t\tinput,
6693
- \t\t\t\t\t\t\ttx.billing.save,
6694
- \t\t\t\t\t\t\ttx.billing.findByCustomerId,
6695
- \t\t\t\t\t\t);
6696
- \t\t\t\t\t}
6697
-
6698
- \t\t\t\t\treturn {
6699
- \t\t\t\t\t\treceived: true,
6700
- \t\t\t\t\t\taccountUpdated: Boolean(account),
6701
- \t\t\t\t\t};
6702
- \t\t\t\t}),
6703
- \t\t});
6704
- \t});
6705
- `;
6706
- }
6707
- function billingRoutesFile(config) {
6708
- const routeFilePath = path.join(config.paths.features, "billing/routes.ts");
6709
- return `import { defineRouteGroup } from "@beignet/next";
6710
- import type { AppContext } from "${relativeModule(routeFilePath, config.paths.appContext)}";
6711
- import * as billingContracts from "@/features/billing/contracts";
6712
- import * as billingUseCases from "@/features/billing/use-cases";
6713
-
6714
- export const billingRoutes = defineRouteGroup<AppContext>()({
6715
- \tname: "billing",
6716
- \troutes: [
6717
- \t\t{
6718
- \t\t\tcontract: billingContracts.getBillingStatus,
6719
- \t\t\tuseCase: billingUseCases.getBillingStatusUseCase,
6720
- \t\t},
6721
- \t\t{
6722
- \t\t\tcontract: billingContracts.createCheckoutSession,
6723
- \t\t\tuseCase: billingUseCases.createCheckoutSessionUseCase,
6724
- \t\t},
6725
- \t\t{
6726
- \t\t\tcontract: billingContracts.createBillingPortalSession,
6727
- \t\t\tuseCase: billingUseCases.createBillingPortalSessionUseCase,
6728
- \t\t},
6729
- \t],
6730
- });
6731
- `;
6732
- }
6733
- function billingTestFile(config) {
6734
- return `import { describe, expect, it } from "bun:test";
6735
- import { createUseCaseTester } from "@beignet/core/application";
6736
- import { createAnonymousActor } from "@beignet/core/ports";
6737
- import { createTestUserActor } from "@beignet/core/ports/testing";
6738
- import {
6739
- \tcreateTestContextFactory,
6740
- \tcreateTestPorts,
6741
- } from "@beignet/core/testing";
6742
- import type { AppContext } from "${aliasModule(config.paths.appContext)}";
6743
- import { appPorts } from "@/infra/app-ports";
6744
- import type { AppTransactionPorts } from "@/ports";
6745
- import { createBillingEntitlements } from "../entitlements";
6746
- import type { BillingAccount, BillingRepository } from "../ports";
6747
- import {
6748
- \tcreateBillingPortalSessionUseCase,
6749
- \tcreateCheckoutSessionUseCase,
6750
- \tgetBillingStatusUseCase,
6751
- \thandlePaymentWebhookUseCase,
6752
- } from "../use-cases";
6753
-
6754
- function createMemoryBillingRepository(): BillingRepository {
6755
- \tconst accounts = new Map<string, BillingAccount>();
6756
-
6757
- \treturn {
6758
- \t\tasync findByTenantId(tenantId) {
6759
- \t\t\treturn accounts.get(tenantId) ?? null;
6760
- \t\t},
6761
-
6762
- \t\tasync findByCustomerId(customerId, provider) {
6763
- \t\t\treturn (
6764
- \t\t\t\t[...accounts.values()].find(
6765
- \t\t\t\t\t(account) =>
6766
- \t\t\t\t\t\taccount.customerId === customerId &&
6767
- \t\t\t\t\t\taccount.provider === provider,
6768
- \t\t\t\t) ?? null
6769
- \t\t\t);
6770
- \t\t},
6771
-
6772
- \t\tasync save(input) {
6773
- \t\t\taccounts.set(input.tenantId, { ...input });
6774
- \t\t\treturn input;
6775
- \t\t},
6776
- \t};
6777
- }
6778
-
6779
- function createHarness(options: { actor?: AppContext["actor"] } = {}) {
6780
- \tconst billing = createMemoryBillingRepository();
6781
- \tconst actor = options.actor ?? createTestUserActor("user_test");
6782
- \tconst fixture = createTestPorts<AppContext["ports"], AppTransactionPorts>({
6783
- \t\tbase: appPorts,
6784
- \t\toverrides: {
6785
- \t\t\tbilling,
6786
- \t\t\tentitlements: createBillingEntitlements(billing),
6787
- \t\t\tgate: appPorts.gate,
6788
- \t\t},
6789
- \t});
6790
- \tconst createContext = createTestContextFactory<
6791
- \t\tAppContext,
6792
- \t\tAppContext["ports"]
6793
- \t>({
6794
- \t\tports: fixture.ports,
6795
- \t\tactor,
6796
- \t\tauth:
6797
- \t\t\tactor.type === "user" && actor.id
6798
- \t\t\t\t? { user: { id: actor.id }, session: { id: "session_test" } }
6799
- \t\t\t\t: null,
6800
- \t\trequestId: "test-request",
6801
- \t\ttraceId: "test-trace",
6802
- \t\ttenant: { id: "tenant_example" },
6803
- \t});
6804
-
6805
- \treturn {
6806
- \t\tbilling,
6807
- \t\tentitlements: fixture.ports.entitlements,
6808
- \t\tpayments: fixture.payments,
6809
- \t\ttester: createUseCaseTester<AppContext>(createContext),
6810
- \t};
6811
- }
6812
-
6813
- describe("billing use cases", () => {
6814
- \tit("requires an authenticated user to read billing status", async () => {
6815
- \t\tconst harness = createHarness({ actor: createAnonymousActor() });
6816
-
6817
- \t\tawait expect(
6818
- \t\t\tharness.tester.run(getBillingStatusUseCase, {}),
6819
- \t\t).rejects.toMatchObject({
6820
- \t\t\tcode: "UNAUTHORIZED",
6821
- \t\t});
6822
- \t});
6823
-
6824
- \tit("creates checkout sessions through the payments port and records billing state", async () => {
6825
- \t\tconst harness = createHarness();
6826
-
6827
- \t\tconst checkout = await harness.tester.run(createCheckoutSessionUseCase, {
6828
- \t\t\tplan: "team",
6829
- \t\t});
6830
-
6831
- \t\texpect(checkout.sessionId).toMatch(/^checkout_/);
6832
- \t\texpect(checkout.url).toContain("/checkout/");
6833
- \t\texpect(harness.payments.checkoutSessions).toHaveLength(1);
6834
- \t\texpect(harness.payments.checkoutSessions[0]?.input).toMatchObject({
6835
- \t\t\tmode: "subscription",
6836
- \t\t\tlineItems: [{ priceId: "price_team_example", quantity: 1 }],
6837
- \t\t\tclientReferenceId: "tenant_example",
6838
- \t\t\tmetadata: {
6839
- \t\t\t\ttenantId: "tenant_example",
6840
- \t\t\t\tplan: "team",
6841
- \t\t\t},
6842
- \t\t});
6843
- \t\texpect(
6844
- \t\t\tharness.payments.checkoutSessions[0]?.input.idempotencyKey,
6845
- \t\t).toBe(undefined);
6846
- \t\tawait expect(
6847
- \t\t\tharness.billing.findByTenantId("tenant_example"),
6848
- \t\t).resolves.toMatchObject({
6849
- \t\t\tcheckoutSessionId: checkout.sessionId,
6850
- \t\t\tplan: "team",
6851
- \t\t\tstatus: "inactive",
6852
- \t\t});
6853
- \t});
6854
-
6855
- \tit("creates billing portal sessions for tenants with provider customers", async () => {
6856
- \t\tconst harness = createHarness();
6857
- \t\tawait harness.billing.save({
6858
- \t\t\ttenantId: "tenant_example",
6859
- \t\t\tprovider: "memory",
6860
- \t\t\tplan: "team",
6861
- \t\t\tstatus: "active",
6862
- \t\t\tcustomerId: "cus_123",
6863
- \t\t\tupdatedAt: new Date(0).toISOString(),
6864
- \t\t});
6865
-
6866
- \t\tconst portal = await harness.tester.run(
6867
- \t\t\tcreateBillingPortalSessionUseCase,
6868
- \t\t\t{},
6869
- \t\t);
6870
-
6871
- \t\texpect(portal.url).toContain("/portal/");
6872
- \t\texpect(harness.payments.billingPortalSessions).toHaveLength(1);
6873
- \t\texpect(harness.payments.billingPortalSessions[0]?.input.customerId).toBe(
6874
- \t\t\t"cus_123",
6875
- \t\t);
6876
- \t});
6877
-
6878
- \tit.each(["active", "trialing"] as const)(
6879
- \t\t"grants billing entitlements for %s accounts",
6880
- \t\tasync (status) => {
6881
- \t\t\tconst harness = createHarness();
6882
- \t\t\tawait harness.billing.save({
6883
- \t\t\t\ttenantId: "tenant_example",
6884
- \t\t\t\tprovider: "stripe",
6885
- \t\t\t\tplan: "team",
6886
- \t\t\t\tstatus,
6887
- \t\t\t\tupdatedAt: new Date(0).toISOString(),
6888
- \t\t\t});
6889
-
6890
- \t\t\tawait expect(
6891
- \t\t\t\tharness.entitlements.can({
6892
- \t\t\t\t\tentitlement: "todos.create",
6893
- \t\t\t\t\tsubject: { type: "tenant", id: "tenant_example" },
6894
- \t\t\t\t}),
6895
- \t\t\t).resolves.toBe(true);
6896
- \t\t},
6897
- \t);
6898
-
6899
- \tit.each(["inactive", "past_due", "canceled"] as const)(
6900
- \t\t"denies billing entitlements for %s accounts",
6901
- \t\tasync (status) => {
6902
- \t\t\tconst harness = createHarness();
6903
- \t\t\tawait harness.billing.save({
6904
- \t\t\t\ttenantId: "tenant_example",
6905
- \t\t\t\tprovider: "stripe",
6906
- \t\t\t\tplan: "team",
6907
- \t\t\t\tstatus,
6908
- \t\t\t\tupdatedAt: new Date(0).toISOString(),
6909
- \t\t\t});
6910
-
6911
- \t\t\tawait expect(
6912
- \t\t\t\tharness.entitlements.inspect({
6913
- \t\t\t\t\tentitlement: "todos.create",
6914
- \t\t\t\t\tsubject: { type: "tenant", id: "tenant_example" },
6915
- \t\t\t\t}),
6916
- \t\t\t).resolves.toMatchObject({
6917
- \t\t\t\tallowed: false,
6918
- \t\t\t\tcode: "BILLING_INACTIVE",
6919
- \t\t\t});
6920
- \t\t},
6921
- \t);
6922
-
6923
- \tit("handles checkout and subscription webhooks idempotently", async () => {
6924
- \t\tconst harness = createHarness();
6925
- \t\tconst checkoutEvent = {
6926
- \t\t\tid: "evt_checkout_completed",
6927
- \t\t\ttype: "checkout.session.completed",
6928
- \t\t\tprovider: "stripe",
6929
- \t\t\tdata: {
6930
- \t\t\t\tid: "cs_123",
6931
- \t\t\t\tclient_reference_id: "tenant_example",
6932
- \t\t\t\tcustomer: "cus_123",
6933
- \t\t\t\tsubscription: "sub_123",
6934
- \t\t\t\tmetadata: {
6935
- \t\t\t\t\ttenantId: "tenant_example",
6936
- \t\t\t\t\tplan: "team",
6937
- \t\t\t\t},
6938
- \t\t\t},
6939
- \t\t};
6940
- \t\tconst subscriptionEvent = {
6941
- \t\t\tid: "evt_subscription_updated",
6942
- \t\t\ttype: "customer.subscription.updated",
6943
- \t\t\tprovider: "stripe",
6944
- \t\t\tdata: {
6945
- \t\t\t\tid: "sub_123",
6946
- \t\t\t\tcustomer: "cus_123",
6947
- \t\t\t\tstatus: "active",
6948
- \t\t\t\tcurrent_period_end: 1_799_798_400,
6949
- \t\t\t\tcancel_at_period_end: true,
6950
- \t\t\t},
6951
- \t\t};
6952
-
6953
- \t\tawait expect(
6954
- \t\t\tharness.tester.run(handlePaymentWebhookUseCase, checkoutEvent),
6955
- \t\t).resolves.toEqual({ received: true, accountUpdated: true });
6956
- \t\tawait expect(
6957
- \t\t\tharness.tester.run(handlePaymentWebhookUseCase, checkoutEvent),
6958
- \t\t).resolves.toEqual({ received: true, accountUpdated: true });
6959
- \t\tawait expect(
6960
- \t\t\tharness.tester.run(handlePaymentWebhookUseCase, subscriptionEvent),
6961
- \t\t).resolves.toEqual({ received: true, accountUpdated: true });
6962
- \t\tawait expect(
6963
- \t\t\tharness.tester.run(getBillingStatusUseCase, {}),
6964
- \t\t).resolves.toMatchObject({
6965
- \t\t\ttenantId: "tenant_example",
6966
- \t\t\tactive: true,
6967
- \t\t\tstatus: "active",
6968
- \t\t\tcustomerId: "cus_123",
6969
- \t\t\tsubscriptionId: "sub_123",
6970
- \t\t\tcurrentPeriodEnd: "2027-01-13T00:00:00.000Z",
6971
- \t\t\tcancelAtPeriodEnd: true,
6972
- \t\t\tcheckoutSessionId: "cs_123",
6973
- \t\t\tlastEventId: "evt_subscription_updated",
6974
- \t\t});
6975
- \t});
6976
-
6977
- \tit("marks active subscriptions as past due when invoice payment fails", async () => {
6978
- \t\tconst harness = createHarness();
6979
- \t\tawait harness.billing.save({
6980
- \t\t\ttenantId: "tenant_example",
6981
- \t\t\tprovider: "stripe",
6982
- \t\t\tplan: "team",
6983
- \t\t\tstatus: "active",
6984
- \t\t\tcustomerId: "cus_123",
6985
- \t\t\tsubscriptionId: "sub_123",
6986
- \t\t\tupdatedAt: new Date(0).toISOString(),
6987
- \t\t});
6988
-
6989
- \t\tawait expect(
6990
- \t\t\tharness.tester.run(handlePaymentWebhookUseCase, {
6991
- \t\t\t\tid: "evt_invoice_failed",
6992
- \t\t\t\ttype: "invoice.payment_failed",
6993
- \t\t\t\tprovider: "stripe",
6994
- \t\t\t\tdata: {
6995
- \t\t\t\t\tcustomer: "cus_123",
6996
- \t\t\t\t\tsubscription: "sub_123",
6997
- \t\t\t\t},
6998
- \t\t\t}),
6999
- \t\t).resolves.toEqual({ received: true, accountUpdated: true });
7000
- \t\tawait expect(
7001
- \t\t\tharness.billing.findByTenantId("tenant_example"),
7002
- \t\t).resolves.toMatchObject({
7003
- \t\t\tstatus: "past_due",
7004
- \t\t\tlastEventId: "evt_invoice_failed",
7005
- \t\t});
7006
- \t});
7007
-
7008
- \tit("preserves pending cancellation when an invoice succeeds", async () => {
7009
- \t\tconst harness = createHarness();
7010
- \t\tawait harness.billing.save({
7011
- \t\t\ttenantId: "tenant_example",
7012
- \t\t\tprovider: "stripe",
7013
- \t\t\tplan: "team",
7014
- \t\t\tstatus: "active",
7015
- \t\t\tcustomerId: "cus_123",
7016
- \t\t\tsubscriptionId: "sub_123",
7017
- \t\t\tcancelAtPeriodEnd: true,
7018
- \t\t\tupdatedAt: new Date(0).toISOString(),
7019
- \t\t});
7020
-
7021
- \t\tawait expect(
7022
- \t\t\tharness.tester.run(handlePaymentWebhookUseCase, {
7023
- \t\t\t\tid: "evt_invoice_succeeded",
7024
- \t\t\t\ttype: "invoice.payment_succeeded",
7025
- \t\t\t\tprovider: "stripe",
7026
- \t\t\t\tdata: {
7027
- \t\t\t\t\tcustomer: "cus_123",
7028
- \t\t\t\t\tsubscription: "sub_123",
7029
- \t\t\t\t},
7030
- \t\t\t}),
7031
- \t\t).resolves.toEqual({ received: true, accountUpdated: true });
7032
- \t\tawait expect(
7033
- \t\t\tharness.billing.findByTenantId("tenant_example"),
7034
- \t\t).resolves.toMatchObject({
7035
- \t\t\tstatus: "active",
7036
- \t\t\tcancelAtPeriodEnd: true,
7037
- \t\t\tlastEventId: "evt_invoice_succeeded",
7038
- \t\t});
7039
- \t});
7040
- });
7041
- `;
7042
- }
7043
- function billingDrizzleSchemaFile(database) {
7044
- const dialect = drizzleDialects[database];
7045
- const idColumn = database === "mysql"
7046
- ? (columnName) => `varchar("${columnName}", { length: 255 })`
7047
- : (columnName) => `text("${columnName}")`;
7048
- const imports = database === "mysql"
7049
- ? "mysqlTable, uniqueIndex, varchar"
7050
- : `${dialect.tableFunction}, text, uniqueIndex`;
7051
- return `import type { BillingPlan, BillingStatus } from "@/features/billing/schemas";
7052
- import { ${imports} } from "${dialect.columnModule}";
7053
-
7054
- export const billingAccounts = ${dialect.tableFunction}(
7055
- \t"billing_accounts",
7056
- \t{
7057
- \t\ttenantId: ${idColumn("tenant_id")}.primaryKey(),
7058
- \t\tplan: ${idColumn("plan")}.$type<BillingPlan>().notNull(),
7059
- \t\tstatus: ${idColumn("status")}.$type<BillingStatus>().notNull(),
7060
- \t\tprovider: ${idColumn("provider")}.notNull(),
7061
- \t\tcustomerId: ${idColumn("customer_id")},
7062
- \t\tsubscriptionId: ${idColumn("subscription_id")},
7063
- \t\tcurrentPeriodEnd: ${idColumn("current_period_end")},
7064
- \t\tcancelAtPeriodEnd: ${idColumn("cancel_at_period_end")}.$type<"true" | "false">(),
7065
- \t\tcheckoutSessionId: ${idColumn("checkout_session_id")},
7066
- \t\tlastEventId: ${idColumn("last_event_id")},
7067
- \t\tupdatedAt: ${idColumn("updated_at")}.notNull(),
7068
- \t},
7069
- \t(table) => ({
7070
- \t\tproviderCustomerUnique: uniqueIndex(
7071
- \t\t\t"billing_accounts_provider_customer_unique",
7072
- \t\t).on(table.provider, table.customerId),
7073
- \t\tproviderSubscriptionUnique: uniqueIndex(
7074
- \t\t\t"billing_accounts_provider_subscription_unique",
7075
- \t\t).on(table.provider, table.subscriptionId),
7076
- \t}),
7077
- );
7078
- `;
7079
- }
7080
- function billingDrizzleRepositoryFile(config, database) {
7081
- const dialect = drizzleDialects[database];
7082
- const billingPortPath = path.join(config.paths.features, "billing/ports.ts");
7083
- const upsert = database === "mysql"
7084
- ? `\t\t\tconst row = toRow(input);
7085
- \t\t\tawait db
7086
- \t\t\t\t.insert(schema.billingAccounts)
7087
- \t\t\t\t.values(row)
7088
- \t\t\t\t.onDuplicateKeyUpdate({ set: row });
7089
-
7090
- \t\t\tconst [saved] = await db
7091
- \t\t\t\t.select()
7092
- \t\t\t\t.from(schema.billingAccounts)
7093
- \t\t\t\t.where(eq(schema.billingAccounts.tenantId, input.tenantId))
7094
- \t\t\t\t.limit(1);
7095
-
7096
- \t\t\tif (!saved) {
7097
- \t\t\t\tthrow new Error("Billing account save did not return a row.");
7098
- \t\t\t}
7099
-
7100
- \t\t\treturn toBillingAccount(saved);`
7101
- : `\t\t\tconst [row] = await db
7102
- \t\t\t\t.insert(schema.billingAccounts)
7103
- \t\t\t\t.values(toRow(input))
7104
- \t\t\t\t.onConflictDoUpdate({
7105
- \t\t\t\t\ttarget: schema.billingAccounts.tenantId,
7106
- \t\t\t\t\tset: toRow(input),
7107
- \t\t\t\t})
7108
- \t\t\t\t.returning();
7109
-
7110
- \t\t\tif (!row) {
7111
- \t\t\t\tthrow new Error("Billing account save did not return a row.");
7112
- \t\t\t}
7113
-
7114
- \t\t\treturn toBillingAccount(row);`;
7115
- return `import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
7116
- import { and, eq } from "drizzle-orm";
7117
- import type {
7118
- \tBillingAccount,
7119
- \tBillingRepository,
7120
- \tSaveBillingAccountInput,
7121
- } from "${aliasModule(billingPortPath)}";
7122
- import * as schema from "@/infra/db/schema";
7123
-
7124
- type BillingAccountRow = typeof schema.billingAccounts.$inferSelect;
7125
-
7126
- function toBillingAccount(row: BillingAccountRow): BillingAccount {
7127
- \treturn {
7128
- \t\ttenantId: row.tenantId,
7129
- \t\tplan: row.plan,
7130
- \t\tstatus: row.status,
7131
- \t\tprovider: row.provider,
7132
- \t\t...(row.customerId ? { customerId: row.customerId } : {}),
7133
- \t\t...(row.subscriptionId ? { subscriptionId: row.subscriptionId } : {}),
7134
- \t\t...(row.currentPeriodEnd ? { currentPeriodEnd: row.currentPeriodEnd } : {}),
7135
- \t\t...(row.cancelAtPeriodEnd
7136
- \t\t\t? { cancelAtPeriodEnd: row.cancelAtPeriodEnd === "true" }
7137
- \t\t\t: {}),
7138
- \t\t...(row.checkoutSessionId
7139
- \t\t\t? { checkoutSessionId: row.checkoutSessionId }
7140
- \t\t\t: {}),
7141
- \t\t...(row.lastEventId ? { lastEventId: row.lastEventId } : {}),
7142
- \t\tupdatedAt: row.updatedAt,
7143
- \t};
7144
- }
7145
-
7146
- function toRow(input: SaveBillingAccountInput) {
7147
- \treturn {
7148
- \t\ttenantId: input.tenantId,
7149
- \t\tplan: input.plan,
7150
- \t\tstatus: input.status,
7151
- \t\tprovider: input.provider,
7152
- \t\tcustomerId: input.customerId ?? null,
7153
- \t\tsubscriptionId: input.subscriptionId ?? null,
7154
- \t\tcurrentPeriodEnd: input.currentPeriodEnd ?? null,
7155
- \t\tcancelAtPeriodEnd:
7156
- \t\t\tinput.cancelAtPeriodEnd === undefined
7157
- \t\t\t\t? null
7158
- \t\t\t\t: input.cancelAtPeriodEnd
7159
- \t\t\t\t\t? ("true" as const)
7160
- \t\t\t\t\t: ("false" as const),
7161
- \t\tcheckoutSessionId: input.checkoutSessionId ?? null,
7162
- \t\tlastEventId: input.lastEventId ?? null,
7163
- \t\tupdatedAt: input.updatedAt,
7164
- \t};
7165
- }
7166
-
7167
- export function createDrizzleBillingRepository(
7168
- \tdb: ${dialect.dbTypeName}<typeof schema>,
7169
- ): BillingRepository {
7170
- \treturn {
7171
- \t\tasync findByTenantId(tenantId) {
7172
- \t\t\tconst [row] = await db
7173
- \t\t\t\t.select()
7174
- \t\t\t\t.from(schema.billingAccounts)
7175
- \t\t\t\t.where(eq(schema.billingAccounts.tenantId, tenantId))
7176
- \t\t\t\t.limit(1);
7177
-
7178
- \t\t\treturn row ? toBillingAccount(row) : null;
7179
- \t\t},
7180
-
7181
- \t\tasync findByCustomerId(customerId, provider) {
7182
- \t\t\tconst rows = await db
7183
- \t\t\t\t.select()
7184
- \t\t\t\t.from(schema.billingAccounts)
7185
- \t\t\t\t.where(
7186
- \t\t\t\t\tand(
7187
- \t\t\t\t\t\teq(schema.billingAccounts.customerId, customerId),
7188
- \t\t\t\t\t\teq(schema.billingAccounts.provider, provider),
7189
- \t\t\t\t\t),
7190
- \t\t\t\t)
7191
- \t\t\t\t.limit(2);
7192
-
7193
- \t\t\tif (rows.length > 1) {
7194
- \t\t\t\tthrow new Error(
7195
- \t\t\t\t\t\`Multiple billing accounts found for provider "\${provider}" customer "\${customerId}".\`,
7196
- \t\t\t\t);
7197
- \t\t\t}
7198
-
7199
- \t\t\treturn rows[0] ? toBillingAccount(rows[0]) : null;
7200
- \t\t},
7201
-
7202
- \t\tasync save(input) {
7203
- ${upsert}
7204
- \t\t},
7205
- \t};
7206
- }
7207
- `;
7208
- }
7209
- function billingWebhookRouteFile() {
7210
- return `import { createPaymentWebhookRoute } from "@beignet/next";
7211
- import { handlePaymentWebhookUseCase } from "@/features/billing/use-cases";
7212
- import { server } from "@/server";
7213
-
7214
- export const runtime = "nodejs";
7215
-
7216
- export const { POST } = createPaymentWebhookRoute({
7217
- \tserver,
7218
- \thandle: async ({ ctx, event }) => {
7219
- \t\tawait handlePaymentWebhookUseCase.run({ ctx, input: event });
7220
- \t\treturn {
7221
- \t\t\tstatus: 200,
7222
- \t\t\tbody: {
7223
- \t\t\t\treceived: true,
7224
- \t\t\t},
7225
- \t\t};
7226
- \t},
7227
- });
7228
- `;
7229
- }
7230
3598
  function featureUiComponentFile(names, config) {
7231
3599
  const componentPath = featureUiComponentFilePath(names, config);
7232
3600
  const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
@@ -7345,12 +3713,12 @@ function scheduleRouteFile(names, config) {
7345
3713
  return `import { createScheduleRoute } from "@beignet/next";
7346
3714
  import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
7347
3715
  import { env } from "@/lib/env";
7348
- import { server } from "${aliasModule(config.paths.server)}";
3716
+ import { getServer } from "${aliasModule(config.paths.server)}";
7349
3717
 
7350
3718
  export const runtime = "nodejs";
7351
3719
 
7352
3720
  export const { GET, POST } = createScheduleRoute({
7353
- \tserver,
3721
+ \tserver: getServer,
7354
3722
  \tschedules: [${names.scheduleExportName}],
7355
3723
  \tschedule: ${names.scheduleExportName}.name,
7356
3724
  \tsecret: env.CRON_SECRET,