tina4-nodejs 3.13.147 → 3.13.149

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.
Files changed (45) hide show
  1. package/CLAUDE.md +4 -3
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +10901 -9855
  4. package/packages/cli/src/commands/generate.ts +184 -25
  5. package/packages/core/dist/index.js +10874 -9828
  6. package/packages/core/src/devAdmin.ts +72 -1
  7. package/packages/core/src/dispatchPipeline.ts +4 -12
  8. package/packages/core/src/index.ts +3 -0
  9. package/packages/core/src/messenger.ts +57 -1
  10. package/packages/core/src/router.ts +11 -1
  11. package/packages/core/src/server.ts +5 -2
  12. package/packages/core/src/session.ts +45 -3
  13. package/packages/core/templates/crud/form.twig +26 -0
  14. package/packages/core/templates/crud/modals.twig +31 -0
  15. package/packages/core/templates/crud/page.twig +401 -0
  16. package/packages/core/templates/crud/table.twig +69 -0
  17. package/packages/frond/dist/index.js +9 -1
  18. package/packages/frond/src/engine.ts +9 -0
  19. package/packages/frond/src/index.ts +1 -1
  20. package/packages/orm/dist/index.js +1283 -227
  21. package/packages/orm/src/adapters/mongodb.ts +209 -34
  22. package/packages/orm/src/autoCrud.ts +9 -0
  23. package/packages/orm/src/crud.ts +273 -0
  24. package/packages/orm/src/crudHelpers.ts +194 -0
  25. package/packages/orm/src/crudRender.ts +202 -0
  26. package/packages/orm/src/crudTable.ts +171 -0
  27. package/packages/orm/src/index.ts +7 -1
  28. package/packages/orm/src/migration.ts +167 -4
  29. package/packages/orm/src/query.ts +99 -40
  30. package/packages/orm/src/sqlTranslator.ts +8 -1
  31. package/packages/orm/src/types.ts +19 -0
  32. package/types/core/src/devAdmin.d.ts +23 -0
  33. package/types/core/src/messenger.d.ts +4 -0
  34. package/types/core/src/router.d.ts +0 -5
  35. package/types/core/src/session.d.ts +25 -0
  36. package/types/frond/src/engine.d.ts +6 -0
  37. package/types/frond/src/index.d.ts +1 -1
  38. package/types/orm/src/crud.d.ts +82 -0
  39. package/types/orm/src/crudHelpers.d.ts +63 -0
  40. package/types/orm/src/crudRender.d.ts +38 -0
  41. package/types/orm/src/crudTable.d.ts +45 -0
  42. package/types/orm/src/index.d.ts +3 -1
  43. package/types/orm/src/migration.d.ts +0 -18
  44. package/types/orm/src/query.d.ts +0 -12
  45. package/types/orm/src/types.d.ts +19 -0
@@ -1467,7 +1467,10 @@ var init_sqlTranslator = __esm({
1467
1467
  static appendLimit(sql, limit, offset) {
1468
1468
  if (limit === void 0 || limit === null || limit <= 0) return sql;
1469
1469
  if (_SQLTranslator.hasTrailingLimit(sql)) return sql;
1470
- const trimmed = sql.replace(/[\s;]+$/, "");
1470
+ const trailing = " \n\r\f\v;";
1471
+ let end = sql.length;
1472
+ while (end > 0 && trailing.includes(sql[end - 1])) end--;
1473
+ const trimmed = sql.slice(0, end);
1471
1474
  const suffix = offset !== void 0 && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`;
1472
1475
  return `${trimmed}
1473
1476
  ${suffix}`;
@@ -2791,11 +2794,11 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
2791
2794
  if (resolvedExpiresIn !== 0) {
2792
2795
  claims.exp = now + resolvedExpiresIn * 60;
2793
2796
  }
2794
- const h = base64urlEncode(Buffer.from(JSON.stringify(header)));
2797
+ const h2 = base64urlEncode(Buffer.from(JSON.stringify(header)));
2795
2798
  const p = base64urlEncode(Buffer.from(JSON.stringify(claims)));
2796
- const signingInput = `${h}.${p}`;
2799
+ const signingInput = `${h2}.${p}`;
2797
2800
  const signature = sign(signingInput, resolvedSecret, resolvedAlgorithm);
2798
- return `${h}.${p}.${signature}`;
2801
+ return `${h2}.${p}.${signature}`;
2799
2802
  }
2800
2803
  function validToken(token, secret, algorithm) {
2801
2804
  const resolvedSecret = secret ?? process.env.TINA4_SECRET ?? "";
@@ -2806,8 +2809,8 @@ function validToken(token, secret, algorithm) {
2806
2809
  try {
2807
2810
  const parts = token.split(".");
2808
2811
  if (parts.length !== 3) return null;
2809
- const [h, p, sig] = parts;
2810
- const header = JSON.parse(base64urlDecode(h).toString());
2812
+ const [h2, p, sig] = parts;
2813
+ const header = JSON.parse(base64urlDecode(h2).toString());
2811
2814
  if (header.alg !== resolvedAlgorithm) return null;
2812
2815
  if (HMAC_DIGESTS.has(resolvedAlgorithm)) {
2813
2816
  const weak = insecureSecretMessage(resolvedSecret);
@@ -2816,7 +2819,7 @@ function validToken(token, secret, algorithm) {
2816
2819
  return null;
2817
2820
  }
2818
2821
  }
2819
- const signingInput = `${h}.${p}`;
2822
+ const signingInput = `${h2}.${p}`;
2820
2823
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
2821
2824
  return null;
2822
2825
  }
@@ -4593,7 +4596,7 @@ function any(path10, handler, middlewares, meta) {
4593
4596
  function websocket(path10, handler, options) {
4594
4597
  return defaultRouter.websocket(path10, handler, options);
4595
4598
  }
4596
- var WsRouteRef, RouteRef, Router, RouteGroup, defaultRouter;
4599
+ var WsRouteRef, RouteRef, Router, RouteGroup, TINA4_DEFAULT_ROUTER, globalRouterHost, defaultRouter;
4597
4600
  var init_router = __esm({
4598
4601
  "../core/src/router.ts"() {
4599
4602
  "use strict";
@@ -5230,7 +5233,9 @@ var init_router = __esm({
5230
5233
  callback(nestedGroup);
5231
5234
  }
5232
5235
  };
5233
- defaultRouter = new Router();
5236
+ TINA4_DEFAULT_ROUTER = /* @__PURE__ */ Symbol.for("tina4.defaultRouter");
5237
+ globalRouterHost = globalThis;
5238
+ defaultRouter = globalRouterHost[TINA4_DEFAULT_ROUTER] ?? (globalRouterHost[TINA4_DEFAULT_ROUTER] = new Router());
5234
5239
  }
5235
5240
  });
5236
5241
 
@@ -5480,6 +5485,7 @@ __export(engine_exports, {
5480
5485
  Frond: () => Frond,
5481
5486
  MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
5482
5487
  TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
5488
+ escapeHtml: () => escapeHtml,
5483
5489
  expressionFormCache: () => expressionFormCache,
5484
5490
  filterChainCache: () => filterChainCache,
5485
5491
  pathParseCache: () => pathParseCache,
@@ -6404,6 +6410,9 @@ function splitArgs(raw) {
6404
6410
  function htmlEscape(str) {
6405
6411
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
6406
6412
  }
6413
+ function escapeHtml(value) {
6414
+ return htmlEscape(String(value));
6415
+ }
6407
6416
  function jsEscape(value) {
6408
6417
  let out = "";
6409
6418
  for (const ch of String(value)) {
@@ -6580,11 +6589,11 @@ function _buildFormTokenJwt(descriptor = "") {
6580
6589
  if (_formTokenSessionId) {
6581
6590
  payload.session_id = _formTokenSessionId;
6582
6591
  }
6583
- const h = _b64url(Buffer.from(JSON.stringify(header)));
6592
+ const h2 = _b64url(Buffer.from(JSON.stringify(header)));
6584
6593
  const p = _b64url(Buffer.from(JSON.stringify(payload)));
6585
- const sigInput = `${h}.${p}`;
6594
+ const sigInput = `${h2}.${p}`;
6586
6595
  const sig = _b64url(createHmac2("sha256", secret).update(sigInput).digest());
6587
- return `${h}.${p}.${sig}`;
6596
+ return `${h2}.${p}.${sig}`;
6588
6597
  }
6589
6598
  function _generateFormToken(descriptor = "") {
6590
6599
  const token = _buildFormTokenJwt(descriptor);
@@ -11833,6 +11842,7 @@ __export(session_exports, {
11833
11842
  isSecureScheme: () => isSecureScheme,
11834
11843
  isValidSessionId: () => isValidSessionId,
11835
11844
  sessionCookieName: () => sessionCookieName,
11845
+ sessionCookieToSend: () => sessionCookieToSend,
11836
11846
  sessionStrictMode: () => sessionStrictMode
11837
11847
  });
11838
11848
  import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
@@ -11893,6 +11903,15 @@ function buildSessionCookie(sessionId, ttl, cookieName, forwardedProto, socketEn
11893
11903
  parts.push(`Max-Age=${ttl}`);
11894
11904
  return parts.join("; ");
11895
11905
  }
11906
+ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
11907
+ const newSid = sess.sessionId ?? sess.getSessionId?.();
11908
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
11909
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
11910
+ const xfProto = rawReq.headers["x-forwarded-proto"];
11911
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
11912
+ const socketEncrypted = rawReq.socket?.encrypted === true;
11913
+ return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
11914
+ }
11896
11915
  var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
11897
11916
  var init_session = __esm({
11898
11917
  "../core/src/session.ts"() {
@@ -12255,7 +12274,6 @@ var init_session = __esm({
12255
12274
  const now = Math.floor(Date.now() / 1e3);
12256
12275
  this.data = { _created: now, _accessed: now };
12257
12276
  this.dirty = false;
12258
- this.safeWrite(this.sessionId, this.data, this.ttl);
12259
12277
  this.stored = false;
12260
12278
  this.loaded = /* @__PURE__ */ new Map();
12261
12279
  this.cleared = false;
@@ -12406,6 +12424,19 @@ var init_session = __esm({
12406
12424
  const result = this.flash(key);
12407
12425
  return result !== void 0 ? result : defaultValue;
12408
12426
  }
12427
+ /**
12428
+ * Whether this session is only an id: nothing has stored it and it holds no
12429
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
12430
+ * Such a session has no record and needs no cookie, so save() writes nothing
12431
+ * for it and the server sends no cookie for it. It stops being fresh when
12432
+ * something is stored in it. Calls that mark a session changed without leaving
12433
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
12434
+ * empty session) leave it fresh: a record with no data is not a session.
12435
+ */
12436
+ isFresh() {
12437
+ if (this.stored) return false;
12438
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
12439
+ }
12409
12440
  /**
12410
12441
  * Get the current session ID.
12411
12442
  */
@@ -12468,7 +12499,7 @@ var init_session = __esm({
12468
12499
  */
12469
12500
  save() {
12470
12501
  if (!this.sessionId || !this.data) return true;
12471
- if (!this.dirty && !this.stored) return true;
12502
+ if (this.isFresh()) return true;
12472
12503
  let record = this.data;
12473
12504
  if (this.stored) {
12474
12505
  const current = this.safeRead(this.sessionId);
@@ -12655,7 +12686,7 @@ function errorLabel(err) {
12655
12686
  return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
12656
12687
  }
12657
12688
  async function sessionAutoStart(rawReq, rawRes, req2) {
12658
- const { Session: Session2, buildSessionCookie: buildSessionCookie2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
12689
+ const { Session: Session2, sessionCookieToSend: sessionCookieToSend2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
12659
12690
  const cookieHeader = rawReq.headers.cookie ?? "";
12660
12691
  const cookiePrefix = sessionCookieName2() + "=";
12661
12692
  let existingSid;
@@ -12694,13 +12725,9 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
12694
12725
  } catch {
12695
12726
  }
12696
12727
  }
12697
- const newSid = sess.sessionId ?? sess.getSessionId?.();
12698
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
12699
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
12700
- const xfProto = rawReq.headers["x-forwarded-proto"];
12701
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
12702
- const socketEncrypted = rawReq.socket?.encrypted === true;
12703
- rawRes.appendHeader("Set-Cookie", buildSessionCookie2(newSid, ttl, void 0, forwardedProto, socketEncrypted));
12728
+ const sessionCookie = sessionCookieToSend2(sess, existingSid, rawReq, rawRes);
12729
+ if (sessionCookie !== null) {
12730
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
12704
12731
  }
12705
12732
  return origEnd(...args);
12706
12733
  };
@@ -13809,8 +13836,9 @@ __export(generate_exports, {
13809
13836
  toSnake: () => toSnake,
13810
13837
  toTableName: () => toTableName
13811
13838
  });
13812
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "node:fs";
13813
- import { join as join15, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
13839
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, readdirSync as readdirSync6, copyFileSync } from "node:fs";
13840
+ import { join as join15, relative as relative2, resolve as resolve6, sep as sep3, dirname as dirname7 } from "node:path";
13841
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
13814
13842
  function ensureDir(dir) {
13815
13843
  if (__resolution.dryRun) return;
13816
13844
  if (!existsSync13(dir)) {
@@ -13893,7 +13921,7 @@ function currentResolution() {
13893
13921
  transformations: [...__resolution.body.transformations]
13894
13922
  };
13895
13923
  if (__resolution.body.edit_hints) {
13896
- body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
13924
+ body.edit_hints = __resolution.body.edit_hints.map((h2) => ({ ...h2 }));
13897
13925
  }
13898
13926
  if (__resolution.body.next) {
13899
13927
  body.next = [...__resolution.body.next];
@@ -14419,23 +14447,144 @@ ${aiFill(`delete_${singular}`, {
14419
14447
  }
14420
14448
  function generateCrud(name, flags) {
14421
14449
  const table2 = resolveTable(name, flags, { announce: false });
14422
- const routeName = pluralizeReserved(toSnake(name));
14423
14450
  const isPublic = Boolean(flags.public);
14424
14451
  if (!__resolution.jsonMode) console.log(`
14425
14452
  Generating CRUD for ${name}...
14426
14453
  `);
14427
14454
  generateModel(name, flags, false);
14428
- generateRoute(routeName, { ...flags, model: name }, false);
14429
- generateForm(name, flags);
14430
- generateView(name, flags);
14431
- generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
14455
+ generateCrudAdminRoute(name, table2, isPublic);
14456
+ if (!flags["no-templates"]) copyCrudTemplates();
14457
+ generateCrudGateTest(name, table2, isPublic);
14432
14458
  if (!__resolution.jsonMode) {
14433
14459
  console.log(`
14434
14460
  CRUD generation complete for ${name}.`);
14435
14461
  console.log(" Run: tina4nodejs migrate");
14436
- console.log(" Visit: /swagger to see the API docs");
14462
+ console.log(` Visit: /admin/${table2} for the admin UI, or /swagger for the API docs`);
14463
+ }
14464
+ }
14465
+ function generateCrudAdminRoute(model, table2, isPublic) {
14466
+ const dir = resolve6("src/routes/admin", table2);
14467
+ ensureDir(dir);
14468
+ pushRoute(`/admin/${table2}`);
14469
+ if (__resolution.target === "crud") {
14470
+ setResolutionField("file_path", `src/routes/admin/${table2}/get.ts`);
14471
+ }
14472
+ const secureExport = isPublic ? "" : "export const secure = true;\n\n";
14473
+ const publicOpt = isPublic ? ", public: true" : "";
14474
+ const writeDoc = isPublic ? "--public: the admin page and the writes are OPEN (no token)." : "Secure by default: the admin page AND the writes require a valid Bearer token (pass --public to open them).";
14475
+ const pageDoc = isPublic ? "" : `
14476
+ // NOTE: the admin page is secured (export const secure = true). A browser needs a valid
14477
+ // token/session to open it; wire your login (Auth / Session) or run
14478
+ // \`generate crud ${model} --public\` for an open page. The AutoCrud READ API
14479
+ // (GET /api/${table2}, GET /api/${table2}/{id}) stays public by AutoCrud's default.`;
14480
+ const content = `import ${model} from "../../../models/${model}.js";
14481
+ import { Crud } from "tina4-nodejs/orm";
14482
+ import type { Tina4Request, Tina4Response } from "tina4-nodejs";
14483
+
14484
+ // ${model} admin \u2014 one server-rendered CRUD page (searchable, sortable,
14485
+ // paginated table + create/edit/delete modals). The REST backend (GET list,
14486
+ // GET/{id}, POST, PUT, DELETE) is AutoCrud; this file owns only the GET admin
14487
+ // page. ${writeDoc}${pageDoc}
14488
+ //
14489
+ // Restyle the UI by editing src/templates/crud/*.twig (copied into this app).
14490
+ // Register the AutoCrud backend for the model at load (idempotent).
14491
+ await Crud.registerBackend(${model}, { public: ${isPublic} });
14492
+
14493
+ ${secureExport}export const meta = { summary: "${model} admin", tags: ["admin"] };
14494
+
14495
+ export default async function (req: Tina4Request, res: Tina4Response) {
14496
+ return res.html(await Crud.toCrud(req, { model: ${model}, title: "${model} Admin"${publicOpt} }));
14497
+ }
14498
+ `;
14499
+ writeFileSafe(join15(dir, "get.ts"), content);
14500
+ }
14501
+ function frameworkCrudTemplatesDir() {
14502
+ const here = dirname7(fileURLToPath4(import.meta.url));
14503
+ const candidates = [
14504
+ resolve6(here, "../../../core/templates/crud"),
14505
+ // monorepo: packages/cli/src/commands -> packages/core/templates/crud
14506
+ resolve6(here, "../../core/templates/crud"),
14507
+ // built layout fallback
14508
+ resolve6(here, "../templates/crud")
14509
+ // flattened publish fallback
14510
+ ];
14511
+ return candidates.find((c) => existsSync13(c)) ?? null;
14512
+ }
14513
+ function copyCrudTemplates() {
14514
+ const sourceDir = frameworkCrudTemplatesDir();
14515
+ if (!sourceDir) {
14516
+ if (!__resolution.jsonMode) console.log(" Skipped crud templates (framework templates not found)");
14517
+ return;
14518
+ }
14519
+ const targetDir = resolve6("src/templates/crud");
14520
+ ensureDir(targetDir);
14521
+ for (const file of readdirSync6(sourceDir).filter((f) => f.endsWith(".twig")).sort()) {
14522
+ const dest = join15(targetDir, file);
14523
+ if (existsSync13(dest)) {
14524
+ if (!__resolution.jsonMode) console.log(` File already exists: ${dest}`);
14525
+ continue;
14526
+ }
14527
+ if (__resolution.dryRun) continue;
14528
+ copyFileSync(join15(sourceDir, file), dest);
14529
+ __resolution.actionsTaken.push(`wrote ${dest}`);
14530
+ if (!__resolution.jsonMode) console.log(` Created ${dest}`);
14437
14531
  }
14438
14532
  }
14533
+ function generateCrudGateTest(model, table2, isPublic) {
14534
+ const dir = resolve6("tests");
14535
+ ensureDir(dir);
14536
+ const posture = isPublic ? "open (--public)" : "gated";
14537
+ const writeCase = isPublic ? ` // --public opened the write: no 401 gate (the handler, not the gate, answers).
14538
+ assert("anonymous POST is NOT gated (--public)",
14539
+ (await client.post("/api/${table2}", { json: { name: "test" } })).status !== 401);` : ` // Secure by default: a tokenless POST is rejected by the gate with 401.
14540
+ assert("anonymous POST is gated -> 401",
14541
+ (await client.post("/api/${table2}", { json: { name: "test" } })).status === 401);
14542
+ // A valid Bearer token passes the gate (the gate no longer answers 401).
14543
+ const token = getToken({ userId: 1 });
14544
+ assert("authenticated POST passes the gate (not 401)",
14545
+ (await client.post("/api/${table2}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status !== 401);`;
14546
+ const pageCase = isPublic ? ` assert("admin page is NOT gated (--public)", (await client.get("/admin/${table2}")).status !== 401);` : ` assert("admin page is secured -> 401 without a token", (await client.get("/admin/${table2}")).status === 401);`;
14547
+ const content = `/**
14548
+ * ${model} CRUD admin (ADR-0094) \u2014 reads public, writes ${posture}, page ${isPublic ? "public" : "secured"}.
14549
+ *
14550
+ * Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
14551
+ * JWT, real route discovery. The admin route registers the AutoCrud backend at
14552
+ * load; discoverRoutes imports it, so the /api/${table2} REST routes exist here.
14553
+ * This gate test proves the SECURE-BY-DEFAULT posture (what registers, what is
14554
+ * gated vs open) \u2014 adapter-independent, since the auth gate runs before the
14555
+ * handler. Run with: npx tsx tests/${table2}.test.ts
14556
+ */
14557
+ import { dirname, resolve } from "node:path";
14558
+ import { fileURLToPath } from "node:url";
14559
+ import { defaultRouter, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
14560
+
14561
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
14562
+ const here = dirname(fileURLToPath(import.meta.url));
14563
+
14564
+ let pass = 0;
14565
+ let fail = 0;
14566
+ function assert(label: string, ok: boolean): void {
14567
+ if (ok) { pass++; console.log(\` PASS \${label}\`); }
14568
+ else { fail++; console.log(\` FAIL \${label}\`); }
14569
+ }
14570
+
14571
+ // Discovering the routes imports src/routes/admin/${table2}/get.ts, whose
14572
+ // top-level Crud.registerBackend() adds the AutoCrud routes to defaultRouter.
14573
+ for (const def of await discoverRoutes(resolve(here, "../src/routes"))) defaultRouter.addRoute(def);
14574
+ const patterns = defaultRouter.getRoutes().map((r) => \`\${r.method} \${r.pattern}\`);
14575
+ const client = new TestClient();
14576
+
14577
+ assert("AutoCrud GET list route registered", patterns.includes("GET /api/${table2}"));
14578
+ assert("AutoCrud POST route registered", patterns.includes("POST /api/${table2}"));
14579
+ assert("admin page route registered", patterns.includes("GET /admin/${table2}"));
14580
+ ${pageCase}
14581
+ ${writeCase}
14582
+
14583
+ console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
14584
+ process.exit(fail > 0 ? 1 : 0);
14585
+ `;
14586
+ writeFileSafe(join15(dir, `${table2}.test.ts`), content);
14587
+ }
14439
14588
  function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
14440
14589
  const ts = timestamp();
14441
14590
  const dir = resolve6("migrations");
@@ -14577,7 +14726,7 @@ import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
14577
14726
  import { initDatabase } from "tina4-nodejs/orm";
14578
14727
  import ${model} from "../src/models/${model}.js";
14579
14728
 
14580
- process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
14729
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
14581
14730
  const here = dirname(fileURLToPath(import.meta.url));
14582
14731
 
14583
14732
  let pass = 0;
@@ -15540,7 +15689,7 @@ import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
15540
15689
  import { initDatabase } from "tina4-nodejs/orm";
15541
15690
  import User from "../src/models/User.js";
15542
15691
 
15543
- process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
15692
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
15544
15693
  delete process.env.TINA4_API_KEY;
15545
15694
  const here = dirname(fileURLToPath(import.meta.url));`;
15546
15695
  const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
@@ -15727,7 +15876,7 @@ var init_generate = __esm({
15727
15876
  GENERATORS = {
15728
15877
  model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
15729
15878
  route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
15730
- crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
15879
+ crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public] [--no-templates]', summary: "Model + migration + AutoCrud-backed admin page (toCrud) + editable crud/ templates + gate test" },
15731
15880
  migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
15732
15881
  middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
15733
15882
  test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
@@ -15756,8 +15905,9 @@ var init_generate = __esm({
15756
15905
  ],
15757
15906
  crud: ({ name, table: table2 }) => [
15758
15907
  `Apply the migration: npx tina4nodejs migrate`,
15759
- `Serve and try: npx tina4nodejs serve -> visit /swagger`,
15760
- `Run the gate test: npx tsx tests/${pluralizeReserved(toSnake(name))}.test.ts`,
15908
+ `Serve and try: npx tina4nodejs serve -> visit /admin/${table2} (or /swagger)`,
15909
+ `Run the gate test: npx tsx tests/${table2}.test.ts`,
15910
+ `Restyle the UI: edit src/templates/crud/*.twig`,
15761
15911
  `Change fields: edit src/models/${name}.ts then re-run generate crud`
15762
15912
  ],
15763
15913
  migration: () => [
@@ -18706,8 +18856,8 @@ __export(context_exports, {
18706
18856
  fts5Supported: () => fts5Supported
18707
18857
  });
18708
18858
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
18709
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
18710
- import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as join17, relative as relative4, resolve as resolve8 } from "node:path";
18859
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync8, realpathSync as realpathSync5 } from "node:fs";
18860
+ import { basename as basename4, dirname as dirname9, extname as extname4, isAbsolute as isAbsolute4, join as join17, relative as relative4, resolve as resolve8 } from "node:path";
18711
18861
  function fts5Supported() {
18712
18862
  try {
18713
18863
  const conn = new DatabaseSync2(":memory:");
@@ -18730,7 +18880,7 @@ function realResolve(abs) {
18730
18880
  } catch {
18731
18881
  }
18732
18882
  try {
18733
- return join17(realpathSync5(dirname8(abs)), basename4(abs));
18883
+ return join17(realpathSync5(dirname9(abs)), basename4(abs));
18734
18884
  } catch {
18735
18885
  return abs;
18736
18886
  }
@@ -18818,7 +18968,7 @@ var init_context = __esm({
18818
18968
  const check = fts5Check ?? fts5Supported;
18819
18969
  this.available = Boolean(check());
18820
18970
  if (!this.available) return;
18821
- const parent = dirname8(this.path);
18971
+ const parent = dirname9(this.path);
18822
18972
  if (parent !== "" && parent !== ".") {
18823
18973
  mkdirSync10(parent, { recursive: true });
18824
18974
  }
@@ -18896,7 +19046,7 @@ var init_context = __esm({
18896
19046
  this.root = rootAbs;
18897
19047
  let total = 0;
18898
19048
  const walk2 = (dir) => {
18899
- const entries = readdirSync7(dir, { withFileTypes: true });
19049
+ const entries = readdirSync8(dir, { withFileTypes: true });
18900
19050
  const files = [];
18901
19051
  const subdirs = [];
18902
19052
  for (const e of entries) {
@@ -20812,7 +20962,7 @@ var init_mongoBackend = __esm({
20812
20962
  return null;
20813
20963
  }
20814
20964
  }
20815
- push(queue, payload, delay, priority) {
20965
+ push(queue, payload, delay2, priority) {
20816
20966
  const id = randomUUID4();
20817
20967
  const now = (/* @__PURE__ */ new Date()).toISOString();
20818
20968
  const job = {
@@ -20821,7 +20971,7 @@ var init_mongoBackend = __esm({
20821
20971
  status: "pending",
20822
20972
  createdAt: now,
20823
20973
  attempts: 0,
20824
- delayUntil: delay ? new Date(Date.now() + delay * 1e3).toISOString() : null,
20974
+ delayUntil: delay2 ? new Date(Date.now() + delay2 * 1e3).toISOString() : null,
20825
20975
  // The pop sort has always been { priority: -1, createdAt: 1 }, but this
20826
20976
  // field was never written, so every job scored undefined and the queue
20827
20977
  // ran pure FIFO. Priority did not even reach here: the backend interface
@@ -20990,7 +21140,7 @@ var init_job = __esm({
20990
21140
  });
20991
21141
 
20992
21142
  // ../core/src/queueBackends/liteBackend.ts
20993
- import { mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
21143
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
20994
21144
  import { join as join18 } from "node:path";
20995
21145
  import { randomUUID as randomUUID5 } from "node:crypto";
20996
21146
  var LiteBackend;
@@ -21052,7 +21202,7 @@ var init_liteBackend = __esm({
21052
21202
  */
21053
21203
  close() {
21054
21204
  }
21055
- push(queue, payload, delay, priority) {
21205
+ push(queue, payload, delay2, priority) {
21056
21206
  const dir = this.ensureDir(queue);
21057
21207
  const id = randomUUID5();
21058
21208
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -21062,7 +21212,7 @@ var init_liteBackend = __esm({
21062
21212
  status: "pending",
21063
21213
  createdAt: now,
21064
21214
  attempts: 0,
21065
- delayUntil: delay ? new Date(Date.now() + delay * 1e3).toISOString() : null,
21215
+ delayUntil: delay2 ? new Date(Date.now() + delay2 * 1e3).toISOString() : null,
21066
21216
  priority: priority ?? 0,
21067
21217
  topic: queue,
21068
21218
  error: void 0
@@ -21081,7 +21231,7 @@ var init_liteBackend = __esm({
21081
21231
  const dir = this.ensureDir(queue);
21082
21232
  let filenames;
21083
21233
  try {
21084
- filenames = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21234
+ filenames = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21085
21235
  } catch {
21086
21236
  return [];
21087
21237
  }
@@ -21146,7 +21296,7 @@ var init_liteBackend = __esm({
21146
21296
  const reservedDir = this.ensureReservedDir(queue);
21147
21297
  let filenames;
21148
21298
  try {
21149
- filenames = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
21299
+ filenames = readdirSync9(reservedDir).filter((f) => f.endsWith(".queue-data"));
21150
21300
  } catch {
21151
21301
  return;
21152
21302
  }
@@ -21257,7 +21407,7 @@ var init_liteBackend = __esm({
21257
21407
  const scanDir = isDead ? this.ensureFailedDir(queue) : isReserved ? this.ensureReservedDir(queue) : this.ensureDir(queue);
21258
21408
  let files;
21259
21409
  try {
21260
- files = readdirSync8(scanDir).filter((f) => f.endsWith(".queue-data"));
21410
+ files = readdirSync9(scanDir).filter((f) => f.endsWith(".queue-data"));
21261
21411
  } catch {
21262
21412
  return 0;
21263
21413
  }
@@ -21278,7 +21428,7 @@ var init_liteBackend = __esm({
21278
21428
  const dir = this.ensureDir(queue);
21279
21429
  let count = 0;
21280
21430
  try {
21281
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21431
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21282
21432
  for (const file of files) {
21283
21433
  unlinkSync6(join18(dir, file));
21284
21434
  count++;
@@ -21288,7 +21438,7 @@ var init_liteBackend = __esm({
21288
21438
  const failedDir = join18(dir, "failed");
21289
21439
  try {
21290
21440
  if (existsSync16(failedDir)) {
21291
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21441
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21292
21442
  for (const file of files) {
21293
21443
  unlinkSync6(join18(failedDir, file));
21294
21444
  count++;
@@ -21299,7 +21449,7 @@ var init_liteBackend = __esm({
21299
21449
  const reservedDir = join18(dir, "reserved");
21300
21450
  try {
21301
21451
  if (existsSync16(reservedDir)) {
21302
- const files = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
21452
+ const files = readdirSync9(reservedDir).filter((f) => f.endsWith(".queue-data"));
21303
21453
  for (const file of files) {
21304
21454
  unlinkSync6(join18(reservedDir, file));
21305
21455
  count++;
@@ -21321,7 +21471,7 @@ var init_liteBackend = __esm({
21321
21471
  const dir = this.ensureDir(queue);
21322
21472
  const results = [];
21323
21473
  try {
21324
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
21474
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data")).sort();
21325
21475
  for (const file of files) {
21326
21476
  try {
21327
21477
  const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
@@ -21345,7 +21495,7 @@ var init_liteBackend = __esm({
21345
21495
  */
21346
21496
  retry(queue, jobId, delaySeconds) {
21347
21497
  try {
21348
- const queues = readdirSync8(this.basePath);
21498
+ const queues = readdirSync9(this.basePath);
21349
21499
  for (const q of queues) {
21350
21500
  const failedDir = join18(this.basePath, q, "failed");
21351
21501
  const filePath = join18(failedDir, `${jobId}.queue-data`);
@@ -21371,7 +21521,7 @@ var init_liteBackend = __esm({
21371
21521
  const failedDir = this.ensureFailedDir(queue);
21372
21522
  const results = [];
21373
21523
  try {
21374
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
21524
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
21375
21525
  for (const file of files) {
21376
21526
  try {
21377
21527
  const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
@@ -21392,7 +21542,7 @@ var init_liteBackend = __esm({
21392
21542
  if (isDead) {
21393
21543
  const failedDir = this.ensureFailedDir(queue);
21394
21544
  try {
21395
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21545
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21396
21546
  for (const file of files) {
21397
21547
  try {
21398
21548
  unlinkSync6(join18(failedDir, file));
@@ -21405,7 +21555,7 @@ var init_liteBackend = __esm({
21405
21555
  } else {
21406
21556
  const dir = this.ensureDir(queue);
21407
21557
  try {
21408
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21558
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21409
21559
  for (const file of files) {
21410
21560
  try {
21411
21561
  const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
@@ -21431,7 +21581,7 @@ var init_liteBackend = __esm({
21431
21581
  const queueDir = this.ensureDir(queue);
21432
21582
  let count = 0;
21433
21583
  try {
21434
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21584
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21435
21585
  for (const file of files) {
21436
21586
  try {
21437
21587
  const filePath = join18(failedDir, file);
@@ -21458,7 +21608,7 @@ var init_liteBackend = __esm({
21458
21608
  const dir = this.ensureDir(queue);
21459
21609
  let files;
21460
21610
  try {
21461
- files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21611
+ files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21462
21612
  } catch {
21463
21613
  return null;
21464
21614
  }
@@ -21699,11 +21849,11 @@ var init_queue = __esm({
21699
21849
  *
21700
21850
  * @param priority — Higher value = higher priority. Default 0.
21701
21851
  */
21702
- push(payload, delay, priority = 0) {
21852
+ push(payload, delay2, priority = 0) {
21703
21853
  if (this.externalBackend) {
21704
- return this.externalBackend.push(this.topic, payload, delay, priority);
21854
+ return this.externalBackend.push(this.topic, payload, delay2, priority);
21705
21855
  }
21706
- return this.liteBackend.push(this.topic, payload, delay, priority);
21856
+ return this.liteBackend.push(this.topic, payload, delay2, priority);
21707
21857
  }
21708
21858
  /**
21709
21859
  * Atomically claim the next available job from this queue's topic. Returns null if empty.
@@ -21934,11 +22084,11 @@ var init_queue = __esm({
21934
22084
  * shares the same retarget path consume()/process() use — keeping produce and
21935
22085
  * consume symmetric on the same topic argument.
21936
22086
  */
21937
- produce(topic, payload, priority = 0, delay = 0) {
22087
+ produce(topic, payload, priority = 0, delay2 = 0) {
21938
22088
  const previous = this.topic;
21939
22089
  this.retarget(topic);
21940
22090
  try {
21941
- return this.push(payload, delay, priority);
22091
+ return this.push(payload, delay2, priority);
21942
22092
  } finally {
21943
22093
  this.retarget(previous);
21944
22094
  }
@@ -22939,7 +23089,7 @@ var init_projectIndex = __esm({
22939
23089
  }
22940
23090
  }
22941
23091
  hits.sort((a, b) => b[0] - a[0]);
22942
- return hits.slice(0, Math.max(1, limit)).map((h) => h[1]);
23092
+ return hits.slice(0, Math.max(1, limit)).map((h2) => h2[1]);
22943
23093
  },
22944
23094
  fileEntry(relPath) {
22945
23095
  ProjectIndex.refresh();
@@ -23468,7 +23618,7 @@ __export(docs_exports, {
23468
23618
  });
23469
23619
  import * as fs8 from "node:fs";
23470
23620
  import * as path8 from "node:path";
23471
- import { fileURLToPath as fileURLToPath4 } from "node:url";
23621
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
23472
23622
  function detectFrameworkRoots() {
23473
23623
  const corePackageRoot = path8.resolve(__DIRNAME, "..");
23474
23624
  const packagesRoot = path8.resolve(corePackageRoot, "..");
@@ -23875,7 +24025,7 @@ var init_docs = __esm({
23875
24025
  "hash"
23876
24026
  ]);
23877
24027
  _DOCS_CACHE = /* @__PURE__ */ new Map();
23878
- __FILENAME = fileURLToPath4(import.meta.url);
24028
+ __FILENAME = fileURLToPath5(import.meta.url);
23879
24029
  __DIRNAME = path8.dirname(__FILENAME);
23880
24030
  USER_DIRS = ["src/orm", "src/routes", "src/app", "src/services", "src/models"];
23881
24031
  CAMEL_SPLIT = /([a-z0-9])([A-Z])|([A-Z]+)([A-Z][a-z])/g;
@@ -24252,11 +24402,12 @@ ${end}
24252
24402
 
24253
24403
  // ../core/src/devAdmin.ts
24254
24404
  import { cpus as osCpus } from "node:os";
24255
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync12, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync15, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
24256
- import { join as join22, dirname as dirname10, resolve as resolve12, relative as relative9, sep as sep6, basename as basename5 } from "node:path";
24257
- import { fileURLToPath as fileURLToPath5 } from "node:url";
24405
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync13, mkdirSync as mkdirSync14, copyFileSync as copyFileSync2, statSync as statSync15, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
24406
+ import { join as join22, dirname as dirname11, resolve as resolve12, relative as relative9, sep as sep6, basename as basename5 } from "node:path";
24407
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
24258
24408
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
24259
- function escapeHtml(value) {
24409
+ import net2 from "node:net";
24410
+ function escapeHtml2(value) {
24260
24411
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
24261
24412
  }
24262
24413
  function devSameOriginOk(req2) {
@@ -24294,6 +24445,42 @@ function devHostAllowed(hostHeader) {
24294
24445
  if (configured) allowed.add(configured);
24295
24446
  return allowed.has(name);
24296
24447
  }
24448
+ function ipInCidr(ip, entry) {
24449
+ try {
24450
+ ip = (ip ?? "").trim();
24451
+ if (ip.toLowerCase().startsWith("::ffff:")) ip = ip.slice(7);
24452
+ const ipFam = net2.isIP(ip);
24453
+ if (!ipFam) return false;
24454
+ entry = (entry ?? "").trim();
24455
+ const bl = new net2.BlockList();
24456
+ if (entry.includes("/")) {
24457
+ const [addr, bitsStr] = entry.split("/");
24458
+ const bits = Number(bitsStr);
24459
+ const fam = net2.isIP(addr);
24460
+ if (!fam || !Number.isInteger(bits)) return false;
24461
+ bl.addSubnet(addr, bits, fam === 4 ? "ipv4" : "ipv6");
24462
+ } else {
24463
+ const fam = net2.isIP(entry);
24464
+ if (!fam) return false;
24465
+ bl.addAddress(entry, fam === 4 ? "ipv4" : "ipv6");
24466
+ }
24467
+ return bl.check(ip, ipFam === 4 ? "ipv4" : "ipv6");
24468
+ } catch {
24469
+ return false;
24470
+ }
24471
+ }
24472
+ function devPeerAllowed(remoteIp) {
24473
+ const ip = (remoteIp ?? "").trim();
24474
+ if (isLoopback(ip)) return true;
24475
+ const configured = (process.env.TINA4_DEV_ALLOWED_PEERS ?? "").trim();
24476
+ if (!configured) return false;
24477
+ return configured.split(",").some((entry) => entry.trim() !== "" && ipInCidr(ip, entry));
24478
+ }
24479
+ function devToolbarAllowed(req2) {
24480
+ if (!devHostAllowed(req2.header("host"))) return false;
24481
+ const peer = req2.socket?.remoteAddress ?? "";
24482
+ return devPeerAllowed(peer);
24483
+ }
24297
24484
  function devRequestDenial(req2) {
24298
24485
  const path10 = (req2.path ?? "").replace(/\/+/g, "/");
24299
24486
  const isMcp = DEV_MCP_PREFIXES.some((prefix) => path10 === prefix || path10.startsWith(prefix + "/"));
@@ -24303,9 +24490,12 @@ function devRequestDenial(req2) {
24303
24490
  if (!devSameOriginOk(req2)) {
24304
24491
  return { status: 403, error: "dev-admin: refused (cross-origin request)" };
24305
24492
  }
24493
+ if (DEV_PUBLIC_ASSETS.has(path10)) {
24494
+ return null;
24495
+ }
24306
24496
  if (!isMcp) {
24307
24497
  const peer = req2.socket?.remoteAddress ?? "";
24308
- if (!(isLoopback(peer) || mcpTokenOk(req2, true))) {
24498
+ if (!(devPeerAllowed(peer) || mcpTokenOk(req2, true))) {
24309
24499
  return { status: 403, error: "dev-admin: refused (non-loopback peer)" };
24310
24500
  }
24311
24501
  }
@@ -24394,7 +24584,7 @@ function mapQueueJob(job, topic, status2) {
24394
24584
  function readQueueDir(dir, topic, status2) {
24395
24585
  if (!existsSync20(dir)) return [];
24396
24586
  const jobs = [];
24397
- for (const filename of readdirSync12(dir).sort()) {
24587
+ for (const filename of readdirSync13(dir).sort()) {
24398
24588
  if (!filename.endsWith(".queue-data")) continue;
24399
24589
  try {
24400
24590
  jobs.push(mapQueueJob(JSON.parse(readFileSync18(join22(dir, filename), "utf-8")), topic, status2));
@@ -24536,12 +24726,12 @@ function upsertDevEnvVar(key, value) {
24536
24726
  }
24537
24727
  function formatUptime(seconds) {
24538
24728
  const d = Math.floor(seconds / 86400);
24539
- const h = Math.floor(seconds % 86400 / 3600);
24729
+ const h2 = Math.floor(seconds % 86400 / 3600);
24540
24730
  const m = Math.floor(seconds % 3600 / 60);
24541
24731
  const s = Math.floor(seconds % 60);
24542
24732
  const parts = [];
24543
24733
  if (d > 0) parts.push(`${d}d`);
24544
- if (h > 0) parts.push(`${h}h`);
24734
+ if (h2 > 0) parts.push(`${h2}h`);
24545
24735
  if (m > 0) parts.push(`${m}m`);
24546
24736
  parts.push(`${s}s`);
24547
24737
  return parts.join(" ");
@@ -24562,7 +24752,7 @@ function parseEnvFile() {
24562
24752
  function walkDirRecursive(dir) {
24563
24753
  const results = [];
24564
24754
  if (!existsSync20(dir)) return results;
24565
- for (const entry of readdirSync12(dir)) {
24755
+ for (const entry of readdirSync13(dir)) {
24566
24756
  const full = join22(dir, entry);
24567
24757
  if (statSync15(full).isDirectory()) {
24568
24758
  results.push(...walkDirRecursive(full));
@@ -24592,8 +24782,8 @@ function handleGalleryDeploy(router) {
24592
24782
  for (const srcFile of allFiles) {
24593
24783
  const rel = relative9(gallerySrc, srcFile);
24594
24784
  const dest = join22(projectSrc, rel);
24595
- mkdirSync14(dirname10(dest), { recursive: true });
24596
- copyFileSync(srcFile, dest);
24785
+ mkdirSync14(dirname11(dest), { recursive: true });
24786
+ copyFileSync2(srcFile, dest);
24597
24787
  copied.push(rel);
24598
24788
  }
24599
24789
  try {
@@ -24623,9 +24813,9 @@ function realPathOf(target) {
24623
24813
  present = true;
24624
24814
  } catch {
24625
24815
  }
24626
- if (present || dirname10(existing) === existing) break;
24816
+ if (present || dirname11(existing) === existing) break;
24627
24817
  tail.unshift(basename5(existing));
24628
- existing = dirname10(existing);
24818
+ existing = dirname11(existing);
24629
24819
  }
24630
24820
  return join22(realpathSync6(existing), ...tail);
24631
24821
  }
@@ -24749,9 +24939,9 @@ function applyMcpOutcome(res, outcome) {
24749
24939
  }
24750
24940
  function renderToolbarHtml(ctx) {
24751
24941
  const nodeVersion = process.version;
24752
- const method = escapeHtml(ctx.method);
24753
- const path10 = escapeHtml(ctx.path);
24754
- const matchedPattern = escapeHtml(ctx.matchedPattern);
24942
+ const method = escapeHtml2(ctx.method);
24943
+ const path10 = escapeHtml2(ctx.path);
24944
+ const matchedPattern = escapeHtml2(ctx.matchedPattern);
24755
24945
  const reload = ctx.reload === false ? "0" : "1";
24756
24946
  return `<link rel="stylesheet" href="/__dev/toolbar.css">
24757
24947
  <div id="tina4-dev-toolbar" data-reload="${reload}">
@@ -24937,7 +25127,7 @@ function toolbarJs() {
24937
25127
  connect();
24938
25128
  })();`;
24939
25129
  }
24940
- var cpuCount, DEV_MCP_PREFIXES, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
25130
+ var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
24941
25131
  var init_devAdmin = __esm({
24942
25132
  "../core/src/devAdmin.ts"() {
24943
25133
  "use strict";
@@ -24950,6 +25140,7 @@ var init_devAdmin = __esm({
24950
25140
  init_version();
24951
25141
  cpuCount = osCpus().length;
24952
25142
  DEV_MCP_PREFIXES = ["/__dev/api/mcp", "/__dev/mcp"];
25143
+ DEV_PUBLIC_ASSETS = /* @__PURE__ */ new Set(["/__dev/toolbar.css", "/__dev/toolbar.js"]);
24953
25144
  DEV_SECRET_BASENAMES = /* @__PURE__ */ new Set([
24954
25145
  ".env",
24955
25146
  ".envrc",
@@ -25651,7 +25842,7 @@ var init_devAdmin = __esm({
25651
25842
  const queueDir = queueBasePath2();
25652
25843
  let topics = [];
25653
25844
  if (existsSync20(queueDir)) {
25654
- topics = readdirSync12(queueDir).filter((d) => {
25845
+ topics = readdirSync13(queueDir).filter((d) => {
25655
25846
  try {
25656
25847
  return statSync15(join22(queueDir, d)).isDirectory();
25657
25848
  } catch {
@@ -26155,13 +26346,13 @@ var init_devAdmin = __esm({
26155
26346
  res.json({ success: false, error: msg });
26156
26347
  }
26157
26348
  };
26158
- __devAdminFilename = fileURLToPath5(import.meta.url);
26159
- __devAdminDirname = dirname10(__devAdminFilename);
26349
+ __devAdminFilename = fileURLToPath6(import.meta.url);
26350
+ __devAdminDirname = dirname11(__devAdminFilename);
26160
26351
  handleGalleryList = (_req, res) => {
26161
26352
  const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
26162
26353
  const items = [];
26163
26354
  if (existsSync20(galleryDir)) {
26164
- const entries = readdirSync12(galleryDir).sort();
26355
+ const entries = readdirSync13(galleryDir).sort();
26165
26356
  for (const entry of entries) {
26166
26357
  const entryPath = join22(galleryDir, entry);
26167
26358
  const metaFile = join22(entryPath, "meta.json");
@@ -26265,7 +26456,7 @@ var init_devAdmin = __esm({
26265
26456
  if (cwdInGit !== "") cwdInGit += "/";
26266
26457
  }
26267
26458
  const entries = [];
26268
- for (const name of readdirSync12(target).sort()) {
26459
+ for (const name of readdirSync13(target).sort()) {
26269
26460
  if (devFilesHidden(name)) continue;
26270
26461
  const full = join22(target, name);
26271
26462
  const entryRel = relative9(root, full).replace(/\\/g, "/");
@@ -26296,7 +26487,7 @@ var init_devAdmin = __esm({
26296
26487
  if (isDir) {
26297
26488
  hasChildren = false;
26298
26489
  try {
26299
- for (const c of readdirSync12(full)) {
26490
+ for (const c of readdirSync13(full)) {
26300
26491
  if (devFilesHidden(c)) continue;
26301
26492
  hasChildren = true;
26302
26493
  break;
@@ -26388,7 +26579,7 @@ var init_devAdmin = __esm({
26388
26579
  return;
26389
26580
  }
26390
26581
  try {
26391
- mkdirSync14(dirname10(target), { recursive: true });
26582
+ mkdirSync14(dirname11(target), { recursive: true });
26392
26583
  const existed = existsSync20(target);
26393
26584
  writeFileSync13(target, content, "utf-8");
26394
26585
  try {
@@ -26460,7 +26651,7 @@ var init_devAdmin = __esm({
26460
26651
  }
26461
26652
  try {
26462
26653
  const { renameSync: renameSync3 } = await import("node:fs");
26463
- mkdirSync14(dirname10(dst), { recursive: true });
26654
+ mkdirSync14(dirname11(dst), { recursive: true });
26464
26655
  renameSync3(src, dst);
26465
26656
  res.json({ ok: true, from: relative9(root, src), to: relative9(root, dst) });
26466
26657
  } catch (e) {
@@ -26481,8 +26672,8 @@ var init_devAdmin = __esm({
26481
26672
  return;
26482
26673
  }
26483
26674
  try {
26484
- const { rmSync } = await import("node:fs");
26485
- rmSync(target, { recursive: true, force: true });
26675
+ const { rmSync: rmSync2 } = await import("node:fs");
26676
+ rmSync2(target, { recursive: true, force: true });
26486
26677
  res.json({ ok: true, deleted: relative9(root, target) });
26487
26678
  } catch (e) {
26488
26679
  res.json({ error: e.message }, 500);
@@ -26817,10 +27008,10 @@ var init_devAdmin = __esm({
26817
27008
  });
26818
27009
  };
26819
27010
  handleDevAdminJs = async (_req, res) => {
26820
- const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
26821
- const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
26822
- const { fileURLToPath: fileURLToPath8 } = await import("node:url");
26823
- const dir = dirname15(fileURLToPath8(import.meta.url));
27011
+ const { readFileSync: readFileSync27, existsSync: existsSync29 } = await import("node:fs");
27012
+ const { dirname: dirname17, join: join33, resolve: resolve21 } = await import("node:path");
27013
+ const { fileURLToPath: fileURLToPath10 } = await import("node:url");
27014
+ const dir = dirname17(fileURLToPath10(import.meta.url));
26824
27015
  const candidates = [
26825
27016
  join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
26826
27017
  // src/../public/js/
@@ -26831,7 +27022,7 @@ var init_devAdmin = __esm({
26831
27022
  // project public/
26832
27023
  ];
26833
27024
  for (const jsPath of candidates) {
26834
- if (existsSync28(jsPath)) {
27025
+ if (existsSync29(jsPath)) {
26835
27026
  try {
26836
27027
  const content = readFileSync27(jsPath, "utf-8");
26837
27028
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
@@ -26856,7 +27047,7 @@ var init_devAdmin = __esm({
26856
27047
  });
26857
27048
 
26858
27049
  // ../core/src/i18n.ts
26859
- import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync21 } from "node:fs";
27050
+ import { readFileSync as readFileSync19, readdirSync as readdirSync14, existsSync as existsSync21 } from "node:fs";
26860
27051
  import { join as join23, resolve as resolve13 } from "node:path";
26861
27052
  var I18n;
26862
27053
  var init_i18n = __esm({
@@ -26938,7 +27129,7 @@ var init_i18n = __esm({
26938
27129
  return [this._defaultLocale];
26939
27130
  }
26940
27131
  try {
26941
- const files = readdirSync13(this._localeDir);
27132
+ const files = readdirSync14(this._localeDir);
26942
27133
  const locales = files.filter((f) => f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")).map((f) => f.replace(/\.(json|ya?ml)$/, "")).sort();
26943
27134
  return locales.length > 0 ? locales : [this._defaultLocale];
26944
27135
  } catch {
@@ -28194,10 +28385,10 @@ var init_docsAutoDiscovery = __esm({
28194
28385
 
28195
28386
  // ../core/src/server.ts
28196
28387
  import { randomBytes as randomBytes7 } from "node:crypto";
28197
- import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative10 } from "node:path";
28198
- import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
28388
+ import { resolve as resolve15, dirname as dirname12, join as join25, relative as relative10 } from "node:path";
28389
+ import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
28199
28390
  import { isatty } from "node:tty";
28200
- import { fileURLToPath as fileURLToPath6 } from "node:url";
28391
+ import { fileURLToPath as fileURLToPath7 } from "node:url";
28201
28392
  import { execFileSync as execFileSync3, exec } from "node:child_process";
28202
28393
  import cluster from "node:cluster";
28203
28394
  import os2 from "node:os";
@@ -28237,7 +28428,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
28237
28428
  if (!existsSync23(dir)) return;
28238
28429
  let hasSql = false;
28239
28430
  try {
28240
- hasSql = readdirSync14(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
28431
+ hasSql = readdirSync15(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
28241
28432
  } catch {
28242
28433
  return;
28243
28434
  }
@@ -28407,7 +28598,7 @@ function injectDevToolbar(html, ctx) {
28407
28598
  function walkGalleryFiles(dir) {
28408
28599
  const results = [];
28409
28600
  if (!existsSync23(dir)) return results;
28410
- for (const f of readdirSync14(dir)) {
28601
+ for (const f of readdirSync15(dir)) {
28411
28602
  const full = join25(dir, f);
28412
28603
  if (statSync16(full).isDirectory()) results.push(...walkGalleryFiles(full));
28413
28604
  else results.push(full);
@@ -28419,7 +28610,7 @@ function getGalleryDeployedState() {
28419
28610
  const state = {};
28420
28611
  if (!existsSync23(galleryDir)) return state;
28421
28612
  try {
28422
- const entries = readdirSync14(galleryDir).sort();
28613
+ const entries = readdirSync15(galleryDir).sort();
28423
28614
  for (const entry of entries) {
28424
28615
  const entryPath = join25(galleryDir, entry);
28425
28616
  const metaFile = join25(entryPath, "meta.json");
@@ -28470,7 +28661,7 @@ function resolveTemplate(pathname, templatesDir) {
28470
28661
  const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
28471
28662
  if (existsSync23(pagesDir)) {
28472
28663
  const scan = (dir, prefix) => {
28473
- for (const entry of readdirSync14(dir, { withFileTypes: true })) {
28664
+ for (const entry of readdirSync15(dir, { withFileTypes: true })) {
28474
28665
  if (entry.name.startsWith("_")) continue;
28475
28666
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
28476
28667
  if (entry.isDirectory()) {
@@ -28789,7 +28980,7 @@ function isInjectableHtml(res) {
28789
28980
  return isHtmlResponse(res) && !res.raw.getHeader("content-encoding");
28790
28981
  }
28791
28982
  function injectIntoHtml(ctx, devToolbar, html) {
28792
- if (!devToolbar) return injectFeedbackWidget(ctx.req, html);
28983
+ if (!devToolbar || !devToolbarAllowed(ctx.req)) return injectFeedbackWidget(ctx.req, html);
28793
28984
  const toolbarCtx = {
28794
28985
  version: TINA4_VERSION,
28795
28986
  method: ctx.req.method ?? "GET",
@@ -29157,7 +29348,7 @@ function wireLocaleGlobal(frondEngine, base) {
29157
29348
  const localeDir = resolve15(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
29158
29349
  if (!existsSync23(localeDir)) return;
29159
29350
  try {
29160
- const localeFiles = readdirSync14(localeDir).filter((file) => file.endsWith(".json"));
29351
+ const localeFiles = readdirSync15(localeDir).filter((file) => file.endsWith(".json"));
29161
29352
  if (localeFiles.length === 0 || frondEngine.globals?.t) return;
29162
29353
  const i18nInstance = new I18n(process.env.TINA4_LOCALE ?? "en", localeDir);
29163
29354
  frondEngine.addGlobal("t", (key, params) => i18nInstance.t(key, params));
@@ -29572,8 +29763,8 @@ var init_server = __esm({
29572
29763
  init_i18n();
29573
29764
  init_background();
29574
29765
  init_version();
29575
- __filename = fileURLToPath6(import.meta.url);
29576
- __dirname = dirname11(__filename);
29766
+ __filename = fileURLToPath7(import.meta.url);
29767
+ __dirname = dirname12(__filename);
29577
29768
  BUILTIN_ERROR_TEMPLATES_DIR = resolve15(__dirname, "..", "templates");
29578
29769
  BUILTIN_PUBLIC_DIR = resolve15(__dirname, "..", "public");
29579
29770
  swaggerAssetsEnabled = false;
@@ -29735,7 +29926,7 @@ var init_env = __esm({
29735
29926
 
29736
29927
  // ../core/src/fakeData.ts
29737
29928
  import { randomInt, randomUUID as randomUUID6 } from "node:crypto";
29738
- import { existsSync as existsSync24, readdirSync as readdirSync15 } from "node:fs";
29929
+ import { existsSync as existsSync24, readdirSync as readdirSync16 } from "node:fs";
29739
29930
  import { resolve as resolve16, join as join26 } from "node:path";
29740
29931
  function mulberry32(seed) {
29741
29932
  let s = seed | 0;
@@ -30235,7 +30426,7 @@ var init_fakeData = __esm({
30235
30426
  async seedDir(seedDir) {
30236
30427
  const dir = resolve16(seedDir ?? "src/seeds");
30237
30428
  if (!existsSync24(dir)) return [];
30238
- const files = readdirSync15(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
30429
+ const files = readdirSync16(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
30239
30430
  const executed = [];
30240
30431
  for (const file of files) {
30241
30432
  const fullPath = join26(dir, file);
@@ -30341,7 +30532,7 @@ var init_mqttMessage = __esm({
30341
30532
  });
30342
30533
 
30343
30534
  // ../core/src/mqtt.ts
30344
- import net2 from "node:net";
30535
+ import net3 from "node:net";
30345
30536
  import tls from "node:tls";
30346
30537
  import { randomBytes as randomBytes8 } from "node:crypto";
30347
30538
  import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
@@ -30812,7 +31003,7 @@ var init_mqtt = __esm({
30812
31003
  if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
30813
31004
  sock = tls.connect(opts, () => settle(() => resolve21(sock)));
30814
31005
  } else {
30815
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
31006
+ sock = net3.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
30816
31007
  }
30817
31008
  sock.once("error", (err) => {
30818
31009
  settle(() => {
@@ -31029,7 +31220,7 @@ var init_mqtt = __esm({
31029
31220
  });
31030
31221
 
31031
31222
  // ../core/src/service.ts
31032
- import { readdirSync as readdirSync16, statSync as statSync17, watchFile, unwatchFile } from "node:fs";
31223
+ import { readdirSync as readdirSync17, statSync as statSync17, watchFile, unwatchFile } from "node:fs";
31033
31224
  import { join as join27, extname as extname7 } from "node:path";
31034
31225
  import { pathToFileURL } from "node:url";
31035
31226
  function matchCronField(field, value) {
@@ -31197,7 +31388,7 @@ var init_service = __esm({
31197
31388
  const discovered = [];
31198
31389
  let entries;
31199
31390
  try {
31200
- entries = readdirSync16(dir);
31391
+ entries = readdirSync17(dir);
31201
31392
  } catch {
31202
31393
  return discovered;
31203
31394
  }
@@ -31324,7 +31515,7 @@ var init_service = __esm({
31324
31515
  const dir = serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
31325
31516
  let entries;
31326
31517
  try {
31327
- entries = readdirSync16(dir);
31518
+ entries = readdirSync17(dir);
31328
31519
  } catch {
31329
31520
  return;
31330
31521
  }
@@ -31431,9 +31622,9 @@ function ipToBytes(ip) {
31431
31622
  }
31432
31623
  function parseCidr(cidr) {
31433
31624
  const [netStr, bitsStr] = cidr.split("/");
31434
- const net4 = ipToBytes(netStr);
31435
- if (!net4) return null;
31436
- return { net: net4, bits: Number(bitsStr) };
31625
+ const net5 = ipToBytes(netStr);
31626
+ if (!net5) return null;
31627
+ return { net: net5, bits: Number(bitsStr) };
31437
31628
  }
31438
31629
  function inCidr(packed, cidr) {
31439
31630
  if (packed.length !== cidr.net.length) return false;
@@ -32012,11 +32203,11 @@ var init_api = __esm({
32012
32203
  path: null
32013
32204
  };
32014
32205
  }
32015
- const net4 = await this.performRequest("GET", url, headers, data, MAX_REDIRECTS);
32016
- if (net4.kind === "error") {
32017
- return { http_code: null, headers: {}, error: net4.error, path: null };
32206
+ const net5 = await this.performRequest("GET", url, headers, data, MAX_REDIRECTS);
32207
+ if (net5.kind === "error") {
32208
+ return { http_code: null, headers: {}, error: net5.error, path: null };
32018
32209
  }
32019
- const res = net4.res;
32210
+ const res = net5.res;
32020
32211
  const code = res.statusCode ?? null;
32021
32212
  const respHeaders = flattenHeaders(res.headers);
32022
32213
  if (code === null || code >= 400) {
@@ -32274,11 +32465,11 @@ var init_api = __esm({
32274
32465
  if (this.transportFn) {
32275
32466
  return this.callTransport(method, url, headers, data);
32276
32467
  }
32277
- const net4 = await this.performRequest(method, url, headers, data, MAX_REDIRECTS);
32278
- if (net4.kind === "error") {
32279
- return { http_code: null, body: null, headers: {}, error: net4.error };
32468
+ const net5 = await this.performRequest(method, url, headers, data, MAX_REDIRECTS);
32469
+ if (net5.kind === "error") {
32470
+ return { http_code: null, body: null, headers: {}, error: net5.error };
32280
32471
  }
32281
- return this.readResponse(net4.res);
32472
+ return this.readResponse(net5.res);
32282
32473
  }
32283
32474
  /**
32284
32475
  * Invoke a user-injected transport and normalize its result. The transport
@@ -32455,7 +32646,7 @@ var init_api = __esm({
32455
32646
  });
32456
32647
 
32457
32648
  // ../core/src/messenger.ts
32458
- import net3 from "node:net";
32649
+ import net4 from "node:net";
32459
32650
  import tls2 from "node:tls";
32460
32651
  import { readFileSync as readFileSync23 } from "node:fs";
32461
32652
  import { basename as basename7 } from "node:path";
@@ -32467,6 +32658,32 @@ function normaliseEncryption(value, kind) {
32467
32658
  }
32468
32659
  return normalised;
32469
32660
  }
32661
+ function resolveSendTimeoutMs(optionTimeout) {
32662
+ if (optionTimeout === void 0 || optionTimeout === null) {
32663
+ return resolveEnvTimeoutSeconds(process.env.TINA4_MAIL_TIMEOUT) * 1e3;
32664
+ }
32665
+ if (!Number.isInteger(optionTimeout) || optionTimeout < 1) {
32666
+ throw new Error(
32667
+ `Messenger timeout must be a whole number of seconds and at least 1, got ${optionTimeout}.`
32668
+ );
32669
+ }
32670
+ return optionTimeout * 1e3;
32671
+ }
32672
+ function resolveEnvTimeoutSeconds(raw) {
32673
+ if (!raw || raw.trim() === "") return DEFAULT_TIMEOUT_SECONDS;
32674
+ const trimmed = raw.trim();
32675
+ const seconds = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN;
32676
+ if (Number.isInteger(seconds) && seconds >= 1) return seconds;
32677
+ warnBadMailTimeout(raw);
32678
+ return DEFAULT_TIMEOUT_SECONDS;
32679
+ }
32680
+ function warnBadMailTimeout(raw) {
32681
+ if (_timeoutWarned.has(raw)) return;
32682
+ _timeoutWarned.add(raw);
32683
+ Log.warning(
32684
+ `TINA4_MAIL_TIMEOUT must be a whole number of seconds and at least 1, got ${JSON.stringify(raw)} - using the default of ${DEFAULT_TIMEOUT_SECONDS} seconds`
32685
+ );
32686
+ }
32470
32687
  function failOnIdle(socket, timeoutMs, what) {
32471
32688
  socket.setTimeout(timeoutMs, () => {
32472
32689
  socket.destroy(new Error(`${what} timed out after ${timeoutMs / 1e3}s`));
@@ -32478,7 +32695,7 @@ function connectVerifiedTls(host, port, socket) {
32478
32695
  host,
32479
32696
  port,
32480
32697
  socket,
32481
- servername: net3.isIP(host) ? void 0 : host,
32698
+ servername: net4.isIP(host) ? void 0 : host,
32482
32699
  rejectUnauthorized: true
32483
32700
  });
32484
32701
  return new Promise((resolve21, reject) => {
@@ -32490,7 +32707,7 @@ function connectVerifiedTls(host, port, socket) {
32490
32707
  });
32491
32708
  }
32492
32709
  function connectPlain(host, port) {
32493
- const socket = net3.createConnection({ host, port });
32710
+ const socket = net4.createConnection({ host, port });
32494
32711
  return new Promise((resolve21, reject) => {
32495
32712
  socket.once("connect", () => {
32496
32713
  socket.removeListener("error", reject);
@@ -32728,7 +32945,7 @@ function decodeTransfer(body, encoding) {
32728
32945
  }
32729
32946
  }
32730
32947
  if (enc === "quoted-printable") {
32731
- return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
32948
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h2) => String.fromCharCode(parseInt(h2, 16)));
32732
32949
  }
32733
32950
  return body;
32734
32951
  }
@@ -32845,7 +33062,7 @@ function parseFullMessage(uid, response) {
32845
33062
  function createMessenger() {
32846
33063
  return new Messenger();
32847
33064
  }
32848
- var MAIL_ENCRYPTION_VALUES, SMTP_SEND_TIMEOUT_MS, SMTP_CHECK_TIMEOUT_MS, MessengerConnectionError, SmtpCommandError, Messenger, imapTagCounter;
33065
+ var MAIL_ENCRYPTION_VALUES, SMTP_SEND_TIMEOUT_MS, SMTP_CHECK_TIMEOUT_MS, _timeoutWarned, DEFAULT_TIMEOUT_SECONDS, MessengerConnectionError, SmtpCommandError, Messenger, imapTagCounter;
32849
33066
  var init_messenger = __esm({
32850
33067
  "../core/src/messenger.ts"() {
32851
33068
  "use strict";
@@ -32855,6 +33072,8 @@ var init_messenger = __esm({
32855
33072
  MAIL_ENCRYPTION_VALUES = ["ssl", "tls", "starttls", "none"];
32856
33073
  SMTP_SEND_TIMEOUT_MS = 3e4;
32857
33074
  SMTP_CHECK_TIMEOUT_MS = 1e4;
33075
+ _timeoutWarned = /* @__PURE__ */ new Set();
33076
+ DEFAULT_TIMEOUT_SECONDS = 30;
32858
33077
  MessengerConnectionError = class extends Error {
32859
33078
  constructor(message) {
32860
33079
  super(message);
@@ -32884,6 +33103,8 @@ var init_messenger = __esm({
32884
33103
  imapUser;
32885
33104
  imapPass;
32886
33105
  imapEncryption;
33106
+ /** Resolved SMTP send timeout, in milliseconds (see the constructor). */
33107
+ timeoutMs;
32887
33108
  constructor(options) {
32888
33109
  this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
32889
33110
  this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
@@ -32905,6 +33126,7 @@ var init_messenger = __esm({
32905
33126
  this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
32906
33127
  this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
32907
33128
  this.imapEncryption = normaliseEncryption(options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls", "IMAP");
33129
+ this.timeoutMs = resolveSendTimeoutMs(options?.timeout);
32908
33130
  }
32909
33131
  /**
32910
33132
  * How the SMTP connection is secured (ADR-0071 section 1). Port 465 is the
@@ -33040,7 +33262,7 @@ var init_messenger = __esm({
33040
33262
  async sendSmtp(options, recipients, messageId) {
33041
33263
  let socket = null;
33042
33264
  try {
33043
- socket = await this.openSmtpSession();
33265
+ socket = await this.openSmtpSession(this.timeoutMs);
33044
33266
  await this.authenticateSmtp(socket);
33045
33267
  await this.sendSmtpEnvelope(socket, recipients.allRecipients);
33046
33268
  await this.sendSmtpMessage(socket, options, recipients, messageId);
@@ -34089,13 +34311,13 @@ var init_htmlElement = __esm({
34089
34311
  // ../core/src/ai.ts
34090
34312
  import { existsSync as existsSync26, mkdirSync as mkdirSync17, writeFileSync as writeFileSync16, readFileSync as readFileSync24 } from "node:fs";
34091
34313
  import { homedir } from "node:os";
34092
- import { join as join28, resolve as resolve17, relative as relative11, dirname as dirname12 } from "node:path";
34093
- import { fileURLToPath as fileURLToPath7 } from "node:url";
34314
+ import { join as join28, resolve as resolve17, relative as relative11, dirname as dirname13 } from "node:path";
34315
+ import { fileURLToPath as fileURLToPath8 } from "node:url";
34094
34316
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
34095
34317
  import { createInterface } from "node:readline";
34096
34318
  function readVersion() {
34097
34319
  try {
34098
- const thisDir = dirname12(fileURLToPath7(import.meta.url));
34320
+ const thisDir = dirname13(fileURLToPath8(import.meta.url));
34099
34321
  const rootPkg = resolve17(thisDir, "..", "..", "..", "package.json");
34100
34322
  const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
34101
34323
  return pkg.version ?? "0.0.0";
@@ -34311,7 +34533,7 @@ function replaceMarkerBlock(existing, block, start2, end) {
34311
34533
  }
34312
34534
  function looksLikeOldFrameworkInstall(existing) {
34313
34535
  const head = existing.replace(/^\s+/, "").slice(0, 400);
34314
- return OLD_FRAMEWORK_HEADERS.some((h) => head.startsWith(h));
34536
+ return OLD_FRAMEWORK_HEADERS.some((h2) => head.startsWith(h2));
34315
34537
  }
34316
34538
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
34317
34539
  const block = skillBlock(contextFile);
@@ -34341,7 +34563,7 @@ function installForTool(root, tool, context) {
34341
34563
  if (tool.configDir) {
34342
34564
  mkdirSync17(join28(root, tool.configDir), { recursive: true });
34343
34565
  }
34344
- const parentDir = dirname12(contextPath);
34566
+ const parentDir = dirname13(contextPath);
34345
34567
  mkdirSync17(parentDir, { recursive: true });
34346
34568
  const action = writeOrMerge(contextPath, tool.contextFile, context);
34347
34569
  const rel = relative11(root, contextPath);
@@ -34718,7 +34940,7 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
34718
34940
  }
34719
34941
  function generateClaudeCodeContext() {
34720
34942
  try {
34721
- const thisDir = dirname12(fileURLToPath7(import.meta.url));
34943
+ const thisDir = dirname13(fileURLToPath8(import.meta.url));
34722
34944
  const repoRoot = resolve17(thisDir, "..", "..", "..");
34723
34945
  const claudeMdPath = join28(repoRoot, "CLAUDE.md");
34724
34946
  if (existsSync26(claudeMdPath)) {
@@ -35566,8 +35788,8 @@ var init_aiClient = __esm({
35566
35788
  static retryDelay(headers, deadline) {
35567
35789
  const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
35568
35790
  const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
35569
- const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
35570
- return new Promise((resolve21) => setTimeout(resolve21, delay));
35791
+ const delay2 = Math.min(requested, Math.max(0, deadline - performance.now()));
35792
+ return new Promise((resolve21) => setTimeout(resolve21, delay2));
35571
35793
  }
35572
35794
  static async requestJson(config, headers, body) {
35573
35795
  const deadline = performance.now() + config.totalTimeout * 1e3;
@@ -38622,7 +38844,7 @@ __export(sqlite_exports, {
38622
38844
  });
38623
38845
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
38624
38846
  import { mkdirSync as mkdirSync18 } from "node:fs";
38625
- import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18, sep as sep7 } from "node:path";
38847
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join29, resolve as resolve18, sep as sep7 } from "node:path";
38626
38848
  function isIdentifier(str) {
38627
38849
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
38628
38850
  }
@@ -38659,7 +38881,7 @@ function resolveSqlitePath(dbPath) {
38659
38881
  }
38660
38882
  const cwd = resolve18(process.cwd());
38661
38883
  const path10 = join29(cwd, dbPath);
38662
- const parent = dirname13(path10);
38884
+ const parent = dirname14(path10);
38663
38885
  const absParent = resolve18(parent);
38664
38886
  if (absParent !== cwd && !absParent.startsWith(cwd + sep7)) {
38665
38887
  throw new Error(
@@ -41088,9 +41310,75 @@ __export(mongodb_exports, {
41088
41310
  function isMatchAllWhere(where) {
41089
41311
  return where != null && MATCH_ALL_WHERE.test(where);
41090
41312
  }
41313
+ function isSqlWs(ch) {
41314
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
41315
+ }
41316
+ function isWordChar(ch) {
41317
+ return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_";
41318
+ }
41319
+ function onlyWs(sql, start2, end) {
41320
+ for (let i = start2; i < end; i++) if (!isSqlWs(sql[i])) return false;
41321
+ return true;
41322
+ }
41323
+ function keywordAt(upper, keyword, from) {
41324
+ let at = upper.indexOf(keyword, from);
41325
+ while (at >= 0) {
41326
+ const end = at + keyword.length;
41327
+ const okBefore = at === 0 || isSqlWs(upper[at - 1]);
41328
+ const okAfter = end >= upper.length || isSqlWs(upper[end]);
41329
+ if (okBefore && okAfter) return at;
41330
+ at = upper.indexOf(keyword, at + 1);
41331
+ }
41332
+ return -1;
41333
+ }
41334
+ function earliest(positions, fallback) {
41335
+ let min = fallback;
41336
+ for (const p of positions) if (p >= 0 && p < min) min = p;
41337
+ return min;
41338
+ }
41339
+ function readIdentifier(sql, from) {
41340
+ let i = from;
41341
+ while (i < sql.length && isSqlWs(sql[i])) i++;
41342
+ if (i < sql.length && QUOTE_CHARS.includes(sql[i])) i++;
41343
+ const start2 = i;
41344
+ while (i < sql.length && isWordChar(sql[i])) i++;
41345
+ if (i === start2) return null;
41346
+ const name = sql.slice(start2, i);
41347
+ if (i < sql.length && QUOTE_CHARS.includes(sql[i])) i++;
41348
+ return [name, i];
41349
+ }
41350
+ function keywordGapEnd(upper, sql, keyword, from) {
41351
+ const at = keywordAt(upper, keyword, from);
41352
+ if (at < 0 || !onlyWs(sql, from, at)) return -1;
41353
+ return at + keyword.length;
41354
+ }
41355
+ function stripQuotes(value) {
41356
+ let result = value;
41357
+ if (result.length > 0 && (result[0] === '"' || result[0] === "'")) result = result.slice(1);
41358
+ const last = result.length - 1;
41359
+ if (last >= 0 && (result[last] === '"' || result[last] === "'")) result = result.slice(0, last);
41360
+ return result;
41361
+ }
41362
+ function splitOnAnd(where) {
41363
+ const parts = [];
41364
+ const lower = where.toLowerCase();
41365
+ let segStart = 0;
41366
+ let i = 0;
41367
+ while (i < where.length) {
41368
+ if (lower.startsWith("and", i) && i > 0 && isSqlWs(where[i - 1]) && i + 3 < where.length && isSqlWs(where[i + 3])) {
41369
+ parts.push(where.slice(segStart, i));
41370
+ i += 3;
41371
+ segStart = i;
41372
+ continue;
41373
+ }
41374
+ i++;
41375
+ }
41376
+ parts.push(where.slice(segStart));
41377
+ return parts;
41378
+ }
41091
41379
  function parseWhereClause(where, params, paramOffset = 0) {
41092
41380
  const filter = {};
41093
- const parts = where.split(/\s+AND\s+/i);
41381
+ const parts = splitOnAnd(where);
41094
41382
  let paramIndex = paramOffset;
41095
41383
  for (const part of parts) {
41096
41384
  const trimmed = part.trim();
@@ -41163,27 +41451,57 @@ function requireWriteFilter(filter, operation, table2) {
41163
41451
  );
41164
41452
  }
41165
41453
  }
41166
- function parseSelect(sql, params) {
41167
- const match = sql.match(/^SELECT\s+(.*?)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.*?))?(?:\s+ORDER\s+BY\s+(.*?))?(?:\s+LIMIT\s+(\d+))?(?:\s+OFFSET\s+(\d+))?$/is);
41168
- if (!match) return null;
41169
- const [, cols, collection, whereClause, orderBy, limitStr, skipStr] = match;
41454
+ function buildProjection(cols) {
41170
41455
  const projection = {};
41171
- if (cols && cols.trim() !== "*") {
41172
- for (const col of cols.split(",")) {
41173
- const name = col.trim().replace(/^["']|["']$/g, "");
41174
- if (name && name !== "*") projection[name] = 1;
41175
- }
41176
- }
41177
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41456
+ if (!cols || cols.trim() === "*") return projection;
41457
+ for (const col of cols.split(",")) {
41458
+ const name = stripQuotes(col.trim());
41459
+ if (name && name !== "*") projection[name] = 1;
41460
+ }
41461
+ return projection;
41462
+ }
41463
+ function stripTrailingDirection(part) {
41464
+ const lower = part.toLowerCase();
41465
+ let suffix = 0;
41466
+ if (lower.endsWith("asc")) suffix = 3;
41467
+ else if (lower.endsWith("desc")) suffix = 4;
41468
+ else return part;
41469
+ const beforeIdx = part.length - suffix - 1;
41470
+ if (beforeIdx < 0 || !isSqlWs(part[beforeIdx])) return part;
41471
+ let i = beforeIdx;
41472
+ while (i >= 0 && isSqlWs(part[i])) i--;
41473
+ return part.slice(0, i + 1);
41474
+ }
41475
+ function parseOrderBy(orderBy) {
41178
41476
  const sort = {};
41179
- if (orderBy) {
41180
- for (const part of orderBy.split(",")) {
41181
- const trimmed = part.trim();
41182
- const desc = /DESC$/i.test(trimmed);
41183
- const col = trimmed.replace(/\s+(ASC|DESC)$/i, "").replace(/^["']|["']$/g, "").trim();
41184
- sort[col] = desc ? -1 : 1;
41185
- }
41477
+ for (const rawPart of orderBy.split(",")) {
41478
+ const trimmed = rawPart.trim();
41479
+ const desc = trimmed.toLowerCase().endsWith("desc");
41480
+ const col = stripQuotes(stripTrailingDirection(trimmed)).trim();
41481
+ sort[col] = desc ? -1 : 1;
41186
41482
  }
41483
+ return sort;
41484
+ }
41485
+ function parseSelect(sql, params) {
41486
+ const upper = sql.toUpperCase();
41487
+ if (!(upper.startsWith("SELECT") && (sql.length === 6 || isSqlWs(sql[6])))) return null;
41488
+ const fromAt = keywordAt(upper, "FROM", 6);
41489
+ if (fromAt < 0) return null;
41490
+ const cols = sql.slice(6, fromAt).trim();
41491
+ const ident = readIdentifier(sql, fromAt + 4);
41492
+ if (!ident) return null;
41493
+ const [collection, afterColl] = ident;
41494
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41495
+ const orderAt = keywordAt(upper, "ORDER BY", afterColl);
41496
+ const limitAt = keywordAt(upper, "LIMIT", afterColl);
41497
+ const offsetAt = keywordAt(upper, "OFFSET", afterColl);
41498
+ const end = sql.length;
41499
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5, earliest([orderAt, limitAt, offsetAt], end)).trim();
41500
+ const orderBy = orderAt < 0 ? void 0 : sql.slice(orderAt + 8, earliest([limitAt, offsetAt], end)).trim();
41501
+ const limitStr = limitAt < 0 ? void 0 : sql.slice(limitAt + 5, earliest([offsetAt], end)).trim();
41502
+ const skipStr = offsetAt < 0 ? void 0 : sql.slice(offsetAt + 6).trim();
41503
+ const projection = buildProjection(cols);
41504
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41187
41505
  return {
41188
41506
  type: "find",
41189
41507
  collection,
@@ -41191,7 +41509,7 @@ function parseSelect(sql, params) {
41191
41509
  projection: Object.keys(projection).length > 0 ? projection : void 0,
41192
41510
  limit: limitStr ? parseInt(limitStr, 10) : void 0,
41193
41511
  skip: skipStr ? parseInt(skipStr, 10) : void 0,
41194
- sort: orderBy ? sort : void 0
41512
+ sort: orderBy ? parseOrderBy(orderBy) : void 0
41195
41513
  };
41196
41514
  }
41197
41515
  function parseInsert(sql, params) {
@@ -41227,20 +41545,33 @@ function parseSetClause(setClause, params) {
41227
41545
  return { document, consumed };
41228
41546
  }
41229
41547
  function parseUpdate(sql, params) {
41230
- const match = sql.match(/^UPDATE\s+["']?(\w+)["']?\s+SET\s+(.*?)(?:\s+WHERE\s+(.+))?$/is);
41231
- if (!match) return null;
41232
- const [, collection, setClause, whereClause] = match;
41548
+ const upper = sql.toUpperCase();
41549
+ if (!(upper.startsWith("UPDATE") && sql.length > 6 && isSqlWs(sql[6]))) return null;
41550
+ const ident = readIdentifier(sql, 6);
41551
+ if (!ident) return null;
41552
+ const [collection, afterColl] = ident;
41553
+ const setAt = keywordAt(upper, "SET", afterColl);
41554
+ if (setAt < 0 || !onlyWs(sql, afterColl, setAt)) return null;
41555
+ const whereAt = keywordAt(upper, "WHERE", setAt + 3);
41556
+ const setClause = sql.slice(setAt + 3, whereAt < 0 ? sql.length : whereAt).trim();
41557
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41233
41558
  const set = parseSetClause(setClause, params);
41234
- const matchAll = isMatchAllWhere(whereClause?.trim());
41235
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params, set.consumed).filter : {};
41559
+ const matchAll = isMatchAllWhere(whereClause);
41560
+ const filter = whereClause ? parseWhereClause(whereClause, params, set.consumed).filter : {};
41236
41561
  return { type: "updateMany", collection, filter, update: { $set: set.document }, matchAll };
41237
41562
  }
41238
41563
  function parseDelete(sql, params) {
41239
- const match = sql.match(/^DELETE\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
41240
- if (!match) return null;
41241
- const [, collection, whereClause] = match;
41242
- const matchAll = isMatchAllWhere(whereClause?.trim());
41243
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41564
+ const upper = sql.toUpperCase();
41565
+ if (!upper.startsWith("DELETE")) return null;
41566
+ const fromAt = keywordAt(upper, "FROM", 6);
41567
+ if (fromAt < 0 || !onlyWs(sql, 6, fromAt)) return null;
41568
+ const ident = readIdentifier(sql, fromAt + 4);
41569
+ if (!ident) return null;
41570
+ const [collection, afterColl] = ident;
41571
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41572
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41573
+ const matchAll = isMatchAllWhere(whereClause);
41574
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41244
41575
  return { type: "deleteMany", collection, filter, matchAll };
41245
41576
  }
41246
41577
  function parseCreate(sql) {
@@ -41248,10 +41579,23 @@ function parseCreate(sql) {
41248
41579
  return match ? { type: "raw", collection: match[1] } : null;
41249
41580
  }
41250
41581
  function parseCount(sql, params) {
41251
- const match = sql.match(/^SELECT\s+COUNT\(\*\)\s+AS\s+(\w+)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
41252
- if (!match) return null;
41253
- const [, alias, collection, whereClause] = match;
41254
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41582
+ const upper = sql.toUpperCase();
41583
+ if (!upper.startsWith("SELECT")) return null;
41584
+ const afterCount = keywordGapEnd(upper, sql, "COUNT(*)", 6);
41585
+ if (afterCount < 0) return null;
41586
+ const afterAs = keywordGapEnd(upper, sql, "AS", afterCount);
41587
+ if (afterAs < 0) return null;
41588
+ const aliasIdent = readIdentifier(sql, afterAs);
41589
+ if (!aliasIdent) return null;
41590
+ const [alias, afterAlias] = aliasIdent;
41591
+ const afterFrom = keywordGapEnd(upper, sql, "FROM", afterAlias);
41592
+ if (afterFrom < 0) return null;
41593
+ const coll = readIdentifier(sql, afterFrom);
41594
+ if (!coll) return null;
41595
+ const [collection, afterColl] = coll;
41596
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41597
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41598
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41255
41599
  return { type: "aggregate", collection, pipeline: [{ $match: filter }, { $count: alias }] };
41256
41600
  }
41257
41601
  function parseSql(sql, params = []) {
@@ -41277,12 +41621,13 @@ function fieldTypeToJsonSchema(def) {
41277
41621
  return { bsonType: "string" };
41278
41622
  }
41279
41623
  }
41280
- var MATCH_ALL_WHERE, MongodbAdapter;
41624
+ var MATCH_ALL_WHERE, QUOTE_CHARS, MongodbAdapter;
41281
41625
  var init_mongodb = __esm({
41282
41626
  "src/adapters/mongodb.ts"() {
41283
41627
  "use strict";
41284
41628
  init_connectTimeout();
41285
41629
  MATCH_ALL_WHERE = /^\s*1\s*=\s*1\s*$/;
41630
+ QUOTE_CHARS = `"'`;
41286
41631
  MongodbAdapter = class {
41287
41632
  constructor(config) {
41288
41633
  this.config = config;
@@ -43606,13 +43951,13 @@ var init_database = __esm({
43606
43951
  });
43607
43952
 
43608
43953
  // src/model.ts
43609
- import { readdirSync as readdirSync17, statSync as statSync18 } from "node:fs";
43954
+ import { readdirSync as readdirSync18, statSync as statSync18 } from "node:fs";
43610
43955
  import { join as join30, extname as extname8 } from "node:path";
43611
43956
  async function discoverModels(modelsDir) {
43612
43957
  const models = [];
43613
43958
  let files;
43614
43959
  try {
43615
- files = readdirSync17(modelsDir);
43960
+ files = readdirSync18(modelsDir);
43616
43961
  } catch {
43617
43962
  return models;
43618
43963
  }
@@ -43659,8 +44004,10 @@ var init_model = __esm({
43659
44004
  });
43660
44005
 
43661
44006
  // src/migration.ts
43662
- import { existsSync as existsSync27, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17 } from "node:fs";
44007
+ import { existsSync as existsSync27, readdirSync as readdirSync19, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17, rmSync } from "node:fs";
43663
44008
  import { join as join31, resolve as resolve19 } from "node:path";
44009
+ import { tmpdir } from "node:os";
44010
+ import { createHash as createHash10 } from "node:crypto";
43664
44011
  function unwrapAdapter(db) {
43665
44012
  let cur = db;
43666
44013
  while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
@@ -44139,17 +44486,113 @@ function warnUnprefixedMigrations(files) {
44139
44486
  );
44140
44487
  }
44141
44488
  }
44489
+ async function acquireMigrationLock(db, dir) {
44490
+ let engine = "";
44491
+ try {
44492
+ engine = (db.getDatabaseType() || "").toLowerCase();
44493
+ } catch {
44494
+ engine = "";
44495
+ }
44496
+ try {
44497
+ if (engine.startsWith("postgres")) {
44498
+ await adapterQuery(db, `SELECT pg_advisory_lock(${PG_ADVISORY_KEY}) AS locked`);
44499
+ return { kind: "postgres" };
44500
+ }
44501
+ if (engine.startsWith("mysql")) {
44502
+ await adapterQuery(db, `SELECT GET_LOCK('${MIGRATION_LOCK_NAME}', -1) AS locked`);
44503
+ return { kind: "mysql" };
44504
+ }
44505
+ if (engine === "mssql" || engine === "sqlserver") {
44506
+ await adapterExecute(
44507
+ db,
44508
+ `DECLARE @res INT; EXEC @res = sp_getapplock @Resource = '${MIGRATION_LOCK_NAME}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = -1`
44509
+ );
44510
+ return { kind: "mssql" };
44511
+ }
44512
+ } catch (err) {
44513
+ Log.debug(`DB migration lock unavailable (${engine}): ${err instanceof Error ? err.message : err}; using a file lock`);
44514
+ }
44515
+ return acquireFileLock(dir);
44516
+ }
44517
+ async function acquireFileLock(dir) {
44518
+ const lockFile = join31(tmpdir(), `tina4-migration-${createHash10("sha256").update(resolve19(dir)).digest("hex")}.lock`);
44519
+ const deadline = Date.now() + 6e4;
44520
+ for (; ; ) {
44521
+ try {
44522
+ writeFileSync17(lockFile, String(process.pid), { flag: "wx" });
44523
+ return { kind: "file", resource: lockFile };
44524
+ } catch (err) {
44525
+ if (err.code !== "EEXIST") {
44526
+ Log.debug(`file migration lock unavailable: ${err instanceof Error ? err.message : err}; running unlocked`);
44527
+ return { kind: "none" };
44528
+ }
44529
+ if (isStaleLock(lockFile)) {
44530
+ try {
44531
+ rmSync(lockFile, { force: true });
44532
+ } catch {
44533
+ }
44534
+ continue;
44535
+ }
44536
+ if (Date.now() > deadline) {
44537
+ Log.debug("file migration lock wait timed out; running unlocked");
44538
+ return { kind: "none" };
44539
+ }
44540
+ await delay(50);
44541
+ }
44542
+ }
44543
+ }
44544
+ function isStaleLock(lockFile) {
44545
+ let raw;
44546
+ try {
44547
+ raw = readFileSync25(lockFile, "utf-8").trim();
44548
+ } catch {
44549
+ return false;
44550
+ }
44551
+ const pid = parseInt(raw, 10);
44552
+ if (!raw || !Number.isFinite(pid) || pid <= 0) {
44553
+ return false;
44554
+ }
44555
+ try {
44556
+ process.kill(pid, 0);
44557
+ return false;
44558
+ } catch (err) {
44559
+ return err.code === "ESRCH";
44560
+ }
44561
+ }
44562
+ async function releaseMigrationLock(db, lock) {
44563
+ try {
44564
+ if (lock.kind === "postgres") {
44565
+ await adapterQuery(db, `SELECT pg_advisory_unlock(${PG_ADVISORY_KEY}) AS released`);
44566
+ } else if (lock.kind === "mysql") {
44567
+ await adapterQuery(db, `SELECT RELEASE_LOCK('${MIGRATION_LOCK_NAME}') AS released`);
44568
+ } else if (lock.kind === "mssql") {
44569
+ await adapterExecute(db, `EXEC sp_releaseapplock @Resource = '${MIGRATION_LOCK_NAME}', @LockOwner = 'Session'`);
44570
+ } else if (lock.kind === "file" && lock.resource) {
44571
+ rmSync(lock.resource, { recursive: true, force: true });
44572
+ }
44573
+ } catch (err) {
44574
+ Log.debug(`migration lock release failed (${lock.kind}): ${err instanceof Error ? err.message : err}`);
44575
+ }
44576
+ }
44142
44577
  async function migrate(adapter, options) {
44143
44578
  const db = adapter ?? getAdapter();
44144
44579
  const dir = resolve19(options?.migrationsDir ?? "migrations");
44145
44580
  const delimiter2 = options?.delimiter ?? ";";
44146
- const result = { applied: [], skipped: [], failed: [] };
44147
44581
  if (!existsSync27(dir)) {
44148
- return result;
44582
+ return { applied: [], skipped: [], failed: [] };
44149
44583
  }
44584
+ const lock = await acquireMigrationLock(db, dir);
44585
+ try {
44586
+ return await runPendingMigrations(db, dir, delimiter2);
44587
+ } finally {
44588
+ await releaseMigrationLock(db, lock);
44589
+ }
44590
+ }
44591
+ async function runPendingMigrations(db, dir, delimiter2) {
44592
+ const result = { applied: [], skipped: [], failed: [] };
44150
44593
  await ensureMigrationTableOn(db);
44151
44594
  const files = sortMigrationFiles(
44152
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44595
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44153
44596
  );
44154
44597
  if (files.length === 0) return result;
44155
44598
  warnUnprefixedMigrations(files);
@@ -44222,14 +44665,14 @@ async function status(adapter, options) {
44222
44665
  }
44223
44666
  if (!await adapterTableExists(db, MIGRATION_TABLE)) {
44224
44667
  const files2 = sortMigrationFiles(
44225
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44668
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44226
44669
  );
44227
44670
  result.pending = files2;
44228
44671
  return result;
44229
44672
  }
44230
44673
  await ensureMigrationTableOn(db);
44231
44674
  const files = sortMigrationFiles(
44232
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44675
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44233
44676
  );
44234
44677
  const appliedNames = /* @__PURE__ */ new Set();
44235
44678
  try {
@@ -44329,7 +44772,7 @@ export class ${className} {
44329
44772
  writeFileSync17(filePath, content, "utf-8");
44330
44773
  return filePath;
44331
44774
  }
44332
- var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
44775
+ var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, MIGRATION_LOCK_NAME, PG_ADVISORY_KEY, delay, Migration;
44333
44776
  var init_migration = __esm({
44334
44777
  "src/migration.ts"() {
44335
44778
  "use strict";
@@ -44354,6 +44797,12 @@ var init_migration = __esm({
44354
44797
  };
44355
44798
  SMART_QUOTE_RE = new RegExp(`[${Object.keys(SMART_QUOTES).join("")}]`, "g");
44356
44799
  SET_TERM_RE = /^SET\s+TERM\s+(\S+)$/i;
44800
+ MIGRATION_LOCK_NAME = "tina4_migration_lock";
44801
+ PG_ADVISORY_KEY = BigInt.asIntN(
44802
+ 64,
44803
+ BigInt("0x" + createHash10("sha256").update(MIGRATION_LOCK_NAME).digest("hex").slice(0, 16))
44804
+ ).toString();
44805
+ delay = (ms) => new Promise((r) => setTimeout(r, ms));
44357
44806
  Migration = class {
44358
44807
  db;
44359
44808
  dir;
@@ -44410,7 +44859,7 @@ var init_migration = __esm({
44410
44859
  const dir = resolve19(this.dir);
44411
44860
  if (!existsSync27(dir)) return [];
44412
44861
  return sortMigrationFiles(
44413
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44862
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44414
44863
  );
44415
44864
  }
44416
44865
  };
@@ -44418,17 +44867,40 @@ var init_migration = __esm({
44418
44867
  });
44419
44868
 
44420
44869
  // src/query.ts
44421
- function resolveFieldColumn(fields, columnOf, key) {
44422
- const field = resolveField(fields, columnOf, key);
44423
- return field === null ? null : columnOf(field);
44870
+ function resolveFieldColumn(fields, columnOf2, key) {
44871
+ const field = resolveField(fields, columnOf2, key);
44872
+ return field === null ? null : columnOf2(field);
44424
44873
  }
44425
- function resolveField(fields, columnOf, key) {
44874
+ function resolveField(fields, columnOf2, key) {
44426
44875
  if (Object.hasOwn(fields, key)) return key;
44427
44876
  for (const field of Object.keys(fields)) {
44428
- if (columnOf(field) === key) return field;
44877
+ if (columnOf2(field) === key) return field;
44429
44878
  }
44430
44879
  return null;
44431
44880
  }
44881
+ function applySearch(options, conditions, params, quote) {
44882
+ const searchTerm = (options.search ?? "").trim();
44883
+ if (searchTerm === "" || !options.searchColumns || options.searchColumns.length === 0) return;
44884
+ const likeParts = options.searchColumns.map((col) => `${quote(col)} LIKE ?`);
44885
+ conditions.push(`(${likeParts.join(" OR ")})`);
44886
+ for (const _ of options.searchColumns) params.push(`%${searchTerm}%`);
44887
+ }
44888
+ function orderPart(rawPart, bareDir, column2) {
44889
+ const trimmed = rawPart.trim();
44890
+ if (trimmed === "") return null;
44891
+ const descending = trimmed.startsWith("-");
44892
+ const col = column2("sort", descending ? trimmed.slice(1) : trimmed);
44893
+ return `${col} ${bareDir ?? (descending ? "DESC" : "ASC")}`;
44894
+ }
44895
+ function buildOrderClause(options, column2) {
44896
+ if (!options.sort) return "";
44897
+ const rawParts = options.sort.split(",");
44898
+ const first = rawParts[0].trim();
44899
+ const singleBare = rawParts.length === 1 && first !== "" && !first.startsWith("-");
44900
+ const bareDir = singleBare && String(options.sortDir).toLowerCase() === "desc" ? "DESC" : null;
44901
+ const parts = rawParts.map((part) => orderPart(part, bareDir, column2)).filter((term) => term !== null);
44902
+ return parts.length > 0 ? `ORDER BY ${parts.join(", ")}` : "";
44903
+ }
44432
44904
  function buildQuery(tableName, options, extraConditions, quote = quoteIdentifierAnsi, resolveColumn = () => null) {
44433
44905
  const conditions = [];
44434
44906
  const params = [];
@@ -44456,19 +44928,9 @@ function buildQuery(tableName, options, extraConditions, quote = quoteIdentifier
44456
44928
  }
44457
44929
  }
44458
44930
  }
44931
+ applySearch(options, conditions, params, quote);
44459
44932
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
44460
- let orderClause = "";
44461
- if (options.sort) {
44462
- const parts = [];
44463
- for (const part of options.sort.split(",")) {
44464
- const trimmed = part.trim();
44465
- if (trimmed === "") continue;
44466
- const descending = trimmed.startsWith("-");
44467
- const col = column2("sort", descending ? trimmed.slice(1) : trimmed);
44468
- parts.push(`${col} ${descending ? "DESC" : "ASC"}`);
44469
- }
44470
- if (parts.length > 0) orderClause = `ORDER BY ${parts.join(", ")}`;
44471
- }
44933
+ const orderClause = buildOrderClause(options, column2);
44472
44934
  const limit = Math.min(options.limit ?? DEFAULT_ROW_CAP, DEFAULT_ROW_CAP);
44473
44935
  const page = Math.max(options.page ?? 1, 1);
44474
44936
  const offset = (page - 1) * limit;
@@ -44486,32 +44948,41 @@ function buildQuery(tableName, options, extraConditions, quote = quoteIdentifier
44486
44948
  filterParams: params
44487
44949
  };
44488
44950
  }
44489
- function parseQueryString(query) {
44490
- const options = {};
44951
+ function parseFilterParams(query) {
44491
44952
  const filter = /* @__PURE__ */ Object.create(null);
44492
44953
  for (const [key, value] of Object.entries(query)) {
44493
44954
  if (key.startsWith("sort[")) {
44494
44955
  throw new InvalidQueryParameterError("Query parameter 'sort' must be a single comma-separated string");
44495
44956
  }
44496
44957
  if (!key.startsWith("filter[")) continue;
44497
- const filterMatch = key.match(/^filter\[([^\]]*)\](.*)$/s);
44498
- if (!filterMatch) throw new UnknownFieldError("filter", key.slice("filter[".length));
44499
- const [, field, rest] = filterMatch;
44500
- if (rest === "") {
44501
- filter[field] = value;
44502
- continue;
44503
- }
44504
- const operator = rest.match(/^\[([^[\]]*)\]$/)?.[1];
44505
- if (operator === void 0 || !Object.hasOwn(operatorMap, operator)) throw filterValueError(field);
44506
- if (!filter[field] || typeof filter[field] !== "object") {
44507
- filter[field] = {};
44508
- }
44509
- filter[field][operator] = value;
44958
+ addFilterKey(filter, key, value);
44959
+ }
44960
+ return filter;
44961
+ }
44962
+ function addFilterKey(filter, key, value) {
44963
+ const filterMatch = key.match(/^filter\[([^\]]*)\](.*)$/s);
44964
+ if (!filterMatch) throw new UnknownFieldError("filter", key.slice("filter[".length));
44965
+ const [, field, rest] = filterMatch;
44966
+ if (rest === "") {
44967
+ filter[field] = value;
44968
+ return;
44969
+ }
44970
+ const operator = rest.match(/^\[([^[\]]*)\]$/)?.[1];
44971
+ if (operator === void 0 || !Object.hasOwn(operatorMap, operator)) throw filterValueError(field);
44972
+ if (!filter[field] || typeof filter[field] !== "object") {
44973
+ filter[field] = {};
44510
44974
  }
44975
+ filter[field][operator] = value;
44976
+ }
44977
+ function parseQueryString(query) {
44978
+ const options = {};
44979
+ const filter = parseFilterParams(query);
44511
44980
  if (Object.keys(filter).length > 0) {
44512
44981
  options.filter = filter;
44513
44982
  }
44514
44983
  if (query.sort) options.sort = query.sort;
44984
+ if (query.search !== void 0) options.search = query.search;
44985
+ if (query.sort_dir !== void 0) options.sortDir = query.sort_dir;
44515
44986
  if (query.page) options.page = parseInt(query.page, 10);
44516
44987
  if (query.limit) options.limit = parseInt(query.limit, 10);
44517
44988
  if (query.offset !== void 0) {
@@ -44647,12 +45118,12 @@ var init_validation = __esm({
44647
45118
  });
44648
45119
 
44649
45120
  // src/autoCrud.ts
44650
- function allowListedBody(fields, columnOf, pkField, body, isCreate) {
45121
+ function allowListedBody(fields, columnOf2, pkField, body, isCreate) {
44651
45122
  const pkDef = fields[pkField];
44652
45123
  const stripPk = isCreate ? pkDef?.autoIncrement === true : true;
44653
45124
  const allowed = {};
44654
45125
  for (const [key, value] of Object.entries(body ?? {})) {
44655
- const field = resolveField(fields, columnOf, key);
45126
+ const field = resolveField(fields, columnOf2, key);
44656
45127
  if (field === null) continue;
44657
45128
  if (field === "is_deleted") continue;
44658
45129
  if (stripPk && field === pkField) continue;
@@ -44696,6 +45167,9 @@ function generateCrudRoutes(models, options = {}) {
44696
45167
  let built;
44697
45168
  try {
44698
45169
  const qp = parseQueryString(req2.query ?? {});
45170
+ if ((qp.search ?? "").trim() !== "") {
45171
+ qp.searchColumns = Object.entries(fields).filter(([, def]) => def.type === "string" || def.type === "text").map(([name]) => getDbCol(name));
45172
+ }
44699
45173
  built = buildQuery(tableName, qp, extraConditions, q, (key) => resolveFieldColumn(fields, getDbCol, key));
44700
45174
  } catch (err) {
44701
45175
  if (err instanceof UnknownFieldError) {
@@ -44943,6 +45417,567 @@ var init_autoCrud = __esm({
44943
45417
  }
44944
45418
  });
44945
45419
 
45420
+ // src/crudHelpers.ts
45421
+ function columnOf(model, prop) {
45422
+ return model.fieldMapping?.[prop] ?? prop;
45423
+ }
45424
+ function primaryKeyProp(model) {
45425
+ for (const [name, def] of Object.entries(model.fields)) {
45426
+ if (def.primaryKey) return name;
45427
+ }
45428
+ return "id";
45429
+ }
45430
+ function searchableProps(model) {
45431
+ return Object.entries(model.fields).filter(([, def]) => def.type === "string" || def.type === "text").map(([name]) => name);
45432
+ }
45433
+ function columnAlignment(model, col) {
45434
+ const type2 = model?.fields?.[col]?.type;
45435
+ return type2 === "integer" || type2 === "number" || type2 === "float" || type2 === "decimal" ? "text-end" : "text-start";
45436
+ }
45437
+ function prettyLabel(col) {
45438
+ return String(col).split("_").map((p) => p ? p.charAt(0).toUpperCase() + p.slice(1) : p).join(" ");
45439
+ }
45440
+ function crudSortProp(model, requested, pkProp) {
45441
+ const req2 = (requested ?? "").trim();
45442
+ if (req2 === "") return pkProp;
45443
+ if (Object.hasOwn(model.fields, req2)) return req2;
45444
+ for (const name of Object.keys(model.fields)) {
45445
+ if (columnOf(model, name) === req2) return name;
45446
+ }
45447
+ return pkProp;
45448
+ }
45449
+ function cellValue(record, col) {
45450
+ return record[col];
45451
+ }
45452
+ function buildCells(columns, record, editable, aligns) {
45453
+ return columns.map((col, index) => {
45454
+ const raw = cellValue(record, col);
45455
+ const value = h(raw === null || raw === void 0 ? "" : raw);
45456
+ const css = aligns[index];
45457
+ return editable ? `<td class="${css}" contenteditable="true" data-field="${h(col)}">${value}</td>` : `<td class="${css}">${value}</td>`;
45458
+ }).join("");
45459
+ }
45460
+ function stripOrderAndLimit(sql) {
45461
+ const cut = (line, keyword) => {
45462
+ const hasNewline = line.endsWith("\n");
45463
+ const content = hasNewline ? line.slice(0, -1) : line;
45464
+ const at = content.toUpperCase().indexOf(keyword);
45465
+ if (at < 0) return line;
45466
+ const after = at + keyword.length;
45467
+ if (content.length <= after) return line;
45468
+ return content.slice(0, at) + (hasNewline ? "\n" : "");
45469
+ };
45470
+ const src = String(sql);
45471
+ const lines = [];
45472
+ let start2 = 0;
45473
+ for (let i = 0; i < src.length; i++) {
45474
+ if (src[i] === "\n") {
45475
+ lines.push(src.slice(start2, i + 1));
45476
+ start2 = i + 1;
45477
+ }
45478
+ }
45479
+ lines.push(src.slice(start2));
45480
+ return lines.map((line) => cut(cut(line, "ORDER BY "), "LIMIT ")).join("").trim();
45481
+ }
45482
+ function extractColumns(sql) {
45483
+ const colsStr = selectProjection(sql);
45484
+ if (colsStr === null || colsStr === "*") return ["*"];
45485
+ return colsStr.split(",").map((c) => columnAlias(c.trim()));
45486
+ }
45487
+ function selectProjection(sql) {
45488
+ const upper = sql.toUpperCase();
45489
+ const selectAt = keywordIndex(upper, "SELECT", 0);
45490
+ if (selectAt < 0) return null;
45491
+ const colsStart = selectAt + "SELECT".length;
45492
+ const fromAt = keywordIndex(upper, "FROM", colsStart);
45493
+ if (fromAt < 0) return null;
45494
+ return sql.slice(colsStart, fromAt).trim();
45495
+ }
45496
+ function columnAlias(trimmed) {
45497
+ const asAt = keywordIndex(trimmed.toUpperCase(), "AS", 0);
45498
+ if (asAt >= 0) {
45499
+ const after = trimmed.slice(asAt + "AS".length).trim();
45500
+ let endIdx = 0;
45501
+ while (endIdx < after.length && !SQL_WS.includes(after[endIdx]) && after[endIdx] !== ",") endIdx++;
45502
+ const word = after.slice(0, endIdx);
45503
+ if (word) return word;
45504
+ }
45505
+ if (trimmed.includes(".")) return trimmed.split(".").pop().trim();
45506
+ return trimmed;
45507
+ }
45508
+ function keywordIndex(upperHaystack, keyword, from) {
45509
+ let at = upperHaystack.indexOf(keyword, from);
45510
+ while (at >= 0) {
45511
+ const afterIdx = at + keyword.length;
45512
+ const boundaryBefore = at === 0 || SQL_WS.includes(upperHaystack[at - 1]);
45513
+ const boundaryAfter = afterIdx >= upperHaystack.length || SQL_WS.includes(upperHaystack[afterIdx]);
45514
+ if (boundaryBefore && boundaryAfter) return at;
45515
+ at = upperHaystack.indexOf(keyword, at + 1);
45516
+ }
45517
+ return -1;
45518
+ }
45519
+ var h, SQL_WS;
45520
+ var init_crudHelpers = __esm({
45521
+ "src/crudHelpers.ts"() {
45522
+ "use strict";
45523
+ init_engine();
45524
+ h = escapeHtml;
45525
+ SQL_WS = " \n\r\f\v";
45526
+ }
45527
+ });
45528
+
45529
+ // src/crudTable.ts
45530
+ function sortIndicator(sortCol, col, sortDir) {
45531
+ if (String(sortCol) !== String(col)) return "";
45532
+ const arrow = sortDir === "asc" ? "&#9650;" : "&#9660;";
45533
+ return ` <span class="sort-indicator">${arrow}</span>`;
45534
+ }
45535
+ function sortUrl(requestPath, col, nextDir, page, search, limit) {
45536
+ const query = `sort=${encodeURIComponent(col)}&sort_dir=${nextDir}&page=${page}&search=${encodeURIComponent(search)}&limit=${limit}`;
45537
+ return h(`${requestPath}?${query}`);
45538
+ }
45539
+ function pageUrl(requestPath, p, search, sortCol, sortDir, limit) {
45540
+ const query = `page=${p}&search=${encodeURIComponent(search)}&sort=${encodeURIComponent(sortCol)}&sort_dir=${sortDir}&limit=${limit}`;
45541
+ return h(`${requestPath}?${query}`);
45542
+ }
45543
+ function pageControls(page, totalPages, requestPath, search, sortCol, sortDir, limit) {
45544
+ if (totalPages <= 1) return [];
45545
+ const controls = [];
45546
+ if (page > 1) {
45547
+ controls.push({
45548
+ label: "Prev",
45549
+ page: page - 1,
45550
+ active: false,
45551
+ inactive: true,
45552
+ url: pageUrl(requestPath, page - 1, search, sortCol, sortDir, limit)
45553
+ });
45554
+ }
45555
+ let startPage = Math.max(page - 3, 1);
45556
+ const endPage = Math.min(startPage + 6, totalPages);
45557
+ startPage = Math.max(endPage - 6, 1);
45558
+ for (let p = startPage; p <= endPage; p++) {
45559
+ controls.push({
45560
+ label: p,
45561
+ page: p,
45562
+ active: p === page,
45563
+ inactive: p !== page,
45564
+ url: pageUrl(requestPath, p, search, sortCol, sortDir, limit)
45565
+ });
45566
+ }
45567
+ if (page < totalPages) {
45568
+ controls.push({
45569
+ label: "Next",
45570
+ page: page + 1,
45571
+ active: false,
45572
+ inactive: true,
45573
+ url: pageUrl(requestPath, page + 1, search, sortCol, sortDir, limit)
45574
+ });
45575
+ }
45576
+ return controls;
45577
+ }
45578
+ function jsConfig(opts) {
45579
+ const aligns = {};
45580
+ const labels = {};
45581
+ for (const col of opts.columns) {
45582
+ aligns[col] = columnAlignment(opts.model, col);
45583
+ labels[col] = prettyLabel(col);
45584
+ }
45585
+ const config = {
45586
+ api: opts.apiPath,
45587
+ pk: opts.pk,
45588
+ columns: opts.columns,
45589
+ editable: opts.editable,
45590
+ aligns,
45591
+ labels,
45592
+ limit: opts.limit,
45593
+ search: opts.search,
45594
+ sort: opts.sortCol,
45595
+ sort_dir: opts.sortDir,
45596
+ page: opts.page
45597
+ };
45598
+ return JSON.stringify(config).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
45599
+ }
45600
+ function headerCell(opts, col, align) {
45601
+ const header = { label: h(prettyLabel(col)), align };
45602
+ if (!opts.sortable) {
45603
+ header.plain = true;
45604
+ return header;
45605
+ }
45606
+ const nextDir = String(opts.sortCol) === String(col) && opts.sortDir === "asc" ? "desc" : "asc";
45607
+ header.sortable = true;
45608
+ header.col = h(col);
45609
+ header.next_dir = nextDir;
45610
+ header.url = sortUrl(opts.requestPath, col, nextDir, opts.page, opts.search, opts.limit);
45611
+ header.indicator = sortIndicator(opts.sortCol, col, opts.sortDir);
45612
+ return header;
45613
+ }
45614
+ function tableData(opts) {
45615
+ const aligns = opts.columns.map((col) => columnAlignment(opts.model, col));
45616
+ const headers = opts.columns.map((col, index) => headerCell(opts, col, aligns[index]));
45617
+ const rows = opts.records.map((record) => ({
45618
+ id: h(cellValue(record, opts.pk) ?? ""),
45619
+ cells: buildCells(opts.columns, record, opts.editable, aligns)
45620
+ }));
45621
+ return {
45622
+ headers,
45623
+ rows,
45624
+ empty: opts.records.length === 0,
45625
+ colspan: opts.columns.length + 1,
45626
+ editable: opts.editable,
45627
+ readonly: !opts.editable,
45628
+ inline_script: opts.inlineScript,
45629
+ table_name: h(opts.tableName),
45630
+ table_id_attr: opts.tableId ? ` id="${h(opts.tableId)}"` : ""
45631
+ };
45632
+ }
45633
+ var init_crudTable = __esm({
45634
+ "src/crudTable.ts"() {
45635
+ "use strict";
45636
+ init_crudHelpers();
45637
+ }
45638
+ });
45639
+
45640
+ // src/crudRender.ts
45641
+ import { existsSync as existsSync28 } from "node:fs";
45642
+ import { resolve as pathResolve, join as pathJoin, dirname as dirname15 } from "node:path";
45643
+ import { fileURLToPath as fileURLToPath9 } from "node:url";
45644
+ function frondFor(dir) {
45645
+ let engine = frondCache2.get(dir);
45646
+ if (!engine) {
45647
+ setCspNonceProvider(currentCspNonce);
45648
+ engine = new Frond(dir);
45649
+ frondCache2.set(dir, engine);
45650
+ }
45651
+ return engine;
45652
+ }
45653
+ function renderCrudTemplate(name, data) {
45654
+ const templateFile = `crud/${name}.twig`;
45655
+ const userDir = pathResolve(process.cwd(), "src", "templates");
45656
+ if (existsSync28(pathJoin(userDir, templateFile))) {
45657
+ return frondFor(userDir).render(templateFile, data);
45658
+ }
45659
+ return frondFor(FRAMEWORK_TEMPLATES_DIR).render(templateFile, data);
45660
+ }
45661
+ function renderModalForm(mode, columns, pk, edit) {
45662
+ const fields = columns.map((col) => {
45663
+ const label = prettyLabel(col);
45664
+ return {
45665
+ id: `${mode}-${col}`,
45666
+ name: h(col),
45667
+ label: h(label),
45668
+ value: "",
45669
+ placeholder: h(`Enter ${label.toLowerCase()}`),
45670
+ type: "text",
45671
+ required_attr: "",
45672
+ input: true
45673
+ };
45674
+ });
45675
+ return renderCrudTemplate("form", {
45676
+ wrap: true,
45677
+ form_id_attr: ` id="form-${mode}"`,
45678
+ action: "",
45679
+ form_method: "POST",
45680
+ method_override: null,
45681
+ edit,
45682
+ mode,
45683
+ pk: h(pk),
45684
+ modal_footer: true,
45685
+ submit_button: false,
45686
+ fields
45687
+ });
45688
+ }
45689
+ function renderModals(editableColumns, pk) {
45690
+ return renderCrudTemplate("modals", {
45691
+ create_form: renderModalForm("create", editableColumns, pk, false),
45692
+ edit_form: renderModalForm("edit", editableColumns, pk, true)
45693
+ });
45694
+ }
45695
+ function generateTable(records, options = {}) {
45696
+ const rows = records ?? [];
45697
+ const tableName = String(options.tableName ?? "data");
45698
+ const primaryKey = String(options.primaryKey ?? "id");
45699
+ const editable = options.editable ?? true;
45700
+ const columns = rows.length === 0 ? [] : Object.keys(rows[0]);
45701
+ return renderCrudTemplate(
45702
+ "table",
45703
+ tableData({
45704
+ columns,
45705
+ records: rows,
45706
+ pk: primaryKey,
45707
+ tableName,
45708
+ editable,
45709
+ sortable: false,
45710
+ inlineScript: editable,
45711
+ requestPath: "",
45712
+ search: "",
45713
+ sortCol: "",
45714
+ sortDir: "asc",
45715
+ page: 1,
45716
+ limit: 10,
45717
+ tableId: `crud-${tableName}`,
45718
+ model: null
45719
+ })
45720
+ );
45721
+ }
45722
+ function buildOptions(options, selectedValue) {
45723
+ return (options ?? []).map((opt) => {
45724
+ const selected = String(opt.value) === String(selectedValue) ? " selected" : "";
45725
+ return `<option value="${h(opt.value)}"${selected}>${h(opt.label)}</option>`;
45726
+ }).join("");
45727
+ }
45728
+ function fieldInputShape(field, base) {
45729
+ const value = field.value;
45730
+ switch (field.type ?? "string") {
45731
+ case "text":
45732
+ return { ...base, textarea: true };
45733
+ case "boolean":
45734
+ return { ...base, checkbox: true, checked_attr: value ? " checked" : "" };
45735
+ case "select":
45736
+ return { ...base, select: true, options_html: buildOptions(field.options, value) };
45737
+ case "date":
45738
+ return { ...base, input: true, type: "date" };
45739
+ case "integer":
45740
+ case "number":
45741
+ case "float":
45742
+ case "decimal":
45743
+ return { ...base, input: true, type: "number" };
45744
+ default:
45745
+ return { ...base, input: true, type: "text" };
45746
+ }
45747
+ }
45748
+ function buildCustomField(field) {
45749
+ const name = String(field.name ?? "");
45750
+ const label = field.label ?? name.charAt(0).toUpperCase() + name.slice(1);
45751
+ const base = {
45752
+ id: h(name),
45753
+ name: h(name),
45754
+ label: h(String(label)),
45755
+ value: h(field.value ?? ""),
45756
+ placeholder: "",
45757
+ required_attr: field.required ? " required" : ""
45758
+ };
45759
+ return fieldInputShape(field, base);
45760
+ }
45761
+ function generateForm2(fields, options = {}) {
45762
+ const verb = String(options.method ?? "POST").toUpperCase();
45763
+ return renderCrudTemplate("form", {
45764
+ wrap: true,
45765
+ form_id_attr: "",
45766
+ action: h(options.action ?? "/"),
45767
+ form_method: h(verb),
45768
+ method_override: ["PUT", "PATCH", "DELETE"].includes(verb) ? verb : null,
45769
+ edit: false,
45770
+ modal_footer: false,
45771
+ submit_button: true,
45772
+ fields: (fields ?? []).map((f) => buildCustomField(f))
45773
+ });
45774
+ }
45775
+ var FRAMEWORK_TEMPLATES_DIR, frondCache2;
45776
+ var init_crudRender = __esm({
45777
+ "src/crudRender.ts"() {
45778
+ "use strict";
45779
+ init_engine();
45780
+ init_csp();
45781
+ init_crudHelpers();
45782
+ init_crudTable();
45783
+ FRAMEWORK_TEMPLATES_DIR = pathResolve(
45784
+ dirname15(fileURLToPath9(import.meta.url)),
45785
+ "..",
45786
+ "..",
45787
+ "core",
45788
+ "templates"
45789
+ );
45790
+ frondCache2 = /* @__PURE__ */ new Map();
45791
+ }
45792
+ });
45793
+
45794
+ // src/crud.ts
45795
+ function _resetCrudRegistrations() {
45796
+ registeredTables.clear();
45797
+ }
45798
+ function discoveredFrom(model) {
45799
+ return {
45800
+ filePath: `src/models/${model.name ?? model.tableName}.ts`,
45801
+ modelClass: model,
45802
+ definition: {
45803
+ tableName: model.tableName,
45804
+ className: model.name,
45805
+ fields: model.fields,
45806
+ fieldMapping: model.fieldMapping,
45807
+ softDelete: model.softDelete ?? false,
45808
+ tableFilter: model.tableFilter,
45809
+ dbName: model._db
45810
+ }
45811
+ };
45812
+ }
45813
+ async function registerBackend(model, options = {}) {
45814
+ const prefix = options.prefix ?? "/api";
45815
+ const key = `${prefix}::${model.tableName}`;
45816
+ if (registeredTables.has(key)) return;
45817
+ const routes = generateCrudRoutes([discoveredFrom(model)], { public: options.public === true });
45818
+ for (const route of routes) defaultRouter.addRoute(route);
45819
+ registeredTables.add(key);
45820
+ }
45821
+ async function fetchModelData(model, opts) {
45822
+ const orderBy = `${opts.sort} ${opts.sortDir.toUpperCase()}`;
45823
+ const search = opts.search.trim();
45824
+ const searchable = search === "" ? [] : searchableProps(model);
45825
+ let rows;
45826
+ if (searchable.length === 0) {
45827
+ rows = await model.all(opts.limit, opts.offset, void 0, orderBy);
45828
+ } else {
45829
+ const whereClause = searchable.map((prop) => `${columnOf(model, prop)} LIKE ?`).join(" OR ");
45830
+ const params = searchable.map(() => `%${search}%`);
45831
+ rows = await model.where(whereClause, params, opts.limit, opts.offset, void 0, orderBy);
45832
+ }
45833
+ const total = typeof rows.getTotalRecords === "function" ? rows.getTotalRecords() : rows.length;
45834
+ const records = rows.map((r) => typeof r.toDict === "function" ? r.toDict() : r);
45835
+ return [records, total];
45836
+ }
45837
+ async function fetchSqlData(sql, opts) {
45838
+ let adapter;
45839
+ try {
45840
+ adapter = getAdapter();
45841
+ } catch {
45842
+ return [[], 0];
45843
+ }
45844
+ const base = stripOrderAndLimit(sql);
45845
+ const dir = opts.sortDir.toUpperCase();
45846
+ const search = opts.search.trim();
45847
+ if (search === "") {
45848
+ const countRow2 = await adapterQuery(adapter, `SELECT COUNT(*) as cnt FROM (${base}) AS _crud_cnt`, []);
45849
+ const total2 = Number(countRow2[0]?.cnt ?? 0);
45850
+ const rows2 = await adapterFetch(adapter, `${base} ORDER BY ${opts.sort} ${dir}`, [], opts.limit, opts.offset);
45851
+ return [rows2, total2];
45852
+ }
45853
+ const columns = extractColumns(sql);
45854
+ const searchParts = columns.map((col) => `CAST(${col} AS TEXT) LIKE ?`);
45855
+ const params = columns.map(() => `%${search}%`);
45856
+ const countRow = await adapterQuery(
45857
+ adapter,
45858
+ `SELECT COUNT(*) as cnt FROM (${base}) AS _crud_cnt WHERE ${searchParts.join(" OR ")}`,
45859
+ params
45860
+ );
45861
+ const total = Number(countRow[0]?.cnt ?? 0);
45862
+ const rows = await adapterFetch(
45863
+ adapter,
45864
+ `SELECT * FROM (${base}) AS _crud_sub WHERE ${searchParts.join(" OR ")} ORDER BY ${opts.sort} ${dir}`,
45865
+ params,
45866
+ opts.limit,
45867
+ opts.offset
45868
+ );
45869
+ return [rows, total];
45870
+ }
45871
+ function parseCrudRequest(request, model, pkProp, limit) {
45872
+ const query = request && request.query || {};
45873
+ const page = Math.max(parseInt(String(query.page ?? "1"), 10) || 1, 1);
45874
+ const search = String(query.search ?? "").trim();
45875
+ const sortProp = crudSortProp(model, query.sort, pkProp);
45876
+ const sortColumn = columnOf(model, sortProp);
45877
+ const sortDir = query.sort_dir === "desc" ? "desc" : "asc";
45878
+ const requestPath = request && typeof request.path === "string" ? request.path : "/";
45879
+ return { page, search, sortProp, sortColumn, sortDir, offset: (page - 1) * limit, requestPath };
45880
+ }
45881
+ function clampLimit(raw) {
45882
+ const limit = Number(raw ?? 10);
45883
+ return !Number.isFinite(limit) || limit <= 0 ? 10 : limit;
45884
+ }
45885
+ async function toCrud(request, options) {
45886
+ const model = options.model;
45887
+ if (!model || !model.tableName || !model.fields) {
45888
+ throw new Error("Crud.toCrud requires a model (an ORM class)");
45889
+ }
45890
+ const sql = options.sql;
45891
+ const title = String(options.title ?? "CRUD");
45892
+ const prefix = String(options.prefix ?? "/api");
45893
+ const limit = clampLimit(options.limit);
45894
+ const tableName = String(model.tableName);
45895
+ const pkProp = primaryKeyProp(model);
45896
+ const columns = Object.keys(model.fields);
45897
+ await registerBackend(model, { prefix, public: options.public });
45898
+ const req2 = parseCrudRequest(request, model, pkProp, limit);
45899
+ const fetchOpts = {
45900
+ search: req2.search,
45901
+ sort: req2.sortColumn,
45902
+ sortDir: req2.sortDir,
45903
+ limit,
45904
+ offset: req2.offset
45905
+ };
45906
+ const [records, total] = sql ? await fetchSqlData(sql, fetchOpts) : await fetchModelData(model, fetchOpts);
45907
+ const totalPages = total > 0 ? Math.ceil(total / limit) : 1;
45908
+ const apiPath = `${prefix}/${tableName}`;
45909
+ const editableColumns = columns.filter((c) => c !== pkProp);
45910
+ const tableHtml = renderCrudTemplate(
45911
+ "table",
45912
+ tableData({
45913
+ columns,
45914
+ records,
45915
+ pk: pkProp,
45916
+ tableName,
45917
+ editable: false,
45918
+ sortable: true,
45919
+ inlineScript: false,
45920
+ requestPath: req2.requestPath,
45921
+ search: req2.search,
45922
+ sortCol: req2.sortProp,
45923
+ sortDir: req2.sortDir,
45924
+ page: req2.page,
45925
+ limit,
45926
+ tableId: null,
45927
+ model
45928
+ })
45929
+ );
45930
+ return renderCrudTemplate("page", {
45931
+ title: h(title),
45932
+ search: h(req2.search),
45933
+ request_path: h(req2.requestPath),
45934
+ info_count: records.length,
45935
+ info_total: total,
45936
+ info_page: req2.page,
45937
+ info_total_pages: totalPages,
45938
+ table_html: tableHtml,
45939
+ modals_html: renderModals(editableColumns, pkProp),
45940
+ show_pagination: totalPages > 1,
45941
+ controls: pageControls(req2.page, totalPages, req2.requestPath, req2.search, req2.sortProp, req2.sortDir, limit),
45942
+ config_json: jsConfig({
45943
+ apiPath,
45944
+ pk: pkProp,
45945
+ columns,
45946
+ editable: editableColumns,
45947
+ model,
45948
+ limit,
45949
+ search: req2.search,
45950
+ sortCol: req2.sortProp,
45951
+ sortDir: req2.sortDir,
45952
+ page: req2.page
45953
+ })
45954
+ });
45955
+ }
45956
+ var registeredTables, Crud, CRUD;
45957
+ var init_crud = __esm({
45958
+ "src/crud.ts"() {
45959
+ "use strict";
45960
+ init_router();
45961
+ init_autoCrud();
45962
+ init_database();
45963
+ init_crudHelpers();
45964
+ init_crudTable();
45965
+ init_crudRender();
45966
+ init_crudRender();
45967
+ init_crudHelpers();
45968
+ registeredTables = /* @__PURE__ */ new Set();
45969
+ Crud = {
45970
+ toCrud,
45971
+ registerBackend,
45972
+ generateTable,
45973
+ generateForm: generateForm2,
45974
+ stripOrderAndLimit,
45975
+ _resetCrudRegistrations
45976
+ };
45977
+ CRUD = Crud;
45978
+ }
45979
+ });
45980
+
44946
45981
  // src/queryBuilder.ts
44947
45982
  var QueryBuilder;
44948
45983
  var init_queryBuilder = __esm({
@@ -47295,7 +48330,7 @@ var init_seeder = __esm({
47295
48330
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
47296
48331
  import { randomBytes as randomBytes11 } from "node:crypto";
47297
48332
  import { mkdirSync as mkdirSync20 } from "node:fs";
47298
- import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
48333
+ import { dirname as dirname16, isAbsolute as isAbsolute6, join as join32 } from "node:path";
47299
48334
  function iso(d) {
47300
48335
  return d.toISOString();
47301
48336
  }
@@ -47566,7 +48601,7 @@ function resolveStorePath(dbPath) {
47566
48601
  let path10 = dbPath;
47567
48602
  if (!isAbsolute6(path10)) {
47568
48603
  path10 = join32(process.cwd(), path10);
47569
- mkdirSync20(dirname14(path10), { recursive: true });
48604
+ mkdirSync20(dirname16(path10), { recursive: true });
47570
48605
  }
47571
48606
  return path10;
47572
48607
  }
@@ -49316,7 +50351,9 @@ __export(index_exports, {
49316
50351
  AutoCrud: () => AutoCrud,
49317
50352
  BaseModel: () => BaseModel,
49318
50353
  CONNECT_TIMEOUT_TOLERANCE_MS: () => CONNECT_TIMEOUT_TOLERANCE_MS,
50354
+ CRUD: () => CRUD,
49319
50355
  CachedDatabaseAdapter: () => CachedDatabaseAdapter,
50356
+ Crud: () => Crud,
49320
50357
  Cursor: () => Cursor,
49321
50358
  DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
49322
50359
  DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS,
@@ -49363,6 +50400,7 @@ __export(index_exports, {
49363
50400
  SqliteCollection: () => SqliteCollection,
49364
50401
  SqliteDatabase: () => SqliteDatabase,
49365
50402
  UnknownFieldError: () => UnknownFieldError,
50403
+ _resetCrudRegistrations: () => _resetCrudRegistrations,
49366
50404
  adapterColumns: () => adapterColumns,
49367
50405
  adapterCommit: () => adapterCommit,
49368
50406
  adapterCreateTable: () => adapterCreateTable,
@@ -49395,6 +50433,8 @@ __export(index_exports, {
49395
50433
  ensureMigrationTable: () => ensureMigrationTable,
49396
50434
  extractLastInsertId: () => extractLastInsertId,
49397
50435
  generateCrudRoutes: () => generateCrudRoutes,
50436
+ generateForm: () => generateForm2,
50437
+ generateTable: () => generateTable,
49398
50438
  getAdapter: () => getAdapter,
49399
50439
  getAppliedMigrations: () => getAppliedMigrations,
49400
50440
  getCollection: () => getCollection,
@@ -49414,10 +50454,13 @@ __export(index_exports, {
49414
50454
  realtime: () => realtime,
49415
50455
  recordMigration: () => recordMigration,
49416
50456
  redactCredentials: () => redactCredentials,
50457
+ registerBackend: () => registerBackend,
49417
50458
  removeMigrationRecord: () => removeMigrationRecord,
49418
50459
  resetDefaultStore: () => resetDefaultStore,
49419
50460
  resetRequestCaches: () => resetRequestCaches2,
49420
50461
  resolveDbPool: () => resolveDbPool,
50462
+ resolveField: () => resolveField,
50463
+ resolveFieldColumn: () => resolveFieldColumn,
49421
50464
  resolveFirebirdCharset: () => resolveFirebirdCharset,
49422
50465
  resolveGraphConnectTimeout: () => resolveGraphConnectTimeout,
49423
50466
  resolveSqlitePath: () => resolveSqlitePath,
@@ -49435,8 +50478,10 @@ __export(index_exports, {
49435
50478
  splitStatements: () => splitStatements,
49436
50479
  status: () => status,
49437
50480
  storageKey: () => storageKey,
50481
+ stripOrderAndLimit: () => stripOrderAndLimit,
49438
50482
  stripTrailingSemicolons: () => stripTrailingSemicolons,
49439
50483
  syncModels: () => syncModels,
50484
+ toCrud: () => toCrud,
49440
50485
  validate: () => validate,
49441
50486
  withConnectTimeout: () => withConnectTimeout,
49442
50487
  wrapWithCache: () => wrapWithCache
@@ -49452,6 +50497,7 @@ var init_index = __esm({
49452
50497
  init_model();
49453
50498
  init_migration();
49454
50499
  init_autoCrud();
50500
+ init_crud();
49455
50501
  init_query();
49456
50502
  init_validation();
49457
50503
  init_baseModel();
@@ -49483,7 +50529,9 @@ export {
49483
50529
  AutoCrud,
49484
50530
  BaseModel,
49485
50531
  CONNECT_TIMEOUT_TOLERANCE_MS,
50532
+ CRUD,
49486
50533
  CachedDatabaseAdapter,
50534
+ Crud,
49487
50535
  Cursor,
49488
50536
  DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
49489
50537
  DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS,
@@ -49530,6 +50578,7 @@ export {
49530
50578
  SqliteCollection,
49531
50579
  SqliteDatabase,
49532
50580
  UnknownFieldError,
50581
+ _resetCrudRegistrations,
49533
50582
  adapterColumns,
49534
50583
  adapterCommit,
49535
50584
  adapterCreateTable,
@@ -49562,6 +50611,8 @@ export {
49562
50611
  ensureMigrationTable,
49563
50612
  extractLastInsertId,
49564
50613
  generateCrudRoutes,
50614
+ generateForm2 as generateForm,
50615
+ generateTable,
49565
50616
  getAdapter,
49566
50617
  getAppliedMigrations,
49567
50618
  getCollection,
@@ -49581,10 +50632,13 @@ export {
49581
50632
  realtime,
49582
50633
  recordMigration,
49583
50634
  redactCredentials,
50635
+ registerBackend,
49584
50636
  removeMigrationRecord,
49585
50637
  resetDefaultStore,
49586
50638
  resetRequestCaches2 as resetRequestCaches,
49587
50639
  resolveDbPool,
50640
+ resolveField,
50641
+ resolveFieldColumn,
49588
50642
  resolveFirebirdCharset,
49589
50643
  resolveGraphConnectTimeout,
49590
50644
  resolveSqlitePath,
@@ -49602,8 +50656,10 @@ export {
49602
50656
  splitStatements,
49603
50657
  status,
49604
50658
  storageKey,
50659
+ stripOrderAndLimit,
49605
50660
  stripTrailingSemicolons,
49606
50661
  syncModels,
50662
+ toCrud,
49607
50663
  validate,
49608
50664
  withConnectTimeout,
49609
50665
  wrapWithCache