@beignet/cli 0.0.25 → 0.0.27
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 +22 -0
- package/README.md +32 -11
- 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.js +2 -2
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -13
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +51 -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 +575 -0
- package/dist/make/shared.d.ts.map +1 -0
- package/dist/make/shared.js +2166 -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 +71 -3659
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +44 -3
- 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/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.js +4 -4
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +8 -1
- package/dist/templates/base.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 +6 -1
- package/src/choices.ts +28 -0
- package/src/db.ts +2 -2
- package/src/index.ts +170 -13
- package/src/inspect.ts +68 -2
- package/src/make/inbox.ts +2015 -0
- package/src/make/payments.ts +2182 -0
- package/src/make/shared.ts +3550 -0
- package/src/make/tenancy.ts +4809 -0
- package/src/make.ts +380 -5289
- package/src/mcp.ts +65 -2
- package/src/provider-add.ts +926 -0
- package/src/registry-edits.ts +47 -0
- package/src/task.ts +8 -10
- package/src/templates/agents.ts +4 -4
- package/src/templates/base.ts +8 -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, wirePortProviders, writeGeneratedFile, writePlannedGeneratedFile, } from "./make/shared.js";
|
|
6
|
+
import { appendToArrayExpression, appendToNamedArray, 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);
|
|
@@ -1315,171 +1208,6 @@ async function applyOutboxDrainWiring(result, targetDir, plan, options) {
|
|
|
1315
1208
|
...plan.cronSecret.skippedFiles,
|
|
1316
1209
|
]);
|
|
1317
1210
|
}
|
|
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
1211
|
export async function makeUpload(options) {
|
|
1484
1212
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
1485
1213
|
const names = uploadNames(options.name);
|
|
@@ -1626,247 +1354,6 @@ async function makeFeatureUi(options) {
|
|
|
1626
1354
|
skippedFiles,
|
|
1627
1355
|
};
|
|
1628
1356
|
}
|
|
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
1357
|
async function makeFeatureArtifact(options) {
|
|
1871
1358
|
const plannedFiles = await planGeneratedFiles(options.targetDir, options.files, {
|
|
1872
1359
|
force: options.force,
|
|
@@ -1908,44 +1395,6 @@ async function makeFeatureArtifact(options) {
|
|
|
1908
1395
|
skippedFiles,
|
|
1909
1396
|
};
|
|
1910
1397
|
}
|
|
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
1398
|
async function updateResourceWiring(targetDir, names, config, persistenceOrOptions, maybeOptions) {
|
|
1950
1399
|
const persistence = typeof persistenceOrOptions === "string"
|
|
1951
1400
|
? persistenceOrOptions
|
|
@@ -2009,1265 +1458,18 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
2009
1458
|
updated.add(routesFile);
|
|
2010
1459
|
return [...updated];
|
|
2011
1460
|
}
|
|
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;
|
|
1461
|
+
function typeHasTopLevelProperty(source, typeName, propertyName) {
|
|
1462
|
+
const body = typeBodySource(source, typeName);
|
|
1463
|
+
return body !== undefined && hasTopLevelObjectProperty(body, propertyName);
|
|
2084
1464
|
}
|
|
2085
|
-
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
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);
|
|
3262
|
-
}
|
|
3263
|
-
function useCaseNames(input) {
|
|
3264
|
-
const parts = input
|
|
3265
|
-
.trim()
|
|
3266
|
-
.split("/")
|
|
3267
|
-
.map((part) => part.trim())
|
|
3268
|
-
.filter(Boolean);
|
|
3269
|
-
if (parts.length !== 2) {
|
|
3270
|
-
throw new Error("Use case name must use feature/action format, for example projects/archive-project.");
|
|
1465
|
+
function useCaseNames(input) {
|
|
1466
|
+
const parts = input
|
|
1467
|
+
.trim()
|
|
1468
|
+
.split("/")
|
|
1469
|
+
.map((part) => part.trim())
|
|
1470
|
+
.filter(Boolean);
|
|
1471
|
+
if (parts.length !== 2) {
|
|
1472
|
+
throw new Error("Use case name must use feature/action format, for example projects/archive-project.");
|
|
3271
1473
|
}
|
|
3272
1474
|
const feature = identifierNames(parts[0], "Use case feature");
|
|
3273
1475
|
const action = identifierNames(parts[1], "Use case action");
|
|
@@ -3289,32 +1491,6 @@ function useCaseNames(input) {
|
|
|
3289
1491
|
kind: inferUseCaseKind(action.kebab),
|
|
3290
1492
|
};
|
|
3291
1493
|
}
|
|
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
1494
|
function policyNames(input) {
|
|
3319
1495
|
const names = identifierNames(input, "Policy name");
|
|
3320
1496
|
const resolvedNames = {
|
|
@@ -3324,32 +1500,6 @@ function policyNames(input) {
|
|
|
3324
1500
|
assertGeneratedIdentifiers(input, "Policy name", [resolvedNames.policyName]);
|
|
3325
1501
|
return resolvedNames;
|
|
3326
1502
|
}
|
|
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
1503
|
function eventNames(input) {
|
|
3354
1504
|
const names = featureArtifactNames(normalizeEventReference(input), "Event");
|
|
3355
1505
|
const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
|
|
@@ -3396,16 +1546,6 @@ function factoryNames(input) {
|
|
|
3396
1546
|
factoryExportName,
|
|
3397
1547
|
};
|
|
3398
1548
|
}
|
|
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
1549
|
function defaultFactoryNamesForSeed(names) {
|
|
3410
1550
|
return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
|
|
3411
1551
|
}
|
|
@@ -3469,127 +1609,12 @@ function normalizeEventReference(input) {
|
|
|
3469
1609
|
return trimmed;
|
|
3470
1610
|
return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
|
|
3471
1611
|
}
|
|
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
1612
|
function inferUseCaseKind(action) {
|
|
3566
1613
|
if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
|
|
3567
1614
|
return "query";
|
|
3568
1615
|
}
|
|
3569
1616
|
return "command";
|
|
3570
1617
|
}
|
|
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
1618
|
function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
|
|
3594
1619
|
auth: false,
|
|
3595
1620
|
tenant: false,
|
|
@@ -3685,54 +1710,6 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
|
|
|
3685
1710
|
}
|
|
3686
1711
|
return files;
|
|
3687
1712
|
}
|
|
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
1713
|
function featureUiComponentFiles(names, config) {
|
|
3737
1714
|
const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
|
|
3738
1715
|
const componentPath = featureUiComponentFilePath(names, config);
|
|
@@ -3894,125 +1871,12 @@ function outboxFiles(config) {
|
|
|
3894
1871
|
},
|
|
3895
1872
|
];
|
|
3896
1873
|
}
|
|
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
1874
|
function drizzleResourceSchemaFilePath(names, config) {
|
|
3913
1875
|
return path.join(infrastructureDir(config), "db", "schema", `${names.pluralKebab}.ts`);
|
|
3914
1876
|
}
|
|
3915
1877
|
function outboxDrizzleSchemaFilePath(config) {
|
|
3916
1878
|
return path.join(infrastructureDir(config), "db", "schema", "outbox-messages.ts");
|
|
3917
1879
|
}
|
|
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
1880
|
function resourceUseCaseDir(names, config) {
|
|
4017
1881
|
return useCaseFeatureDir(names.pluralKebab, config);
|
|
4018
1882
|
}
|
|
@@ -4022,12 +1886,6 @@ function resourceUseCaseIndexPath(names, config) {
|
|
|
4022
1886
|
function resourceSchemaFilePath(names, config) {
|
|
4023
1887
|
return path.join(resourceFeatureDir(names, config), "schemas.ts");
|
|
4024
1888
|
}
|
|
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
1889
|
function resourceTestFilePath(names, config) {
|
|
4032
1890
|
if (usesFeatureOwnedTests(config)) {
|
|
4033
1891
|
return path.join(resourceFeatureDir(names, config), "tests", `${names.pluralKebab}.test.ts`);
|
|
@@ -4046,46 +1904,6 @@ function policyFilePath(names, config) {
|
|
|
4046
1904
|
}
|
|
4047
1905
|
return path.join(config.paths.policies, `${names.kebab}.ts`);
|
|
4048
1906
|
}
|
|
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
1907
|
function uploadRoutePath(config) {
|
|
4090
1908
|
return path.join(config.paths.routes, "uploads", "[uploadName]", "[action]", "route.ts");
|
|
4091
1909
|
}
|
|
@@ -4095,9 +1913,6 @@ function outboxDrainRoutePath(config) {
|
|
|
4095
1913
|
function scheduleRouteFilePath(names, config) {
|
|
4096
1914
|
return path.join(config.paths.routes, "cron", names.feature.kebab, names.artifact.kebab, "route.ts");
|
|
4097
1915
|
}
|
|
4098
|
-
function useCaseFilePath(names, config) {
|
|
4099
|
-
return path.join(useCaseFeatureDir(names.feature.kebab, config), `${names.action.kebab}.ts`);
|
|
4100
|
-
}
|
|
4101
1916
|
function useCaseIndexFile(names, config) {
|
|
4102
1917
|
const filePath = path.join(useCaseFeatureDir(names.feature.kebab, config), "index.ts");
|
|
4103
1918
|
return {
|
|
@@ -4141,161 +1956,16 @@ async function updateFeatureUiIndex(targetDir, file, options) {
|
|
|
4141
1956
|
await writeFile(destination, next);
|
|
4142
1957
|
return "updated";
|
|
4143
1958
|
}
|
|
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)}`;
|
|
4281
|
-
}
|
|
4282
|
-
}
|
|
4283
|
-
if (next === existing)
|
|
4284
|
-
return "skipped";
|
|
4285
|
-
if (!options.dryRun)
|
|
4286
|
-
await writeFile(destination, next);
|
|
4287
|
-
return "updated";
|
|
4288
|
-
}
|
|
4289
1959
|
async function updateTaskRegistry(targetDir, names, config, options) {
|
|
4290
1960
|
const destination = path.join(targetDir, config.paths.tasks);
|
|
4291
1961
|
const existing = await readOptionalFile(destination);
|
|
4292
1962
|
const featureTasksPath = path.join(featureArtifactDir(names, "tasks", config), "index.ts");
|
|
4293
1963
|
const registryName = `${names.featureSingularCamel}Tasks`;
|
|
4294
1964
|
const importLine = `import { ${registryName} } from "${relativeModule(config.paths.tasks, featureTasksPath)}";`;
|
|
4295
|
-
const defineTasksImport = `import { defineTasks } from "@beignet/core/tasks";`;
|
|
1965
|
+
const defineTasksImport = `import { defineTasks, type TaskRunContextArgs } from "@beignet/core/tasks";`;
|
|
4296
1966
|
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
4297
1967
|
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
4298
|
-
const serverImport = `import {
|
|
1968
|
+
const serverImport = `import { getServer } from "${relativeModule(config.paths.tasks, config.paths.server)}";`;
|
|
4299
1969
|
const registryEntry = `...${registryName}`;
|
|
4300
1970
|
if (existing === undefined) {
|
|
4301
1971
|
const content = `${defineTasksImport}
|
|
@@ -4308,13 +1978,20 @@ export const tasks = defineTasks([
|
|
|
4308
1978
|
\t${registryEntry},
|
|
4309
1979
|
] as const);
|
|
4310
1980
|
|
|
4311
|
-
export async function createTaskContext(
|
|
1981
|
+
export async function createTaskContext(
|
|
1982
|
+
\targs?: TaskRunContextArgs,
|
|
1983
|
+
): Promise<AppContext> {
|
|
1984
|
+
\tconst server = await getServer();
|
|
4312
1985
|
\treturn server.createServiceContext({
|
|
1986
|
+
\t\t// \`--tenant\` flows through TaskRunContextArgs; the starter has no tenant
|
|
1987
|
+
\t\t// lookup, so the raw value scopes the service context directly.
|
|
1988
|
+
\t\ttenantId: args?.tenant,
|
|
4313
1989
|
\t\tactor: createServiceActor("beignet-cli"),
|
|
4314
1990
|
\t});
|
|
4315
1991
|
}
|
|
4316
1992
|
|
|
4317
1993
|
export async function stopTaskContext(): Promise<void> {
|
|
1994
|
+
\tconst server = await getServer();
|
|
4318
1995
|
\tawait server.stop();
|
|
4319
1996
|
}
|
|
4320
1997
|
`;
|
|
@@ -4325,7 +2002,9 @@ export async function stopTaskContext(): Promise<void> {
|
|
|
4325
2002
|
return "created";
|
|
4326
2003
|
}
|
|
4327
2004
|
let next = existing;
|
|
4328
|
-
|
|
2005
|
+
// Match any `defineTasks` import shape so registries that also import
|
|
2006
|
+
// `TaskRunContextArgs` (or predate it) are not given a duplicate import.
|
|
2007
|
+
if (!(next.includes("defineTasks") && next.includes('"@beignet/core/tasks"'))) {
|
|
4329
2008
|
next = insertAfterImports(next, defineTasksImport);
|
|
4330
2009
|
}
|
|
4331
2010
|
if (!next.includes(importLine)) {
|
|
@@ -4351,61 +2030,30 @@ async function updateScheduleRegistry(targetDir, names, config, options) {
|
|
|
4351
2030
|
const featureSchedulesPath = path.join(featureArtifactDir(names, "schedules", config), "index.ts");
|
|
4352
2031
|
const registryName = `${names.featureSingularCamel}Schedules`;
|
|
4353
2032
|
const importLine = `import { ${registryName} } from "${aliasModule(featureSchedulesPath)}";`;
|
|
4354
|
-
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
4355
|
-
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
4356
|
-
const serverImport = `import {
|
|
4357
|
-
const registryEntry = `...${registryName}`;
|
|
4358
|
-
if (existing === undefined) {
|
|
4359
|
-
const content = `${serviceActorImport}
|
|
4360
|
-
${appContextImport}
|
|
4361
|
-
${importLine}
|
|
4362
|
-
${serverImport}
|
|
4363
|
-
|
|
4364
|
-
export const schedules = [${registryEntry}] as const;
|
|
4365
|
-
|
|
4366
|
-
export async function createScheduleContext(): Promise<AppContext> {
|
|
4367
|
-
\treturn server.createServiceContext({
|
|
4368
|
-
\t\tactor: createServiceActor("beignet-schedule"),
|
|
4369
|
-
\t});
|
|
4370
|
-
}
|
|
4371
|
-
|
|
4372
|
-
export async function stopScheduleContext(): Promise<void> {
|
|
4373
|
-
\tawait server.stop();
|
|
4374
|
-
}
|
|
4375
|
-
`;
|
|
4376
|
-
if (!options.dryRun) {
|
|
4377
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
4378
|
-
await writeFile(destination, content);
|
|
4379
|
-
}
|
|
4380
|
-
return "created";
|
|
4381
|
-
}
|
|
4382
|
-
let next = existing;
|
|
4383
|
-
if (!next.includes(importLine)) {
|
|
4384
|
-
next = insertAfterImports(next, importLine);
|
|
4385
|
-
}
|
|
4386
|
-
const registry = arrayInitializerInfo(next, "schedules");
|
|
4387
|
-
if (!registry) {
|
|
4388
|
-
next = `${next.trimEnd()}\n\nexport const schedules = [${registryEntry}] as const;\n`;
|
|
4389
|
-
}
|
|
4390
|
-
else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
4391
|
-
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
4392
|
-
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
|
|
4393
|
-
}
|
|
4394
|
-
if (next === existing)
|
|
4395
|
-
return "skipped";
|
|
4396
|
-
if (!options.dryRun)
|
|
4397
|
-
await writeFile(destination, next);
|
|
4398
|
-
return "updated";
|
|
4399
|
-
}
|
|
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"))}";`;
|
|
2033
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
2034
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
2035
|
+
const serverImport = `import { getServer } from "${relativeModule(config.paths.schedules, config.paths.server)}";`;
|
|
4406
2036
|
const registryEntry = `...${registryName}`;
|
|
4407
2037
|
if (existing === undefined) {
|
|
4408
|
-
const content =
|
|
2038
|
+
const content = `${serviceActorImport}
|
|
2039
|
+
${appContextImport}
|
|
2040
|
+
${importLine}
|
|
2041
|
+
${serverImport}
|
|
2042
|
+
|
|
2043
|
+
export const schedules = [${registryEntry}] as const;
|
|
2044
|
+
|
|
2045
|
+
export async function createScheduleContext(): Promise<AppContext> {
|
|
2046
|
+
\tconst server = await getServer();
|
|
2047
|
+
\treturn server.createServiceContext({
|
|
2048
|
+
\t\tactor: createServiceActor("beignet-schedule"),
|
|
2049
|
+
\t});
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
export async function stopScheduleContext(): Promise<void> {
|
|
2053
|
+
\tconst server = await getServer();
|
|
2054
|
+
\tawait server.stop();
|
|
2055
|
+
}
|
|
2056
|
+
`;
|
|
4409
2057
|
if (!options.dryRun) {
|
|
4410
2058
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
4411
2059
|
await writeFile(destination, content);
|
|
@@ -4416,9 +2064,9 @@ async function updateSeedEntrypoint(targetDir, names, config, options) {
|
|
|
4416
2064
|
if (!next.includes(importLine)) {
|
|
4417
2065
|
next = insertAfterImports(next, importLine);
|
|
4418
2066
|
}
|
|
4419
|
-
const registry = arrayInitializerInfo(next, "
|
|
2067
|
+
const registry = arrayInitializerInfo(next, "schedules");
|
|
4420
2068
|
if (!registry) {
|
|
4421
|
-
next = `${next.trimEnd()}\n\
|
|
2069
|
+
next = `${next.trimEnd()}\n\nexport const schedules = [${registryEntry}] as const;\n`;
|
|
4422
2070
|
}
|
|
4423
2071
|
else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
4424
2072
|
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
@@ -4506,22 +2154,6 @@ async function applyOutboxRegistryUpdate(result, targetDir, names, kind, config,
|
|
|
4506
2154
|
result.files.push(config.paths.outbox);
|
|
4507
2155
|
}
|
|
4508
2156
|
}
|
|
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
2157
|
function schemasFile(names, mode, options) {
|
|
4526
2158
|
return `import { z } from "zod";
|
|
4527
2159
|
|
|
@@ -5924,26 +3556,6 @@ export const ${names.uploadExportName} = defineUpload<
|
|
|
5924
3556
|
});
|
|
5925
3557
|
`;
|
|
5926
3558
|
}
|
|
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
3559
|
function uploadRouteFile(names, config) {
|
|
5948
3560
|
const registryName = `${names.featureSingularCamel}Uploads`;
|
|
5949
3561
|
return `import {
|
|
@@ -5953,19 +3565,21 @@ function uploadRouteFile(names, config) {
|
|
|
5953
3565
|
} from "@beignet/core/uploads";
|
|
5954
3566
|
import { createUploadRoute } from "@beignet/next";
|
|
5955
3567
|
import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "uploads", config), "index.ts"))}";
|
|
5956
|
-
import {
|
|
3568
|
+
import { getServer } from "${aliasModule(config.paths.server)}";
|
|
5957
3569
|
|
|
5958
3570
|
const uploadRegistry = defineUploads({
|
|
5959
3571
|
\t${names.featureSingularCamel}: ${registryName},
|
|
5960
3572
|
});
|
|
5961
3573
|
|
|
5962
|
-
const
|
|
5963
|
-
\
|
|
5964
|
-
\tctx: () => server.createContextFromNext(),
|
|
5965
|
-
\tstorage: server.ports.storage,
|
|
5966
|
-
});
|
|
3574
|
+
export const { POST } = createUploadRoute(async () => {
|
|
3575
|
+
\tconst server = await getServer();
|
|
5967
3576
|
|
|
5968
|
-
|
|
3577
|
+
\treturn createUploadRouter({
|
|
3578
|
+
\t\tuploads: uploadsFromRegistry(uploadRegistry),
|
|
3579
|
+
\t\tctx: () => server.createContextFromNext(),
|
|
3580
|
+
\t\tstorage: server.ports.storage,
|
|
3581
|
+
\t});
|
|
3582
|
+
});
|
|
5969
3583
|
`;
|
|
5970
3584
|
}
|
|
5971
3585
|
function outboxRegistryFile(config, registration) {
|
|
@@ -5980,7 +3594,7 @@ function outboxRegistryFile(config, registration) {
|
|
|
5980
3594
|
return `import { defineOutboxRegistry } from "@beignet/core/outbox";
|
|
5981
3595
|
import { createServiceActor } from "@beignet/core/ports";
|
|
5982
3596
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
5983
|
-
${featureImport}import {
|
|
3597
|
+
${featureImport}import { getServer } from "${relativeModule(config.paths.outbox, config.paths.server)}";
|
|
5984
3598
|
|
|
5985
3599
|
export const outboxRegistry = defineOutboxRegistry({
|
|
5986
3600
|
\tevents: [${eventEntry}],
|
|
@@ -5988,12 +3602,14 @@ export const outboxRegistry = defineOutboxRegistry({
|
|
|
5988
3602
|
});
|
|
5989
3603
|
|
|
5990
3604
|
export async function createOutboxDrainContext(): Promise<AppContext> {
|
|
3605
|
+
\tconst server = await getServer();
|
|
5991
3606
|
\treturn server.createServiceContext({
|
|
5992
3607
|
\t\tactor: createServiceActor("beignet-outbox"),
|
|
5993
3608
|
\t});
|
|
5994
3609
|
}
|
|
5995
3610
|
|
|
5996
3611
|
export async function stopOutboxDrainContext(): Promise<void> {
|
|
3612
|
+
\tconst server = await getServer();
|
|
5997
3613
|
\tawait server.stop();
|
|
5998
3614
|
}
|
|
5999
3615
|
`;
|
|
@@ -6001,13 +3617,13 @@ export async function stopOutboxDrainContext(): Promise<void> {
|
|
|
6001
3617
|
function outboxDrainRouteFile(config) {
|
|
6002
3618
|
return `import { createOutboxDrainRoute } from "@beignet/next";
|
|
6003
3619
|
import { env } from "@/lib/env";
|
|
6004
|
-
import {
|
|
3620
|
+
import { getServer } from "${aliasModule(config.paths.server)}";
|
|
6005
3621
|
import { outboxRegistry } from "${aliasModule(config.paths.outbox)}";
|
|
6006
3622
|
|
|
6007
3623
|
export const runtime = "nodejs";
|
|
6008
3624
|
|
|
6009
3625
|
export const { GET, POST } = createOutboxDrainRoute({
|
|
6010
|
-
\tserver,
|
|
3626
|
+
\tserver: getServer,
|
|
6011
3627
|
\tregistry: outboxRegistry,
|
|
6012
3628
|
\tsecret: env.CRON_SECRET,
|
|
6013
3629
|
\tbatchSize: 100,
|
|
@@ -6023,1210 +3639,6 @@ function outboxFeatureRegistry(names, kind, config) {
|
|
|
6023
3639
|
importLine: `import { ${name} } from "${aliasModule(featureIndexPath)}";`,
|
|
6024
3640
|
};
|
|
6025
3641
|
}
|
|
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
3642
|
function featureUiComponentFile(names, config) {
|
|
7231
3643
|
const componentPath = featureUiComponentFilePath(names, config);
|
|
7232
3644
|
const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
|
|
@@ -7345,12 +3757,12 @@ function scheduleRouteFile(names, config) {
|
|
|
7345
3757
|
return `import { createScheduleRoute } from "@beignet/next";
|
|
7346
3758
|
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
7347
3759
|
import { env } from "@/lib/env";
|
|
7348
|
-
import {
|
|
3760
|
+
import { getServer } from "${aliasModule(config.paths.server)}";
|
|
7349
3761
|
|
|
7350
3762
|
export const runtime = "nodejs";
|
|
7351
3763
|
|
|
7352
3764
|
export const { GET, POST } = createScheduleRoute({
|
|
7353
|
-
\tserver,
|
|
3765
|
+
\tserver: getServer,
|
|
7354
3766
|
\tschedules: [${names.scheduleExportName}],
|
|
7355
3767
|
\tschedule: ${names.scheduleExportName}.name,
|
|
7356
3768
|
\tsecret: env.CRON_SECRET,
|