@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.
- package/CHANGELOG.md +26 -0
- package/README.md +59 -15
- package/dist/check.d.ts +61 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +164 -0
- package/dist/check.js.map +1 -0
- package/dist/choices.d.ts +8 -0
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +14 -0
- package/dist/choices.js.map +1 -1
- package/dist/db.d.ts +8 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +4 -4
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +156 -21
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +121 -3
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts +63 -0
- package/dist/make/inbox.d.ts.map +1 -0
- package/dist/make/inbox.js +1747 -0
- package/dist/make/inbox.js.map +1 -0
- package/dist/make/payments.d.ts +61 -0
- package/dist/make/payments.d.ts.map +1 -0
- package/dist/make/payments.js +1900 -0
- package/dist/make/payments.js.map +1 -0
- package/dist/make/shared.d.ts +592 -0
- package/dist/make/shared.d.ts.map +1 -0
- package/dist/make/shared.js +2233 -0
- package/dist/make/shared.js.map +1 -0
- package/dist/make/tenancy.d.ts +101 -0
- package/dist/make/tenancy.d.ts.map +1 -0
- package/dist/make/tenancy.js +4409 -0
- package/dist/make/tenancy.js.map +1 -0
- package/dist/make.d.ts +8 -44
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +57 -3689
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +45 -4
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts +19 -0
- package/dist/provider-add.d.ts.map +1 -0
- package/dist/provider-add.js +691 -0
- package/dist/provider-add.js.map +1 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +77 -1
- package/dist/provider-audit.js.map +1 -1
- package/dist/registry-edits.d.ts +9 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +32 -0
- package/dist/registry-edits.js.map +1 -1
- package/dist/task.d.ts +2 -0
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +2 -0
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +29 -11
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +42 -12
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +7 -1
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +44 -37
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +5 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +5 -0
- package/dist/templates/shared.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +1 -1
- package/src/check.ts +246 -0
- package/src/choices.ts +28 -0
- package/src/db.ts +4 -4
- package/src/index.ts +232 -21
- package/src/inspect.ts +168 -2
- package/src/make/inbox.ts +2015 -0
- package/src/make/payments.ts +2182 -0
- package/src/make/shared.ts +3638 -0
- package/src/make/tenancy.ts +4809 -0
- package/src/make.ts +388 -5355
- package/src/mcp.ts +66 -3
- package/src/provider-add.ts +926 -0
- package/src/provider-audit.ts +95 -3
- package/src/registry-edits.ts +47 -0
- package/src/task.ts +8 -10
- package/src/templates/agents.ts +29 -11
- package/src/templates/base.ts +43 -12
- package/src/templates/db/sqlite.ts +7 -1
- package/src/templates/server.ts +44 -37
- package/src/templates/shared.ts +5 -0
- 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,
|
|
5
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
2013
|
-
const
|
|
2014
|
-
|
|
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 (
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
5963
|
-
\
|
|
5964
|
-
\tctx: () => server.createContextFromNext(),
|
|
5965
|
-
\tstorage: server.ports.storage,
|
|
5966
|
-
});
|
|
3530
|
+
export const { POST } = createUploadRoute(async () => {
|
|
3531
|
+
\tconst server = await getServer();
|
|
5967
3532
|
|
|
5968
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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,
|