tina4-nodejs 3.13.148 → 3.13.150
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 +4 -3
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +10898 -9844
- package/packages/cli/src/commands/generate.ts +184 -25
- package/packages/core/dist/index.js +10864 -9810
- package/packages/core/public/bundled-component-provenance.json +2 -2
- package/packages/core/public/js/tina4-dev-admin-split.html +202 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +147 -138
- package/packages/core/src/devAdmin.ts +105 -1
- package/packages/core/src/index.ts +3 -0
- package/packages/core/src/messenger.ts +57 -1
- package/packages/core/src/metrics.ts +5 -1
- package/packages/core/src/plan.ts +1 -1
- package/packages/core/src/router.ts +11 -1
- package/packages/core/src/server.ts +5 -2
- package/packages/core/templates/crud/form.twig +26 -0
- package/packages/core/templates/crud/modals.twig +31 -0
- package/packages/core/templates/crud/page.twig +401 -0
- package/packages/core/templates/crud/table.twig +69 -0
- package/packages/frond/dist/index.js +9 -1
- package/packages/frond/src/engine.ts +9 -0
- package/packages/frond/src/index.ts +1 -1
- package/packages/orm/dist/index.js +1283 -219
- package/packages/orm/src/adapters/mongodb.ts +209 -34
- package/packages/orm/src/autoCrud.ts +9 -0
- package/packages/orm/src/crud.ts +273 -0
- package/packages/orm/src/crudHelpers.ts +194 -0
- package/packages/orm/src/crudRender.ts +202 -0
- package/packages/orm/src/crudTable.ts +171 -0
- package/packages/orm/src/index.ts +7 -1
- package/packages/orm/src/migration.ts +167 -4
- package/packages/orm/src/query.ts +99 -40
- package/packages/orm/src/sqlTranslator.ts +8 -1
- package/packages/orm/src/types.ts +19 -0
- package/types/core/src/devAdmin.d.ts +23 -0
- package/types/core/src/messenger.d.ts +4 -0
- package/types/core/src/router.d.ts +0 -5
- package/types/frond/src/engine.d.ts +6 -0
- package/types/frond/src/index.d.ts +1 -1
- package/types/orm/src/crud.d.ts +82 -0
- package/types/orm/src/crudHelpers.d.ts +63 -0
- package/types/orm/src/crudRender.d.ts +38 -0
- package/types/orm/src/crudTable.d.ts +45 -0
- package/types/orm/src/index.d.ts +3 -1
- package/types/orm/src/migration.d.ts +0 -18
- package/types/orm/src/query.d.ts +0 -12
- package/types/orm/src/types.d.ts +19 -0
|
@@ -40,8 +40,9 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
40
40
|
* tina4nodejs generate websocket chat
|
|
41
41
|
* tina4nodejs generate listener user.created
|
|
42
42
|
*/
|
|
43
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
44
|
-
import { join, relative, resolve, sep } from "node:path";
|
|
43
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync } from "node:fs";
|
|
44
|
+
import { join, relative, resolve, sep, dirname } from "node:path";
|
|
45
|
+
import { fileURLToPath } from "node:url";
|
|
45
46
|
import { parseFlags } from "../util.js";
|
|
46
47
|
|
|
47
48
|
// ── Field type mapping ──────────────────────────────────────────────
|
|
@@ -652,7 +653,7 @@ export interface GeneratorSpec {
|
|
|
652
653
|
export const GENERATORS: Record<string, GeneratorSpec> = {
|
|
653
654
|
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
|
|
654
655
|
route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
655
|
-
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]',
|
|
656
|
+
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public] [--no-templates]', summary: "Model + migration + AutoCrud-backed admin page (toCrud) + editable crud/ templates + gate test" },
|
|
656
657
|
migration: { handler: (n, f) => generateMigration(n, f, undefined, undefined, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
657
658
|
middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
658
659
|
test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
|
|
@@ -695,8 +696,9 @@ const NEXT_STEPS: Record<string, (c: NextContext) => string[]> = {
|
|
|
695
696
|
],
|
|
696
697
|
crud: ({ name, table }) => [
|
|
697
698
|
`Apply the migration: npx tina4nodejs migrate`,
|
|
698
|
-
`Serve and try: npx tina4nodejs serve -> visit /swagger`,
|
|
699
|
-
`Run the gate test: npx tsx tests/${
|
|
699
|
+
`Serve and try: npx tina4nodejs serve -> visit /admin/${table} (or /swagger)`,
|
|
700
|
+
`Run the gate test: npx tsx tests/${table}.test.ts`,
|
|
701
|
+
`Restyle the UI: edit src/templates/crud/*.twig`,
|
|
700
702
|
`Change fields: edit src/models/${name}.ts then re-run generate crud`,
|
|
701
703
|
],
|
|
702
704
|
migration: () => [
|
|
@@ -1224,40 +1226,197 @@ ${aiFill(`delete_${singular}`, {
|
|
|
1224
1226
|
// ── CRUD ────────────────────────────────────────────────────────────
|
|
1225
1227
|
|
|
1226
1228
|
function generateCrud(name: string, flags: Record<string, string | boolean>): void {
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
+
// ADR-0094: `generate crud` scaffolds an admin PAGE rendered by Crud.toCrud,
|
|
1230
|
+
// whose REST backend is delegated entirely to AutoCrud (no hand-written
|
|
1231
|
+
// list/detail/write routes). The generated page route registers AutoCrud for
|
|
1232
|
+
// the model (secure-by-default; --public opens writes) and renders toCrud; the
|
|
1233
|
+
// overridable crud/*.twig templates are copied into the app so the developer
|
|
1234
|
+
// can edit them immediately.
|
|
1235
|
+
//
|
|
1236
|
+
// The table name (singular, e.g. "product") is the canonical path: the model
|
|
1237
|
+
// declares tableName "<table>", AutoCrud serves /api/<table>, and the gate
|
|
1238
|
+
// test + page route all key off it.
|
|
1229
1239
|
const table = resolveTable(name, flags, { announce: false });
|
|
1230
|
-
const routeName = pluralizeReserved(toSnake(name));
|
|
1231
1240
|
const isPublic = Boolean(flags.public);
|
|
1232
1241
|
|
|
1233
|
-
// Human-only banners; suppressed under --json to keep stdout parseable
|
|
1234
|
-
// (a console.log during --json produced invalid JSON). writeFileSafe already
|
|
1235
|
-
// gates its own "Created …" lines the same way.
|
|
1242
|
+
// Human-only banners; suppressed under --json to keep stdout parseable.
|
|
1236
1243
|
if (!__resolution.jsonMode) console.log(`\n Generating CRUD for ${name}...\n`);
|
|
1237
1244
|
|
|
1238
1245
|
// 1. Model + migration (its own model test is suppressed — the gate test
|
|
1239
1246
|
// below is CRUD's single, broader co-emitted test).
|
|
1240
1247
|
generateModel(name, flags, false);
|
|
1241
1248
|
|
|
1242
|
-
// 2.
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
generateRoute(routeName, { ...flags, model: name }, false);
|
|
1246
|
-
|
|
1247
|
-
// 3. Form
|
|
1248
|
-
generateForm(name, flags);
|
|
1249
|
+
// 2. Admin page route: registers AutoCrud (secure-by-default; --public opens
|
|
1250
|
+
// writes) and renders the toCrud admin UI at /admin/<table>.
|
|
1251
|
+
generateCrudAdminRoute(name, table, isPublic);
|
|
1249
1252
|
|
|
1250
|
-
//
|
|
1251
|
-
|
|
1253
|
+
// 3. Copy the overridable crud/*.twig templates into the app (unless
|
|
1254
|
+
// --no-templates) so the developer owns an editable copy straight away.
|
|
1255
|
+
if (!flags["no-templates"]) copyCrudTemplates();
|
|
1252
1256
|
|
|
1253
|
-
//
|
|
1254
|
-
|
|
1257
|
+
// 4. Test — secure-by-default gate test (behavioural, real TestClient).
|
|
1258
|
+
generateCrudGateTest(name, table, isPublic);
|
|
1255
1259
|
|
|
1256
1260
|
if (!__resolution.jsonMode) {
|
|
1257
1261
|
console.log(`\n CRUD generation complete for ${name}.`);
|
|
1258
1262
|
console.log(" Run: tina4nodejs migrate");
|
|
1259
|
-
console.log(
|
|
1263
|
+
console.log(` Visit: /admin/${table} for the admin UI, or /swagger for the API docs`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Write the admin page route (src/routes/admin/<table>/get.ts). It registers the
|
|
1269
|
+
* AutoCrud REST backend for the model at load (idempotent) and renders the
|
|
1270
|
+
* toCrud admin page — ADR-0094: toCrud owns no routes of its own, so the backend
|
|
1271
|
+
* is AutoCrud and only the GET page lives here. Secure-by-default; `isPublic`
|
|
1272
|
+
* opens the page AND the writes.
|
|
1273
|
+
*/
|
|
1274
|
+
function generateCrudAdminRoute(model: string, table: string, isPublic: boolean): void {
|
|
1275
|
+
const dir = resolve("src/routes/admin", table);
|
|
1276
|
+
ensureDir(dir);
|
|
1277
|
+
|
|
1278
|
+
pushRoute(`/admin/${table}`);
|
|
1279
|
+
if (__resolution.target === "crud") {
|
|
1280
|
+
setResolutionField("file_path", `src/routes/admin/${table}/get.ts`);
|
|
1260
1281
|
}
|
|
1282
|
+
|
|
1283
|
+
const secureExport = isPublic ? "" : "export const secure = true;\n\n";
|
|
1284
|
+
const publicOpt = isPublic ? ", public: true" : "";
|
|
1285
|
+
const writeDoc = isPublic
|
|
1286
|
+
? "--public: the admin page and the writes are OPEN (no token)."
|
|
1287
|
+
: "Secure by default: the admin page AND the writes require a valid Bearer token (pass --public to open them).";
|
|
1288
|
+
const pageDoc = isPublic
|
|
1289
|
+
? ""
|
|
1290
|
+
: `\n// NOTE: the admin page is secured (export const secure = true). A browser needs a valid\n// token/session to open it; wire your login (Auth / Session) or run\n// \`generate crud ${model} --public\` for an open page. The AutoCrud READ API\n// (GET /api/${table}, GET /api/${table}/{id}) stays public by AutoCrud's default.`;
|
|
1291
|
+
|
|
1292
|
+
const content = `import ${model} from "../../../models/${model}.js";
|
|
1293
|
+
import { Crud } from "tina4-nodejs/orm";
|
|
1294
|
+
import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
1295
|
+
|
|
1296
|
+
// ${model} admin — one server-rendered CRUD page (searchable, sortable,
|
|
1297
|
+
// paginated table + create/edit/delete modals). The REST backend (GET list,
|
|
1298
|
+
// GET/{id}, POST, PUT, DELETE) is AutoCrud; this file owns only the GET admin
|
|
1299
|
+
// page. ${writeDoc}${pageDoc}
|
|
1300
|
+
//
|
|
1301
|
+
// Restyle the UI by editing src/templates/crud/*.twig (copied into this app).
|
|
1302
|
+
// Register the AutoCrud backend for the model at load (idempotent).
|
|
1303
|
+
await Crud.registerBackend(${model}, { public: ${isPublic} });
|
|
1304
|
+
|
|
1305
|
+
${secureExport}export const meta = { summary: "${model} admin", tags: ["admin"] };
|
|
1306
|
+
|
|
1307
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
1308
|
+
return res.html(await Crud.toCrud(req, { model: ${model}, title: "${model} Admin"${publicOpt} }));
|
|
1309
|
+
}
|
|
1310
|
+
`;
|
|
1311
|
+
writeFileSafe(join(dir, "get.ts"), content);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/** The framework's shipped crud/ templates directory (monorepo + published). */
|
|
1315
|
+
function frameworkCrudTemplatesDir(): string | null {
|
|
1316
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1317
|
+
const candidates = [
|
|
1318
|
+
resolve(here, "../../../core/templates/crud"), // monorepo: packages/cli/src/commands -> packages/core/templates/crud
|
|
1319
|
+
resolve(here, "../../core/templates/crud"), // built layout fallback
|
|
1320
|
+
resolve(here, "../templates/crud"), // flattened publish fallback
|
|
1321
|
+
];
|
|
1322
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Copy the framework's overridable crud/*.twig templates into the app's
|
|
1327
|
+
* src/templates/crud/ so the developer can edit them in place (an app copy wins
|
|
1328
|
+
* over the framework's via toCrud's app-first resolution). Existing files are
|
|
1329
|
+
* left untouched.
|
|
1330
|
+
*/
|
|
1331
|
+
function copyCrudTemplates(): void {
|
|
1332
|
+
const sourceDir = frameworkCrudTemplatesDir();
|
|
1333
|
+
if (!sourceDir) {
|
|
1334
|
+
if (!__resolution.jsonMode) console.log(" Skipped crud templates (framework templates not found)");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const targetDir = resolve("src/templates/crud");
|
|
1338
|
+
ensureDir(targetDir);
|
|
1339
|
+
for (const file of readdirSync(sourceDir).filter((f) => f.endsWith(".twig")).sort()) {
|
|
1340
|
+
const dest = join(targetDir, file);
|
|
1341
|
+
if (existsSync(dest)) {
|
|
1342
|
+
if (!__resolution.jsonMode) console.log(` File already exists: ${dest}`);
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (__resolution.dryRun) continue;
|
|
1346
|
+
copyFileSync(join(sourceDir, file), dest);
|
|
1347
|
+
__resolution.actionsTaken.push(`wrote ${dest}`);
|
|
1348
|
+
if (!__resolution.jsonMode) console.log(` Created ${dest}`);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Emit the CRUD gate test (tests/<table>.test.ts): a real boot-gate through the
|
|
1354
|
+
* TestClient over the generated admin route + AutoCrud backend. No mocks — real
|
|
1355
|
+
* Router, real auth gate, real JWT, real route discovery. It proves the
|
|
1356
|
+
* secure-by-default POSTURE (the point of a "gate" test): which routes register,
|
|
1357
|
+
* and which are gated vs open. The posture checks are adapter-independent (the
|
|
1358
|
+
* auth gate runs before the handler), so they hold regardless of the DB; the
|
|
1359
|
+
* full DB-backed CRUD behaviour is covered by the framework's own crud tests.
|
|
1360
|
+
*/
|
|
1361
|
+
function generateCrudGateTest(model: string, table: string, isPublic: boolean): void {
|
|
1362
|
+
const dir = resolve("tests");
|
|
1363
|
+
ensureDir(dir);
|
|
1364
|
+
const posture = isPublic ? "open (--public)" : "gated";
|
|
1365
|
+
const writeCase = isPublic
|
|
1366
|
+
? ` // --public opened the write: no 401 gate (the handler, not the gate, answers).
|
|
1367
|
+
assert("anonymous POST is NOT gated (--public)",
|
|
1368
|
+
(await client.post("/api/${table}", { json: { name: "test" } })).status !== 401);`
|
|
1369
|
+
: ` // Secure by default: a tokenless POST is rejected by the gate with 401.
|
|
1370
|
+
assert("anonymous POST is gated -> 401",
|
|
1371
|
+
(await client.post("/api/${table}", { json: { name: "test" } })).status === 401);
|
|
1372
|
+
// A valid Bearer token passes the gate (the gate no longer answers 401).
|
|
1373
|
+
const token = getToken({ userId: 1 });
|
|
1374
|
+
assert("authenticated POST passes the gate (not 401)",
|
|
1375
|
+
(await client.post("/api/${table}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status !== 401);`;
|
|
1376
|
+
const pageCase = isPublic
|
|
1377
|
+
? ` assert("admin page is NOT gated (--public)", (await client.get("/admin/${table}")).status !== 401);`
|
|
1378
|
+
: ` assert("admin page is secured -> 401 without a token", (await client.get("/admin/${table}")).status === 401);`;
|
|
1379
|
+
|
|
1380
|
+
const content = `/**
|
|
1381
|
+
* ${model} CRUD admin (ADR-0094) — reads public, writes ${posture}, page ${isPublic ? "public" : "secured"}.
|
|
1382
|
+
*
|
|
1383
|
+
* Real end-to-end via TestClient: no mocks — real Router, real auth gate, real
|
|
1384
|
+
* JWT, real route discovery. The admin route registers the AutoCrud backend at
|
|
1385
|
+
* load; discoverRoutes imports it, so the /api/${table} REST routes exist here.
|
|
1386
|
+
* This gate test proves the SECURE-BY-DEFAULT posture (what registers, what is
|
|
1387
|
+
* gated vs open) — adapter-independent, since the auth gate runs before the
|
|
1388
|
+
* handler. Run with: npx tsx tests/${table}.test.ts
|
|
1389
|
+
*/
|
|
1390
|
+
import { dirname, resolve } from "node:path";
|
|
1391
|
+
import { fileURLToPath } from "node:url";
|
|
1392
|
+
import { defaultRouter, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
|
|
1393
|
+
|
|
1394
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
|
|
1395
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1396
|
+
|
|
1397
|
+
let pass = 0;
|
|
1398
|
+
let fail = 0;
|
|
1399
|
+
function assert(label: string, ok: boolean): void {
|
|
1400
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
1401
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Discovering the routes imports src/routes/admin/${table}/get.ts, whose
|
|
1405
|
+
// top-level Crud.registerBackend() adds the AutoCrud routes to defaultRouter.
|
|
1406
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) defaultRouter.addRoute(def);
|
|
1407
|
+
const patterns = defaultRouter.getRoutes().map((r) => \`\${r.method} \${r.pattern}\`);
|
|
1408
|
+
const client = new TestClient();
|
|
1409
|
+
|
|
1410
|
+
assert("AutoCrud GET list route registered", patterns.includes("GET /api/${table}"));
|
|
1411
|
+
assert("AutoCrud POST route registered", patterns.includes("POST /api/${table}"));
|
|
1412
|
+
assert("admin page route registered", patterns.includes("GET /admin/${table}"));
|
|
1413
|
+
${pageCase}
|
|
1414
|
+
${writeCase}
|
|
1415
|
+
|
|
1416
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
1417
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
1418
|
+
`;
|
|
1419
|
+
writeFileSafe(join(dir, `${table}.test.ts`), content);
|
|
1261
1420
|
}
|
|
1262
1421
|
|
|
1263
1422
|
// ── Migration ───────────────────────────────────────────────────────
|
|
@@ -1474,7 +1633,7 @@ import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
|
|
|
1474
1633
|
import { initDatabase } from "tina4-nodejs/orm";
|
|
1475
1634
|
import ${model} from "../src/models/${model}.js";
|
|
1476
1635
|
|
|
1477
|
-
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
1636
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
|
|
1478
1637
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1479
1638
|
|
|
1480
1639
|
let pass = 0;
|
|
@@ -2615,7 +2774,7 @@ import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
|
|
|
2615
2774
|
import { initDatabase } from "tina4-nodejs/orm";
|
|
2616
2775
|
import User from "../src/models/User.js";
|
|
2617
2776
|
|
|
2618
|
-
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
2777
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
|
|
2619
2778
|
delete process.env.TINA4_API_KEY;
|
|
2620
2779
|
const here = dirname(fileURLToPath(import.meta.url));`;
|
|
2621
2780
|
const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
|