tina4-nodejs 3.13.69 → 3.13.70
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/package.json +8 -1
- package/packages/core/src/ai.ts +4 -4
- package/packages/core/src/cache.ts +1 -1
- package/packages/core/src/devAdmin.ts +7 -7
- package/packages/core/src/mcp.ts +113 -30
- package/packages/core/src/response.ts +3 -3
- package/packages/orm/src/adapters/firebird.ts +46 -2
- package/packages/orm/src/autoCrud.ts +1 -1
- package/packages/orm/src/baseModel.ts +35 -8
- package/packages/orm/src/cachedDatabase.ts +2 -2
- package/packages/orm/src/index.ts +2 -2
- package/packages/orm/src/migration.ts +12 -4
- package/packages/orm/src/realtime/realtime.ts +1 -1
- package/packages/orm/src/realtime/storage.ts +1 -1
- package/packages/orm/src/seeder.ts +67 -3
- package/packages/swagger/src/generator.ts +2 -2
- package/packages/swagger/src/ui.ts +1 -1
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.70)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.70 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
|
@@ -1238,7 +1238,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
|
|
|
1238
1238
|
## v3 Features Summary
|
|
1239
1239
|
|
|
1240
1240
|
- **45 built-in features**, zero third-party dependencies
|
|
1241
|
-
- **5,
|
|
1241
|
+
- **5,379 tests** passing across 154 files (build + typecheck green; 9 PostgreSQL/Valkey service-gated failures)
|
|
1242
1242
|
- **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
|
|
1243
1243
|
- **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
|
|
1244
1244
|
- **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.13.
|
|
3
|
+
"version": "3.13.70",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
|
|
6
6
|
"keywords": [
|
|
@@ -60,6 +60,13 @@
|
|
|
60
60
|
"engines": {
|
|
61
61
|
"node": ">=22.0.0"
|
|
62
62
|
},
|
|
63
|
+
"optionalDependencies": {
|
|
64
|
+
"@aws-sdk/client-s3": "^3.600.0",
|
|
65
|
+
"@aws-sdk/s3-request-presigner": "^3.600.0",
|
|
66
|
+
"mongodb": "^6.0.0",
|
|
67
|
+
"pg": "^8.20.0",
|
|
68
|
+
"redis": "^4.7.0"
|
|
69
|
+
},
|
|
63
70
|
"devDependencies": {
|
|
64
71
|
"@types/node": "^22.10.0",
|
|
65
72
|
"esbuild": "^0.24.0",
|
package/packages/core/src/ai.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Simple menu-driven installer for AI tool context files.
|
|
5
5
|
* The user picks which tools they use, we install the appropriate files.
|
|
6
6
|
*
|
|
7
|
-
* import { showMenu, installSelected } from "
|
|
7
|
+
* import { showMenu, installSelected } from "tina4-nodejs";
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
@@ -693,7 +693,7 @@ Set \`TINA4_DATABASE_URL\` in \`.env\` (e.g. \`postgres://localhost:5432/mydb\`)
|
|
|
693
693
|
|
|
694
694
|
## Auth
|
|
695
695
|
|
|
696
|
-
JWT auth built in. \`import { createToken, validateToken, hashPassword, checkPassword } from "
|
|
696
|
+
JWT auth built in. \`import { createToken, validateToken, hashPassword, checkPassword } from "tina4-nodejs"\`.
|
|
697
697
|
GET routes are public. POST/PUT/PATCH/DELETE require auth by default.
|
|
698
698
|
`;
|
|
699
699
|
}
|
|
@@ -862,7 +862,7 @@ Set \`TINA4_DATABASE_URL\` in \`.env\` (e.g. \`sqlite:///path/to/db.sqlite\`, \`
|
|
|
862
862
|
|
|
863
863
|
## Auth
|
|
864
864
|
|
|
865
|
-
JWT auth built in. \`import { createToken, validateToken, hashPassword, checkPassword } from "
|
|
865
|
+
JWT auth built in. \`import { createToken, validateToken, hashPassword, checkPassword } from "tina4-nodejs"\`.
|
|
866
866
|
GET routes are public. POST/PUT/PATCH/DELETE require auth by default.
|
|
867
867
|
|
|
868
868
|
## Testing
|
|
@@ -873,7 +873,7 @@ npx tina4nodejs test # Run all tests
|
|
|
873
873
|
|
|
874
874
|
Add test files in \`test/\` directory. Use built-in inline testing:
|
|
875
875
|
\`\`\`typescript
|
|
876
|
-
import { tests, assertEqual, runAll } from "
|
|
876
|
+
import { tests, assertEqual, runAll } from "tina4-nodejs";
|
|
877
877
|
\`\`\`
|
|
878
878
|
|
|
879
879
|
## Important
|
|
@@ -1017,7 +1017,7 @@ class DatabaseBackend implements CacheBackend {
|
|
|
1017
1017
|
process.env.TINA4_AUTO_CACHING = "false";
|
|
1018
1018
|
process.env.TINA4_DB_CACHE = "false";
|
|
1019
1019
|
try {
|
|
1020
|
-
const mod: any = await import("
|
|
1020
|
+
const mod: any = await import("../../orm/src/index.js").catch(() => null);
|
|
1021
1021
|
if (!mod || !mod.initDatabase) { this.available = false; return; }
|
|
1022
1022
|
const db = await mod.initDatabase({ url: this.url });
|
|
1023
1023
|
await db.execute(
|
|
@@ -518,7 +518,7 @@ export class DevAdmin {
|
|
|
518
518
|
|
|
519
519
|
// Auto-discover ORM models from BaseModel registry
|
|
520
520
|
try {
|
|
521
|
-
const orm = await import("
|
|
521
|
+
const orm = await import("../../orm/src/index.js");
|
|
522
522
|
const registry = (orm as any).BaseModel?._modelRegistry as Record<string, any> | undefined;
|
|
523
523
|
if (registry) {
|
|
524
524
|
for (const modelClass of Object.values(registry)) {
|
|
@@ -746,7 +746,7 @@ function handleStatus(router: Router): RouteHandler {
|
|
|
746
746
|
const mailboxCounts = DevMailboxStore.count();
|
|
747
747
|
let dbTableCount = 0;
|
|
748
748
|
try {
|
|
749
|
-
const { getAdapter } = await import("
|
|
749
|
+
const { getAdapter } = await import("../../orm/src/index.js");
|
|
750
750
|
const db = getAdapter();
|
|
751
751
|
dbTableCount = db.tables().length;
|
|
752
752
|
} catch { /* no database connected */ }
|
|
@@ -852,7 +852,7 @@ const handleSystem: RouteHandler = async (_req, res) => {
|
|
|
852
852
|
let dbTableCount: number | undefined;
|
|
853
853
|
let dbConnected = false;
|
|
854
854
|
try {
|
|
855
|
-
const { getAdapter } = await import("
|
|
855
|
+
const { getAdapter } = await import("../../orm/src/index.js");
|
|
856
856
|
const db = getAdapter();
|
|
857
857
|
dbTableCount = db.tables().length;
|
|
858
858
|
dbConnected = true;
|
|
@@ -1067,7 +1067,7 @@ const handleTable: RouteHandler = (req, res) => {
|
|
|
1067
1067
|
|
|
1068
1068
|
const handleTables: RouteHandler = async (_req, res) => {
|
|
1069
1069
|
try {
|
|
1070
|
-
const { getAdapter } = await import("
|
|
1070
|
+
const { getAdapter } = await import("../../orm/src/index.js");
|
|
1071
1071
|
const db = getAdapter();
|
|
1072
1072
|
const tables = db.tables();
|
|
1073
1073
|
res.json({ tables });
|
|
@@ -1093,7 +1093,7 @@ const handleSeed: RouteHandler = async (req, res) => {
|
|
|
1093
1093
|
return;
|
|
1094
1094
|
}
|
|
1095
1095
|
try {
|
|
1096
|
-
const orm = await import("
|
|
1096
|
+
const orm = await import("../../orm/src/index.js");
|
|
1097
1097
|
const db = orm.getAdapter();
|
|
1098
1098
|
const { seedTable } = orm;
|
|
1099
1099
|
// A shared FakeData seeds the RNG so a `seed` makes the run reproducible.
|
|
@@ -1144,7 +1144,7 @@ const handleQuery: RouteHandler = async (req, res) => {
|
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
1146
|
try {
|
|
1147
|
-
const { getAdapter } = await import("
|
|
1147
|
+
const { getAdapter } = await import("../../orm/src/index.js");
|
|
1148
1148
|
const db = getAdapter();
|
|
1149
1149
|
|
|
1150
1150
|
// Split multiple statements on semicolons
|
|
@@ -1519,7 +1519,7 @@ const handleConnectionsTest: RouteHandler = async (req, res) => {
|
|
|
1519
1519
|
}
|
|
1520
1520
|
try {
|
|
1521
1521
|
// Try to use the ORM's initDatabase if available
|
|
1522
|
-
const { initDatabase } = await import("
|
|
1522
|
+
const { initDatabase } = await import("../../orm/src/index.js").catch(() => ({ initDatabase: null }));
|
|
1523
1523
|
if (!initDatabase) {
|
|
1524
1524
|
res.json({ success: false, error: "Database module (@tina4/orm) not available" });
|
|
1525
1525
|
return;
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -39,11 +39,13 @@ const req = createRequire(import.meta.url);
|
|
|
39
39
|
* MCP client hits /__dev/mcp from a from-source dev server.
|
|
40
40
|
*/
|
|
41
41
|
function reqSibling(pkg: "orm" | "swagger" | "frond"): Record<string, unknown> {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// Resolve the sibling package from its in-repo/installed SOURCE path. A bare
|
|
43
|
+
// `@tina4/${pkg}` specifier does NOT resolve in a consumer install — the root
|
|
44
|
+
// `tina4-nodejs` package is the only thing on disk, there is no `@tina4/*` in
|
|
45
|
+
// node_modules (that was the #32 break) — so go straight to the relative src.
|
|
46
|
+
// createRequire + tsx resolves the .ts here (synchronous dev-tool path); each
|
|
47
|
+
// caller already wraps this in try/catch and degrades gracefully on failure.
|
|
48
|
+
return req(`../../${pkg}/src/index.ts`) as Record<string, unknown>;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// ── Types ─────────────────────────────────────────────────────
|
|
@@ -1187,13 +1189,43 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1187
1189
|
|
|
1188
1190
|
server.registerTool(
|
|
1189
1191
|
"route_test",
|
|
1190
|
-
(args) => {
|
|
1191
|
-
//
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
}
|
|
1192
|
+
async (args) => {
|
|
1193
|
+
// Dispatch the route through the REAL in-process TestClient (parity with
|
|
1194
|
+
// the Python master's route_test, which drives its TestClient). Previously
|
|
1195
|
+
// an echo stub that returned {info,method,path} and never dispatched.
|
|
1196
|
+
// The TestClient builds a mock request, matches the route on the active
|
|
1197
|
+
// router, runs it through the real auth gate, and executes the handler —
|
|
1198
|
+
// exactly like a live request, no socket. Returns {status, body, contentType}.
|
|
1199
|
+
try {
|
|
1200
|
+
const { TestClient } = req("./testClient.js") as typeof import("./testClient.js");
|
|
1201
|
+
const { defaultRouter } = req("./router.js") as typeof import("./router.js");
|
|
1202
|
+
// Same router accessor route_list uses: prefer the running server's
|
|
1203
|
+
// router, fall back to the default global router.
|
|
1204
|
+
const router = (globalThis as any).__tina4_router ?? defaultRouter;
|
|
1205
|
+
const client = new TestClient(router);
|
|
1206
|
+
const method = String(args.method ?? "GET").toUpperCase();
|
|
1207
|
+
const routePath = String(args.path ?? "");
|
|
1208
|
+
const bodyStr = args.body ? String(args.body) : undefined;
|
|
1209
|
+
let headers: Record<string, string> = {};
|
|
1210
|
+
try {
|
|
1211
|
+
headers = args.headers ? JSON.parse(String(args.headers)) : {};
|
|
1212
|
+
} catch {
|
|
1213
|
+
headers = {};
|
|
1214
|
+
}
|
|
1215
|
+
const options = { body: bodyStr, headers };
|
|
1216
|
+
let resp;
|
|
1217
|
+
switch (method) {
|
|
1218
|
+
case "GET": resp = await client.get(routePath, options); break;
|
|
1219
|
+
case "POST": resp = await client.post(routePath, options); break;
|
|
1220
|
+
case "PUT": resp = await client.put(routePath, options); break;
|
|
1221
|
+
case "PATCH": resp = await client.patch(routePath, options); break;
|
|
1222
|
+
case "DELETE": resp = await client.delete(routePath, options); break;
|
|
1223
|
+
default: return { error: `Unsupported method: ${method}` };
|
|
1224
|
+
}
|
|
1225
|
+
return { status: resp.status, body: resp.text(), contentType: resp.contentType };
|
|
1226
|
+
} catch (e) {
|
|
1227
|
+
return { error: (e as Error).message };
|
|
1228
|
+
}
|
|
1197
1229
|
},
|
|
1198
1230
|
"Call a route and return the response",
|
|
1199
1231
|
schemaFromParams([
|
|
@@ -1379,18 +1411,28 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1379
1411
|
"migration_status",
|
|
1380
1412
|
async (_args) => {
|
|
1381
1413
|
try {
|
|
1382
|
-
const db = (globalThis as any).__tina4_db;
|
|
1383
|
-
if (!db) return { error: "No database connection" };
|
|
1384
1414
|
// Use the real migration API (parity with the Python master, whose MCP
|
|
1385
|
-
// migration_status calls
|
|
1386
|
-
//
|
|
1387
|
-
//
|
|
1388
|
-
// @tina4/
|
|
1415
|
+
// migration_status calls status(db)). Load orm via dynamic ESM import
|
|
1416
|
+
// (the same mechanism server.ts uses for autoMigrateOnStartup) —
|
|
1417
|
+
// reqSibling()'s createRequire fails here because @tina4/orm imports
|
|
1418
|
+
// @tina4/core (no CJS "exports" main).
|
|
1389
1419
|
const orm = await import("../../orm/src/index.js");
|
|
1390
1420
|
if (typeof orm.status !== "function") {
|
|
1391
1421
|
return { error: "Migration API not available (install @tina4/orm)" };
|
|
1392
1422
|
}
|
|
1393
|
-
|
|
1423
|
+
// Pass the RAW adapter, NOT globalThis.__tina4_db. status()/migrate()
|
|
1424
|
+
// read applied-state via adapterQuery -> adapter.query(); the Database
|
|
1425
|
+
// WRAPPER has no .query/.queryAsync (only fetch/execute/tableExists), so
|
|
1426
|
+
// passing it made adapterQuery throw, got swallowed, and every migration
|
|
1427
|
+
// was reported "pending". getAdapter() is the same accessor the server
|
|
1428
|
+
// boot (server.ts) and the CLI (commands/migrate.ts) pass — and it works.
|
|
1429
|
+
let adapter;
|
|
1430
|
+
try {
|
|
1431
|
+
adapter = orm.getAdapter();
|
|
1432
|
+
} catch {
|
|
1433
|
+
return { error: "No database connection" };
|
|
1434
|
+
}
|
|
1435
|
+
const st = await orm.status(adapter, { migrationsDir: path.join(projectRoot, "migrations") });
|
|
1394
1436
|
return { completed: st.completed, pending: st.pending };
|
|
1395
1437
|
} catch (e) {
|
|
1396
1438
|
return { error: (e as Error).message };
|
|
@@ -1422,17 +1464,26 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1422
1464
|
"migration_run",
|
|
1423
1465
|
async (_args) => {
|
|
1424
1466
|
try {
|
|
1425
|
-
const db = (globalThis as any).__tina4_db;
|
|
1426
|
-
if (!db) return { error: "No database connection" };
|
|
1427
1467
|
// Run pending migrations for real (parity with the Python master's MCP
|
|
1428
|
-
// migration_run ->
|
|
1429
|
-
//
|
|
1430
|
-
//
|
|
1468
|
+
// migration_run -> migrate(db)). Load orm via dynamic ESM import
|
|
1469
|
+
// (server.ts's mechanism); reqSibling's createRequire fails because
|
|
1470
|
+
// @tina4/orm imports @tina4/core.
|
|
1431
1471
|
const orm = await import("../../orm/src/index.js");
|
|
1432
1472
|
if (typeof orm.migrate !== "function") {
|
|
1433
1473
|
return { error: "Migration API not available (install @tina4/orm)" };
|
|
1434
1474
|
}
|
|
1435
|
-
|
|
1475
|
+
// Pass the RAW adapter, NOT globalThis.__tina4_db (see migration_status
|
|
1476
|
+
// above). Passing the wrapper made migrate() unable to read applied-state
|
|
1477
|
+
// (adapterQuery threw + was swallowed), so it re-applied every migration
|
|
1478
|
+
// on every call — non-idempotent. With the raw adapter the second run
|
|
1479
|
+
// reads passed=1 rows and skips them (idempotent).
|
|
1480
|
+
let adapter;
|
|
1481
|
+
try {
|
|
1482
|
+
adapter = orm.getAdapter();
|
|
1483
|
+
} catch {
|
|
1484
|
+
return { error: "No database connection" };
|
|
1485
|
+
}
|
|
1486
|
+
const result = await orm.migrate(adapter, { migrationsDir: path.join(projectRoot, "migrations") });
|
|
1436
1487
|
return { applied: result.applied, skipped: result.skipped, failed: result.failed };
|
|
1437
1488
|
} catch (e) {
|
|
1438
1489
|
return { error: (e as Error).message };
|
|
@@ -1585,12 +1636,44 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1585
1636
|
"seed_table",
|
|
1586
1637
|
async (args) => {
|
|
1587
1638
|
try {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1639
|
+
// Load orm via dynamic ESM import (NOT reqSibling/createRequire): the
|
|
1640
|
+
// CJS require cache is a SEPARATE module instance from the ESM one that
|
|
1641
|
+
// initDatabase() populated, so its getAdapter() sees a null adapter
|
|
1642
|
+
// ("No database connection"). The migration tools already use await
|
|
1643
|
+
// import for exactly this reason — share the live adapter.
|
|
1644
|
+
const orm = await import("../../orm/src/index.js") as {
|
|
1645
|
+
seedTable?: (db: unknown, table: string, count: number, fieldMap?: Record<string, () => unknown>) => Promise<{ seeded: number; failed: number }>;
|
|
1646
|
+
autoFieldMap?: (db: unknown, table: string) => Promise<Record<string, () => unknown>>;
|
|
1647
|
+
getAdapter?: () => unknown;
|
|
1648
|
+
};
|
|
1649
|
+
if (typeof orm.seedTable !== "function" || typeof orm.autoFieldMap !== "function") {
|
|
1650
|
+
return { error: "Seeder API not available (install @tina4/orm)" };
|
|
1651
|
+
}
|
|
1652
|
+
// RAW adapter, NOT globalThis.__tina4_db: autoFieldMap introspects via
|
|
1653
|
+
// adapter.columns() and seedTable inserts via adapter.execute() — the
|
|
1654
|
+
// Database WRAPPER has neither (its introspection method is getColumns).
|
|
1655
|
+
// getAdapter() is the same accessor migration_* use above.
|
|
1656
|
+
let adapter;
|
|
1657
|
+
try {
|
|
1658
|
+
adapter = orm.getAdapter?.();
|
|
1659
|
+
} catch {
|
|
1660
|
+
return { error: "No database connection" };
|
|
1661
|
+
}
|
|
1662
|
+
if (!adapter) return { error: "No database connection" };
|
|
1591
1663
|
const count = (args.count as number) || 10;
|
|
1592
|
-
|
|
1593
|
-
|
|
1664
|
+
// Build a column->generator map from the table's real columns (parity
|
|
1665
|
+
// with Python's seed_table, which calls auto_field_map). seedTable with
|
|
1666
|
+
// NO field map is a deliberate no-op (0 rows) — this is how a caller opts
|
|
1667
|
+
// into automatic generation, so the tool must supply one or nothing
|
|
1668
|
+
// inserts (the exact bug this fixes).
|
|
1669
|
+
const fieldMap = await orm.autoFieldMap(adapter, args.table as string);
|
|
1670
|
+
if (!fieldMap || Object.keys(fieldMap).length === 0) {
|
|
1671
|
+
return { error: `Table '${args.table}' not found or has no seedable columns` };
|
|
1672
|
+
}
|
|
1673
|
+
const summary = await orm.seedTable(adapter, args.table as string, count, fieldMap);
|
|
1674
|
+
// Return an INTEGER inserted count (Python shape {table, inserted}); the
|
|
1675
|
+
// seeder returns a SeedSummary, so surface .seeded (+ .failed for context).
|
|
1676
|
+
return { table: args.table, inserted: summary.seeded, failed: summary.failed };
|
|
1594
1677
|
} catch (e) {
|
|
1595
1678
|
return { error: (e as Error).message };
|
|
1596
1679
|
}
|
|
@@ -52,7 +52,7 @@ export async function getFrond(): Promise<InstanceType<any>> {
|
|
|
52
52
|
const dir = _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
|
|
53
53
|
let engine = _frondCache.get(dir);
|
|
54
54
|
if (!engine) {
|
|
55
|
-
const { Frond } = await import("
|
|
55
|
+
const { Frond } = await import("../../frond/src/engine.js");
|
|
56
56
|
engine = new Frond(dir);
|
|
57
57
|
_frondCache.set(dir, engine);
|
|
58
58
|
}
|
|
@@ -68,7 +68,7 @@ export async function getFrameworkFrond(): Promise<InstanceType<any> | null> {
|
|
|
68
68
|
const frameworkDir = nodePath.resolve(nodePath.dirname(import.meta.url.replace("file://", "")), "..", "templates");
|
|
69
69
|
if (!_frameworkFrond && fs.existsSync(frameworkDir)) {
|
|
70
70
|
try {
|
|
71
|
-
const { Frond } = await import("
|
|
71
|
+
const { Frond } = await import("../../frond/src/engine.js");
|
|
72
72
|
_frameworkFrond = new Frond(frameworkDir);
|
|
73
73
|
} catch { return null; }
|
|
74
74
|
}
|
|
@@ -332,7 +332,7 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
332
332
|
templateDir?: string,
|
|
333
333
|
): Promise<Tina4Response> {
|
|
334
334
|
try {
|
|
335
|
-
const { Frond } = await import("
|
|
335
|
+
const { Frond } = await import("../../frond/src/engine.js");
|
|
336
336
|
const dir = templateDir ?? _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
|
|
337
337
|
let engine = _frondCache.get(dir);
|
|
338
338
|
if (!engine) {
|
|
@@ -56,6 +56,12 @@ const WIN_DRIVE_RE = /^\/?[A-Za-z]:[/\\]/;
|
|
|
56
56
|
* slashes. Anything path-like is kept as a path.
|
|
57
57
|
*/
|
|
58
58
|
export function normalizeFirebirdDbIdentifier(rawPath: string): string {
|
|
59
|
+
// php #160: a `?charset=` (or any query) tacked onto a connection URL must
|
|
60
|
+
// NOT leak into the Firebird database identifier — a path/alias never
|
|
61
|
+
// legitimately contains `?`. The charset itself is resolved separately by
|
|
62
|
+
// resolveFirebirdCharset(). Strip the query before decoding/normalising.
|
|
63
|
+
const queryIndex = rawPath.indexOf("?");
|
|
64
|
+
if (queryIndex >= 0) rawPath = rawPath.slice(0, queryIndex);
|
|
59
65
|
let decoded = decodeURIComponent(rawPath);
|
|
60
66
|
|
|
61
67
|
// Classic double-slash form: //abs/path -> /abs/path
|
|
@@ -87,6 +93,36 @@ export function normalizeFirebirdDbIdentifier(rawPath: string): string {
|
|
|
87
93
|
return decoded.startsWith("/") ? decoded : "/" + decoded;
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the Firebird connection charset (php #160 / parity with the Python
|
|
98
|
+
* master's `_resolve_firebird_charset`).
|
|
99
|
+
*
|
|
100
|
+
* The adapter used to pass NO charset, deferring to the driver's implicit
|
|
101
|
+
* default, which double-encodes UTF-8 bytes stored under a legacy `NONE`
|
|
102
|
+
* database. This resolves the charset from, in precedence order:
|
|
103
|
+
*
|
|
104
|
+
* 1. the connection URL query — `firebird://host:port/path?charset=NONE`
|
|
105
|
+
* 2. an explicit `charset` on the FirebirdConfig object passed to the adapter
|
|
106
|
+
* 3. the `TINA4_DATABASE_CHARSET` environment variable
|
|
107
|
+
* 4. the `UTF8` default
|
|
108
|
+
*
|
|
109
|
+
* Pure config resolution over its inputs (URL string, explicit charset, env) —
|
|
110
|
+
* it opens NO connection, so it is unit-testable without a live server.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveFirebirdCharset(connectionString: string, explicitCharset?: string): string {
|
|
113
|
+
let urlCharset: string | undefined;
|
|
114
|
+
const queryIndex = (connectionString ?? "").indexOf("?");
|
|
115
|
+
if (queryIndex >= 0) {
|
|
116
|
+
urlCharset = new URLSearchParams(connectionString.slice(queryIndex + 1)).get("charset") ?? undefined;
|
|
117
|
+
}
|
|
118
|
+
return (
|
|
119
|
+
urlCharset ||
|
|
120
|
+
explicitCharset ||
|
|
121
|
+
process.env.TINA4_DATABASE_CHARSET ||
|
|
122
|
+
"UTF8"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
90
126
|
export interface FirebirdConfig {
|
|
91
127
|
host?: string;
|
|
92
128
|
port?: number;
|
|
@@ -95,6 +131,8 @@ export interface FirebirdConfig {
|
|
|
95
131
|
database?: string;
|
|
96
132
|
role?: string;
|
|
97
133
|
pageSize?: number;
|
|
134
|
+
/** Connection charset. Overridden by a `?charset=` URL query; see resolveFirebirdCharset. */
|
|
135
|
+
charset?: string;
|
|
98
136
|
}
|
|
99
137
|
|
|
100
138
|
export class FirebirdAdapter implements DatabaseAdapter {
|
|
@@ -120,6 +158,9 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
120
158
|
password: parsed.password ?? "masterkey",
|
|
121
159
|
role: undefined,
|
|
122
160
|
pageSize: 4096,
|
|
161
|
+
// php #160: honour ?charset= in the URL and TINA4_DATABASE_CHARSET so a
|
|
162
|
+
// legacy NONE database isn't force-connected under a mismatched charset.
|
|
163
|
+
charset: resolveFirebirdCharset(this.config),
|
|
123
164
|
};
|
|
124
165
|
} else {
|
|
125
166
|
fbConfig = {
|
|
@@ -130,6 +171,8 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
130
171
|
password: this.config.password ?? "masterkey",
|
|
131
172
|
role: this.config.role,
|
|
132
173
|
pageSize: this.config.pageSize ?? 4096,
|
|
174
|
+
// php #160: explicit config.charset wins over env, else UTF8 default.
|
|
175
|
+
charset: resolveFirebirdCharset("", this.config.charset),
|
|
133
176
|
};
|
|
134
177
|
}
|
|
135
178
|
|
|
@@ -160,9 +203,10 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
160
203
|
}
|
|
161
204
|
|
|
162
205
|
private parseUrl(url: string): { host?: string; port?: number; user?: string; password?: string; database?: string } {
|
|
163
|
-
// firebird://user:pass@host:port/path/to/db.fdb
|
|
206
|
+
// firebird://user:pass@host:port/path/to/db.fdb[?charset=...]
|
|
164
207
|
// The path part after the host is normalised by normalizeFirebirdDbIdentifier()
|
|
165
|
-
// in connect()
|
|
208
|
+
// in connect() (which also strips any `?charset=` query — see php #160); here
|
|
209
|
+
// we just preserve it (with the leading "/" the regex strips).
|
|
166
210
|
const match = url.match(/firebird:\/\/(?:([^:]+):([^@]+)@)?([^:/]+)(?::(\d+))?\/(.*)/);
|
|
167
211
|
if (match) {
|
|
168
212
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouteDefinition, Tina4Request, Tina4Response } from "
|
|
1
|
+
import type { RouteDefinition, Tina4Request, Tina4Response } from "../../core/src/index.js";
|
|
2
2
|
import type { DiscoveredModel } from "./model.js";
|
|
3
3
|
import { getAdapter, adapterQuery, adapterExecute } from "./database.js";
|
|
4
4
|
import { buildQuery, parseQueryString } from "./query.js";
|
|
@@ -8,7 +8,7 @@ import { validate as validateFields } from "./validation.js";
|
|
|
8
8
|
import { QueryBuilder } from "./queryBuilder.js";
|
|
9
9
|
import { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
10
10
|
import { QueryCache } from "./sqlTranslation.js";
|
|
11
|
-
import { Log } from "
|
|
11
|
+
import { Log } from "../../core/src/index.js";
|
|
12
12
|
import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from "./types.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -701,23 +701,50 @@ export class BaseModel {
|
|
|
701
701
|
await adapterExecute(db, `UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pkCol}" = ?`, values);
|
|
702
702
|
} else {
|
|
703
703
|
// Insert
|
|
704
|
+
//
|
|
705
|
+
// #165: an INSERT must OMIT a column the caller never assigned so a
|
|
706
|
+
// `NOT NULL DEFAULT <x>` column gets its DB default instead of an
|
|
707
|
+
// explicit NULL that violates the constraint. In TS an unset column is
|
|
708
|
+
// `undefined` (the constructor only seeds a field that declares a
|
|
709
|
+
// non-null ORM default — everything else stays `undefined`), so the
|
|
710
|
+
// `this[name] !== undefined` filter already drops unset columns; a
|
|
711
|
+
// value the caller set to `null` IS included and written as an explicit
|
|
712
|
+
// NULL (parity with Python's _assigned_fields split — JS distinguishes
|
|
713
|
+
// undefined-unset from null-explicit natively). A resolved non-null ORM
|
|
714
|
+
// default is still written (no regression).
|
|
704
715
|
const insertFields = Object.entries(ModelClass.fields).filter(
|
|
705
716
|
([name, def]) => !(def.primaryKey && def.autoIncrement) && this[name] !== undefined,
|
|
706
717
|
);
|
|
707
718
|
|
|
708
|
-
const columns = insertFields.map(([k]) => `"${ModelClass.getDbColumn(k)}"`).join(", ");
|
|
709
|
-
const placeholders = insertFields.map(() => "?").join(", ");
|
|
710
|
-
const values = insertFields.map(([k, def]) => toDbFieldValue(def, this[k]));
|
|
711
|
-
|
|
712
719
|
// For auto-increment PKs on engines that need it (PostgreSQL),
|
|
713
720
|
// RETURNING the PK column lets us read the engine-assigned id back.
|
|
714
721
|
// SQLite ignores the RETURNING clause harmlessly (it supports it
|
|
715
722
|
// since 3.35) and we still prefer its lastInsertRowid; for other
|
|
716
723
|
// engines extractLastInsertId() reads rows[0].id.
|
|
717
724
|
const wantReturning = pkField?.autoIncrement && db.constructor.name !== "SQLiteAdapter";
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
725
|
+
const returningClause = wantReturning ? ` RETURNING "${pkCol}"` : "";
|
|
726
|
+
|
|
727
|
+
let insertSql: string;
|
|
728
|
+
let values: unknown[];
|
|
729
|
+
if (insertFields.length === 0) {
|
|
730
|
+
// #165: every insertable column was left unset — let the DB apply ALL
|
|
731
|
+
// its column defaults rather than emitting an empty column list
|
|
732
|
+
// (`() VALUES ()` is invalid on SQLite/PostgreSQL/Firebird/MSSQL, which
|
|
733
|
+
// spell the all-defaults insert `DEFAULT VALUES`; only MySQL uses the
|
|
734
|
+
// empty-parens form). Mirrors the Python master's engine split.
|
|
735
|
+
insertSql =
|
|
736
|
+
(db.constructor.name === "MysqlAdapter"
|
|
737
|
+
? `INSERT INTO "${ModelClass.tableName}" () VALUES ()`
|
|
738
|
+
: `INSERT INTO "${ModelClass.tableName}" DEFAULT VALUES`) + returningClause;
|
|
739
|
+
values = [];
|
|
740
|
+
} else {
|
|
741
|
+
const columns = insertFields.map(([k]) => `"${ModelClass.getDbColumn(k)}"`).join(", ");
|
|
742
|
+
const placeholders = insertFields.map(() => "?").join(", ");
|
|
743
|
+
values = insertFields.map(([k, def]) => toDbFieldValue(def, this[k]));
|
|
744
|
+
insertSql =
|
|
745
|
+
`INSERT INTO "${ModelClass.tableName}" (${columns}) VALUES (${placeholders})` +
|
|
746
|
+
returningClause;
|
|
747
|
+
}
|
|
721
748
|
|
|
722
749
|
const result = await adapterExecute(db, insertSql, values);
|
|
723
750
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import { QueryCache } from "./sqlTranslation.js";
|
|
36
36
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "./types.js";
|
|
37
|
-
import type { CacheBackend } from "
|
|
37
|
+
import type { CacheBackend } from "../../core/src/index.js";
|
|
38
38
|
|
|
39
39
|
function isTruthy(val: string | undefined): boolean {
|
|
40
40
|
return ["true", "1", "yes", "on"].includes((val ?? "").trim().toLowerCase());
|
|
@@ -148,7 +148,7 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
148
148
|
try {
|
|
149
149
|
// Dynamic import keeps @tina4/orm free of an import-time cycle with
|
|
150
150
|
// @tina4/core (whose `database` backend dynamically imports @tina4/orm).
|
|
151
|
-
const core: any = await import("
|
|
151
|
+
const core: any = await import("../../core/src/index.js");
|
|
152
152
|
const b: CacheBackend = await core.createBackend({
|
|
153
153
|
backend: this.backendName,
|
|
154
154
|
cacheUrl: process.env.TINA4_DB_CACHE_URL,
|
|
@@ -57,7 +57,7 @@ export { SQLTranslator, QueryCache } from "./sqlTranslation.js";
|
|
|
57
57
|
export { CachedDatabaseAdapter } from "./cachedDatabase.js";
|
|
58
58
|
export type { CachedAdapterOptions } from "./cachedDatabase.js";
|
|
59
59
|
export { FakeData } from "./fakeData.js";
|
|
60
|
-
export { seedTable, seedOrm, seedModels } from "./seeder.js";
|
|
60
|
+
export { seedTable, seedOrm, seedModels, autoFieldMap } from "./seeder.js";
|
|
61
61
|
export type { SeedSummary, SeedOptions } from "./seeder.js";
|
|
62
62
|
|
|
63
63
|
// DocStore — pymongo-style document store with a zero-config SQLite (JSON1) fallback
|
|
@@ -78,7 +78,7 @@ export { MysqlAdapter } from "./adapters/mysql.js";
|
|
|
78
78
|
export type { MysqlConfig } from "./adapters/mysql.js";
|
|
79
79
|
export { MssqlAdapter } from "./adapters/mssql.js";
|
|
80
80
|
export type { MssqlConfig } from "./adapters/mssql.js";
|
|
81
|
-
export { FirebirdAdapter, normalizeFirebirdDbIdentifier } from "./adapters/firebird.js";
|
|
81
|
+
export { FirebirdAdapter, normalizeFirebirdDbIdentifier, resolveFirebirdCharset } from "./adapters/firebird.js";
|
|
82
82
|
export type { FirebirdConfig } from "./adapters/firebird.js";
|
|
83
83
|
export { MongodbAdapter } from "./adapters/mongodb.js";
|
|
84
84
|
export type { MongoConfig } from "./adapters/mongodb.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { Log } from "
|
|
3
|
+
import { Log } from "../../core/src/index.js";
|
|
4
4
|
import type { FieldDefinition, DatabaseAdapter } from "./types.js";
|
|
5
5
|
import type { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
6
6
|
import type { DiscoveredModel } from "./model.js";
|
|
@@ -1075,8 +1075,16 @@ export async function status(
|
|
|
1075
1075
|
for (const row of rows) {
|
|
1076
1076
|
if (row.migration_name) appliedNames.add(row.migration_name);
|
|
1077
1077
|
}
|
|
1078
|
-
} catch {
|
|
1079
|
-
//
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
// The tracking table exists (checked above) but reading applied-state
|
|
1080
|
+
// failed — do NOT silently report every migration as pending. That
|
|
1081
|
+
// masked a real bug: passing the Database WRAPPER (no .query) instead of a
|
|
1082
|
+
// raw adapter made adapterQuery throw here, so status() reported all-pending
|
|
1083
|
+
// and the MCP migration_run re-applied everything each call. Log loud so a
|
|
1084
|
+
// wrong-arg / bad adapter can never again hide as "nothing applied yet".
|
|
1085
|
+
// (Python master #57 "don't swallow" lesson.) Applied-state stays empty for
|
|
1086
|
+
// this call, but the cause is now visible instead of masked.
|
|
1087
|
+
Log.error(`Migration status: could not read applied migrations from "${MIGRATION_TABLE}" — ${(err as Error).message}`);
|
|
1080
1088
|
}
|
|
1081
1089
|
|
|
1082
1090
|
for (const file of files) {
|
|
@@ -1191,7 +1199,7 @@ export async function createClassMigration(
|
|
|
1191
1199
|
const content =
|
|
1192
1200
|
`// Migration: ${description}\n` +
|
|
1193
1201
|
`// Created: ${now.toISOString()}\n\n` +
|
|
1194
|
-
`import type { DatabaseAdapter } from "
|
|
1202
|
+
`import type { DatabaseAdapter } from "tina4-nodejs/orm";\n\n` +
|
|
1195
1203
|
`export class ${className} {\n` +
|
|
1196
1204
|
` async up(db: DatabaseAdapter): Promise<void> {\n` +
|
|
1197
1205
|
` // db.execute("CREATE TABLE ...");\n` +
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type WebSocketConnection,
|
|
11
11
|
type Tina4Request,
|
|
12
12
|
type Tina4Response,
|
|
13
|
-
} from "
|
|
13
|
+
} from "../../../core/src/index.js";
|
|
14
14
|
import Workspace from "./models/workspace.js";
|
|
15
15
|
import Channel from "./models/channel.js";
|
|
16
16
|
import ChannelMember from "./models/channelMember.js";
|
|
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from "node:fs";
|
|
3
3
|
import { resolve, sep } from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { Log } from "
|
|
5
|
+
import { Log } from "../../../core/src/index.js";
|
|
6
6
|
|
|
7
7
|
// ESM has no global `require`; create one so the OPTIONAL aws-sdk can be loaded
|
|
8
8
|
// synchronously in S3Storage's constructor. A missing driver throws here and
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
// real parent PKs, and warns on clear type mismatches.
|
|
15
15
|
|
|
16
16
|
import { FakeData } from "./fakeData.js";
|
|
17
|
-
import { adapterExecute, adapterFetch } from "./database.js";
|
|
18
|
-
import { Log } from "
|
|
19
|
-
import type { DatabaseAdapter, FieldDefinition } from "./types.js";
|
|
17
|
+
import { adapterExecute, adapterFetch, adapterColumns } from "./database.js";
|
|
18
|
+
import { Log } from "../../core/src/index.js";
|
|
19
|
+
import type { DatabaseAdapter, FieldDefinition, FieldType } from "./types.js";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Result of a seed run — `{ seeded, failed, errors }`.
|
|
@@ -75,6 +75,70 @@ async function clearTable(db: DatabaseAdapter, tableName: string): Promise<void>
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Map a raw SQL column type string to a Tina4 FieldType so FakeData.forField()
|
|
80
|
+
* can pick a generator. Substring match (case-insensitive) covers the engine
|
|
81
|
+
* spellings — INTEGER/INT, REAL/FLOAT/DOUBLE/NUMERIC/DECIMAL, BOOL, DATE/TIME,
|
|
82
|
+
* TEXT/CLOB. Anything else is a plain string.
|
|
83
|
+
*/
|
|
84
|
+
function sqlTypeToFieldType(sqlType: string): FieldType {
|
|
85
|
+
const t = (sqlType || "").toUpperCase();
|
|
86
|
+
if (t.includes("INT")) return "integer";
|
|
87
|
+
if (t.includes("BOOL")) return "boolean";
|
|
88
|
+
if (t.includes("REAL") || t.includes("FLOA") || t.includes("DOUB") || t.includes("NUM") || t.includes("DEC")) return "number";
|
|
89
|
+
if (t.includes("DATE") || t.includes("TIME")) return "datetime";
|
|
90
|
+
if (t.includes("TEXT") || t.includes("CLOB")) return "text";
|
|
91
|
+
return "string";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Introspect `table`'s columns and build a column->generator field map for
|
|
96
|
+
* {@link seedTable}, skipping the auto-increment / `id` primary key (the engine
|
|
97
|
+
* assigns it). Mirrors the Python master's `auto_field_map`
|
|
98
|
+
* (tina4_python/seeder/__init__.py): the shared "seed a table I did not
|
|
99
|
+
* hand-write generators for" helper that both the dev-admin seed endpoint and
|
|
100
|
+
* the MCP `seed_table` dev tool use — `seedTable` itself stays explicit
|
|
101
|
+
* (no map = no rows), and this is how a caller opts into automatic generation.
|
|
102
|
+
*
|
|
103
|
+
* Reuses `FakeData.forField()` (column-name + type heuristics) so the generated
|
|
104
|
+
* data matches every other Tina4 seeding path. Returns an empty map when the
|
|
105
|
+
* table has no seedable columns, so `seedTable` then seeds nothing rather than
|
|
106
|
+
* crashing.
|
|
107
|
+
*
|
|
108
|
+
* @param db - A DatabaseAdapter instance (pass `getAdapter()`, NOT the Database
|
|
109
|
+
* wrapper — the wrapper has no `columns()`).
|
|
110
|
+
* @param table - The table to introspect.
|
|
111
|
+
* @param fake - Optional shared FakeData (pass one seeded via `new FakeData(n)`
|
|
112
|
+
* for reproducible output).
|
|
113
|
+
* @returns `{ column -> () => value }`, ready to hand to `seedTable`.
|
|
114
|
+
*/
|
|
115
|
+
export async function autoFieldMap(
|
|
116
|
+
db: DatabaseAdapter,
|
|
117
|
+
table: string,
|
|
118
|
+
fake: FakeData = new FakeData(),
|
|
119
|
+
): Promise<Record<string, () => unknown>> {
|
|
120
|
+
const columns = await adapterColumns(db, table);
|
|
121
|
+
const fieldMap: Record<string, () => unknown> = {};
|
|
122
|
+
for (const col of columns) {
|
|
123
|
+
const name = col.name;
|
|
124
|
+
const sqlType = String(col.type ?? "").toUpperCase();
|
|
125
|
+
// Skip the auto-increment surrogate key so the INSERT omits it and the
|
|
126
|
+
// engine assigns it. Mirrors Python: primary key AND (AUTO/SERIAL/IDENTITY
|
|
127
|
+
// in the type OR the column is literally `id` — SQLite reports just
|
|
128
|
+
// "INTEGER" for an AUTOINCREMENT rowid alias, so the name catch is needed).
|
|
129
|
+
if (col.primaryKey === true &&
|
|
130
|
+
(sqlType.includes("AUTO") || sqlType.includes("SERIAL") ||
|
|
131
|
+
sqlType.includes("IDENTITY") || name.toLowerCase() === "id")) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const fieldType = sqlTypeToFieldType(sqlType);
|
|
135
|
+
// Bind the type + name per column; forField applies the name heuristics
|
|
136
|
+
// (email/phone/name/...) before falling back to the type.
|
|
137
|
+
fieldMap[name] = () => fake.forField({ type: fieldType }, name);
|
|
138
|
+
}
|
|
139
|
+
return fieldMap;
|
|
140
|
+
}
|
|
141
|
+
|
|
78
142
|
/**
|
|
79
143
|
* Seed a database table with fake data using raw SQL inserts.
|
|
80
144
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { RouteDefinition } from "
|
|
2
|
-
import type { ModelDefinition, FieldDefinition } from "
|
|
1
|
+
import type { RouteDefinition } from "../../core/src/index.js";
|
|
2
|
+
import type { ModelDefinition, FieldDefinition } from "../../orm/src/index.js";
|
|
3
3
|
|
|
4
4
|
interface OpenAPISpecInfo {
|
|
5
5
|
title: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Tina4Request, Tina4Response, RouteDefinition } from "
|
|
1
|
+
import type { Tina4Request, Tina4Response, RouteDefinition } from "../../core/src/index.js";
|
|
2
2
|
|
|
3
3
|
// The UI assets load from a CDN by default (a documented architecture decision —
|
|
4
4
|
// we don't vendor ~1.4MB of swagger-ui-dist, to stay small). Air-gapped
|