@beignet/cli 0.0.31 → 0.0.33
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 +107 -0
- package/README.md +70 -17
- package/dist/check.d.ts +2 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +16 -0
- package/dist/check.js.map +1 -1
- package/dist/choices.d.ts +22 -2
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +62 -0
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +6 -2
- package/dist/create-prompts.d.ts.map +1 -1
- package/dist/create-prompts.js +1 -1
- package/dist/create-prompts.js.map +1 -1
- package/dist/db.d.ts +5 -5
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +5 -5
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -27
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +5 -0
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +805 -62
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +5 -3
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +58 -29
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +12 -3
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +52 -11
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.d.ts.map +1 -1
- package/dist/make/tenancy.js +6 -4
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +136 -65
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +114 -1
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +190 -0
- package/dist/outbox.js.map +1 -1
- package/dist/preflight.d.ts +90 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +423 -0
- package/dist/preflight.js.map +1 -0
- package/dist/provider-add.d.ts +10 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +255 -16
- package/dist/provider-add.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +2 -3
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +6 -3
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +5 -11
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +2 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +10 -5
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +5 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +13 -1
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.d.ts +5 -0
- package/dist/templates/testing.d.ts.map +1 -0
- package/dist/templates/testing.js +542 -0
- package/dist/templates/testing.js.map +1 -0
- package/dist/templates/todos.js +2 -2
- package/package.json +3 -2
- package/skills/app-structure/SKILL.md +12 -4
- package/src/check.ts +23 -0
- package/src/choices.ts +84 -0
- package/src/config.ts +4 -0
- package/src/create-prompts.ts +8 -2
- package/src/db.ts +12 -12
- package/src/index.ts +592 -37
- package/src/inspect.ts +1253 -156
- package/src/make/inbox.ts +5 -3
- package/src/make/payments.ts +58 -29
- package/src/make/shared.ts +87 -13
- package/src/make/tenancy.ts +6 -4
- package/src/make.ts +179 -65
- package/src/outbox.ts +363 -0
- package/src/preflight.ts +596 -0
- package/src/provider-add.ts +270 -16
- package/src/task.ts +2 -3
- package/src/templates/agents.ts +6 -3
- package/src/templates/base.ts +5 -13
- package/src/templates/index.ts +2 -0
- package/src/templates/server.ts +10 -5
- package/src/templates/shared.ts +15 -1
- package/src/templates/testing.ts +545 -0
- package/src/templates/todos.ts +2 -2
- package/src/test-helpers/generated-app.ts +10 -6
package/src/inspect.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
4
4
|
import { createPainter } from "./ansi.js";
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
type GithubAnnotationSeverity,
|
|
21
21
|
} from "./github-annotations.js";
|
|
22
22
|
import {
|
|
23
|
+
createListenersRegistrySource,
|
|
24
|
+
listenersFilePath,
|
|
23
25
|
providersFilePath,
|
|
26
|
+
updateListenersRegistrySource,
|
|
24
27
|
wireListenersProviderSource,
|
|
25
28
|
} from "./make/shared.js";
|
|
26
29
|
import {
|
|
@@ -49,6 +52,12 @@ import {
|
|
|
49
52
|
matchingDelimiterIndex,
|
|
50
53
|
outboxRegistryValueInfo,
|
|
51
54
|
} from "./registry-edits.js";
|
|
55
|
+
import {
|
|
56
|
+
currentGeneratedPackageScripts,
|
|
57
|
+
externalVersions,
|
|
58
|
+
legacyGeneratedPackageScripts,
|
|
59
|
+
} from "./templates/shared.js";
|
|
60
|
+
import { testSupportTemplateFiles } from "./templates/testing.js";
|
|
52
61
|
import { getCliVersion } from "./version.js";
|
|
53
62
|
|
|
54
63
|
type InspectAppOptions = {
|
|
@@ -246,7 +255,7 @@ export async function applyDoctorFixes(
|
|
|
246
255
|
const matchedRoutes = matchRoutes(contracts, routeExports);
|
|
247
256
|
const fixes: InspectFix[] = [];
|
|
248
257
|
|
|
249
|
-
const packageFix = await
|
|
258
|
+
const packageFix = await fixPackageScripts(targetDir, files, convention);
|
|
250
259
|
if (packageFix) fixes.push(packageFix);
|
|
251
260
|
|
|
252
261
|
const routeGroupFix = await fixUnregisteredRouteGroups(
|
|
@@ -398,7 +407,11 @@ export function formatDoctor(
|
|
|
398
407
|
return lines.join("\n");
|
|
399
408
|
}
|
|
400
409
|
|
|
401
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Doctor diagnostic codes that describe production hardening concerns.
|
|
412
|
+
* `beignet preflight` promotes these to gate failures.
|
|
413
|
+
*/
|
|
414
|
+
export const productionHardeningDiagnosticCodes = new Set([
|
|
402
415
|
"BEIGNET_AUTH_ROUTE_MISSING",
|
|
403
416
|
"BEIGNET_AUTH_TRUSTED_ORIGINS_MISSING",
|
|
404
417
|
"BEIGNET_BETTER_AUTH_SECRET_PLACEHOLDER",
|
|
@@ -414,6 +427,7 @@ const productionHardeningDiagnosticCodes = new Set([
|
|
|
414
427
|
"BEIGNET_PROVIDER_ENV_MISSING",
|
|
415
428
|
"BEIGNET_PROVIDER_TABLE_MISSING",
|
|
416
429
|
"BEIGNET_READINESS_ROUTE_MISSING",
|
|
430
|
+
"BEIGNET_SECURITY_HEADERS_MISSING",
|
|
417
431
|
"BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
418
432
|
"BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
|
|
419
433
|
"BEIGNET_UPLOAD_SIZE_LIMIT_MISSING",
|
|
@@ -434,7 +448,7 @@ function productionHardeningChecklist(
|
|
|
434
448
|
"Secrets and provider credentials are unique per environment, validated at startup, never committed, and never logged.",
|
|
435
449
|
"Auth routes, tenant resolution, authorization policies, and trusted origins derive authority from verified sessions, API keys, or trusted gateway metadata.",
|
|
436
450
|
"Devtools, OpenAPI, cron, webhooks, and operational routes have intentional exposure and app-owned authorization.",
|
|
437
|
-
"CORS origins, proxy IP trust, rate-limit keys, and request body limits match the production host topology.",
|
|
451
|
+
"Security headers, CORS origins, proxy IP trust, rate-limit keys, and request body limits match the production host topology.",
|
|
438
452
|
"Uploads and storage define max sizes, authorization or explicit public access, object visibility, safe content headers, and short direct-upload expirations.",
|
|
439
453
|
"Provider-backed dependencies have bounded readiness checks, worker shutdown behavior, webhook secrets, and least-privilege credentials.",
|
|
440
454
|
];
|
|
@@ -1428,6 +1442,12 @@ async function inspectDiagnostics(
|
|
|
1428
1442
|
config,
|
|
1429
1443
|
convention,
|
|
1430
1444
|
)),
|
|
1445
|
+
...(await inspectRuntimeManifestDrift(
|
|
1446
|
+
targetDir,
|
|
1447
|
+
files,
|
|
1448
|
+
config,
|
|
1449
|
+
convention,
|
|
1450
|
+
)),
|
|
1431
1451
|
...(await inspectResourceSlices(
|
|
1432
1452
|
targetDir,
|
|
1433
1453
|
files,
|
|
@@ -1965,6 +1985,7 @@ const readinessRelevantProviderPackages = new Set([
|
|
|
1965
1985
|
"@beignet/provider-auth-better-auth",
|
|
1966
1986
|
"@beignet/provider-db-drizzle",
|
|
1967
1987
|
"@beignet/provider-error-reporting-sentry",
|
|
1988
|
+
"@beignet/provider-event-bus-redis",
|
|
1968
1989
|
"@beignet/provider-flags-openfeature",
|
|
1969
1990
|
"@beignet/provider-jobs-bullmq",
|
|
1970
1991
|
"@beignet/provider-jobs-inngest",
|
|
@@ -1973,7 +1994,7 @@ const readinessRelevantProviderPackages = new Set([
|
|
|
1973
1994
|
"@beignet/provider-mail-smtp",
|
|
1974
1995
|
"@beignet/provider-payments-stripe",
|
|
1975
1996
|
"@beignet/provider-rate-limit-upstash",
|
|
1976
|
-
"@beignet/provider-redis",
|
|
1997
|
+
"@beignet/provider-cache-redis",
|
|
1977
1998
|
"@beignet/provider-search-meilisearch",
|
|
1978
1999
|
"@beignet/provider-storage-s3",
|
|
1979
2000
|
]);
|
|
@@ -1999,11 +2020,20 @@ async function inspectProductionReadiness(
|
|
|
1999
2020
|
path.dirname(config.paths.infrastructurePorts),
|
|
2000
2021
|
);
|
|
2001
2022
|
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
2023
|
+
let hasSecurityHeadersHook = false;
|
|
2002
2024
|
|
|
2003
2025
|
for (const file of files) {
|
|
2004
2026
|
if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
|
|
2005
2027
|
|
|
2006
2028
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2029
|
+
const routeServerOrInfra = isRouteServerOrInfraFile(file, config);
|
|
2030
|
+
|
|
2031
|
+
if (
|
|
2032
|
+
routeServerOrInfra &&
|
|
2033
|
+
containsCallExpression(source, "createSecurityHeadersHooks")
|
|
2034
|
+
) {
|
|
2035
|
+
hasSecurityHeadersHook = true;
|
|
2036
|
+
}
|
|
2007
2037
|
|
|
2008
2038
|
if (
|
|
2009
2039
|
containsCallExpression(source, "createDevtoolsRoute") &&
|
|
@@ -2059,10 +2089,7 @@ async function inspectProductionReadiness(
|
|
|
2059
2089
|
});
|
|
2060
2090
|
}
|
|
2061
2091
|
|
|
2062
|
-
if (
|
|
2063
|
-
isRouteServerOrInfraFile(file, config) &&
|
|
2064
|
-
containsRequestTenantHeaderAuthority(source)
|
|
2065
|
-
) {
|
|
2092
|
+
if (routeServerOrInfra && containsRequestTenantHeaderAuthority(source)) {
|
|
2066
2093
|
diagnostics.push({
|
|
2067
2094
|
severity: "warning",
|
|
2068
2095
|
code: "BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
@@ -2072,7 +2099,7 @@ async function inspectProductionReadiness(
|
|
|
2072
2099
|
}
|
|
2073
2100
|
|
|
2074
2101
|
if (
|
|
2075
|
-
|
|
2102
|
+
routeServerOrInfra &&
|
|
2076
2103
|
containsCallExpression(source, "createCorsHooks") &&
|
|
2077
2104
|
/\bcredentials\s*:\s*true\b/.test(source) &&
|
|
2078
2105
|
/(?:\borigin\b|\borigins\b)\s*:\s*(?:["']\*["']|\[\s*["']\*["'])/.test(
|
|
@@ -2088,6 +2115,15 @@ async function inspectProductionReadiness(
|
|
|
2088
2115
|
}
|
|
2089
2116
|
}
|
|
2090
2117
|
|
|
2118
|
+
if (files.includes(config.paths.server) && !hasSecurityHeadersHook) {
|
|
2119
|
+
diagnostics.push({
|
|
2120
|
+
severity: "warning",
|
|
2121
|
+
code: "BEIGNET_SECURITY_HEADERS_MISSING",
|
|
2122
|
+
file: config.paths.server,
|
|
2123
|
+
message: `${config.paths.server} does not install createSecurityHeadersHooks(...). Add the hook or equivalent platform-managed headers before exposing browser traffic.`,
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2091
2127
|
for (const file of configFiles) {
|
|
2092
2128
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2093
2129
|
|
|
@@ -2182,7 +2218,7 @@ async function inspectProductionReadiness(
|
|
|
2182
2218
|
severity: "warning",
|
|
2183
2219
|
code: "BEIGNET_PROVIDER_TABLE_MISSING",
|
|
2184
2220
|
file: schemaDir,
|
|
2185
|
-
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema
|
|
2221
|
+
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema sync, include it in your Drizzle schema/migrations, or remove the provider registration.`,
|
|
2186
2222
|
});
|
|
2187
2223
|
}
|
|
2188
2224
|
}
|
|
@@ -2229,7 +2265,7 @@ async function inspectProductionReadiness(
|
|
|
2229
2265
|
severity: "warning",
|
|
2230
2266
|
code: "BEIGNET_PROVIDER_TABLE_MISSING",
|
|
2231
2267
|
file: schemaDir,
|
|
2232
|
-
message: `${provider.packageName} is installed, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema
|
|
2268
|
+
message: `${provider.packageName} is installed, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema sync, include it in your Drizzle schema/migrations, or remove the unused provider package.`,
|
|
2233
2269
|
});
|
|
2234
2270
|
}
|
|
2235
2271
|
}
|
|
@@ -3482,6 +3518,92 @@ async function inspectWorkflowRegistrationDrift(
|
|
|
3482
3518
|
return diagnostics;
|
|
3483
3519
|
}
|
|
3484
3520
|
|
|
3521
|
+
type RuntimeManifestIdentifierSets = {
|
|
3522
|
+
file: string;
|
|
3523
|
+
listeners: Set<string>;
|
|
3524
|
+
schedules: Set<string>;
|
|
3525
|
+
tasks: Set<string>;
|
|
3526
|
+
events: Set<string>;
|
|
3527
|
+
jobs: Set<string>;
|
|
3528
|
+
};
|
|
3529
|
+
|
|
3530
|
+
async function inspectRuntimeManifestDrift(
|
|
3531
|
+
targetDir: string,
|
|
3532
|
+
files: string[],
|
|
3533
|
+
config: ResolvedBeignetConfig,
|
|
3534
|
+
convention: InspectConvention,
|
|
3535
|
+
): Promise<InspectDiagnostic[]> {
|
|
3536
|
+
if (!convention.resourceGenerator) return [];
|
|
3537
|
+
|
|
3538
|
+
const manifest = await readRuntimeManifestIdentifiers(targetDir, files);
|
|
3539
|
+
if (!manifest) return [];
|
|
3540
|
+
|
|
3541
|
+
const registries = await readFeatureWorkflowRegistries(
|
|
3542
|
+
targetDir,
|
|
3543
|
+
files,
|
|
3544
|
+
config,
|
|
3545
|
+
);
|
|
3546
|
+
const diagnostics: InspectDiagnostic[] = [];
|
|
3547
|
+
|
|
3548
|
+
const checks: readonly {
|
|
3549
|
+
kind: WorkflowRegistryKind;
|
|
3550
|
+
registered: Set<string>;
|
|
3551
|
+
code: string;
|
|
3552
|
+
label: string;
|
|
3553
|
+
}[] = [
|
|
3554
|
+
{
|
|
3555
|
+
kind: "listeners",
|
|
3556
|
+
registered: manifest.listeners,
|
|
3557
|
+
code: "BEIGNET_RUNTIME_MANIFEST_LISTENER_MISSING",
|
|
3558
|
+
label: "listeners",
|
|
3559
|
+
},
|
|
3560
|
+
{
|
|
3561
|
+
kind: "schedules",
|
|
3562
|
+
registered: manifest.schedules,
|
|
3563
|
+
code: "BEIGNET_RUNTIME_MANIFEST_SCHEDULE_MISSING",
|
|
3564
|
+
label: "schedules",
|
|
3565
|
+
},
|
|
3566
|
+
{
|
|
3567
|
+
kind: "tasks",
|
|
3568
|
+
registered: manifest.tasks,
|
|
3569
|
+
code: "BEIGNET_RUNTIME_MANIFEST_TASK_MISSING",
|
|
3570
|
+
label: "tasks",
|
|
3571
|
+
},
|
|
3572
|
+
{
|
|
3573
|
+
kind: "events",
|
|
3574
|
+
registered: manifest.events,
|
|
3575
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_EVENT_MISSING",
|
|
3576
|
+
label: "outbox events",
|
|
3577
|
+
},
|
|
3578
|
+
{
|
|
3579
|
+
kind: "jobs",
|
|
3580
|
+
registered: manifest.jobs,
|
|
3581
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_JOB_MISSING",
|
|
3582
|
+
label: "outbox jobs",
|
|
3583
|
+
},
|
|
3584
|
+
];
|
|
3585
|
+
|
|
3586
|
+
for (const check of checks) {
|
|
3587
|
+
const missing = unregisteredWorkflowRegistries(
|
|
3588
|
+
registries.filter((registry) => registry.kind === check.kind),
|
|
3589
|
+
check.registered,
|
|
3590
|
+
);
|
|
3591
|
+
|
|
3592
|
+
for (const { registry, missingMembers } of missing) {
|
|
3593
|
+
for (const member of missingMembers) {
|
|
3594
|
+
diagnostics.push({
|
|
3595
|
+
severity: "warning",
|
|
3596
|
+
code: check.code,
|
|
3597
|
+
file: manifest.file,
|
|
3598
|
+
message: `${workflowArtifactOrigin(registry, member)}, but it is not listed in defineRuntimeManifest(...) ${check.label} in ${manifest.file}, so runtime integrity cannot fail fast if it is omitted from a runtime registry.`,
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
return diagnostics;
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3485
3607
|
function workflowArtifactOrigin(
|
|
3486
3608
|
registry: FeatureWorkflowRegistry,
|
|
3487
3609
|
member: string,
|
|
@@ -3588,7 +3710,7 @@ async function workflowRegistrationDrift(
|
|
|
3588
3710
|
|
|
3589
3711
|
const listenerRegistries = byKind("listeners");
|
|
3590
3712
|
if (listenerRegistries.length > 0) {
|
|
3591
|
-
const wiring = await listenerWiringReferences(targetDir, files);
|
|
3713
|
+
const wiring = await listenerWiringReferences(targetDir, files, config);
|
|
3592
3714
|
drift.listeners = {
|
|
3593
3715
|
unregistered: unregisteredWorkflowRegistries(
|
|
3594
3716
|
listenerRegistries,
|
|
@@ -3704,6 +3826,86 @@ function exportedMemberFiles(
|
|
|
3704
3826
|
return memberFiles;
|
|
3705
3827
|
}
|
|
3706
3828
|
|
|
3829
|
+
async function readRuntimeManifestIdentifiers(
|
|
3830
|
+
targetDir: string,
|
|
3831
|
+
files: string[],
|
|
3832
|
+
): Promise<RuntimeManifestIdentifierSets | undefined> {
|
|
3833
|
+
for (const file of files) {
|
|
3834
|
+
if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
|
|
3835
|
+
if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
|
|
3836
|
+
|
|
3837
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
3838
|
+
if (!source.includes("defineRuntimeManifest")) continue;
|
|
3839
|
+
|
|
3840
|
+
const identifiers = runtimeManifestIdentifiers(source);
|
|
3841
|
+
if (identifiers) {
|
|
3842
|
+
return {
|
|
3843
|
+
file,
|
|
3844
|
+
...identifiers,
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
return undefined;
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
function runtimeManifestIdentifiers(
|
|
3853
|
+
source: string,
|
|
3854
|
+
): Omit<RuntimeManifestIdentifierSets, "file"> | undefined {
|
|
3855
|
+
const call = /\bdefineRuntimeManifest\s*(?:<[^>]*>\s*)?\(/.exec(source);
|
|
3856
|
+
if (!call) return undefined;
|
|
3857
|
+
|
|
3858
|
+
const arg = firstCallArgInfo(source, call.index + call[0].length);
|
|
3859
|
+
if (!arg) return undefined;
|
|
3860
|
+
|
|
3861
|
+
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
3862
|
+
|
|
3863
|
+
return {
|
|
3864
|
+
listeners: identifiersFromPropertyArray(arg.text, "listeners"),
|
|
3865
|
+
schedules: identifiersFromPropertyArray(arg.text, "schedules"),
|
|
3866
|
+
tasks: identifiersFromPropertyArray(arg.text, "tasks"),
|
|
3867
|
+
events: outbox
|
|
3868
|
+
? identifiersFromPropertyArray(outbox.text, "events")
|
|
3869
|
+
: new Set<string>(),
|
|
3870
|
+
jobs: outbox
|
|
3871
|
+
? identifiersFromPropertyArray(outbox.text, "jobs")
|
|
3872
|
+
: new Set<string>(),
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
function identifiersFromPropertyArray(
|
|
3877
|
+
source: string,
|
|
3878
|
+
property: string,
|
|
3879
|
+
): Set<string> {
|
|
3880
|
+
const value = objectPropertyValueInfo(source, property);
|
|
3881
|
+
if (!value?.text.startsWith("[")) return new Set<string>();
|
|
3882
|
+
return identifiersFromArrayExpression(value.text);
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
function objectPropertyValueInfo(
|
|
3886
|
+
source: string,
|
|
3887
|
+
property: string,
|
|
3888
|
+
): { text: string; start: number; end: number } | undefined {
|
|
3889
|
+
const propertyMatch = new RegExp(`\\b${property}\\s*:`).exec(source);
|
|
3890
|
+
if (!propertyMatch) return undefined;
|
|
3891
|
+
|
|
3892
|
+
let start = propertyMatch.index + propertyMatch[0].length;
|
|
3893
|
+
while (start < source.length && /\s/.test(source[start])) start++;
|
|
3894
|
+
|
|
3895
|
+
const open = source[start];
|
|
3896
|
+
const close = open === "[" ? "]" : open === "{" ? "}" : undefined;
|
|
3897
|
+
if (!close) return undefined;
|
|
3898
|
+
|
|
3899
|
+
const end = matchingDelimiterIndex(source, start, open, close);
|
|
3900
|
+
if (end === -1) return undefined;
|
|
3901
|
+
|
|
3902
|
+
return {
|
|
3903
|
+
text: source.slice(start, end + 1),
|
|
3904
|
+
start,
|
|
3905
|
+
end: end + 1,
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3707
3909
|
function registeredWorkflowIdentifiers(
|
|
3708
3910
|
source: string,
|
|
3709
3911
|
kind: "schedules" | "tasks",
|
|
@@ -3751,6 +3953,7 @@ async function listenedEventNames(
|
|
|
3751
3953
|
async function listenerWiringReferences(
|
|
3752
3954
|
targetDir: string,
|
|
3753
3955
|
files: string[],
|
|
3956
|
+
config: ResolvedBeignetConfig,
|
|
3754
3957
|
): Promise<{
|
|
3755
3958
|
identifiers: Set<string>;
|
|
3756
3959
|
wiringFile?: string;
|
|
@@ -3759,12 +3962,15 @@ async function listenerWiringReferences(
|
|
|
3759
3962
|
const identifiers = new Set<string>();
|
|
3760
3963
|
let wiringFile: string | undefined;
|
|
3761
3964
|
let eventBusFile: string | undefined;
|
|
3965
|
+
let centralListenerRegistryReferenced = false;
|
|
3966
|
+
const listenerRegistryFile = listenersFilePath(config);
|
|
3762
3967
|
|
|
3763
3968
|
for (const file of files) {
|
|
3764
3969
|
if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
|
|
3765
3970
|
if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
|
|
3766
3971
|
|
|
3767
3972
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
3973
|
+
const namedImports = parseNamedImportSources(source);
|
|
3768
3974
|
let foundCall = false;
|
|
3769
3975
|
|
|
3770
3976
|
for (const match of source.matchAll(/\bregisterListeners\s*\(/g)) {
|
|
@@ -3776,9 +3982,21 @@ async function listenerWiringReferences(
|
|
|
3776
3982
|
: source.slice(openParen + 1, closeParen);
|
|
3777
3983
|
|
|
3778
3984
|
foundCall = true;
|
|
3779
|
-
|
|
3985
|
+
const callIdentifiers = identifiersFromArrayExpression(argsText);
|
|
3986
|
+
for (const identifier of callIdentifiers) {
|
|
3780
3987
|
identifiers.add(identifier);
|
|
3781
3988
|
}
|
|
3989
|
+
if (
|
|
3990
|
+
callReferencesCentralListenerRegistry({
|
|
3991
|
+
callIdentifiers,
|
|
3992
|
+
namedImports,
|
|
3993
|
+
importerFile: file,
|
|
3994
|
+
listenerRegistryFile,
|
|
3995
|
+
files,
|
|
3996
|
+
})
|
|
3997
|
+
) {
|
|
3998
|
+
centralListenerRegistryReferenced = true;
|
|
3999
|
+
}
|
|
3782
4000
|
}
|
|
3783
4001
|
|
|
3784
4002
|
if (foundCall) {
|
|
@@ -3788,9 +4006,54 @@ async function listenerWiringReferences(
|
|
|
3788
4006
|
}
|
|
3789
4007
|
}
|
|
3790
4008
|
|
|
4009
|
+
if (
|
|
4010
|
+
centralListenerRegistryReferenced &&
|
|
4011
|
+
files.includes(listenerRegistryFile)
|
|
4012
|
+
) {
|
|
4013
|
+
const source = await readFile(
|
|
4014
|
+
path.join(targetDir, listenerRegistryFile),
|
|
4015
|
+
"utf8",
|
|
4016
|
+
);
|
|
4017
|
+
const central = arrayInitializerInfo(source, "listeners");
|
|
4018
|
+
if (central) {
|
|
4019
|
+
for (const identifier of identifiersFromArrayExpression(central.text)) {
|
|
4020
|
+
identifiers.add(identifier);
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
|
|
3791
4025
|
return { identifiers, wiringFile, eventBusFile };
|
|
3792
4026
|
}
|
|
3793
4027
|
|
|
4028
|
+
function callReferencesCentralListenerRegistry(args: {
|
|
4029
|
+
callIdentifiers: Set<string>;
|
|
4030
|
+
namedImports: Map<string, { importedName: string; sourcePath: string }>;
|
|
4031
|
+
importerFile: string;
|
|
4032
|
+
listenerRegistryFile: string;
|
|
4033
|
+
files: string[];
|
|
4034
|
+
}): boolean {
|
|
4035
|
+
if (
|
|
4036
|
+
args.importerFile === args.listenerRegistryFile &&
|
|
4037
|
+
args.callIdentifiers.has("listeners")
|
|
4038
|
+
) {
|
|
4039
|
+
return true;
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
for (const identifier of args.callIdentifiers) {
|
|
4043
|
+
const imported = args.namedImports.get(identifier);
|
|
4044
|
+
if (!imported || imported.importedName !== "listeners") continue;
|
|
4045
|
+
|
|
4046
|
+
const importedFile = sourceFileFromImport(
|
|
4047
|
+
imported.sourcePath,
|
|
4048
|
+
args.importerFile,
|
|
4049
|
+
args.files,
|
|
4050
|
+
);
|
|
4051
|
+
if (importedFile === args.listenerRegistryFile) return true;
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
return false;
|
|
4055
|
+
}
|
|
4056
|
+
|
|
3794
4057
|
function isWorkflowTestFile(file: string): boolean {
|
|
3795
4058
|
return (
|
|
3796
4059
|
file.endsWith(".test.ts") ||
|
|
@@ -4111,6 +4374,15 @@ async function inspectDatabaseLifecycleDrift(
|
|
|
4111
4374
|
schemaDir,
|
|
4112
4375
|
sourceCache,
|
|
4113
4376
|
)),
|
|
4377
|
+
...(await inspectTenantScopeRepositoryDrift(
|
|
4378
|
+
targetDir,
|
|
4379
|
+
files,
|
|
4380
|
+
resources,
|
|
4381
|
+
config,
|
|
4382
|
+
infrastructurePath,
|
|
4383
|
+
schemaDir,
|
|
4384
|
+
sourceCache,
|
|
4385
|
+
)),
|
|
4114
4386
|
);
|
|
4115
4387
|
}
|
|
4116
4388
|
|
|
@@ -4270,118 +4542,701 @@ async function inspectDatabaseLifecycleDrift(
|
|
|
4270
4542
|
return diagnostics;
|
|
4271
4543
|
}
|
|
4272
4544
|
|
|
4273
|
-
|
|
4274
|
-
capability: string;
|
|
4275
|
-
table: keyof BeignetDatabaseTables;
|
|
4276
|
-
code: string;
|
|
4277
|
-
factoryNames: readonly string[];
|
|
4278
|
-
setupNames: readonly string[];
|
|
4279
|
-
explicitFactoryPattern: string;
|
|
4280
|
-
explicitSetupPattern: string;
|
|
4281
|
-
};
|
|
4282
|
-
|
|
4283
|
-
const durableDrizzleTableRequirements: readonly DurableDrizzleTableRequirement[] =
|
|
4284
|
-
[
|
|
4285
|
-
{
|
|
4286
|
-
capability: "idempotency",
|
|
4287
|
-
table: "idempotency",
|
|
4288
|
-
code: "BEIGNET_IDEMPOTENCY_TABLE_MISSING",
|
|
4289
|
-
factoryNames: [
|
|
4290
|
-
"createDrizzleSqliteIdempotencyPort",
|
|
4291
|
-
"createDrizzlePostgresIdempotencyPort",
|
|
4292
|
-
"createDrizzleMysqlIdempotencyPort",
|
|
4293
|
-
],
|
|
4294
|
-
setupNames: [
|
|
4295
|
-
"createDrizzleSqliteIdempotencySetupStatements",
|
|
4296
|
-
"createDrizzlePostgresIdempotencySetupStatements",
|
|
4297
|
-
"createDrizzleMysqlIdempotencySetupStatements",
|
|
4298
|
-
],
|
|
4299
|
-
explicitFactoryPattern: "createDrizzle[A-Za-z]*Idempotency(?:Port)?",
|
|
4300
|
-
explicitSetupPattern: "createDrizzle[A-Za-z]*IdempotencySetupStatements",
|
|
4301
|
-
},
|
|
4302
|
-
{
|
|
4303
|
-
capability: "outbox",
|
|
4304
|
-
table: "outbox",
|
|
4305
|
-
code: "BEIGNET_OUTBOX_TABLE_MISSING",
|
|
4306
|
-
factoryNames: [
|
|
4307
|
-
"createDrizzleSqliteOutboxPort",
|
|
4308
|
-
"createDrizzlePostgresOutboxPort",
|
|
4309
|
-
"createDrizzleMysqlOutboxPort",
|
|
4310
|
-
],
|
|
4311
|
-
setupNames: [
|
|
4312
|
-
"createDrizzleSqliteOutboxSetupStatements",
|
|
4313
|
-
"createDrizzlePostgresOutboxSetupStatements",
|
|
4314
|
-
"createDrizzleMysqlOutboxSetupStatements",
|
|
4315
|
-
],
|
|
4316
|
-
explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Port)?",
|
|
4317
|
-
explicitSetupPattern: "createDrizzle[A-Za-z]*OutboxSetupStatements",
|
|
4318
|
-
},
|
|
4319
|
-
{
|
|
4320
|
-
capability: "audit",
|
|
4321
|
-
table: "audit",
|
|
4322
|
-
code: "BEIGNET_AUDIT_TABLE_MISSING",
|
|
4323
|
-
factoryNames: [
|
|
4324
|
-
"createDrizzleSqliteAuditLogPort",
|
|
4325
|
-
"createDrizzlePostgresAuditLogPort",
|
|
4326
|
-
"createDrizzleMysqlAuditLogPort",
|
|
4327
|
-
],
|
|
4328
|
-
setupNames: [
|
|
4329
|
-
"createDrizzleSqliteAuditLogSetupStatements",
|
|
4330
|
-
"createDrizzlePostgresAuditLogSetupStatements",
|
|
4331
|
-
"createDrizzleMysqlAuditLogSetupStatements",
|
|
4332
|
-
],
|
|
4333
|
-
explicitFactoryPattern: "createDrizzle[A-Za-z]*AuditLog(?:Port)?",
|
|
4334
|
-
explicitSetupPattern: "createDrizzle[A-Za-z]*AuditLogSetupStatements",
|
|
4335
|
-
},
|
|
4336
|
-
];
|
|
4337
|
-
|
|
4338
|
-
async function inspectDurableDrizzleTableDrift(
|
|
4545
|
+
async function inspectTenantScopeRepositoryDrift(
|
|
4339
4546
|
targetDir: string,
|
|
4340
4547
|
files: string[],
|
|
4548
|
+
resources: string[],
|
|
4341
4549
|
config: ResolvedBeignetConfig,
|
|
4342
4550
|
infrastructurePath: string,
|
|
4343
4551
|
schemaDir: string,
|
|
4344
4552
|
sourceCache: Map<string, string>,
|
|
4345
4553
|
): Promise<InspectDiagnostic[]> {
|
|
4346
4554
|
const diagnostics: InspectDiagnostic[] = [];
|
|
4347
|
-
const
|
|
4555
|
+
const schemaIndexFile = `${schemaDir}/index.ts`;
|
|
4556
|
+
const schemaFiles = files.filter(
|
|
4557
|
+
(file) =>
|
|
4558
|
+
file.startsWith(`${schemaDir}/`) &&
|
|
4559
|
+
file.endsWith(".ts") &&
|
|
4560
|
+
file !== schemaIndexFile,
|
|
4561
|
+
);
|
|
4562
|
+
const tenantScopedTables = await collectTenantScopedDrizzleTables(
|
|
4563
|
+
targetDir,
|
|
4564
|
+
schemaFiles,
|
|
4565
|
+
sourceCache,
|
|
4566
|
+
);
|
|
4567
|
+
const inspectedRepositories = new Set<string>();
|
|
4348
4568
|
|
|
4349
|
-
for (const
|
|
4350
|
-
const
|
|
4569
|
+
for (const resource of resources) {
|
|
4570
|
+
const singular = singularize(resource);
|
|
4571
|
+
const singularPascal = pascalCase(singular);
|
|
4572
|
+
const pluralCamel = camelCase(resource);
|
|
4573
|
+
const schemaFile = `${schemaDir}/${resource}.ts`;
|
|
4574
|
+
const portFile = resourcePortFile(resource, singular, config);
|
|
4575
|
+
const drizzleRepositoryFile = drizzleRepositoryAdapterFile(
|
|
4576
|
+
files,
|
|
4577
|
+
`${infrastructurePath}/${resource}/`,
|
|
4578
|
+
singular,
|
|
4579
|
+
);
|
|
4580
|
+
if (!drizzleRepositoryFile || !files.includes(schemaFile)) continue;
|
|
4581
|
+
|
|
4582
|
+
const schemaSource = await readCachedSource(
|
|
4351
4583
|
targetDir,
|
|
4352
|
-
|
|
4353
|
-
config,
|
|
4354
|
-
requirement,
|
|
4584
|
+
schemaFile,
|
|
4355
4585
|
sourceCache,
|
|
4356
4586
|
);
|
|
4357
|
-
|
|
4587
|
+
const schemaTenantColumns = tenantScopeColumnsFromSource(schemaSource);
|
|
4588
|
+
if (schemaTenantColumns.length === 0) continue;
|
|
4589
|
+
inspectedRepositories.add(drizzleRepositoryFile);
|
|
4358
4590
|
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
continue;
|
|
4591
|
+
if (files.includes(portFile)) {
|
|
4592
|
+
const portSource = await readCachedSource(
|
|
4593
|
+
targetDir,
|
|
4594
|
+
portFile,
|
|
4595
|
+
sourceCache,
|
|
4596
|
+
);
|
|
4597
|
+
if (!/\b[A-Za-z_$][\w$]*\s*:\s*TenantScope\b/.test(portSource)) {
|
|
4598
|
+
diagnostics.push({
|
|
4599
|
+
severity: "warning",
|
|
4600
|
+
code: "BEIGNET_TENANT_SCOPE_PORT_MISSING",
|
|
4601
|
+
file: portFile,
|
|
4602
|
+
message: `${resource} has a tenant-scoped Drizzle schema (${schemaFile}), but ${portFile} does not accept TenantScope in its ${singularPascal}Repository methods. Tenant-owned repository methods should require a branded scope argument instead of raw tenant or workspace IDs.`,
|
|
4603
|
+
});
|
|
4373
4604
|
}
|
|
4374
|
-
|
|
4375
|
-
diagnostics.push({
|
|
4376
|
-
severity: "warning",
|
|
4377
|
-
code: requirement.code,
|
|
4378
|
-
file: schemaDir,
|
|
4379
|
-
message: `This app wires a Drizzle-backed ${requirement.capability} port, but no ${tableName} table setup was found in ${schemaDir}/, drizzle/, or app database setup files. Add the table to your Drizzle schema/migrations, call the provider setup statements, or remove the stale ${requirement.capability} wiring before deployment.`,
|
|
4380
|
-
});
|
|
4381
4605
|
}
|
|
4382
|
-
}
|
|
4383
4606
|
|
|
4384
|
-
|
|
4607
|
+
const repositorySource = await readCachedSource(
|
|
4608
|
+
targetDir,
|
|
4609
|
+
drizzleRepositoryFile,
|
|
4610
|
+
sourceCache,
|
|
4611
|
+
);
|
|
4612
|
+
const tenantPredicate = `eq(schema.${pluralCamel}.tenantId, tenantScopeId(scope))`;
|
|
4613
|
+
const tenantPredicateCount = countSourceOccurrences(
|
|
4614
|
+
repositorySource,
|
|
4615
|
+
tenantPredicate,
|
|
4616
|
+
);
|
|
4617
|
+
const setsTenantFromScope = repositorySource.includes(
|
|
4618
|
+
"tenantId: tenantScopeId(scope)",
|
|
4619
|
+
);
|
|
4620
|
+
if (
|
|
4621
|
+
!repositorySource.includes(`schema.${pluralCamel}`) ||
|
|
4622
|
+
(tenantPredicateCount >= 4 && setsTenantFromScope) ||
|
|
4623
|
+
repositoryHasScopedTenantCoverage(repositorySource, [
|
|
4624
|
+
{
|
|
4625
|
+
name: pluralCamel,
|
|
4626
|
+
file: schemaFile,
|
|
4627
|
+
columns: schemaTenantColumns,
|
|
4628
|
+
},
|
|
4629
|
+
])
|
|
4630
|
+
) {
|
|
4631
|
+
continue;
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
diagnostics.push({
|
|
4635
|
+
severity: "warning",
|
|
4636
|
+
code: "BEIGNET_TENANT_SCOPE_PREDICATE_MISSING",
|
|
4637
|
+
file: drizzleRepositoryFile,
|
|
4638
|
+
message: `${resource} has a tenant-scoped Drizzle schema (${schemaFile}), but ${drizzleRepositoryFile} does not use TenantScope consistently. Tenant-owned repository adapters should derive tenant or workspace IDs from tenantScopeId(scope) and include them in create values and read, update, and delete predicates.`,
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
const repositoryFiles = files.filter(
|
|
4643
|
+
(file) =>
|
|
4644
|
+
file.startsWith(`${infrastructurePath}/`) &&
|
|
4645
|
+
file.includes("/drizzle-") &&
|
|
4646
|
+
file.endsWith("-repository.ts") &&
|
|
4647
|
+
!file.endsWith(".test.ts"),
|
|
4648
|
+
);
|
|
4649
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
4650
|
+
|
|
4651
|
+
for (const repositoryFile of repositoryFiles) {
|
|
4652
|
+
if (inspectedRepositories.has(repositoryFile)) continue;
|
|
4653
|
+
|
|
4654
|
+
const repositorySource = await readCachedSource(
|
|
4655
|
+
targetDir,
|
|
4656
|
+
repositoryFile,
|
|
4657
|
+
sourceCache,
|
|
4658
|
+
);
|
|
4659
|
+
const referencedTables = tenantScopedTables.filter((table) =>
|
|
4660
|
+
repositorySource.includes(`schema.${table.name}`),
|
|
4661
|
+
);
|
|
4662
|
+
if (referencedTables.length === 0) continue;
|
|
4663
|
+
|
|
4664
|
+
const repositoryFeature = repositoryFeatureFromPath(
|
|
4665
|
+
repositoryFile,
|
|
4666
|
+
infrastructurePath,
|
|
4667
|
+
);
|
|
4668
|
+
if (!repositoryFeature || isTenantAuthorityFeature(repositoryFeature)) {
|
|
4669
|
+
continue;
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
const portFile = `${featuresPath}/${repositoryFeature}/ports.ts`;
|
|
4673
|
+
if (!files.includes(portFile)) continue;
|
|
4674
|
+
|
|
4675
|
+
const portSource = await readCachedSource(targetDir, portFile, sourceCache);
|
|
4676
|
+
const repositoryBlocks = repositoryInterfaceBlocks(portSource);
|
|
4677
|
+
if (repositoryBlocks.length === 0) continue;
|
|
4678
|
+
|
|
4679
|
+
const hasTenantScopeBoundary = repositoryBlocks.some((block) =>
|
|
4680
|
+
repositoryInterfaceAcceptsTenantScope(block),
|
|
4681
|
+
);
|
|
4682
|
+
const rawBoundaries = repositoryBlocks.flatMap((block) =>
|
|
4683
|
+
rawTenantRepositoryMethodBoundaries(block, referencedTables),
|
|
4684
|
+
);
|
|
4685
|
+
|
|
4686
|
+
if (rawBoundaries.length > 0) {
|
|
4687
|
+
diagnostics.push({
|
|
4688
|
+
severity: "warning",
|
|
4689
|
+
code: "BEIGNET_TENANT_SCOPE_RAW_ID_BOUNDARY",
|
|
4690
|
+
file: portFile,
|
|
4691
|
+
message: `${portFile} declares tenant-owned repository methods (${rawBoundaries.join(
|
|
4692
|
+
", ",
|
|
4693
|
+
)}) with raw tenant IDs. Use TenantScope at app-facing repository boundaries and unwrap it with tenantScopeId(scope) inside ${repositoryFile}; keep provider-correlation lookups separate from authorization.`,
|
|
4694
|
+
});
|
|
4695
|
+
continue;
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
if (
|
|
4699
|
+
hasTenantScopeBoundary &&
|
|
4700
|
+
!repositoryHasScopedTenantCoverage(repositorySource, referencedTables)
|
|
4701
|
+
) {
|
|
4702
|
+
diagnostics.push({
|
|
4703
|
+
severity: "warning",
|
|
4704
|
+
code: "BEIGNET_TENANT_SCOPE_PREDICATE_MISSING",
|
|
4705
|
+
file: repositoryFile,
|
|
4706
|
+
message: `${repositoryFile} adapts tenant-owned tables (${referencedTables
|
|
4707
|
+
.map((table) => table.name)
|
|
4708
|
+
.join(
|
|
4709
|
+
", ",
|
|
4710
|
+
)}), but its scoped methods do not consistently derive tenant or workspace IDs from tenantScopeId(scope). Include that derived ID in create values and read, update, and delete predicates.`,
|
|
4711
|
+
});
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
return diagnostics;
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
const tenantScopeColumnDefinitions = [
|
|
4719
|
+
{
|
|
4720
|
+
property: "tenantId",
|
|
4721
|
+
sqlName: "tenant_id",
|
|
4722
|
+
label: "tenant",
|
|
4723
|
+
},
|
|
4724
|
+
{
|
|
4725
|
+
property: "workspaceId",
|
|
4726
|
+
sqlName: "workspace_id",
|
|
4727
|
+
label: "workspace",
|
|
4728
|
+
},
|
|
4729
|
+
] as const;
|
|
4730
|
+
|
|
4731
|
+
type TenantScopeColumn = (typeof tenantScopeColumnDefinitions)[number];
|
|
4732
|
+
|
|
4733
|
+
type TenantScopedDrizzleTable = {
|
|
4734
|
+
name: string;
|
|
4735
|
+
file: string;
|
|
4736
|
+
columns: TenantScopeColumn[];
|
|
4737
|
+
};
|
|
4738
|
+
|
|
4739
|
+
async function collectTenantScopedDrizzleTables(
|
|
4740
|
+
targetDir: string,
|
|
4741
|
+
schemaFiles: string[],
|
|
4742
|
+
sourceCache: Map<string, string>,
|
|
4743
|
+
): Promise<TenantScopedDrizzleTable[]> {
|
|
4744
|
+
const tables: TenantScopedDrizzleTable[] = [];
|
|
4745
|
+
|
|
4746
|
+
for (const file of schemaFiles) {
|
|
4747
|
+
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
4748
|
+
for (const table of drizzleTablesFromSource(source, file)) {
|
|
4749
|
+
if (table.columns.length > 0) tables.push(table);
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
return tables;
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
function drizzleTablesFromSource(
|
|
4757
|
+
source: string,
|
|
4758
|
+
file: string,
|
|
4759
|
+
): TenantScopedDrizzleTable[] {
|
|
4760
|
+
const tables: TenantScopedDrizzleTable[] = [];
|
|
4761
|
+
const tablePattern =
|
|
4762
|
+
/\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:sqliteTable|pgTable|mysqlTable)\s*\(/g;
|
|
4763
|
+
|
|
4764
|
+
for (const match of source.matchAll(tablePattern)) {
|
|
4765
|
+
const tableName = match[1];
|
|
4766
|
+
const openIndex = source.indexOf("(", match.index ?? 0);
|
|
4767
|
+
if (!tableName || openIndex === -1) continue;
|
|
4768
|
+
|
|
4769
|
+
const closeIndex = matchingDelimiterIndex(source, openIndex, "(", ")");
|
|
4770
|
+
if (closeIndex === -1) continue;
|
|
4771
|
+
|
|
4772
|
+
const tableSource = source.slice(openIndex, closeIndex + 1);
|
|
4773
|
+
const columns = tenantScopeColumnsFromSource(tableSource);
|
|
4774
|
+
tables.push({ name: tableName, file, columns });
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4777
|
+
return tables;
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4780
|
+
function tenantScopeColumnsFromSource(source: string): TenantScopeColumn[] {
|
|
4781
|
+
return tenantScopeColumnDefinitions.filter((column) => {
|
|
4782
|
+
const propertyPattern = new RegExp(`\\b${column.property}\\s*:`);
|
|
4783
|
+
return (
|
|
4784
|
+
propertyPattern.test(source) ||
|
|
4785
|
+
source.includes(`"${column.sqlName}"`) ||
|
|
4786
|
+
source.includes(`'${column.sqlName}'`)
|
|
4787
|
+
);
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
function repositoryFeatureFromPath(
|
|
4792
|
+
repositoryFile: string,
|
|
4793
|
+
infrastructurePath: string,
|
|
4794
|
+
): string | undefined {
|
|
4795
|
+
const match = new RegExp(
|
|
4796
|
+
`^${escapeRegExp(infrastructurePath)}/([^/]+)/`,
|
|
4797
|
+
).exec(repositoryFile);
|
|
4798
|
+
return match?.[1];
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
function isTenantAuthorityFeature(feature: string): boolean {
|
|
4802
|
+
return feature === "workspaces";
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
function repositoryInterfaceBlocks(source: string): string[] {
|
|
4806
|
+
const blocks: string[] = [];
|
|
4807
|
+
const interfacePattern =
|
|
4808
|
+
/\bexport\s+interface\s+[A-Za-z_$][\w$]*Repository\s*\{/g;
|
|
4809
|
+
|
|
4810
|
+
for (const match of source.matchAll(interfacePattern)) {
|
|
4811
|
+
const openIndex = source.indexOf("{", match.index ?? 0);
|
|
4812
|
+
if (openIndex === -1) continue;
|
|
4813
|
+
|
|
4814
|
+
const closeIndex = matchingDelimiterIndex(source, openIndex, "{", "}");
|
|
4815
|
+
if (closeIndex === -1) continue;
|
|
4816
|
+
|
|
4817
|
+
blocks.push(source.slice(openIndex + 1, closeIndex));
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
return blocks;
|
|
4821
|
+
}
|
|
4822
|
+
|
|
4823
|
+
function repositoryInterfaceAcceptsTenantScope(block: string): boolean {
|
|
4824
|
+
return /\b[A-Za-z_$][\w$]*\s*:\s*TenantScope\b/.test(block);
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
function rawTenantRepositoryMethodBoundaries(
|
|
4828
|
+
block: string,
|
|
4829
|
+
tables: readonly TenantScopedDrizzleTable[],
|
|
4830
|
+
): string[] {
|
|
4831
|
+
const rawMethods: string[] = [];
|
|
4832
|
+
for (const method of repositoryMethodSignatures(block)) {
|
|
4833
|
+
if (isProviderCorrelationMethod(method.name)) continue;
|
|
4834
|
+
|
|
4835
|
+
const rawColumns = tenantColumnsUsedInSource(method.params, tables).filter(
|
|
4836
|
+
(column) => column.property === "tenantId",
|
|
4837
|
+
);
|
|
4838
|
+
if (rawColumns.length === 0) continue;
|
|
4839
|
+
|
|
4840
|
+
rawMethods.push(method.name);
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
return uniqueStringValues(rawMethods);
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4846
|
+
function repositoryMethodSignatures(
|
|
4847
|
+
block: string,
|
|
4848
|
+
): Array<{ name: string; params: string }> {
|
|
4849
|
+
const signatures: Array<{ name: string; params: string }> = [];
|
|
4850
|
+
const methodPattern = /\b([A-Za-z_$][\w$]*)\s*\(/g;
|
|
4851
|
+
|
|
4852
|
+
for (const match of block.matchAll(methodPattern)) {
|
|
4853
|
+
const name = match[1];
|
|
4854
|
+
const openIndex = block.indexOf("(", match.index ?? 0);
|
|
4855
|
+
if (!name || openIndex === -1) continue;
|
|
4856
|
+
|
|
4857
|
+
const closeIndex = matchingDelimiterIndex(block, openIndex, "(", ")");
|
|
4858
|
+
if (closeIndex === -1) continue;
|
|
4859
|
+
|
|
4860
|
+
const afterParams = block.slice(closeIndex + 1).trimStart();
|
|
4861
|
+
if (!afterParams.startsWith(":")) continue;
|
|
4862
|
+
|
|
4863
|
+
signatures.push({
|
|
4864
|
+
name,
|
|
4865
|
+
params: block.slice(openIndex + 1, closeIndex),
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
return signatures;
|
|
4870
|
+
}
|
|
4871
|
+
|
|
4872
|
+
function isProviderCorrelationMethod(name: string): boolean {
|
|
4873
|
+
return /(?:Customer|Provider|Subscription|Checkout|Event|Delivery|External|Token)[A-Za-z0-9]*(?:Id)?$/i.test(
|
|
4874
|
+
name,
|
|
4875
|
+
);
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
function tenantColumnsUsedInSource(
|
|
4879
|
+
source: string,
|
|
4880
|
+
tables: readonly TenantScopedDrizzleTable[],
|
|
4881
|
+
): TenantScopeColumn[] {
|
|
4882
|
+
const columns = new Map<string, TenantScopeColumn>();
|
|
4883
|
+
for (const table of tables) {
|
|
4884
|
+
for (const column of table.columns) {
|
|
4885
|
+
const propertyPattern = new RegExp(`\\b${column.property}\\s*:`);
|
|
4886
|
+
if (propertyPattern.test(source)) {
|
|
4887
|
+
columns.set(column.property, column);
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
return [...columns.values()];
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
function repositoryHasScopedTenantCoverage(
|
|
4896
|
+
source: string,
|
|
4897
|
+
tables: readonly TenantScopedDrizzleTable[],
|
|
4898
|
+
): boolean {
|
|
4899
|
+
if (!source.includes("tenantScopeId(scope)")) return false;
|
|
4900
|
+
|
|
4901
|
+
const allFunctions = functionLikeBlocks(source);
|
|
4902
|
+
const scopedFunctions = allFunctions.filter((block) =>
|
|
4903
|
+
/\bscope\b/.test(block.params),
|
|
4904
|
+
);
|
|
4905
|
+
if (scopedFunctions.length === 0) return false;
|
|
4906
|
+
|
|
4907
|
+
const functionsByName = new Map(
|
|
4908
|
+
allFunctions.map((block) => [block.name, block]),
|
|
4909
|
+
);
|
|
4910
|
+
|
|
4911
|
+
return scopedFunctions.every((block) =>
|
|
4912
|
+
scopedFunctionUsesTenantScope(block, tables, functionsByName),
|
|
4913
|
+
);
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4916
|
+
type FunctionLikeBlock = {
|
|
4917
|
+
name: string;
|
|
4918
|
+
params: string;
|
|
4919
|
+
body: string;
|
|
4920
|
+
};
|
|
4921
|
+
|
|
4922
|
+
function functionLikeBlocks(source: string): FunctionLikeBlock[] {
|
|
4923
|
+
const blocks: FunctionLikeBlock[] = [];
|
|
4924
|
+
const pattern = /(?:\basync\s+)?(?:function\s+)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
4925
|
+
|
|
4926
|
+
for (const match of source.matchAll(pattern)) {
|
|
4927
|
+
const name = match[1];
|
|
4928
|
+
const openParamsIndex = source.indexOf("(", match.index ?? 0);
|
|
4929
|
+
if (!name || openParamsIndex === -1) continue;
|
|
4930
|
+
|
|
4931
|
+
const closeParamsIndex = matchingDelimiterIndex(
|
|
4932
|
+
source,
|
|
4933
|
+
openParamsIndex,
|
|
4934
|
+
"(",
|
|
4935
|
+
")",
|
|
4936
|
+
);
|
|
4937
|
+
if (closeParamsIndex === -1) continue;
|
|
4938
|
+
|
|
4939
|
+
const afterParams = source.slice(closeParamsIndex + 1);
|
|
4940
|
+
const bodyOpenOffset = afterParams.search(/\{/);
|
|
4941
|
+
if (bodyOpenOffset === -1) continue;
|
|
4942
|
+
|
|
4943
|
+
const betweenParamsAndBody = afterParams.slice(0, bodyOpenOffset);
|
|
4944
|
+
if (
|
|
4945
|
+
!/^\s*(?::[^{;=]+)?\s*(?:=>\s*)?$/.test(betweenParamsAndBody) &&
|
|
4946
|
+
!/^\s*$/.test(betweenParamsAndBody)
|
|
4947
|
+
) {
|
|
4948
|
+
continue;
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
const openBodyIndex = closeParamsIndex + 1 + bodyOpenOffset;
|
|
4952
|
+
const closeBodyIndex = matchingDelimiterIndex(
|
|
4953
|
+
source,
|
|
4954
|
+
openBodyIndex,
|
|
4955
|
+
"{",
|
|
4956
|
+
"}",
|
|
4957
|
+
);
|
|
4958
|
+
if (closeBodyIndex === -1) continue;
|
|
4959
|
+
|
|
4960
|
+
blocks.push({
|
|
4961
|
+
name,
|
|
4962
|
+
params: source.slice(openParamsIndex + 1, closeParamsIndex),
|
|
4963
|
+
body: source.slice(openBodyIndex + 1, closeBodyIndex),
|
|
4964
|
+
});
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
return blocks;
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
function scopedFunctionUsesTenantScope(
|
|
4971
|
+
block: FunctionLikeBlock,
|
|
4972
|
+
tables: readonly TenantScopedDrizzleTable[],
|
|
4973
|
+
functionsByName: ReadonlyMap<string, FunctionLikeBlock>,
|
|
4974
|
+
): boolean {
|
|
4975
|
+
if (bodyUsesTenantScopePredicate(block.body, tables)) return true;
|
|
4976
|
+
|
|
4977
|
+
for (const variable of tenantScopeVariables(block.body)) {
|
|
4978
|
+
if (bodyUsesTenantScopePredicate(block.body, tables, variable)) return true;
|
|
4979
|
+
|
|
4980
|
+
for (const callee of functionsCalledWithVariable(block.body, variable)) {
|
|
4981
|
+
const helper = functionsByName.get(callee.name);
|
|
4982
|
+
if (
|
|
4983
|
+
helper &&
|
|
4984
|
+
helperUsesTenantVariable(helper, tables, callee.argumentIndex)
|
|
4985
|
+
) {
|
|
4986
|
+
return true;
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
return false;
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
function tenantScopeVariables(body: string): string[] {
|
|
4995
|
+
const variables = new Set<string>(["tenantScopeId(scope)"]);
|
|
4996
|
+
const assignmentPattern =
|
|
4997
|
+
/\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*tenantScopeId\(scope\)/g;
|
|
4998
|
+
|
|
4999
|
+
for (const match of body.matchAll(assignmentPattern)) {
|
|
5000
|
+
if (match[1]) variables.add(match[1]);
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
return [...variables];
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
function bodyUsesTenantScopePredicate(
|
|
5007
|
+
body: string,
|
|
5008
|
+
tables: readonly TenantScopedDrizzleTable[],
|
|
5009
|
+
variable = "tenantScopeId(scope)",
|
|
5010
|
+
): boolean {
|
|
5011
|
+
for (const table of tables) {
|
|
5012
|
+
for (const column of table.columns) {
|
|
5013
|
+
if (
|
|
5014
|
+
body.includes(
|
|
5015
|
+
`eq(schema.${table.name}.${column.property}, ${variable})`,
|
|
5016
|
+
)
|
|
5017
|
+
) {
|
|
5018
|
+
return true;
|
|
5019
|
+
}
|
|
5020
|
+
if (body.includes(`${column.property}: ${variable}`)) {
|
|
5021
|
+
return true;
|
|
5022
|
+
}
|
|
5023
|
+
if (
|
|
5024
|
+
variable === column.property &&
|
|
5025
|
+
new RegExp(`\\b${column.property}\\s*(?:,|})`).test(body)
|
|
5026
|
+
) {
|
|
5027
|
+
return true;
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
return false;
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
function functionsCalledWithVariable(
|
|
5036
|
+
body: string,
|
|
5037
|
+
variable: string,
|
|
5038
|
+
): Array<{ name: string; argumentIndex: number }> {
|
|
5039
|
+
if (variable.includes("(")) return [];
|
|
5040
|
+
|
|
5041
|
+
const calls: Array<{ name: string; argumentIndex: number }> = [];
|
|
5042
|
+
const callPattern = /\b([A-Za-z_$][\w$]*)\s*\(/g;
|
|
5043
|
+
|
|
5044
|
+
for (const match of body.matchAll(callPattern)) {
|
|
5045
|
+
const name = match[1];
|
|
5046
|
+
const openIndex = body.indexOf("(", match.index ?? 0);
|
|
5047
|
+
if (!name || openIndex === -1) continue;
|
|
5048
|
+
|
|
5049
|
+
const closeIndex = matchingDelimiterIndex(body, openIndex, "(", ")");
|
|
5050
|
+
if (closeIndex === -1) continue;
|
|
5051
|
+
|
|
5052
|
+
const args = splitTopLevelArguments(body.slice(openIndex + 1, closeIndex));
|
|
5053
|
+
const argumentIndex = args.findIndex(
|
|
5054
|
+
(argument) => argument.trim() === variable,
|
|
5055
|
+
);
|
|
5056
|
+
if (argumentIndex !== -1) {
|
|
5057
|
+
calls.push({ name, argumentIndex });
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
|
|
5061
|
+
return calls;
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
function helperUsesTenantVariable(
|
|
5065
|
+
helper: FunctionLikeBlock,
|
|
5066
|
+
tables: readonly TenantScopedDrizzleTable[],
|
|
5067
|
+
argumentIndex: number,
|
|
5068
|
+
): boolean {
|
|
5069
|
+
const params = splitTopLevelArguments(helper.params);
|
|
5070
|
+
const parameter = params[argumentIndex]?.trim();
|
|
5071
|
+
if (!parameter) return false;
|
|
5072
|
+
|
|
5073
|
+
const parameterName = /^([A-Za-z_$][\w$]*)/.exec(parameter)?.[1];
|
|
5074
|
+
return parameterName
|
|
5075
|
+
? bodyUsesTenantScopePredicate(helper.body, tables, parameterName)
|
|
5076
|
+
: false;
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
function splitTopLevelArguments(source: string): string[] {
|
|
5080
|
+
const args: string[] = [];
|
|
5081
|
+
let start = 0;
|
|
5082
|
+
let depth = 0;
|
|
5083
|
+
let inString: string | undefined;
|
|
5084
|
+
let escaped = false;
|
|
5085
|
+
|
|
5086
|
+
for (let index = 0; index < source.length; index++) {
|
|
5087
|
+
const char = source[index];
|
|
5088
|
+
if (inString) {
|
|
5089
|
+
if (escaped) {
|
|
5090
|
+
escaped = false;
|
|
5091
|
+
} else if (char === "\\") {
|
|
5092
|
+
escaped = true;
|
|
5093
|
+
} else if (char === inString) {
|
|
5094
|
+
inString = undefined;
|
|
5095
|
+
}
|
|
5096
|
+
continue;
|
|
5097
|
+
}
|
|
5098
|
+
|
|
5099
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
5100
|
+
inString = char;
|
|
5101
|
+
continue;
|
|
5102
|
+
}
|
|
5103
|
+
if (char === "(" || char === "{" || char === "[") {
|
|
5104
|
+
depth += 1;
|
|
5105
|
+
continue;
|
|
5106
|
+
}
|
|
5107
|
+
if (char === ")" || char === "}" || char === "]") {
|
|
5108
|
+
depth -= 1;
|
|
5109
|
+
continue;
|
|
5110
|
+
}
|
|
5111
|
+
if (char === "," && depth === 0) {
|
|
5112
|
+
args.push(source.slice(start, index));
|
|
5113
|
+
start = index + 1;
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
|
|
5117
|
+
args.push(source.slice(start));
|
|
5118
|
+
return args;
|
|
5119
|
+
}
|
|
5120
|
+
|
|
5121
|
+
function uniqueStringValues(values: readonly string[]): string[] {
|
|
5122
|
+
return [...new Set(values)];
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
type DurableDrizzleTableRequirement = {
|
|
5126
|
+
capability: string;
|
|
5127
|
+
table: keyof BeignetDatabaseTables;
|
|
5128
|
+
code: string;
|
|
5129
|
+
factoryNames: readonly string[];
|
|
5130
|
+
setupNames: readonly string[];
|
|
5131
|
+
explicitFactoryPattern: string;
|
|
5132
|
+
explicitSetupPattern: string;
|
|
5133
|
+
};
|
|
5134
|
+
|
|
5135
|
+
const durableDrizzleTableRequirements: readonly DurableDrizzleTableRequirement[] =
|
|
5136
|
+
[
|
|
5137
|
+
{
|
|
5138
|
+
capability: "idempotency",
|
|
5139
|
+
table: "idempotency",
|
|
5140
|
+
code: "BEIGNET_IDEMPOTENCY_TABLE_MISSING",
|
|
5141
|
+
factoryNames: [
|
|
5142
|
+
"createDrizzleSqliteIdempotencyPort",
|
|
5143
|
+
"createDrizzlePostgresIdempotencyPort",
|
|
5144
|
+
"createDrizzleMysqlIdempotencyPort",
|
|
5145
|
+
],
|
|
5146
|
+
setupNames: [
|
|
5147
|
+
"createDrizzleSqliteIdempotencySetupStatements",
|
|
5148
|
+
"createDrizzlePostgresIdempotencySetupStatements",
|
|
5149
|
+
"createDrizzleMysqlIdempotencySetupStatements",
|
|
5150
|
+
],
|
|
5151
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*Idempotency(?:Port)?",
|
|
5152
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*IdempotencySetupStatements",
|
|
5153
|
+
},
|
|
5154
|
+
{
|
|
5155
|
+
capability: "outbox",
|
|
5156
|
+
table: "outbox",
|
|
5157
|
+
code: "BEIGNET_OUTBOX_TABLE_MISSING",
|
|
5158
|
+
factoryNames: [
|
|
5159
|
+
"createDrizzleSqliteOutboxPort",
|
|
5160
|
+
"createDrizzleSqliteOutboxAdminPort",
|
|
5161
|
+
"createDrizzlePostgresOutboxPort",
|
|
5162
|
+
"createDrizzlePostgresOutboxAdminPort",
|
|
5163
|
+
"createDrizzleMysqlOutboxPort",
|
|
5164
|
+
"createDrizzleMysqlOutboxAdminPort",
|
|
5165
|
+
],
|
|
5166
|
+
setupNames: [
|
|
5167
|
+
"createDrizzleSqliteOutboxSetupStatements",
|
|
5168
|
+
"createDrizzlePostgresOutboxSetupStatements",
|
|
5169
|
+
"createDrizzleMysqlOutboxSetupStatements",
|
|
5170
|
+
],
|
|
5171
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Admin)?(?:Port)?",
|
|
5172
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*OutboxSetupStatements",
|
|
5173
|
+
},
|
|
5174
|
+
{
|
|
5175
|
+
capability: "audit",
|
|
5176
|
+
table: "audit",
|
|
5177
|
+
code: "BEIGNET_AUDIT_TABLE_MISSING",
|
|
5178
|
+
factoryNames: [
|
|
5179
|
+
"createDrizzleSqliteAuditLogPort",
|
|
5180
|
+
"createDrizzlePostgresAuditLogPort",
|
|
5181
|
+
"createDrizzleMysqlAuditLogPort",
|
|
5182
|
+
],
|
|
5183
|
+
setupNames: [
|
|
5184
|
+
"createDrizzleSqliteAuditLogSetupStatements",
|
|
5185
|
+
"createDrizzlePostgresAuditLogSetupStatements",
|
|
5186
|
+
"createDrizzleMysqlAuditLogSetupStatements",
|
|
5187
|
+
],
|
|
5188
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*AuditLog(?:Port)?",
|
|
5189
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*AuditLogSetupStatements",
|
|
5190
|
+
},
|
|
5191
|
+
];
|
|
5192
|
+
|
|
5193
|
+
async function inspectDurableDrizzleTableDrift(
|
|
5194
|
+
targetDir: string,
|
|
5195
|
+
files: string[],
|
|
5196
|
+
config: ResolvedBeignetConfig,
|
|
5197
|
+
infrastructurePath: string,
|
|
5198
|
+
schemaDir: string,
|
|
5199
|
+
sourceCache: Map<string, string>,
|
|
5200
|
+
): Promise<InspectDiagnostic[]> {
|
|
5201
|
+
const diagnostics: InspectDiagnostic[] = [];
|
|
5202
|
+
const usageFiles = durableDrizzleUsageFiles(files, config);
|
|
5203
|
+
|
|
5204
|
+
for (const requirement of durableDrizzleTableRequirements) {
|
|
5205
|
+
const tableNames = await configuredDurableDrizzleTableNames(
|
|
5206
|
+
targetDir,
|
|
5207
|
+
usageFiles,
|
|
5208
|
+
config,
|
|
5209
|
+
requirement,
|
|
5210
|
+
sourceCache,
|
|
5211
|
+
);
|
|
5212
|
+
if (tableNames.size === 0) continue;
|
|
5213
|
+
|
|
5214
|
+
for (const tableName of tableNames) {
|
|
5215
|
+
if (
|
|
5216
|
+
await durableDrizzleTableExists(
|
|
5217
|
+
targetDir,
|
|
5218
|
+
files,
|
|
5219
|
+
config,
|
|
5220
|
+
infrastructurePath,
|
|
5221
|
+
schemaDir,
|
|
5222
|
+
requirement,
|
|
5223
|
+
tableName,
|
|
5224
|
+
sourceCache,
|
|
5225
|
+
)
|
|
5226
|
+
) {
|
|
5227
|
+
continue;
|
|
5228
|
+
}
|
|
5229
|
+
|
|
5230
|
+
diagnostics.push({
|
|
5231
|
+
severity: "warning",
|
|
5232
|
+
code: requirement.code,
|
|
5233
|
+
file: schemaDir,
|
|
5234
|
+
message: `This app wires a Drizzle-backed ${requirement.capability} port, but no ${tableName} table setup was found in ${schemaDir}/, drizzle/, or app database setup files. Add the table to your Drizzle schema/migrations, call the provider setup statements, or remove the stale ${requirement.capability} wiring before deployment.`,
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
return diagnostics;
|
|
4385
5240
|
}
|
|
4386
5241
|
|
|
4387
5242
|
function durableDrizzleUsageFiles(
|
|
@@ -4757,22 +5612,70 @@ async function inspectPackageScripts(
|
|
|
4757
5612
|
await readFile(path.join(targetDir, "package.json"), "utf8"),
|
|
4758
5613
|
) as {
|
|
4759
5614
|
scripts?: Record<string, string>;
|
|
5615
|
+
dependencies?: Record<string, string>;
|
|
5616
|
+
devDependencies?: Record<string, string>;
|
|
4760
5617
|
};
|
|
4761
5618
|
|
|
4762
|
-
|
|
5619
|
+
const diagnostics: InspectDiagnostic[] = [];
|
|
5620
|
+
const scripts = packageJson.scripts ?? {};
|
|
5621
|
+
const hasDirectBunTestImports = await appUsesDirectBunTestImports(
|
|
5622
|
+
targetDir,
|
|
5623
|
+
files,
|
|
5624
|
+
);
|
|
4763
5625
|
|
|
4764
|
-
|
|
4765
|
-
{
|
|
5626
|
+
if (!scripts.test) {
|
|
5627
|
+
diagnostics.push({
|
|
4766
5628
|
severity: "warning",
|
|
4767
5629
|
code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
|
|
4768
5630
|
file: "package.json",
|
|
4769
5631
|
message:
|
|
4770
|
-
'package.json does not define a test script. Add "test": "
|
|
4771
|
-
}
|
|
4772
|
-
|
|
5632
|
+
'package.json does not define a test script. Add "test": "tsx lib/beignet-test-runner.ts" or run beignet doctor --fix.',
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
const legacyScripts = hasDirectBunTestImports
|
|
5637
|
+
? []
|
|
5638
|
+
: legacyGeneratedScriptNames(scripts);
|
|
5639
|
+
if (legacyScripts.length > 0) {
|
|
5640
|
+
diagnostics.push({
|
|
5641
|
+
severity: "warning",
|
|
5642
|
+
code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
|
|
5643
|
+
file: "package.json",
|
|
5644
|
+
message: `package.json still uses legacy generated Bun scripts for ${legacyScripts.join(", ")}. Run beignet doctor --fix to migrate them to package-manager-neutral tsx scripts.`,
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
const needsTsx =
|
|
5649
|
+
hasCurrentGeneratedTsxScript(scripts) &&
|
|
5650
|
+
!packageJson.dependencies?.tsx &&
|
|
5651
|
+
!packageJson.devDependencies?.tsx;
|
|
5652
|
+
if (needsTsx) {
|
|
5653
|
+
diagnostics.push({
|
|
5654
|
+
severity: "warning",
|
|
5655
|
+
code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
|
|
5656
|
+
file: "package.json",
|
|
5657
|
+
message:
|
|
5658
|
+
'package.json uses generated tsx scripts but does not depend on tsx. Add "tsx" to devDependencies or run beignet doctor --fix.',
|
|
5659
|
+
});
|
|
5660
|
+
}
|
|
5661
|
+
|
|
5662
|
+
const missingSupportFiles = missingTestSupportFiles(files);
|
|
5663
|
+
if (
|
|
5664
|
+
scripts.test === currentGeneratedPackageScripts.test &&
|
|
5665
|
+
missingSupportFiles.length > 0
|
|
5666
|
+
) {
|
|
5667
|
+
diagnostics.push({
|
|
5668
|
+
severity: "warning",
|
|
5669
|
+
code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
|
|
5670
|
+
file: "package.json",
|
|
5671
|
+
message: `package.json uses the generated test runner, but ${missingSupportFiles.join(", ")} ${missingSupportFiles.length === 1 ? "is" : "are"} missing. Run beignet doctor --fix to restore test support files.`,
|
|
5672
|
+
});
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
return diagnostics;
|
|
4773
5676
|
}
|
|
4774
5677
|
|
|
4775
|
-
async function
|
|
5678
|
+
async function fixPackageScripts(
|
|
4776
5679
|
targetDir: string,
|
|
4777
5680
|
files: string[],
|
|
4778
5681
|
convention: InspectConvention,
|
|
@@ -4785,20 +5688,162 @@ async function fixMissingTestScript(
|
|
|
4785
5688
|
const original = await readFile(filePath, "utf8");
|
|
4786
5689
|
const packageJson = JSON.parse(original) as {
|
|
4787
5690
|
scripts?: Record<string, string>;
|
|
5691
|
+
dependencies?: Record<string, string>;
|
|
5692
|
+
devDependencies?: Record<string, string>;
|
|
4788
5693
|
};
|
|
4789
5694
|
packageJson.scripts = packageJson.scripts ?? {};
|
|
4790
|
-
|
|
5695
|
+
const updatedScripts: string[] = [];
|
|
5696
|
+
const addedTestScript = !packageJson.scripts.test;
|
|
5697
|
+
const hasDirectBunTestImports = await appUsesDirectBunTestImports(
|
|
5698
|
+
targetDir,
|
|
5699
|
+
files,
|
|
5700
|
+
);
|
|
5701
|
+
|
|
5702
|
+
if (addedTestScript) {
|
|
5703
|
+
packageJson.scripts.test = currentGeneratedPackageScripts.test;
|
|
5704
|
+
updatedScripts.push("test");
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
if (!hasDirectBunTestImports) {
|
|
5708
|
+
for (const [name, legacyCommand] of Object.entries(
|
|
5709
|
+
legacyGeneratedPackageScripts,
|
|
5710
|
+
)) {
|
|
5711
|
+
if (packageJson.scripts[name] === legacyCommand) {
|
|
5712
|
+
packageJson.scripts[name] = currentGeneratedPackageScripts[name];
|
|
5713
|
+
if (!updatedScripts.includes(name)) updatedScripts.push(name);
|
|
5714
|
+
}
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5718
|
+
const needsTsx = hasCurrentGeneratedTsxScript(packageJson.scripts);
|
|
5719
|
+
const addedTsx =
|
|
5720
|
+
needsTsx &&
|
|
5721
|
+
!packageJson.dependencies?.tsx &&
|
|
5722
|
+
!packageJson.devDependencies?.tsx;
|
|
5723
|
+
if (addedTsx) {
|
|
5724
|
+
packageJson.devDependencies = packageJson.devDependencies ?? {};
|
|
5725
|
+
packageJson.devDependencies.tsx = externalVersions.tsx;
|
|
5726
|
+
}
|
|
4791
5727
|
|
|
4792
|
-
packageJson.scripts.test = "bun test";
|
|
4793
5728
|
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
4794
|
-
|
|
5729
|
+
const createdSupportFiles = await createMissingTestSupportFiles(
|
|
5730
|
+
targetDir,
|
|
5731
|
+
files,
|
|
5732
|
+
packageJson.scripts.test === currentGeneratedPackageScripts.test,
|
|
5733
|
+
);
|
|
4795
5734
|
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
5735
|
+
if (next === original && createdSupportFiles.length === 0) return undefined;
|
|
5736
|
+
|
|
5737
|
+
if (next !== original) await writeFile(filePath, next);
|
|
5738
|
+
|
|
5739
|
+
if (addedTestScript) {
|
|
5740
|
+
return {
|
|
5741
|
+
code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
|
|
5742
|
+
file: "package.json",
|
|
5743
|
+
message:
|
|
5744
|
+
createdSupportFiles.length > 0
|
|
5745
|
+
? `Added "test": "tsx lib/beignet-test-runner.ts" and ${createdSupportFiles.join(", ")}.`
|
|
5746
|
+
: 'Added "test": "tsx lib/beignet-test-runner.ts".',
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
if (updatedScripts.length > 0) {
|
|
5751
|
+
return {
|
|
5752
|
+
code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
|
|
5753
|
+
file: "package.json",
|
|
5754
|
+
message: packageScriptFixMessage(
|
|
5755
|
+
updatedScripts,
|
|
5756
|
+
createdSupportFiles,
|
|
5757
|
+
addedTsx,
|
|
5758
|
+
),
|
|
5759
|
+
};
|
|
5760
|
+
}
|
|
5761
|
+
|
|
5762
|
+
if (createdSupportFiles.length > 0) {
|
|
5763
|
+
return {
|
|
5764
|
+
code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
|
|
5765
|
+
file: "package.json",
|
|
5766
|
+
message: `Restored ${createdSupportFiles.join(", ")}.`,
|
|
5767
|
+
};
|
|
5768
|
+
}
|
|
5769
|
+
|
|
5770
|
+
if (addedTsx) {
|
|
5771
|
+
return {
|
|
5772
|
+
code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
|
|
5773
|
+
file: "package.json",
|
|
5774
|
+
message: 'Added "tsx" to devDependencies.',
|
|
5775
|
+
};
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5778
|
+
return undefined;
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
function legacyGeneratedScriptNames(scripts: Record<string, string>): string[] {
|
|
5782
|
+
return Object.entries(legacyGeneratedPackageScripts)
|
|
5783
|
+
.filter(([name, command]) => scripts[name] === command)
|
|
5784
|
+
.map(([name]) => name);
|
|
5785
|
+
}
|
|
5786
|
+
|
|
5787
|
+
async function appUsesDirectBunTestImports(
|
|
5788
|
+
targetDir: string,
|
|
5789
|
+
files: string[],
|
|
5790
|
+
): Promise<boolean> {
|
|
5791
|
+
for (const file of files) {
|
|
5792
|
+
if (!isTestSourceFile(file)) continue;
|
|
5793
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
5794
|
+
if (/\bfrom\s*["']bun:test["']/.test(source)) return true;
|
|
5795
|
+
if (/\bimport\s*\(\s*["']bun:test["']\s*\)/.test(source)) return true;
|
|
5796
|
+
}
|
|
5797
|
+
return false;
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
function hasCurrentGeneratedTsxScript(
|
|
5801
|
+
scripts: Record<string, string>,
|
|
5802
|
+
): boolean {
|
|
5803
|
+
return Object.values(currentGeneratedPackageScripts).some(
|
|
5804
|
+
(command) =>
|
|
5805
|
+
command.startsWith("tsx ") && Object.values(scripts).includes(command),
|
|
5806
|
+
);
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
function missingTestSupportFiles(files: string[]): string[] {
|
|
5810
|
+
return testSupportTemplateFiles
|
|
5811
|
+
.map((file) => file.path)
|
|
5812
|
+
.filter((file) => !files.includes(file));
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5815
|
+
async function createMissingTestSupportFiles(
|
|
5816
|
+
targetDir: string,
|
|
5817
|
+
files: string[],
|
|
5818
|
+
shouldCreate: boolean,
|
|
5819
|
+
): Promise<string[]> {
|
|
5820
|
+
if (!shouldCreate) return [];
|
|
5821
|
+
const createdSupportFiles: string[] = [];
|
|
5822
|
+
for (const file of testSupportTemplateFiles) {
|
|
5823
|
+
if (files.includes(file.path)) continue;
|
|
5824
|
+
const destination = path.join(targetDir, file.path);
|
|
5825
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
5826
|
+
await writeFile(destination, file.content);
|
|
5827
|
+
createdSupportFiles.push(file.path);
|
|
5828
|
+
}
|
|
5829
|
+
return createdSupportFiles;
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
function packageScriptFixMessage(
|
|
5833
|
+
updatedScripts: string[],
|
|
5834
|
+
createdSupportFiles: string[],
|
|
5835
|
+
addedTsx: boolean,
|
|
5836
|
+
): string {
|
|
5837
|
+
const parts = [
|
|
5838
|
+
`Updated generated package scripts: ${updatedScripts.join(", ")}.`,
|
|
5839
|
+
];
|
|
5840
|
+
if (createdSupportFiles.length > 0) {
|
|
5841
|
+
parts.push(`Restored ${createdSupportFiles.join(", ")}.`);
|
|
5842
|
+
}
|
|
5843
|
+
if (addedTsx) {
|
|
5844
|
+
parts.push('Added "tsx" to devDependencies.');
|
|
5845
|
+
}
|
|
5846
|
+
return parts.join(" ");
|
|
4802
5847
|
}
|
|
4803
5848
|
|
|
4804
5849
|
function unmatchedRouteDiagnostic(
|
|
@@ -5475,14 +6520,15 @@ async function fixUnregisteredTasks(
|
|
|
5475
6520
|
}
|
|
5476
6521
|
|
|
5477
6522
|
/**
|
|
5478
|
-
* Wire fully unregistered feature listener registries into the
|
|
5479
|
-
*
|
|
5480
|
-
* generates, so both
|
|
6523
|
+
* Wire fully unregistered feature listener registries into the central
|
|
6524
|
+
* listener registry and ensure the providers file registers that central
|
|
6525
|
+
* registry with the same block `beignet make listener` generates, so both
|
|
6526
|
+
* write paths stay byte-compatible.
|
|
5481
6527
|
*
|
|
5482
|
-
* Bails without writing when the providers file or
|
|
5483
|
-
*
|
|
5484
|
-
* against, or when the registry name is already imported from another
|
|
5485
|
-
* The diagnostic stays in every bail case.
|
|
6528
|
+
* Bails without writing when the providers file or central listener registry
|
|
6529
|
+
* is customized beyond the known anchors, when the app has no eventBus port to
|
|
6530
|
+
* register against, or when the registry name is already imported from another
|
|
6531
|
+
* file. The diagnostic stays in every bail case.
|
|
5486
6532
|
*/
|
|
5487
6533
|
async function fixUnregisteredListeners(
|
|
5488
6534
|
targetDir: string,
|
|
@@ -5518,50 +6564,86 @@ async function fixUnregisteredListeners(
|
|
|
5518
6564
|
if (!/\beventBus\s*[:?]/.test(portsSource)) return undefined;
|
|
5519
6565
|
|
|
5520
6566
|
const providersPath = path.join(targetDir, providersFile);
|
|
5521
|
-
const
|
|
5522
|
-
|
|
5523
|
-
const
|
|
6567
|
+
const originalProviders = await readFile(providersPath, "utf8");
|
|
6568
|
+
const listenersFile = listenersFilePath(config);
|
|
6569
|
+
const listenersPath = path.join(targetDir, listenersFile);
|
|
6570
|
+
const originalListeners = files.includes(listenersFile)
|
|
6571
|
+
? await readFile(listenersPath, "utf8")
|
|
6572
|
+
: undefined;
|
|
6573
|
+
let nextListeners = originalListeners;
|
|
6574
|
+
const candidateNames: string[] = [];
|
|
5524
6575
|
|
|
5525
6576
|
for (const { registry } of candidates) {
|
|
5526
|
-
|
|
6577
|
+
candidateNames.push(registry.registryName);
|
|
6578
|
+
const imported =
|
|
6579
|
+
nextListeners === undefined
|
|
6580
|
+
? undefined
|
|
6581
|
+
: parseNamedImportSources(nextListeners).get(registry.registryName);
|
|
5527
6582
|
if (imported) {
|
|
5528
6583
|
const importedFile = sourceFileFromImport(
|
|
5529
6584
|
imported.sourcePath,
|
|
5530
|
-
|
|
6585
|
+
listenersFile,
|
|
5531
6586
|
files,
|
|
5532
6587
|
);
|
|
5533
6588
|
if (importedFile !== registry.indexFile) return undefined;
|
|
5534
6589
|
}
|
|
5535
6590
|
|
|
5536
|
-
const
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
6591
|
+
const listenerModule = `@/${modulePath(registry.indexFile)}`;
|
|
6592
|
+
const result =
|
|
6593
|
+
nextListeners === undefined
|
|
6594
|
+
? {
|
|
6595
|
+
kind: "updated" as const,
|
|
6596
|
+
source: createListenersRegistrySource({
|
|
6597
|
+
registryName: registry.registryName,
|
|
6598
|
+
listenerModule,
|
|
6599
|
+
}),
|
|
6600
|
+
}
|
|
6601
|
+
: updateListenersRegistrySource(nextListeners, {
|
|
6602
|
+
registryName: registry.registryName,
|
|
6603
|
+
listenerModule,
|
|
6604
|
+
});
|
|
5542
6605
|
if (result.kind === "missing") return undefined;
|
|
5543
6606
|
if (result.kind === "updated") {
|
|
5544
|
-
|
|
5545
|
-
registeredNames.push(registry.registryName);
|
|
6607
|
+
nextListeners = result.source;
|
|
5546
6608
|
}
|
|
5547
6609
|
}
|
|
5548
6610
|
|
|
5549
|
-
if (
|
|
6611
|
+
if (nextListeners === undefined || candidateNames.length === 0) {
|
|
6612
|
+
return undefined;
|
|
6613
|
+
}
|
|
5550
6614
|
|
|
5551
|
-
|
|
6615
|
+
const providerResult = wireListenersProviderSource(originalProviders, {
|
|
6616
|
+
registryName: candidateNames[0] ?? "listeners",
|
|
6617
|
+
featureKebab: "app",
|
|
6618
|
+
listenerModule: `@/${modulePath(listenersFile)}`,
|
|
6619
|
+
config,
|
|
6620
|
+
});
|
|
6621
|
+
if (providerResult.kind === "missing") return undefined;
|
|
6622
|
+
|
|
6623
|
+
const nextProviders =
|
|
6624
|
+
providerResult.kind === "updated"
|
|
6625
|
+
? providerResult.source
|
|
6626
|
+
: originalProviders;
|
|
6627
|
+
|
|
6628
|
+
if (
|
|
6629
|
+
nextListeners === originalListeners &&
|
|
6630
|
+
nextProviders === originalProviders
|
|
6631
|
+
) {
|
|
6632
|
+
return undefined;
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6635
|
+
await mkdir(path.dirname(listenersPath), { recursive: true });
|
|
6636
|
+
await writeFile(listenersPath, nextListeners);
|
|
6637
|
+
if (nextProviders !== originalProviders) {
|
|
6638
|
+
await writeFile(providersPath, nextProviders);
|
|
6639
|
+
}
|
|
5552
6640
|
return {
|
|
5553
6641
|
code: "BEIGNET_LISTENER_UNREGISTERED",
|
|
5554
|
-
file:
|
|
5555
|
-
message: `Registered ${
|
|
6642
|
+
file: listenersFile,
|
|
6643
|
+
message: `Registered ${candidateNames.join(", ")} in ${listenersFile} and wired listeners with registerListeners(...) in ${providersFile}.`,
|
|
5556
6644
|
};
|
|
5557
6645
|
}
|
|
5558
6646
|
|
|
5559
|
-
function listenerFeatureKebab(indexFile: string): string {
|
|
5560
|
-
return path.posix.basename(
|
|
5561
|
-
path.posix.dirname(path.posix.dirname(normalizePath(indexFile))),
|
|
5562
|
-
);
|
|
5563
|
-
}
|
|
5564
|
-
|
|
5565
6647
|
async function fixUnregisteredOutboxEntries(
|
|
5566
6648
|
targetDir: string,
|
|
5567
6649
|
files: string[],
|
|
@@ -6266,7 +7348,9 @@ async function inspectResourceSlices(
|
|
|
6266
7348
|
sourceIncludesAny(portSource, [
|
|
6267
7349
|
`update(input: Update${singularPascal}Input)`,
|
|
6268
7350
|
`update(input: Update${singularPascal}RepositoryInput)`,
|
|
7351
|
+
`update(input: Update${singularPascal}Input, scope: TenantScope)`,
|
|
6269
7352
|
"delete(id: string): Promise<boolean>",
|
|
7353
|
+
"delete(id: string, scope: TenantScope): Promise<boolean>",
|
|
6270
7354
|
]);
|
|
6271
7355
|
|
|
6272
7356
|
if (!hasCrudSignals) continue;
|
|
@@ -6304,6 +7388,7 @@ async function inspectResourceSlices(
|
|
|
6304
7388
|
alternatives: [
|
|
6305
7389
|
`findById(id: string): Promise<${singularPascal} | null>`,
|
|
6306
7390
|
`findById(id: string, filter: ${singularPascal}TenantFilter): Promise<${singularPascal} | null>`,
|
|
7391
|
+
`findById(id: string, scope: TenantScope): Promise<${singularPascal} | null>`,
|
|
6307
7392
|
],
|
|
6308
7393
|
},
|
|
6309
7394
|
{
|
|
@@ -6311,6 +7396,7 @@ async function inspectResourceSlices(
|
|
|
6311
7396
|
alternatives: [
|
|
6312
7397
|
`update(input: Update${singularPascal}Input)`,
|
|
6313
7398
|
`update(input: Update${singularPascal}RepositoryInput)`,
|
|
7399
|
+
`update(input: Update${singularPascal}Input, scope: TenantScope)`,
|
|
6314
7400
|
],
|
|
6315
7401
|
},
|
|
6316
7402
|
{
|
|
@@ -6318,6 +7404,7 @@ async function inspectResourceSlices(
|
|
|
6318
7404
|
alternatives: [
|
|
6319
7405
|
"delete(id: string): Promise<boolean>",
|
|
6320
7406
|
`delete(id: string, filter: ${singularPascal}TenantFilter): Promise<boolean>`,
|
|
7407
|
+
"delete(id: string, scope: TenantScope): Promise<boolean>",
|
|
6321
7408
|
],
|
|
6322
7409
|
},
|
|
6323
7410
|
];
|
|
@@ -6460,6 +7547,16 @@ function sourceIncludesAny(
|
|
|
6460
7547
|
return source ? snippets.some((snippet) => source.includes(snippet)) : false;
|
|
6461
7548
|
}
|
|
6462
7549
|
|
|
7550
|
+
function countSourceOccurrences(source: string, snippet: string): number {
|
|
7551
|
+
let count = 0;
|
|
7552
|
+
let index = source.indexOf(snippet);
|
|
7553
|
+
while (index !== -1) {
|
|
7554
|
+
count += 1;
|
|
7555
|
+
index = source.indexOf(snippet, index + snippet.length);
|
|
7556
|
+
}
|
|
7557
|
+
return count;
|
|
7558
|
+
}
|
|
7559
|
+
|
|
6463
7560
|
function sourceIncludesAll(
|
|
6464
7561
|
source: string,
|
|
6465
7562
|
snippets: readonly string[],
|