tina4-nodejs 3.13.133 → 3.13.134
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/CLAUDE.md +3 -3
- package/README.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +3181 -3051
- package/packages/cli/src/commands/generate.ts +33 -22
- package/packages/cli/src/commands/lint.ts +77 -111
- package/packages/core/dist/index.js +3090 -2952
- package/packages/core/src/.tina4-metrics.json +15004 -0
- package/packages/core/src/aiClient.ts +199 -161
- package/packages/core/src/dispatchPipeline.ts +65 -67
- package/packages/core/src/docs.ts +52 -544
- package/packages/core/src/docsParser.ts +270 -0
- package/packages/core/src/docsScanner.ts +121 -0
- package/packages/core/src/docsSignatures.ts +165 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/logger.ts +68 -82
- package/packages/core/src/mcp.ts +32 -60
- package/packages/core/src/messenger.ts +136 -157
- package/packages/core/src/middleware.ts +56 -60
- package/packages/core/src/plan.ts +78 -70
- package/packages/core/src/projectIndex.ts +15 -288
- package/packages/core/src/projectIndexExtractors.ts +126 -0
- package/packages/core/src/projectIndexStorage.ts +122 -0
- package/packages/core/src/push.ts +281 -0
- package/packages/core/src/server.ts +182 -183
- package/packages/frond/dist/index.js +607 -770
- package/packages/frond/src/engine.ts +670 -818
- package/packages/orm/dist/index.js +3100 -2965
- package/packages/orm/src/adapters/mongodb.ts +99 -144
- package/packages/orm/src/baseModel.ts +429 -515
- package/packages/orm/src/fakeData.ts +73 -61
- package/packages/orm/src/migration.ts +96 -126
- package/packages/orm/src/seeder.ts +6 -238
- package/packages/orm/src/seederTable.ts +101 -0
- package/packages/orm/src/seederTypes.ts +14 -0
- package/packages/orm/src/validation.ts +97 -80
- package/types/core/src/aiClient.d.ts +5 -0
- package/types/core/src/docsParser.d.ts +28 -0
- package/types/core/src/docsScanner.d.ts +1 -0
- package/types/core/src/docsSignatures.d.ts +11 -0
- package/types/core/src/index.d.ts +2 -0
- package/types/core/src/messenger.d.ts +8 -0
- package/types/core/src/projectIndexExtractors.d.ts +3 -0
- package/types/core/src/projectIndexStorage.d.ts +13 -0
- package/types/core/src/push.d.ts +45 -0
- package/types/frond/src/engine.d.ts +25 -0
- package/types/orm/src/fakeData.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +3 -89
- package/types/orm/src/seederTable.d.ts +9 -0
- package/types/orm/src/seederTypes.d.ts +16 -0
|
@@ -1941,6 +1941,57 @@ function startLoopbackSiblings(
|
|
|
1941
1941
|
);
|
|
1942
1942
|
}
|
|
1943
1943
|
|
|
1944
|
+
function startProductionCluster(port: number, host: string): {
|
|
1945
|
+
close: () => void;
|
|
1946
|
+
router: Router;
|
|
1947
|
+
port: number;
|
|
1948
|
+
} {
|
|
1949
|
+
const numCPUs = os.cpus().length;
|
|
1950
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
1951
|
+
const isTty = isatty(1);
|
|
1952
|
+
const color = isTty ? "\x1b[32m" : "";
|
|
1953
|
+
const reset = isTty ? "\x1b[0m" : "";
|
|
1954
|
+
const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
|
|
1955
|
+
if (!isBannerSuppressed()) {
|
|
1956
|
+
const [swaggerLine] = bannerSurfaceLines(port, { swaggerEnabled: swaggerAdvertised(), devAdminEnabled: false });
|
|
1957
|
+
console.log(`${color}
|
|
1958
|
+
Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
|
|
1959
|
+
|
|
1960
|
+
Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)${swaggerLine}
|
|
1961
|
+
Debug: OFF (Log level: ${logLevel})
|
|
1962
|
+
${reset}`);
|
|
1963
|
+
}
|
|
1964
|
+
for (let i = 0; i < numCPUs; i++) cluster.fork();
|
|
1965
|
+
cluster.on("exit", (worker, code) => {
|
|
1966
|
+
if (code !== 0) {
|
|
1967
|
+
console.log(` Worker ${worker.process.pid} exited (code ${code}), restarting...`);
|
|
1968
|
+
cluster.fork();
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
return {
|
|
1972
|
+
close: () => {
|
|
1973
|
+
stopAllBackgroundTasks();
|
|
1974
|
+
for (const id in cluster.workers) cluster.workers[id]?.kill();
|
|
1975
|
+
},
|
|
1976
|
+
router: new Router(),
|
|
1977
|
+
port,
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function wireLocaleGlobal(frondEngine: any, base: string): void {
|
|
1982
|
+
if (!frondEngine) return;
|
|
1983
|
+
const localeDir = resolve(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
|
|
1984
|
+
if (!existsSync(localeDir)) return;
|
|
1985
|
+
try {
|
|
1986
|
+
const localeFiles = readdirSync(localeDir).filter((file: string) => file.endsWith(".json"));
|
|
1987
|
+
if (localeFiles.length === 0 || frondEngine.globals?.t) return;
|
|
1988
|
+
const i18nInstance = new I18n(process.env.TINA4_LOCALE ?? "en", localeDir);
|
|
1989
|
+
frondEngine.addGlobal("t", (key: string, params?: Record<string, string>) => i18nInstance.t(key, params));
|
|
1990
|
+
} catch {
|
|
1991
|
+
// Locale directory unreadable — skip auto-wire.
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1944
1995
|
export async function startServer(config?: Tina4Config): Promise<{
|
|
1945
1996
|
close: () => void;
|
|
1946
1997
|
router: Router;
|
|
@@ -1986,62 +2037,8 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
1986
2037
|
// Cluster mode for production: fork workers based on CPU count
|
|
1987
2038
|
// Only when --production is explicitly set (via TINA4_PRODUCTION env var)
|
|
1988
2039
|
const isProduction = (process.env.TINA4_PRODUCTION ?? "").toLowerCase() === "true";
|
|
1989
|
-
if (cluster.isPrimary && isProduction) {
|
|
1990
|
-
|
|
1991
|
-
if (numCPUs > 1) {
|
|
1992
|
-
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
1993
|
-
const isTty = isatty(1);
|
|
1994
|
-
const color = isTty ? "\x1b[32m" : "";
|
|
1995
|
-
const reset = isTty ? "\x1b[0m" : "";
|
|
1996
|
-
const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
|
|
1997
|
-
|
|
1998
|
-
if (!isBannerSuppressed()) {
|
|
1999
|
-
// Only advertise a surface that is actually reachable (issue #99).
|
|
2000
|
-
// Cluster mode is the production path: debug is OFF, so /__dev always
|
|
2001
|
-
// 404s here and is never advertised; /swagger only when explicitly on.
|
|
2002
|
-
// Cluster mode is the production path: debug is OFF, so /__dev never
|
|
2003
|
-
// advertises here.
|
|
2004
|
-
const [swaggerLine] = bannerSurfaceLines(port, {
|
|
2005
|
-
swaggerEnabled: swaggerAdvertised(),
|
|
2006
|
-
devAdminEnabled: false,
|
|
2007
|
-
});
|
|
2008
|
-
console.log(`${color}
|
|
2009
|
-
______ _ __ __
|
|
2010
|
-
/_ __/(_)___ ____ _/ // /
|
|
2011
|
-
/ / / / __ \\/ __ \`/ // /_
|
|
2012
|
-
/ / / / / / / /_/ /__ __/
|
|
2013
|
-
/_/ /_/_/ /_/\\__,_/ /_/
|
|
2014
|
-
${reset}
|
|
2015
|
-
Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
|
|
2016
|
-
|
|
2017
|
-
Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)${swaggerLine}
|
|
2018
|
-
Debug: OFF (Log level: ${logLevel})
|
|
2019
|
-
`);
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
for (let i = 0; i < numCPUs; i++) {
|
|
2023
|
-
cluster.fork();
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
cluster.on("exit", (worker, code, _signal) => {
|
|
2027
|
-
if (code !== 0) {
|
|
2028
|
-
console.log(` Worker ${worker.process.pid} exited (code ${code}), restarting...`);
|
|
2029
|
-
cluster.fork();
|
|
2030
|
-
}
|
|
2031
|
-
});
|
|
2032
|
-
|
|
2033
|
-
// Return a handle that kills all workers
|
|
2034
|
-
return {
|
|
2035
|
-
close: () => {
|
|
2036
|
-
stopAllBackgroundTasks();
|
|
2037
|
-
for (const id in cluster.workers) {
|
|
2038
|
-
cluster.workers[id]?.kill();
|
|
2039
|
-
}
|
|
2040
|
-
},
|
|
2041
|
-
router: new Router(),
|
|
2042
|
-
port,
|
|
2043
|
-
};
|
|
2044
|
-
}
|
|
2040
|
+
if (cluster.isPrimary && isProduction && os.cpus().length > 1) {
|
|
2041
|
+
return startProductionCluster(port, host);
|
|
2045
2042
|
}
|
|
2046
2043
|
|
|
2047
2044
|
const base = config?.basePath ? resolve(config.basePath) : process.cwd();
|
|
@@ -2112,20 +2109,7 @@ ${reset}
|
|
|
2112
2109
|
}
|
|
2113
2110
|
|
|
2114
2111
|
// Auto-wire i18n → template global t() when locale files exist
|
|
2115
|
-
|
|
2116
|
-
const localeDir = resolve(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
|
|
2117
|
-
if (existsSync(localeDir)) {
|
|
2118
|
-
try {
|
|
2119
|
-
const localeFiles = readdirSync(localeDir).filter((f: string) => f.endsWith(".json"));
|
|
2120
|
-
if (localeFiles.length > 0 && !frondEngine.globals?.t) {
|
|
2121
|
-
const i18nInstance = new I18n(process.env.TINA4_LOCALE ?? "en", localeDir);
|
|
2122
|
-
frondEngine.addGlobal("t", (key: string, params?: Record<string, string>) => i18nInstance.t(key, params));
|
|
2123
|
-
}
|
|
2124
|
-
} catch {
|
|
2125
|
-
// Locale directory unreadable — skip auto-wire
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2112
|
+
wireLocaleGlobal(frondEngine, base);
|
|
2129
2113
|
|
|
2130
2114
|
// Built-in middleware
|
|
2131
2115
|
middleware.use(cors());
|
|
@@ -2166,119 +2150,8 @@ ${reset}
|
|
|
2166
2150
|
console.log(`\n \x1b[36mCSRF\x1b[0m protection enabled (TINA4_CSRF)`);
|
|
2167
2151
|
}
|
|
2168
2152
|
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
const hasModelsDir = existsSync(modelsDir);
|
|
2172
|
-
if (hasOrmDir || hasModelsDir) {
|
|
2173
|
-
try {
|
|
2174
|
-
const orm = await import("../../orm/src/index.js");
|
|
2175
|
-
const dbConfig = config?.database ?? {};
|
|
2176
|
-
await orm.initDatabase({
|
|
2177
|
-
type: dbConfig.type ?? "sqlite",
|
|
2178
|
-
path: dbConfig.path ?? "./data/tina4.db",
|
|
2179
|
-
});
|
|
2180
|
-
|
|
2181
|
-
// Discover from src/orm/ (primary) and src/models/ (fallback), merge results
|
|
2182
|
-
let models = hasOrmDir ? await orm.discoverModels(ormDir) : [];
|
|
2183
|
-
if (hasModelsDir) {
|
|
2184
|
-
const extraModels = await orm.discoverModels(modelsDir);
|
|
2185
|
-
// Only add models not already discovered (src/orm/ takes priority)
|
|
2186
|
-
const existingTables = new Set(models.map((m: any) => m.definition.tableName));
|
|
2187
|
-
for (const m of extraModels) {
|
|
2188
|
-
if (!existingTables.has(m.definition.tableName)) {
|
|
2189
|
-
models.push(m);
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
if (models.length > 0) {
|
|
2194
|
-
console.log(`\n Models discovered:`);
|
|
2195
|
-
await orm.syncModels(models);
|
|
2196
|
-
for (const { definition } of models) {
|
|
2197
|
-
console.log(` \x1b[35m${definition.tableName}\x1b[0m (${Object.keys(definition.fields).length} fields)`);
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
// Generate auto-CRUD routes ONLY for models that explicitly opted in via
|
|
2201
|
-
// `static autoCrud = true` (the documented opt-in gate; default false). The
|
|
2202
|
-
// server previously generated the 5 CRUD endpoints for every discovered model
|
|
2203
|
-
// regardless of the flag, contradicting the documented contract. (Python's
|
|
2204
|
-
// AutoCrud is opt-in too — via an explicit AutoCrud.register/discover call.)
|
|
2205
|
-
const crudModels = orm.crudEligibleModels(models);
|
|
2206
|
-
const crudRoutes = crudModels.length > 0 ? orm.generateCrudRoutes(crudModels) : [];
|
|
2207
|
-
for (const route of crudRoutes) {
|
|
2208
|
-
// Only add if no file-based route already handles this
|
|
2209
|
-
const existing = router.match(route.method, route.pattern.replace(/\{(\w+)\}/g, "test").replace(/\[(\w+)\]/g, "test"));
|
|
2210
|
-
if (!existing) {
|
|
2211
|
-
router.addRoute(route);
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
if (crudRoutes.length > 0) {
|
|
2216
|
-
console.log(`\n Auto-CRUD endpoints:`);
|
|
2217
|
-
for (const route of crudRoutes) {
|
|
2218
|
-
console.log(` \x1b[33m${route.method.padEnd(7)}\x1b[0m ${route.pattern}`);
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
} catch (err) {
|
|
2223
|
-
console.warn(`\n ORM not available (install @tina4/orm to enable):`, err);
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
// Auto-run pending migrations on startup — AFTER initDatabase()/model sync,
|
|
2227
|
-
// BEFORE the server listens. Non-breaking: a failure is logged and boot
|
|
2228
|
-
// continues (the helper never throws). Gated on a migrations/ dir + the
|
|
2229
|
-
// TINA4_AUTO_MIGRATE flag (default on) + a resolvable DB adapter.
|
|
2230
|
-
await autoMigrateOnStartup("migrations", base);
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
// Initialize Swagger — gated on TINA4_SWAGGER_ENABLED (default: enabled
|
|
2234
|
-
// in debug mode, off in production). Loading the swagger module also
|
|
2235
|
-
// pulls in route discovery for the generator, so skip the import entirely
|
|
2236
|
-
// when disabled.
|
|
2237
|
-
try {
|
|
2238
|
-
const swagger = await import("../../swagger/src/index.js");
|
|
2239
|
-
// Single source of truth for BOTH the gated routes and the bundled
|
|
2240
|
-
// public/swagger assets (which static serving would otherwise expose).
|
|
2241
|
-
swaggerAssetsEnabled = swagger.swaggerEnabled();
|
|
2242
|
-
if (!swaggerAssetsEnabled) {
|
|
2243
|
-
// Skip the rest of the swagger block when disabled.
|
|
2244
|
-
throw new Error("__swagger_disabled__");
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
// Collect model definitions for schema generation
|
|
2248
|
-
let modelDefs: Array<{ tableName: string; fields: Record<string, unknown> }> = [];
|
|
2249
|
-
try {
|
|
2250
|
-
const orm = await import("../../orm/src/index.js");
|
|
2251
|
-
const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync(d));
|
|
2252
|
-
const seenTables = new Set<string>();
|
|
2253
|
-
for (const dir of allModelDirs) {
|
|
2254
|
-
const discovered = await orm.discoverModels(dir);
|
|
2255
|
-
for (const m of discovered) {
|
|
2256
|
-
if (!seenTables.has(m.definition.tableName)) {
|
|
2257
|
-
modelDefs.push(m.definition);
|
|
2258
|
-
seenTables.add(m.definition.tableName);
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
} catch {
|
|
2263
|
-
// ORM not available, swagger will work without model schemas
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
// Read router.getRoutes() LIVE on every call — never a captured snapshot
|
|
2267
|
-
// (SWAG-NODE-BOOT-SNAPSHOT, ADR-0004). This used to close over a
|
|
2268
|
-
// boot-time `allRoutes` array, so the spec regenerated per request but
|
|
2269
|
-
// over a FROZEN route list: a route registered after boot (hot-reload, or
|
|
2270
|
-
// DevAdmin.register running after this block — see the /__feedback
|
|
2271
|
-
// exclusion above) never appeared. Python/PHP/Ruby always read the live
|
|
2272
|
-
// route table inside their generate() call; this is that same shape.
|
|
2273
|
-
const getSpec = () => swagger.generate(router.getRoutes(), modelDefs as any);
|
|
2274
|
-
const swaggerRoutes = swagger.createSwaggerRoutes(getSpec);
|
|
2275
|
-
for (const route of swaggerRoutes) {
|
|
2276
|
-
router.addRoute(route);
|
|
2277
|
-
}
|
|
2278
|
-
} catch {
|
|
2279
|
-
// Swagger not available
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2153
|
+
await initializeOrm(config, router, base, ormDir, modelsDir);
|
|
2154
|
+
swaggerAssetsEnabled = await configureSwagger(router, ormDir, modelsDir);
|
|
2282
2155
|
// Register dev admin dashboard routes
|
|
2283
2156
|
if (DevAdmin.isEnabled()) {
|
|
2284
2157
|
DevAdmin.register(router);
|
|
@@ -2641,3 +2514,129 @@ ${reset}
|
|
|
2641
2514
|
});
|
|
2642
2515
|
});
|
|
2643
2516
|
}
|
|
2517
|
+
async function initializeOrm(
|
|
2518
|
+
config: Tina4Config | undefined,
|
|
2519
|
+
router: Router,
|
|
2520
|
+
base: string,
|
|
2521
|
+
ormDir: string,
|
|
2522
|
+
modelsDir: string,
|
|
2523
|
+
): Promise<void> {
|
|
2524
|
+
// Initialize ORM if models directory exists (check src/orm/ first, then src/models/)
|
|
2525
|
+
const hasOrmDir = existsSync(ormDir);
|
|
2526
|
+
const hasModelsDir = existsSync(modelsDir);
|
|
2527
|
+
if (hasOrmDir || hasModelsDir) {
|
|
2528
|
+
try {
|
|
2529
|
+
const orm = await import("../../orm/src/index.js");
|
|
2530
|
+
const dbConfig = config?.database ?? {};
|
|
2531
|
+
await orm.initDatabase({
|
|
2532
|
+
type: dbConfig.type ?? "sqlite",
|
|
2533
|
+
path: dbConfig.path ?? "./data/tina4.db",
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
// Discover from src/orm/ (primary) and src/models/ (fallback), merge results
|
|
2537
|
+
let models = hasOrmDir ? await orm.discoverModels(ormDir) : [];
|
|
2538
|
+
if (hasModelsDir) {
|
|
2539
|
+
const extraModels = await orm.discoverModels(modelsDir);
|
|
2540
|
+
// Only add models not already discovered (src/orm/ takes priority)
|
|
2541
|
+
const existingTables = new Set(models.map((m: any) => m.definition.tableName));
|
|
2542
|
+
for (const m of extraModels) {
|
|
2543
|
+
if (!existingTables.has(m.definition.tableName)) {
|
|
2544
|
+
models.push(m);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
if (models.length > 0) {
|
|
2549
|
+
console.log(`\n Models discovered:`);
|
|
2550
|
+
await orm.syncModels(models);
|
|
2551
|
+
for (const { definition } of models) {
|
|
2552
|
+
console.log(` \x1b[35m${definition.tableName}\x1b[0m (${Object.keys(definition.fields).length} fields)`);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// Generate auto-CRUD routes ONLY for models that explicitly opted in via
|
|
2556
|
+
// `static autoCrud = true` (the documented opt-in gate; default false). The
|
|
2557
|
+
// server previously generated the 5 CRUD endpoints for every discovered model
|
|
2558
|
+
// regardless of the flag, contradicting the documented contract. (Python's
|
|
2559
|
+
// AutoCrud is opt-in too — via an explicit AutoCrud.register/discover call.)
|
|
2560
|
+
const crudModels = orm.crudEligibleModels(models);
|
|
2561
|
+
const crudRoutes = crudModels.length > 0 ? orm.generateCrudRoutes(crudModels) : [];
|
|
2562
|
+
for (const route of crudRoutes) {
|
|
2563
|
+
// Only add if no file-based route already handles this
|
|
2564
|
+
const existing = router.match(route.method, route.pattern.replace(/\{(\w+)\}/g, "test").replace(/\[(\w+)\]/g, "test"));
|
|
2565
|
+
if (!existing) {
|
|
2566
|
+
router.addRoute(route);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
if (crudRoutes.length > 0) {
|
|
2571
|
+
console.log(`\n Auto-CRUD endpoints:`);
|
|
2572
|
+
for (const route of crudRoutes) {
|
|
2573
|
+
console.log(` \x1b[33m${route.method.padEnd(7)}\x1b[0m ${route.pattern}`);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
console.warn(`\n ORM not available (install @tina4/orm to enable):`, err);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// Auto-run pending migrations on startup — AFTER initDatabase()/model sync,
|
|
2582
|
+
// BEFORE the server listens. Non-breaking: a failure is logged and boot
|
|
2583
|
+
// continues (the helper never throws). Gated on a migrations/ dir + the
|
|
2584
|
+
// TINA4_AUTO_MIGRATE flag (default on) + a resolvable DB adapter.
|
|
2585
|
+
await autoMigrateOnStartup("migrations", base);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
async function configureSwagger(router: Router, ormDir: string, modelsDir: string): Promise<boolean> {
|
|
2591
|
+
let enabled = false;
|
|
2592
|
+
// Initialize Swagger — gated on TINA4_SWAGGER_ENABLED (default: enabled
|
|
2593
|
+
// in debug mode, off in production). Loading the swagger module also
|
|
2594
|
+
// pulls in route discovery for the generator, so skip the import entirely
|
|
2595
|
+
// when disabled.
|
|
2596
|
+
try {
|
|
2597
|
+
const swagger = await import("../../swagger/src/index.js");
|
|
2598
|
+
// Single source of truth for BOTH the gated routes and the bundled
|
|
2599
|
+
// public/swagger assets (which static serving would otherwise expose).
|
|
2600
|
+
enabled = swagger.swaggerEnabled();
|
|
2601
|
+
if (!swaggerAssetsEnabled) {
|
|
2602
|
+
// Skip the rest of the swagger block when disabled.
|
|
2603
|
+
throw new Error("__swagger_disabled__");
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// Collect model definitions for schema generation
|
|
2607
|
+
let modelDefs: Array<{ tableName: string; fields: Record<string, unknown> }> = [];
|
|
2608
|
+
try {
|
|
2609
|
+
const orm = await import("../../orm/src/index.js");
|
|
2610
|
+
const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync(d));
|
|
2611
|
+
const seenTables = new Set<string>();
|
|
2612
|
+
for (const dir of allModelDirs) {
|
|
2613
|
+
const discovered = await orm.discoverModels(dir);
|
|
2614
|
+
for (const m of discovered) {
|
|
2615
|
+
if (!seenTables.has(m.definition.tableName)) {
|
|
2616
|
+
modelDefs.push(m.definition);
|
|
2617
|
+
seenTables.add(m.definition.tableName);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
} catch {
|
|
2622
|
+
// ORM not available, swagger will work without model schemas
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// Read router.getRoutes() LIVE on every call — never a captured snapshot
|
|
2626
|
+
// (SWAG-NODE-BOOT-SNAPSHOT, ADR-0004). This used to close over a
|
|
2627
|
+
// boot-time `allRoutes` array, so the spec regenerated per request but
|
|
2628
|
+
// over a FROZEN route list: a route registered after boot (hot-reload, or
|
|
2629
|
+
// DevAdmin.register running after this block — see the /__feedback
|
|
2630
|
+
// exclusion above) never appeared. Python/PHP/Ruby always read the live
|
|
2631
|
+
// route table inside their generate() call; this is that same shape.
|
|
2632
|
+
const getSpec = () => swagger.generate(router.getRoutes(), modelDefs as any);
|
|
2633
|
+
const swaggerRoutes = swagger.createSwaggerRoutes(getSpec);
|
|
2634
|
+
for (const route of swaggerRoutes) {
|
|
2635
|
+
router.addRoute(route);
|
|
2636
|
+
}
|
|
2637
|
+
} catch {
|
|
2638
|
+
// Swagger not available
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
return enabled;
|
|
2642
|
+
}
|