@beignet/cli 0.0.23 → 0.0.25
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 +28 -0
- package/README.md +55 -6
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +30 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +201 -1
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +238 -235
- package/dist/inspect.js.map +1 -1
- package/dist/make.js +16 -15
- package/dist/make.js.map +1 -1
- package/dist/provider-audit.d.ts +131 -0
- package/dist/provider-audit.d.ts.map +1 -0
- package/dist/provider-audit.js +1013 -0
- package/dist/provider-audit.js.map +1 -0
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +3 -2
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/mysql.js +2 -2
- package/dist/templates/db/postgres.js +2 -2
- package/dist/templates/db/sqlite.js +2 -2
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +1 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts +1 -0
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +90 -20
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/todos.js +5 -5
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +22 -4
- package/src/config.ts +99 -0
- package/src/db.ts +316 -1
- package/src/index.ts +180 -1
- package/src/inspect.ts +412 -369
- package/src/make.ts +16 -15
- package/src/provider-audit.ts +1681 -0
- package/src/templates/base.ts +3 -2
- package/src/templates/db/mysql.ts +2 -2
- package/src/templates/db/postgres.ts +2 -2
- package/src/templates/db/sqlite.ts +2 -2
- package/src/templates/index.ts +1 -0
- package/src/templates/server.ts +90 -20
- package/src/templates/todos.ts +5 -5
package/src/inspect.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
type ProviderPackageMetadata,
|
|
6
|
-
parseProviderPackageMetadata,
|
|
7
|
-
} from "@beignet/core/providers";
|
|
3
|
+
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
8
4
|
import { createPainter } from "./ansi.js";
|
|
9
5
|
import {
|
|
10
6
|
type BeignetConfig,
|
|
7
|
+
type BeignetDatabaseTables,
|
|
11
8
|
clientDirPath,
|
|
12
9
|
clientFormsPath,
|
|
13
10
|
clientIndexPath,
|
|
11
|
+
defaultBeignetConfig,
|
|
14
12
|
directoryPath,
|
|
15
13
|
loadBeignetConfig,
|
|
16
14
|
normalizePath,
|
|
@@ -21,6 +19,21 @@ import {
|
|
|
21
19
|
formatGithubAnnotation,
|
|
22
20
|
type GithubAnnotationSeverity,
|
|
23
21
|
} from "./github-annotations.js";
|
|
22
|
+
import {
|
|
23
|
+
detectedProviderVariants,
|
|
24
|
+
diagnosticPackageJsonFile,
|
|
25
|
+
formatProviderMetadataIssues,
|
|
26
|
+
installedPackageNames,
|
|
27
|
+
providerDoctorRulesForInstalledPackages,
|
|
28
|
+
providerOperationalConfigFiles,
|
|
29
|
+
providerRequiredEnvExists,
|
|
30
|
+
providerRequiredTableExists,
|
|
31
|
+
providerVariantRegistrationSeverity,
|
|
32
|
+
readProviderListEntries,
|
|
33
|
+
readProviderPackageMetadataSource,
|
|
34
|
+
registrationTokenHint,
|
|
35
|
+
serverProviderFiles,
|
|
36
|
+
} from "./provider-audit.js";
|
|
24
37
|
import {
|
|
25
38
|
type AppendResult,
|
|
26
39
|
appendToArrayExpression,
|
|
@@ -363,10 +376,58 @@ export function formatDoctor(
|
|
|
363
376
|
);
|
|
364
377
|
}
|
|
365
378
|
|
|
379
|
+
const checklist = productionHardeningChecklist(result.diagnostics);
|
|
380
|
+
if (checklist.length > 0) {
|
|
381
|
+
lines.push("", paint("Production Hardening Checklist:", "bold"), "");
|
|
382
|
+
lines.push(...checklist.map((item) => `- ${item}`));
|
|
383
|
+
}
|
|
384
|
+
|
|
366
385
|
lines.push("", formatDoctorFooter(result));
|
|
367
386
|
return lines.join("\n");
|
|
368
387
|
}
|
|
369
388
|
|
|
389
|
+
const productionHardeningDiagnosticCodes = new Set([
|
|
390
|
+
"BEIGNET_AUTH_ROUTE_MISSING",
|
|
391
|
+
"BEIGNET_AUTH_TRUSTED_ORIGINS_MISSING",
|
|
392
|
+
"BEIGNET_BETTER_AUTH_SECRET_PLACEHOLDER",
|
|
393
|
+
"BEIGNET_BILLING_CHECKOUT_IDEMPOTENCY_MISSING",
|
|
394
|
+
"BEIGNET_BILLING_ENTITLEMENTS_PROVIDER_MISSING",
|
|
395
|
+
"BEIGNET_BILLING_IDEMPOTENCY_PORT_MISSING",
|
|
396
|
+
"BEIGNET_CORS_CREDENTIALS_WILDCARD",
|
|
397
|
+
"BEIGNET_CRON_SECRET_MISSING",
|
|
398
|
+
"BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
|
|
399
|
+
"BEIGNET_ENTITLEMENTS_PORT_MISSING",
|
|
400
|
+
"BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
|
|
401
|
+
"BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
|
|
402
|
+
"BEIGNET_PROVIDER_ENV_MISSING",
|
|
403
|
+
"BEIGNET_PROVIDER_TABLE_MISSING",
|
|
404
|
+
"BEIGNET_READINESS_ROUTE_MISSING",
|
|
405
|
+
"BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
406
|
+
"BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
|
|
407
|
+
"BEIGNET_UPLOAD_SIZE_LIMIT_MISSING",
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
function productionHardeningChecklist(
|
|
411
|
+
diagnostics: InspectDiagnostic[],
|
|
412
|
+
): string[] {
|
|
413
|
+
if (
|
|
414
|
+
!diagnostics.some((diagnostic) =>
|
|
415
|
+
productionHardeningDiagnosticCodes.has(diagnostic.code),
|
|
416
|
+
)
|
|
417
|
+
) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return [
|
|
422
|
+
"Secrets and provider credentials are unique per environment, validated at startup, never committed, and never logged.",
|
|
423
|
+
"Auth routes, tenant resolution, authorization policies, and trusted origins derive authority from verified sessions, API keys, or trusted gateway metadata.",
|
|
424
|
+
"Devtools, OpenAPI, cron, webhooks, and operational routes have intentional exposure and app-owned authorization.",
|
|
425
|
+
"CORS origins, proxy IP trust, rate-limit keys, and request body limits match the production host topology.",
|
|
426
|
+
"Uploads and storage define max sizes, authorization or explicit public access, object visibility, safe content headers, and short direct-upload expirations.",
|
|
427
|
+
"Provider-backed dependencies have bounded readiness checks, worker shutdown behavior, webhook secrets, and least-privilege credentials.",
|
|
428
|
+
];
|
|
429
|
+
}
|
|
430
|
+
|
|
370
431
|
function formatDoctorFooter(result: InspectAppResult): string {
|
|
371
432
|
const count = (severity: InspectDiagnostic["severity"]) =>
|
|
372
433
|
result.diagnostics.filter((diagnostic) => diagnostic.severity === severity)
|
|
@@ -1870,30 +1931,6 @@ async function inspectRouteErrorCatalogDrift(
|
|
|
1870
1931
|
return diagnostics;
|
|
1871
1932
|
}
|
|
1872
1933
|
|
|
1873
|
-
type ProviderDoctorVariantRule = {
|
|
1874
|
-
name: string;
|
|
1875
|
-
displayName: string;
|
|
1876
|
-
tokens: readonly string[];
|
|
1877
|
-
requiredEnv: readonly string[];
|
|
1878
|
-
registrationSeverity?: "warning" | "hint";
|
|
1879
|
-
};
|
|
1880
|
-
|
|
1881
|
-
type ProviderDoctorRule = {
|
|
1882
|
-
packageName: string;
|
|
1883
|
-
displayName: string;
|
|
1884
|
-
tokens: readonly string[];
|
|
1885
|
-
appPorts?: readonly { name: string; type: string }[];
|
|
1886
|
-
requiredEnv?: readonly string[];
|
|
1887
|
-
registrationSeverity?: "warning" | "hint";
|
|
1888
|
-
variants?: readonly ProviderDoctorVariantRule[];
|
|
1889
|
-
};
|
|
1890
|
-
|
|
1891
|
-
type ProviderPackageMetadataSource = {
|
|
1892
|
-
packageName: string;
|
|
1893
|
-
file: string;
|
|
1894
|
-
metadata: unknown;
|
|
1895
|
-
};
|
|
1896
|
-
|
|
1897
1934
|
const readinessRelevantProviderPackages = new Set([
|
|
1898
1935
|
"@beignet/provider-auth-better-auth",
|
|
1899
1936
|
"@beignet/provider-db-drizzle",
|
|
@@ -1928,6 +1965,10 @@ async function inspectProductionReadiness(
|
|
|
1928
1965
|
packageJson,
|
|
1929
1966
|
);
|
|
1930
1967
|
const configFiles = operationalConfigFiles(files, config);
|
|
1968
|
+
const infrastructurePath = directoryPath(
|
|
1969
|
+
path.dirname(config.paths.infrastructurePorts),
|
|
1970
|
+
);
|
|
1971
|
+
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
1931
1972
|
|
|
1932
1973
|
for (const file of files) {
|
|
1933
1974
|
if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
|
|
@@ -1974,6 +2015,32 @@ async function inspectProductionReadiness(
|
|
|
1974
2015
|
});
|
|
1975
2016
|
}
|
|
1976
2017
|
|
|
2018
|
+
if (
|
|
2019
|
+
isFeatureUploadDefinition(file, config) &&
|
|
2020
|
+
containsCallExpression(source, "defineUpload") &&
|
|
2021
|
+
!/\bauthorize\s*(?:\(|:)/.test(source) &&
|
|
2022
|
+
!/\baccess\s*:\s*["']public["']/.test(source)
|
|
2023
|
+
) {
|
|
2024
|
+
diagnostics.push({
|
|
2025
|
+
severity: "warning",
|
|
2026
|
+
code: "BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
|
|
2027
|
+
file,
|
|
2028
|
+
message: `${file} defines a protected upload without authorize(...). Add an authorize hook, or set access: "public" only for intentionally public uploads.`,
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
if (
|
|
2033
|
+
isRouteServerOrInfraFile(file, config) &&
|
|
2034
|
+
containsRequestTenantHeaderAuthority(source)
|
|
2035
|
+
) {
|
|
2036
|
+
diagnostics.push({
|
|
2037
|
+
severity: "warning",
|
|
2038
|
+
code: "BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
2039
|
+
file,
|
|
2040
|
+
message: `${file} reads x-tenant-id and turns it into tenant authority. Resolve request tenants from verified auth/session claims or trusted gateway metadata instead of a caller-controlled header.`,
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1977
2044
|
if (
|
|
1978
2045
|
isRouteServerOrInfraFile(file, config) &&
|
|
1979
2046
|
containsCallExpression(source, "createCorsHooks") &&
|
|
@@ -1991,6 +2058,19 @@ async function inspectProductionReadiness(
|
|
|
1991
2058
|
}
|
|
1992
2059
|
}
|
|
1993
2060
|
|
|
2061
|
+
for (const file of configFiles) {
|
|
2062
|
+
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2063
|
+
|
|
2064
|
+
if (containsBetterAuthLocalSecret(source)) {
|
|
2065
|
+
diagnostics.push({
|
|
2066
|
+
severity: "warning",
|
|
2067
|
+
code: "BEIGNET_BETTER_AUTH_SECRET_PLACEHOLDER",
|
|
2068
|
+
file,
|
|
2069
|
+
message: `${file} contains the known local Better Auth placeholder secret. Keep it out of production configuration and require a unique BETTER_AUTH_SECRET before deployment.`,
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
1994
2074
|
const providerEntries = await readProviderListEntries(
|
|
1995
2075
|
targetDir,
|
|
1996
2076
|
files,
|
|
@@ -2034,7 +2114,12 @@ async function inspectProductionReadiness(
|
|
|
2034
2114
|
)) {
|
|
2035
2115
|
for (const envVar of variant.requiredEnv) {
|
|
2036
2116
|
if (
|
|
2037
|
-
await
|
|
2117
|
+
await providerRequiredEnvExists(
|
|
2118
|
+
targetDir,
|
|
2119
|
+
configFiles,
|
|
2120
|
+
envVar,
|
|
2121
|
+
sourceCache,
|
|
2122
|
+
)
|
|
2038
2123
|
) {
|
|
2039
2124
|
continue;
|
|
2040
2125
|
}
|
|
@@ -2045,12 +2130,44 @@ async function inspectProductionReadiness(
|
|
|
2045
2130
|
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but ${envVar} is not mentioned in app env/config files. ${variant.displayName} requires ${envVar}; add the environment variable or remove the provider registration.`,
|
|
2046
2131
|
});
|
|
2047
2132
|
}
|
|
2133
|
+
|
|
2134
|
+
for (const tableName of configuredProviderRequiredTables(
|
|
2135
|
+
variant.requiredTables,
|
|
2136
|
+
config,
|
|
2137
|
+
)) {
|
|
2138
|
+
if (
|
|
2139
|
+
await providerRequiredTableExists(
|
|
2140
|
+
targetDir,
|
|
2141
|
+
files,
|
|
2142
|
+
config,
|
|
2143
|
+
infrastructurePath,
|
|
2144
|
+
schemaDir,
|
|
2145
|
+
tableName,
|
|
2146
|
+
sourceCache,
|
|
2147
|
+
)
|
|
2148
|
+
) {
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
diagnostics.push({
|
|
2152
|
+
severity: "warning",
|
|
2153
|
+
code: "BEIGNET_PROVIDER_TABLE_MISSING",
|
|
2154
|
+
file: schemaDir,
|
|
2155
|
+
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 generate, include it in your Drizzle schema/migrations, or remove the provider registration.`,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2048
2158
|
}
|
|
2049
2159
|
continue;
|
|
2050
2160
|
}
|
|
2051
2161
|
|
|
2052
2162
|
for (const envVar of provider.requiredEnv ?? []) {
|
|
2053
|
-
if (
|
|
2163
|
+
if (
|
|
2164
|
+
await providerRequiredEnvExists(
|
|
2165
|
+
targetDir,
|
|
2166
|
+
configFiles,
|
|
2167
|
+
envVar,
|
|
2168
|
+
sourceCache,
|
|
2169
|
+
)
|
|
2170
|
+
) {
|
|
2054
2171
|
continue;
|
|
2055
2172
|
}
|
|
2056
2173
|
diagnostics.push({
|
|
@@ -2060,6 +2177,31 @@ async function inspectProductionReadiness(
|
|
|
2060
2177
|
message: `${provider.packageName} is installed, but ${envVar} is not mentioned in app env/config files. Add the expected environment variable or remove the unused provider package.`,
|
|
2061
2178
|
});
|
|
2062
2179
|
}
|
|
2180
|
+
|
|
2181
|
+
for (const tableName of configuredProviderRequiredTables(
|
|
2182
|
+
provider.requiredTables ?? [],
|
|
2183
|
+
config,
|
|
2184
|
+
)) {
|
|
2185
|
+
if (
|
|
2186
|
+
await providerRequiredTableExists(
|
|
2187
|
+
targetDir,
|
|
2188
|
+
files,
|
|
2189
|
+
config,
|
|
2190
|
+
infrastructurePath,
|
|
2191
|
+
schemaDir,
|
|
2192
|
+
tableName,
|
|
2193
|
+
sourceCache,
|
|
2194
|
+
)
|
|
2195
|
+
) {
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
diagnostics.push({
|
|
2199
|
+
severity: "warning",
|
|
2200
|
+
code: "BEIGNET_PROVIDER_TABLE_MISSING",
|
|
2201
|
+
file: schemaDir,
|
|
2202
|
+
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 generate, include it in your Drizzle schema/migrations, or remove the unused provider package.`,
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2063
2205
|
}
|
|
2064
2206
|
|
|
2065
2207
|
if (installedPackages.has("@beignet/provider-auth-better-auth")) {
|
|
@@ -2138,6 +2280,34 @@ function containsReadinessCheck(source: string): boolean {
|
|
|
2138
2280
|
return /\bchecks\s*:/.test(source) || /\.checkHealth\s*\(/.test(source);
|
|
2139
2281
|
}
|
|
2140
2282
|
|
|
2283
|
+
function containsBetterAuthLocalSecret(source: string): boolean {
|
|
2284
|
+
if (!source.includes("local-dev-better-auth-secret-change-me")) return false;
|
|
2285
|
+
|
|
2286
|
+
const hasProductionRuntimeGuard =
|
|
2287
|
+
/\bisProductionRuntime\s*=\s*process\.env\.NODE_ENV\s*===\s*["']production["']\s*&&\s*process\.env\.NEXT_PHASE\s*!==\s*["']phase-production-build["']/.test(
|
|
2288
|
+
source,
|
|
2289
|
+
) &&
|
|
2290
|
+
/BETTER_AUTH_SECRET\s*:\s*isProductionRuntime\s*\?\s*BetterAuthSecret\s*:\s*BetterAuthSecret\.default\(LOCAL_BETTER_AUTH_SECRET\)/s.test(
|
|
2291
|
+
source,
|
|
2292
|
+
);
|
|
2293
|
+
const hasProductionGuard =
|
|
2294
|
+
/BETTER_AUTH_SECRET\s*:\s*process\.env\.NODE_ENV\s*===\s*["']production["']\s*\?\s*BetterAuthSecret\s*:\s*BetterAuthSecret\.default\(LOCAL_BETTER_AUTH_SECRET\)/s.test(
|
|
2295
|
+
source,
|
|
2296
|
+
);
|
|
2297
|
+
|
|
2298
|
+
return !(hasProductionRuntimeGuard || hasProductionGuard);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function containsRequestTenantHeaderAuthority(source: string): boolean {
|
|
2302
|
+
return (
|
|
2303
|
+
/(?:(?:\breq|\brequest)\.headers|(?<!\.)\bheaders)\.get\(\s*["']x-tenant-id["']\s*\)/.test(
|
|
2304
|
+
source,
|
|
2305
|
+
) &&
|
|
2306
|
+
(/\bcreateTenant\s*\(/.test(source) ||
|
|
2307
|
+
/\btenant\s*:\s*\{?\s*id\s*:/.test(source))
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2141
2311
|
async function inspectEntitlementsProductionReadiness(
|
|
2142
2312
|
targetDir: string,
|
|
2143
2313
|
files: string[],
|
|
@@ -2571,13 +2741,7 @@ function operationalConfigFiles(
|
|
|
2571
2741
|
files: string[],
|
|
2572
2742
|
config: ResolvedBeignetConfig,
|
|
2573
2743
|
): string[] {
|
|
2574
|
-
return files
|
|
2575
|
-
if (file === ".env.example") return true;
|
|
2576
|
-
if (file === "drizzle.config.ts") return true;
|
|
2577
|
-
if (file === "lib/env.ts" || file === "lib/better-auth.ts") return true;
|
|
2578
|
-
if (isInfraOrServerFile(file, config) && file.endsWith(".ts")) return true;
|
|
2579
|
-
return false;
|
|
2580
|
-
});
|
|
2744
|
+
return providerOperationalConfigFiles(files, config);
|
|
2581
2745
|
}
|
|
2582
2746
|
|
|
2583
2747
|
async function readPackageJson(
|
|
@@ -2598,83 +2762,6 @@ async function readPackageJson(
|
|
|
2598
2762
|
);
|
|
2599
2763
|
}
|
|
2600
2764
|
|
|
2601
|
-
async function providerDoctorRulesForInstalledPackages(
|
|
2602
|
-
targetDir: string,
|
|
2603
|
-
packageJson:
|
|
2604
|
-
| {
|
|
2605
|
-
dependencies?: Record<string, string>;
|
|
2606
|
-
devDependencies?: Record<string, string>;
|
|
2607
|
-
peerDependencies?: Record<string, string>;
|
|
2608
|
-
}
|
|
2609
|
-
| undefined,
|
|
2610
|
-
): Promise<ProviderDoctorRule[]> {
|
|
2611
|
-
const packageNames = [...installedPackageNames(packageJson)];
|
|
2612
|
-
const rules = await Promise.all(
|
|
2613
|
-
packageNames.map(async (packageName) => {
|
|
2614
|
-
const source = await readProviderPackageMetadataSource(
|
|
2615
|
-
targetDir,
|
|
2616
|
-
packageName,
|
|
2617
|
-
);
|
|
2618
|
-
if (!source) return undefined;
|
|
2619
|
-
const result = parseProviderPackageMetadata(source.metadata);
|
|
2620
|
-
if (!result.success) return undefined;
|
|
2621
|
-
return providerDoctorRuleFromMetadata(packageName, result.metadata);
|
|
2622
|
-
}),
|
|
2623
|
-
);
|
|
2624
|
-
|
|
2625
|
-
return rules.filter((rule): rule is ProviderDoctorRule => Boolean(rule));
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
async function readProviderPackageMetadataSource(
|
|
2629
|
-
targetDir: string,
|
|
2630
|
-
packageName: string,
|
|
2631
|
-
): Promise<ProviderPackageMetadataSource | undefined> {
|
|
2632
|
-
for (const candidate of providerPackageJsonCandidates(
|
|
2633
|
-
targetDir,
|
|
2634
|
-
packageName,
|
|
2635
|
-
)) {
|
|
2636
|
-
const packageJson = await readJsonIfExists<{
|
|
2637
|
-
beignet?: { provider?: unknown };
|
|
2638
|
-
}>(candidate);
|
|
2639
|
-
const metadata = packageJson?.beignet?.provider;
|
|
2640
|
-
if (metadata !== undefined) {
|
|
2641
|
-
return { packageName, file: candidate, metadata };
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
return undefined;
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2648
|
-
function providerPackageJsonCandidates(
|
|
2649
|
-
targetDir: string,
|
|
2650
|
-
packageName: string,
|
|
2651
|
-
): string[] {
|
|
2652
|
-
const candidates = [
|
|
2653
|
-
path.join(
|
|
2654
|
-
targetDir,
|
|
2655
|
-
"node_modules",
|
|
2656
|
-
...packageName.split("/"),
|
|
2657
|
-
"package.json",
|
|
2658
|
-
),
|
|
2659
|
-
];
|
|
2660
|
-
|
|
2661
|
-
if (packageName.startsWith("@beignet/")) {
|
|
2662
|
-
candidates.push(
|
|
2663
|
-
path.resolve(
|
|
2664
|
-
// import.meta.dir is Bun-only; the published bin also runs under
|
|
2665
|
-
// plain Node, where only import.meta.url is available.
|
|
2666
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
2667
|
-
"../../..",
|
|
2668
|
-
"packages",
|
|
2669
|
-
packageName.slice("@beignet/".length),
|
|
2670
|
-
"package.json",
|
|
2671
|
-
),
|
|
2672
|
-
);
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
return candidates;
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
2765
|
async function readJsonIfExists<T>(file: string): Promise<T | undefined> {
|
|
2679
2766
|
try {
|
|
2680
2767
|
return JSON.parse(await readFile(file, "utf8")) as T;
|
|
@@ -2690,218 +2777,6 @@ async function readJsonIfExists<T>(file: string): Promise<T | undefined> {
|
|
|
2690
2777
|
}
|
|
2691
2778
|
}
|
|
2692
2779
|
|
|
2693
|
-
function providerDoctorRuleFromMetadata(
|
|
2694
|
-
packageName: string,
|
|
2695
|
-
metadata: ProviderPackageMetadata,
|
|
2696
|
-
): ProviderDoctorRule {
|
|
2697
|
-
const registration = metadata.registration;
|
|
2698
|
-
|
|
2699
|
-
return {
|
|
2700
|
-
packageName,
|
|
2701
|
-
displayName: metadata.displayName ?? packageName,
|
|
2702
|
-
tokens: registration?.tokens ?? [],
|
|
2703
|
-
appPorts: metadata.appPorts ?? [],
|
|
2704
|
-
requiredEnv: metadata.requiredEnv ?? [],
|
|
2705
|
-
registrationSeverity: registrationSeverityFromMetadata(registration),
|
|
2706
|
-
variants: metadata.variants?.map((variant) => ({
|
|
2707
|
-
name: variant.name,
|
|
2708
|
-
displayName: variant.displayName ?? variant.name,
|
|
2709
|
-
tokens: variant.registration?.tokens ?? [],
|
|
2710
|
-
requiredEnv: variant.requiredEnv ?? [],
|
|
2711
|
-
registrationSeverity: registrationSeverityFromMetadata(
|
|
2712
|
-
variant.registration,
|
|
2713
|
-
),
|
|
2714
|
-
})),
|
|
2715
|
-
};
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
|
-
function registrationSeverityFromMetadata(
|
|
2719
|
-
registration:
|
|
2720
|
-
| { required?: boolean; severity?: "warning" | "hint" }
|
|
2721
|
-
| undefined,
|
|
2722
|
-
): "warning" | "hint" | undefined {
|
|
2723
|
-
return (
|
|
2724
|
-
registration?.severity ??
|
|
2725
|
-
(registration?.required === true ? "warning" : undefined)
|
|
2726
|
-
);
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
function detectedProviderVariants(
|
|
2730
|
-
variants: readonly ProviderDoctorVariantRule[],
|
|
2731
|
-
providerEntries: readonly string[],
|
|
2732
|
-
): ProviderDoctorVariantRule[] {
|
|
2733
|
-
return variants.filter((variant) =>
|
|
2734
|
-
variant.tokens.some((token) =>
|
|
2735
|
-
providerEntries.some((entry) => entry.includes(token)),
|
|
2736
|
-
),
|
|
2737
|
-
);
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
function providerVariantRegistrationSeverity(
|
|
2741
|
-
variants: readonly ProviderDoctorVariantRule[],
|
|
2742
|
-
): "warning" | "hint" | undefined {
|
|
2743
|
-
const severities = variants.map((variant) => variant.registrationSeverity);
|
|
2744
|
-
if (severities.includes("warning")) return "warning";
|
|
2745
|
-
if (severities.includes("hint")) return "hint";
|
|
2746
|
-
return undefined;
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
async function readProviderListEntries(
|
|
2750
|
-
targetDir: string,
|
|
2751
|
-
files: string[],
|
|
2752
|
-
config: ResolvedBeignetConfig,
|
|
2753
|
-
sourceCache: Map<string, string>,
|
|
2754
|
-
): Promise<string[]> {
|
|
2755
|
-
const providerFiles = serverProviderFiles(files, config);
|
|
2756
|
-
const providerSource = (
|
|
2757
|
-
await Promise.all(
|
|
2758
|
-
providerFiles.map((file) =>
|
|
2759
|
-
readCachedSource(targetDir, file, sourceCache),
|
|
2760
|
-
),
|
|
2761
|
-
)
|
|
2762
|
-
).join("\n");
|
|
2763
|
-
const providerListSource = extractProviderListSource(providerSource);
|
|
2764
|
-
return providerListSource === undefined
|
|
2765
|
-
? []
|
|
2766
|
-
: providerListEntries(providerListSource);
|
|
2767
|
-
}
|
|
2768
|
-
|
|
2769
|
-
function registrationTokenHint(tokens: readonly string[]): string {
|
|
2770
|
-
const token = tokens[0];
|
|
2771
|
-
if (!token) return "the provider";
|
|
2772
|
-
return token.startsWith("create") ? `${token}()` : token;
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
function diagnosticPackageJsonFile(targetDir: string, file: string): string {
|
|
2776
|
-
const relative = normalizePath(path.relative(targetDir, file));
|
|
2777
|
-
if (relative.startsWith("../")) return "package.json";
|
|
2778
|
-
return relative;
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
function formatProviderMetadataIssues(
|
|
2782
|
-
issues: readonly { path: string; message: string }[],
|
|
2783
|
-
): string {
|
|
2784
|
-
return issues.map((issue) => `${issue.path} ${issue.message}`).join("; ");
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
|
-
function installedPackageNames(
|
|
2788
|
-
packageJson:
|
|
2789
|
-
| {
|
|
2790
|
-
dependencies?: Record<string, string>;
|
|
2791
|
-
devDependencies?: Record<string, string>;
|
|
2792
|
-
peerDependencies?: Record<string, string>;
|
|
2793
|
-
}
|
|
2794
|
-
| undefined,
|
|
2795
|
-
): Set<string> {
|
|
2796
|
-
return new Set([
|
|
2797
|
-
...Object.keys(packageJson?.dependencies ?? {}),
|
|
2798
|
-
...Object.keys(packageJson?.devDependencies ?? {}),
|
|
2799
|
-
...Object.keys(packageJson?.peerDependencies ?? {}),
|
|
2800
|
-
]);
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
function serverProviderFiles(
|
|
2804
|
-
files: string[],
|
|
2805
|
-
config: ResolvedBeignetConfig,
|
|
2806
|
-
): string[] {
|
|
2807
|
-
const serverDir = directoryPath(path.dirname(config.paths.server));
|
|
2808
|
-
const candidates = [`${serverDir}/providers.ts`, config.paths.server];
|
|
2809
|
-
return candidates.filter((file) => files.includes(file));
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
function extractProviderListSource(source: string): string | undefined {
|
|
2813
|
-
const listMatches = [
|
|
2814
|
-
...source.matchAll(/\bproviders\s*=\s*\[([\s\S]*?)\]\s*(?:;|as const)/g),
|
|
2815
|
-
...source.matchAll(/\bproviders\s*:\s*\[([\s\S]*?)\]/g),
|
|
2816
|
-
];
|
|
2817
|
-
if (listMatches.length === 0) return undefined;
|
|
2818
|
-
return listMatches.map((match) => match[1]).join("\n");
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
function providerListEntries(source: string): string[] {
|
|
2822
|
-
const entries: string[] = [];
|
|
2823
|
-
let current = "";
|
|
2824
|
-
let depth = 0;
|
|
2825
|
-
let quote: '"' | "'" | "`" | undefined;
|
|
2826
|
-
let escaped = false;
|
|
2827
|
-
let lineComment = false;
|
|
2828
|
-
let blockComment = false;
|
|
2829
|
-
|
|
2830
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
2831
|
-
const char = source[index];
|
|
2832
|
-
const next = source[index + 1];
|
|
2833
|
-
|
|
2834
|
-
if (lineComment) {
|
|
2835
|
-
if (char === "\n") {
|
|
2836
|
-
lineComment = false;
|
|
2837
|
-
current += char;
|
|
2838
|
-
}
|
|
2839
|
-
continue;
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2842
|
-
if (blockComment) {
|
|
2843
|
-
if (char === "*" && next === "/") {
|
|
2844
|
-
blockComment = false;
|
|
2845
|
-
index += 1;
|
|
2846
|
-
}
|
|
2847
|
-
continue;
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
if (quote) {
|
|
2851
|
-
current += char;
|
|
2852
|
-
if (escaped) {
|
|
2853
|
-
escaped = false;
|
|
2854
|
-
continue;
|
|
2855
|
-
}
|
|
2856
|
-
if (char === "\\") {
|
|
2857
|
-
escaped = true;
|
|
2858
|
-
continue;
|
|
2859
|
-
}
|
|
2860
|
-
if (char === quote) {
|
|
2861
|
-
quote = undefined;
|
|
2862
|
-
}
|
|
2863
|
-
continue;
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
if (char === "/" && next === "/") {
|
|
2867
|
-
lineComment = true;
|
|
2868
|
-
index += 1;
|
|
2869
|
-
continue;
|
|
2870
|
-
}
|
|
2871
|
-
|
|
2872
|
-
if (char === "/" && next === "*") {
|
|
2873
|
-
blockComment = true;
|
|
2874
|
-
index += 1;
|
|
2875
|
-
continue;
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
2879
|
-
quote = char;
|
|
2880
|
-
current += char;
|
|
2881
|
-
continue;
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
if (char === "(" || char === "[" || char === "{") {
|
|
2885
|
-
depth += 1;
|
|
2886
|
-
} else if (char === ")" || char === "]" || char === "}") {
|
|
2887
|
-
depth = Math.max(0, depth - 1);
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
if (char === "," && depth === 0) {
|
|
2891
|
-
const entry = current.trim();
|
|
2892
|
-
if (entry) entries.push(entry);
|
|
2893
|
-
current = "";
|
|
2894
|
-
continue;
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
current += char;
|
|
2898
|
-
}
|
|
2899
|
-
|
|
2900
|
-
const entry = current.trim();
|
|
2901
|
-
if (entry) entries.push(entry);
|
|
2902
|
-
return entries;
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
2780
|
function findProviderEntryIndex(
|
|
2906
2781
|
entries: string[],
|
|
2907
2782
|
tokens: readonly string[],
|
|
@@ -4319,34 +4194,66 @@ async function inspectDatabaseLifecycleDrift(
|
|
|
4319
4194
|
|
|
4320
4195
|
type DurableDrizzleTableRequirement = {
|
|
4321
4196
|
capability: string;
|
|
4322
|
-
|
|
4197
|
+
table: keyof BeignetDatabaseTables;
|
|
4323
4198
|
code: string;
|
|
4324
|
-
|
|
4325
|
-
|
|
4199
|
+
factoryNames: readonly string[];
|
|
4200
|
+
setupNames: readonly string[];
|
|
4201
|
+
explicitFactoryPattern: string;
|
|
4202
|
+
explicitSetupPattern: string;
|
|
4326
4203
|
};
|
|
4327
4204
|
|
|
4328
4205
|
const durableDrizzleTableRequirements: readonly DurableDrizzleTableRequirement[] =
|
|
4329
4206
|
[
|
|
4330
4207
|
{
|
|
4331
4208
|
capability: "idempotency",
|
|
4332
|
-
|
|
4209
|
+
table: "idempotency",
|
|
4333
4210
|
code: "BEIGNET_IDEMPOTENCY_TABLE_MISSING",
|
|
4334
|
-
|
|
4335
|
-
|
|
4211
|
+
factoryNames: [
|
|
4212
|
+
"createDrizzleSqliteIdempotencyPort",
|
|
4213
|
+
"createDrizzlePostgresIdempotencyPort",
|
|
4214
|
+
"createDrizzleMysqlIdempotencyPort",
|
|
4215
|
+
],
|
|
4216
|
+
setupNames: [
|
|
4217
|
+
"createDrizzleSqliteIdempotencySetupStatements",
|
|
4218
|
+
"createDrizzlePostgresIdempotencySetupStatements",
|
|
4219
|
+
"createDrizzleMysqlIdempotencySetupStatements",
|
|
4220
|
+
],
|
|
4221
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*Idempotency(?:Port)?",
|
|
4222
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*IdempotencySetupStatements",
|
|
4336
4223
|
},
|
|
4337
4224
|
{
|
|
4338
4225
|
capability: "outbox",
|
|
4339
|
-
|
|
4226
|
+
table: "outbox",
|
|
4340
4227
|
code: "BEIGNET_OUTBOX_TABLE_MISSING",
|
|
4341
|
-
|
|
4342
|
-
|
|
4228
|
+
factoryNames: [
|
|
4229
|
+
"createDrizzleSqliteOutboxPort",
|
|
4230
|
+
"createDrizzlePostgresOutboxPort",
|
|
4231
|
+
"createDrizzleMysqlOutboxPort",
|
|
4232
|
+
],
|
|
4233
|
+
setupNames: [
|
|
4234
|
+
"createDrizzleSqliteOutboxSetupStatements",
|
|
4235
|
+
"createDrizzlePostgresOutboxSetupStatements",
|
|
4236
|
+
"createDrizzleMysqlOutboxSetupStatements",
|
|
4237
|
+
],
|
|
4238
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Port)?",
|
|
4239
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*OutboxSetupStatements",
|
|
4343
4240
|
},
|
|
4344
4241
|
{
|
|
4345
4242
|
capability: "audit",
|
|
4346
|
-
|
|
4243
|
+
table: "audit",
|
|
4347
4244
|
code: "BEIGNET_AUDIT_TABLE_MISSING",
|
|
4348
|
-
|
|
4349
|
-
|
|
4245
|
+
factoryNames: [
|
|
4246
|
+
"createDrizzleSqliteAuditLogPort",
|
|
4247
|
+
"createDrizzlePostgresAuditLogPort",
|
|
4248
|
+
"createDrizzleMysqlAuditLogPort",
|
|
4249
|
+
],
|
|
4250
|
+
setupNames: [
|
|
4251
|
+
"createDrizzleSqliteAuditLogSetupStatements",
|
|
4252
|
+
"createDrizzlePostgresAuditLogSetupStatements",
|
|
4253
|
+
"createDrizzleMysqlAuditLogSetupStatements",
|
|
4254
|
+
],
|
|
4255
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*AuditLog(?:Port)?",
|
|
4256
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*AuditLogSetupStatements",
|
|
4350
4257
|
},
|
|
4351
4258
|
];
|
|
4352
4259
|
|
|
@@ -4365,6 +4272,7 @@ async function inspectDurableDrizzleTableDrift(
|
|
|
4365
4272
|
const tableNames = await configuredDurableDrizzleTableNames(
|
|
4366
4273
|
targetDir,
|
|
4367
4274
|
usageFiles,
|
|
4275
|
+
config,
|
|
4368
4276
|
requirement,
|
|
4369
4277
|
sourceCache,
|
|
4370
4278
|
);
|
|
@@ -4375,6 +4283,7 @@ async function inspectDurableDrizzleTableDrift(
|
|
|
4375
4283
|
await durableDrizzleTableExists(
|
|
4376
4284
|
targetDir,
|
|
4377
4285
|
files,
|
|
4286
|
+
config,
|
|
4378
4287
|
infrastructurePath,
|
|
4379
4288
|
schemaDir,
|
|
4380
4289
|
requirement,
|
|
@@ -4412,17 +4321,29 @@ function durableDrizzleUsageFiles(
|
|
|
4412
4321
|
async function configuredDurableDrizzleTableNames(
|
|
4413
4322
|
targetDir: string,
|
|
4414
4323
|
files: string[],
|
|
4324
|
+
config: ResolvedBeignetConfig,
|
|
4415
4325
|
requirement: DurableDrizzleTableRequirement,
|
|
4416
4326
|
sourceCache: Map<string, string>,
|
|
4417
4327
|
): Promise<Set<string>> {
|
|
4418
4328
|
const tableNames = new Set<string>();
|
|
4329
|
+
const providerDefaultTableName =
|
|
4330
|
+
defaultBeignetConfig.database.tables[requirement.table];
|
|
4331
|
+
const configuredTableName = config.database.tables[requirement.table];
|
|
4419
4332
|
|
|
4420
4333
|
for (const file of files) {
|
|
4421
4334
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
4422
|
-
for (const tableName of
|
|
4335
|
+
for (const tableName of tableNamesFromBeignetProviderCalls(
|
|
4336
|
+
source,
|
|
4337
|
+
requirement.factoryNames,
|
|
4338
|
+
providerDefaultTableName,
|
|
4339
|
+
configuredTableName,
|
|
4340
|
+
)) {
|
|
4341
|
+
tableNames.add(tableName);
|
|
4342
|
+
}
|
|
4343
|
+
for (const tableName of tableNamesFromExplicitCalls(
|
|
4423
4344
|
source,
|
|
4424
|
-
requirement.
|
|
4425
|
-
|
|
4345
|
+
requirement.explicitFactoryPattern,
|
|
4346
|
+
configuredTableName,
|
|
4426
4347
|
)) {
|
|
4427
4348
|
tableNames.add(tableName);
|
|
4428
4349
|
}
|
|
@@ -4434,21 +4355,25 @@ async function configuredDurableDrizzleTableNames(
|
|
|
4434
4355
|
async function durableDrizzleTableExists(
|
|
4435
4356
|
targetDir: string,
|
|
4436
4357
|
files: string[],
|
|
4358
|
+
config: ResolvedBeignetConfig,
|
|
4437
4359
|
infrastructurePath: string,
|
|
4438
4360
|
schemaDir: string,
|
|
4439
4361
|
requirement: DurableDrizzleTableRequirement,
|
|
4440
4362
|
tableName: string,
|
|
4441
4363
|
sourceCache: Map<string, string>,
|
|
4442
4364
|
): Promise<boolean> {
|
|
4443
|
-
|
|
4444
|
-
(
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4365
|
+
if (
|
|
4366
|
+
await providerRequiredTableExists(
|
|
4367
|
+
targetDir,
|
|
4368
|
+
files,
|
|
4369
|
+
config,
|
|
4370
|
+
infrastructurePath,
|
|
4371
|
+
schemaDir,
|
|
4372
|
+
tableName,
|
|
4373
|
+
sourceCache,
|
|
4374
|
+
)
|
|
4375
|
+
) {
|
|
4376
|
+
return true;
|
|
4452
4377
|
}
|
|
4453
4378
|
|
|
4454
4379
|
const setupFiles = files.filter(
|
|
@@ -4458,16 +4383,22 @@ async function durableDrizzleTableExists(
|
|
|
4458
4383
|
file.startsWith(`${infrastructurePath}/db/`)) &&
|
|
4459
4384
|
/\.(?:ts|js|mts|mjs|cts|cjs|sql)$/.test(file),
|
|
4460
4385
|
);
|
|
4386
|
+
const providerDefaultTableName =
|
|
4387
|
+
defaultBeignetConfig.database.tables[requirement.table];
|
|
4388
|
+
const configuredTableName = config.database.tables[requirement.table];
|
|
4461
4389
|
for (const file of setupFiles) {
|
|
4462
4390
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
4463
|
-
if (sourceCreatesTable(source, tableName)) return true;
|
|
4464
|
-
|
|
4465
|
-
if (!requirement.setupPattern) continue;
|
|
4466
4391
|
if (
|
|
4467
|
-
|
|
4392
|
+
tableNamesFromBeignetProviderCalls(
|
|
4468
4393
|
source,
|
|
4469
|
-
requirement.
|
|
4470
|
-
|
|
4394
|
+
requirement.setupNames,
|
|
4395
|
+
providerDefaultTableName,
|
|
4396
|
+
configuredTableName,
|
|
4397
|
+
).has(tableName) ||
|
|
4398
|
+
tableNamesFromExplicitCalls(
|
|
4399
|
+
source,
|
|
4400
|
+
requirement.explicitSetupPattern,
|
|
4401
|
+
configuredTableName,
|
|
4471
4402
|
).has(tableName)
|
|
4472
4403
|
) {
|
|
4473
4404
|
return true;
|
|
@@ -4477,17 +4408,30 @@ async function durableDrizzleTableExists(
|
|
|
4477
4408
|
return false;
|
|
4478
4409
|
}
|
|
4479
4410
|
|
|
4480
|
-
function
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4411
|
+
function tableNamesFromBeignetProviderCalls(
|
|
4412
|
+
source: string,
|
|
4413
|
+
exportNames: readonly string[],
|
|
4414
|
+
defaultTableName: string,
|
|
4415
|
+
configuredTableName: string,
|
|
4416
|
+
): Set<string> {
|
|
4417
|
+
const tableNames = new Set<string>();
|
|
4418
|
+
for (const callee of beignetProviderCallees(source, exportNames)) {
|
|
4419
|
+
for (const tableName of tableNamesFromCallee(
|
|
4420
|
+
source,
|
|
4421
|
+
callee,
|
|
4422
|
+
defaultTableName,
|
|
4423
|
+
configuredTableName,
|
|
4424
|
+
)) {
|
|
4425
|
+
tableNames.add(tableName);
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
return tableNames;
|
|
4485
4429
|
}
|
|
4486
4430
|
|
|
4487
|
-
function
|
|
4431
|
+
function tableNamesFromExplicitCalls(
|
|
4488
4432
|
source: string,
|
|
4489
4433
|
calleePattern: string,
|
|
4490
|
-
|
|
4434
|
+
configuredTableName: string,
|
|
4491
4435
|
): Set<string> {
|
|
4492
4436
|
const tableNames = new Set<string>();
|
|
4493
4437
|
const callPattern = new RegExp(
|
|
@@ -4500,16 +4444,115 @@ function tableNamesFromCalls(
|
|
|
4500
4444
|
const prefix = source.slice(Math.max(0, matchIndex - 32), matchIndex);
|
|
4501
4445
|
if (/\bfunction\s+$/.test(prefix)) continue;
|
|
4502
4446
|
|
|
4503
|
-
const
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4447
|
+
const tableName = tableNameFromArgs(match[1] ?? "", configuredTableName);
|
|
4448
|
+
if (tableName) tableNames.add(tableName);
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4451
|
+
return tableNames;
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
type BeignetProviderCallee =
|
|
4455
|
+
| { kind: "identifier"; name: string }
|
|
4456
|
+
| { kind: "namespace"; namespace: string; name: string };
|
|
4457
|
+
|
|
4458
|
+
function beignetProviderCallees(
|
|
4459
|
+
source: string,
|
|
4460
|
+
exportNames: readonly string[],
|
|
4461
|
+
): BeignetProviderCallee[] {
|
|
4462
|
+
const exportNameSet = new Set(exportNames);
|
|
4463
|
+
const callees: BeignetProviderCallee[] = [];
|
|
4464
|
+
|
|
4465
|
+
for (const match of source.matchAll(
|
|
4466
|
+
/\bimport\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']@beignet\/provider-db-drizzle\/(?:sqlite|postgres|mysql)["']/g,
|
|
4467
|
+
)) {
|
|
4468
|
+
for (const specifier of match[1].split(",")) {
|
|
4469
|
+
const cleaned = specifier.trim().replace(/^type\s+/, "");
|
|
4470
|
+
if (!cleaned) continue;
|
|
4471
|
+
const [imported, local] = cleaned.split(/\s+as\s+/);
|
|
4472
|
+
if (!exportNameSet.has(imported)) continue;
|
|
4473
|
+
callees.push({ kind: "identifier", name: local?.trim() || imported });
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
for (const match of source.matchAll(
|
|
4478
|
+
/\bimport\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@beignet\/provider-db-drizzle\/(?:sqlite|postgres|mysql)["']/g,
|
|
4479
|
+
)) {
|
|
4480
|
+
for (const exportName of exportNames) {
|
|
4481
|
+
callees.push({
|
|
4482
|
+
kind: "namespace",
|
|
4483
|
+
namespace: match[1],
|
|
4484
|
+
name: exportName,
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
return callees;
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
function tableNamesFromCallee(
|
|
4493
|
+
source: string,
|
|
4494
|
+
callee: BeignetProviderCallee,
|
|
4495
|
+
defaultTableName: string,
|
|
4496
|
+
configuredTableName: string,
|
|
4497
|
+
): Set<string> {
|
|
4498
|
+
const tableNames = new Set<string>();
|
|
4499
|
+
const calleePattern =
|
|
4500
|
+
callee.kind === "identifier"
|
|
4501
|
+
? `\\b${escapeRegExp(callee.name)}`
|
|
4502
|
+
: `\\b${escapeRegExp(callee.namespace)}\\s*\\.\\s*${escapeRegExp(
|
|
4503
|
+
callee.name,
|
|
4504
|
+
)}`;
|
|
4505
|
+
const callPattern = new RegExp(
|
|
4506
|
+
`${calleePattern}\\s*\\(([\\s\\S]{0,800}?)\\)`,
|
|
4507
|
+
"g",
|
|
4508
|
+
);
|
|
4509
|
+
|
|
4510
|
+
for (const match of source.matchAll(callPattern)) {
|
|
4511
|
+
const matchIndex = match.index ?? 0;
|
|
4512
|
+
const prefix = source.slice(Math.max(0, matchIndex - 32), matchIndex);
|
|
4513
|
+
if (/\bfunction\s+$/.test(prefix)) continue;
|
|
4514
|
+
|
|
4515
|
+
tableNames.add(
|
|
4516
|
+
tableNameFromArgs(match[1] ?? "", configuredTableName) ??
|
|
4517
|
+
defaultTableName,
|
|
4518
|
+
);
|
|
4508
4519
|
}
|
|
4509
4520
|
|
|
4510
4521
|
return tableNames;
|
|
4511
4522
|
}
|
|
4512
4523
|
|
|
4524
|
+
function tableNameFromArgs(
|
|
4525
|
+
argsSource: string,
|
|
4526
|
+
configuredTableName: string,
|
|
4527
|
+
): string | undefined {
|
|
4528
|
+
const literal = /\btableName\s*:\s*["'`]([^"'`]+)["'`]/.exec(argsSource)?.[1];
|
|
4529
|
+
if (literal) return literal;
|
|
4530
|
+
if (/\btableName\s*:/.test(argsSource)) return configuredTableName;
|
|
4531
|
+
return undefined;
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
function configuredProviderRequiredTables(
|
|
4535
|
+
tableNames: readonly string[],
|
|
4536
|
+
config: ResolvedBeignetConfig,
|
|
4537
|
+
): string[] {
|
|
4538
|
+
return tableNames.map((tableName) =>
|
|
4539
|
+
configuredProviderRequiredTable(tableName, config),
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
function configuredProviderRequiredTable(
|
|
4544
|
+
tableName: string,
|
|
4545
|
+
config: ResolvedBeignetConfig,
|
|
4546
|
+
): string {
|
|
4547
|
+
for (const [key, defaultTableName] of Object.entries(
|
|
4548
|
+
defaultBeignetConfig.database.tables,
|
|
4549
|
+
) as Array<[keyof BeignetDatabaseTables, string]>) {
|
|
4550
|
+
if (tableName === defaultTableName) return config.database.tables[key];
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
return tableName;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4513
4556
|
function hasRepositoryAdapterFile(
|
|
4514
4557
|
files: string[],
|
|
4515
4558
|
infrastructureDir: string,
|