tina4-nodejs 3.13.131 → 3.13.133

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.
@@ -262,6 +262,99 @@ var init_databaseResult = __esm({
262
262
  }
263
263
  });
264
264
 
265
+ // src/modelCollection.ts
266
+ var ModelCollection;
267
+ var init_modelCollection = __esm({
268
+ "src/modelCollection.ts"() {
269
+ "use strict";
270
+ ModelCollection = class extends Array {
271
+ /** Total rows matching the query's filter (ignores limit/offset). */
272
+ _total = 0;
273
+ /** The SQL limit that produced this page. */
274
+ _limit = 0;
275
+ /** The SQL offset that produced this page. */
276
+ _offset = 0;
277
+ /**
278
+ * Derived array operations (`map`, `filter`, `slice`, spread, …) build a plain
279
+ * `Array`, never another `ModelCollection`. This is what defuses the Array-
280
+ * subclass constructor/species trap: the engine never calls this constructor
281
+ * with a length during those operations.
282
+ */
283
+ static get [Symbol.species]() {
284
+ return Array;
285
+ }
286
+ /**
287
+ * @param items the page of hydrated model instances (or the length, when the
288
+ * engine constructs a derived array — defended against here).
289
+ * @param total total rows matching the query's filter (ignores limit/offset).
290
+ * @param limit the SQL limit that produced this page.
291
+ * @param offset the SQL offset that produced this page.
292
+ */
293
+ constructor(items, total = 0, limit = 0, offset = 0) {
294
+ super();
295
+ if (typeof items === "number") {
296
+ this.length = items;
297
+ return;
298
+ }
299
+ if (items) {
300
+ for (let i = 0; i < items.length; i++) {
301
+ this[i] = items[i];
302
+ }
303
+ }
304
+ this._total = Math.trunc(total) || 0;
305
+ this._limit = Math.trunc(limit) || 0;
306
+ this._offset = Math.trunc(offset) || 0;
307
+ }
308
+ /**
309
+ * Total rows matching the query's filter, ignoring limit/offset.
310
+ *
311
+ * This is the whole point of the collection: the page slice you are iterating
312
+ * is capped by `limit`, but this number is the full count of matching rows —
313
+ * what a pager needs to render "page 3 of 13".
314
+ */
315
+ getTotalRecords() {
316
+ return this._total;
317
+ }
318
+ /**
319
+ * The canonical pagination envelope — seven snake_case keys, identical to
320
+ * `DatabaseResult.toPaginate()` (ADR-0043) and to the other three frameworks'
321
+ * `toPaginate()` / `to_paginate()`.
322
+ *
323
+ * records the page's rows as plain objects (never re-sliced)
324
+ * total getTotalRecords() — the true total for the filter
325
+ * page floor(offset / per_page) + 1
326
+ * per_page the query's limit
327
+ * total_pages ceil(total / per_page)
328
+ * limit the SQL limit actually applied
329
+ * offset the SQL offset actually applied
330
+ *
331
+ * `records` are model dicts (via `toDict()`, the same serialisation the
332
+ * framework applies to a model in a JSON response), so the JSON a client sees
333
+ * matches `DatabaseResult` exactly — the result is uniform whether the route
334
+ * returned a raw `db.fetch()` or an ORM query.
335
+ */
336
+ toPaginate() {
337
+ const perPage = this._limit > 0 ? this._limit : this.length;
338
+ const page = perPage > 0 ? Math.floor(this._offset / perPage) + 1 : 1;
339
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this._total / perPage)) : 1;
340
+ const records = this.map((model) => {
341
+ const m = model;
342
+ return m && typeof m.toDict === "function" ? m.toDict() : model;
343
+ });
344
+ return {
345
+ records,
346
+ total: this._total,
347
+ page,
348
+ per_page: perPage,
349
+ total_pages: totalPages,
350
+ limit: perPage,
351
+ offset: this._offset
352
+ };
353
+ }
354
+ };
355
+ }
356
+ });
357
+
265
358
  // src/databaseUrl.ts
266
359
  import { inspect } from "node:util";
267
360
  function stripOneSlash(path8) {
@@ -5414,33 +5507,17 @@ var init_engine = __esm({
5414
5507
  i++;
5415
5508
  }
5416
5509
  }
5417
- handleCache(tokens, start2, context) {
5418
- const [content] = stripTag(tokens[start2][1]);
5419
- const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
5420
- const cacheKey = m ? m[1] : "default";
5421
- const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
5422
- sweepExpiredCache(this.fragmentCache);
5423
- const cached = this.fragmentCache.get(cacheKey);
5424
- if (cached) {
5425
- const [htmlContent, expiresAt] = cached;
5426
- if (Date.now() < expiresAt) {
5427
- let i2 = start2 + 1;
5428
- let depth2 = 0;
5429
- while (i2 < tokens.length) {
5430
- if (tokens[i2][0] === "BLOCK") {
5431
- const [tagContent] = stripTag(tokens[i2][1]);
5432
- const tag = tagContent.split(/\s+/)[0] || "";
5433
- if (tag === "cache") depth2++;
5434
- else if (tag === "endcache") {
5435
- if (depth2 === 0) return [htmlContent, i2 + 1];
5436
- depth2--;
5437
- }
5438
- }
5439
- i2++;
5440
- }
5441
- return [htmlContent, i2];
5442
- }
5443
- }
5510
+ /**
5511
+ * Collect the body tokens of a {% <openTag> %}...{% end<openTag> %} block,
5512
+ * starting from the token after the opening tag (start + 1). Nested same-tag
5513
+ * blocks are kept in the body and balanced by depth; the matching closing tag is
5514
+ * consumed but NOT included. Returns [bodyTokens, indexAfterClosingTag].
5515
+ *
5516
+ * canNest guards the open-tag count: handleSetBlock passes it so the inline
5517
+ * {% set x = 1 %} form (which has no {% endset %}) never opens a nested block —
5518
+ * only the block form {% set x %} nests. Omitted, every openTag occurrence nests.
5519
+ */
5520
+ collectBlockBody(tokens, start2, openTag, closeTag, canNest) {
5444
5521
  const bodyTokens = [];
5445
5522
  let i = start2 + 1;
5446
5523
  let depth = 0;
@@ -5448,10 +5525,10 @@ var init_engine = __esm({
5448
5525
  if (tokens[i][0] === "BLOCK") {
5449
5526
  const [tagContent] = stripTag(tokens[i][1]);
5450
5527
  const tag = tagContent.split(/\s+/)[0] || "";
5451
- if (tag === "cache") {
5528
+ if (tag === openTag && (canNest ? canNest(tagContent) : true)) {
5452
5529
  depth++;
5453
5530
  bodyTokens.push(tokens[i]);
5454
- } else if (tag === "endcache") {
5531
+ } else if (tag === closeTag) {
5455
5532
  if (depth === 0) {
5456
5533
  i++;
5457
5534
  break;
@@ -5466,6 +5543,36 @@ var init_engine = __esm({
5466
5543
  }
5467
5544
  i++;
5468
5545
  }
5546
+ return [bodyTokens, i];
5547
+ }
5548
+ handleCache(tokens, start2, context) {
5549
+ const [content] = stripTag(tokens[start2][1]);
5550
+ const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
5551
+ const cacheKey = m ? m[1] : "default";
5552
+ const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
5553
+ sweepExpiredCache(this.fragmentCache);
5554
+ const cached = this.fragmentCache.get(cacheKey);
5555
+ if (cached) {
5556
+ const [htmlContent, expiresAt] = cached;
5557
+ if (Date.now() < expiresAt) {
5558
+ let i2 = start2 + 1;
5559
+ let depth = 0;
5560
+ while (i2 < tokens.length) {
5561
+ if (tokens[i2][0] === "BLOCK") {
5562
+ const [tagContent] = stripTag(tokens[i2][1]);
5563
+ const tag = tagContent.split(/\s+/)[0] || "";
5564
+ if (tag === "cache") depth++;
5565
+ else if (tag === "endcache") {
5566
+ if (depth === 0) return [htmlContent, i2 + 1];
5567
+ depth--;
5568
+ }
5569
+ }
5570
+ i2++;
5571
+ }
5572
+ return [htmlContent, i2];
5573
+ }
5574
+ }
5575
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "cache", "endcache");
5469
5576
  const rendered = this.renderTokens([...bodyTokens], context);
5470
5577
  capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
5471
5578
  this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
@@ -5630,62 +5737,20 @@ var init_engine = __esm({
5630
5737
  handleSetBlock(tokens, start2, context) {
5631
5738
  const [content] = stripTag(tokens[start2][1]);
5632
5739
  const name = (content.split(/\s+/)[1] || "").trim();
5633
- const bodyTokens = [];
5634
- let i = start2 + 1;
5635
- let depth = 0;
5636
- while (i < tokens.length) {
5637
- if (tokens[i][0] === "BLOCK") {
5638
- const [tagContent] = stripTag(tokens[i][1]);
5639
- const tag = tagContent.split(/\s+/)[0] || "";
5640
- if (tag === "set" && !tagContent.includes("=")) {
5641
- depth++;
5642
- bodyTokens.push(tokens[i]);
5643
- } else if (tag === "endset") {
5644
- if (depth === 0) {
5645
- i++;
5646
- break;
5647
- }
5648
- depth--;
5649
- bodyTokens.push(tokens[i]);
5650
- } else {
5651
- bodyTokens.push(tokens[i]);
5652
- }
5653
- } else {
5654
- bodyTokens.push(tokens[i]);
5655
- }
5656
- i++;
5657
- }
5740
+ const [bodyTokens, i] = this.collectBlockBody(
5741
+ tokens,
5742
+ start2,
5743
+ "set",
5744
+ "endset",
5745
+ (tagContent) => !tagContent.includes("=")
5746
+ );
5658
5747
  if (name) {
5659
5748
  context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
5660
5749
  }
5661
5750
  return i;
5662
5751
  }
5663
5752
  handleSpaceless(tokens, start2, context) {
5664
- const bodyTokens = [];
5665
- let i = start2 + 1;
5666
- let depth = 0;
5667
- while (i < tokens.length) {
5668
- if (tokens[i][0] === "BLOCK") {
5669
- const [tagContent] = stripTag(tokens[i][1]);
5670
- const tag = tagContent.split(/\s+/)[0] || "";
5671
- if (tag === "spaceless") {
5672
- depth++;
5673
- bodyTokens.push(tokens[i]);
5674
- } else if (tag === "endspaceless") {
5675
- if (depth === 0) {
5676
- i++;
5677
- break;
5678
- }
5679
- depth--;
5680
- bodyTokens.push(tokens[i]);
5681
- } else {
5682
- bodyTokens.push(tokens[i]);
5683
- }
5684
- } else {
5685
- bodyTokens.push(tokens[i]);
5686
- }
5687
- i++;
5688
- }
5753
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "spaceless", "endspaceless");
5689
5754
  let rendered = this.renderTokens([...bodyTokens], context);
5690
5755
  rendered = rendered.replace(/>\s+</g, "><");
5691
5756
  return [rendered, i];
@@ -5694,31 +5759,7 @@ var init_engine = __esm({
5694
5759
  const [content] = stripTag(tokens[start2][1]);
5695
5760
  const modeMatch = content.match(/^autoescape\s+(false|true)/);
5696
5761
  const autoEscapeOn = !(modeMatch && modeMatch[1] === "false");
5697
- const bodyTokens = [];
5698
- let i = start2 + 1;
5699
- let depth = 0;
5700
- while (i < tokens.length) {
5701
- if (tokens[i][0] === "BLOCK") {
5702
- const [tagContent] = stripTag(tokens[i][1]);
5703
- const tag = tagContent.split(/\s+/)[0] || "";
5704
- if (tag === "autoescape") {
5705
- depth++;
5706
- bodyTokens.push(tokens[i]);
5707
- } else if (tag === "endautoescape") {
5708
- if (depth === 0) {
5709
- i++;
5710
- break;
5711
- }
5712
- depth--;
5713
- bodyTokens.push(tokens[i]);
5714
- } else {
5715
- bodyTokens.push(tokens[i]);
5716
- }
5717
- } else {
5718
- bodyTokens.push(tokens[i]);
5719
- }
5720
- i++;
5721
- }
5762
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "autoescape", "endautoescape");
5722
5763
  if (!autoEscapeOn) {
5723
5764
  const oldAutoEscape = this._autoEscape;
5724
5765
  this._autoEscape = false;
@@ -23511,7 +23552,7 @@ function toolbarJs() {
23511
23552
  connect();
23512
23553
  })();`;
23513
23554
  }
23514
- var cpuCount, DEV_SAFE_METHODS, 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, handleChat, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, handleExecute, 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;
23555
+ var cpuCount, DEV_SAFE_METHODS, 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;
23515
23556
  var init_devAdmin = __esm({
23516
23557
  "../core/src/devAdmin.ts"() {
23517
23558
  "use strict";
@@ -23849,9 +23890,6 @@ var init_devAdmin = __esm({
23849
23890
  { method: "POST", pattern: "/__dev/api/websockets/disconnect", handler: handleWebsocketsDisconnect },
23850
23891
  // Tools
23851
23892
  { method: "POST", pattern: "/__dev/api/tool", handler: handleTool },
23852
- // Chat — proxies to Rust agent /chat (SSE passthrough). Forwards
23853
- // active_file and any other body keys verbatim. See proxyToSupervisor.
23854
- { method: "POST", pattern: "/__dev/api/chat", handler: handleChat },
23855
23893
  // Threads — proxies to Rust agent /threads. Mirrors Python's
23856
23894
  // _api_threads + _api_threads_sub.
23857
23895
  { method: "GET", pattern: "/__dev/api/threads", handler: handleThreads },
@@ -23930,8 +23968,6 @@ var init_devAdmin = __esm({
23930
23968
  { method: "GET", pattern: "/__dev/api/supervise/diff", handler: handleSuperviseStub },
23931
23969
  { method: "POST", pattern: "/__dev/api/supervise/commit", handler: handleSuperviseStub },
23932
23970
  { method: "POST", pattern: "/__dev/api/supervise/cancel", handler: handleSuperviseStub },
23933
- // Execute — proxies to the framework_port+2000 Rust agent (SSE passthrough)
23934
- { method: "POST", pattern: "/__dev/api/execute", handler: handleExecute },
23935
23971
  // Framework-grounding MCP token config — self-contained (.env upsert)
23936
23972
  { method: "GET", pattern: "/__dev/api/grounding/status", handler: handleGroundingStatus },
23937
23973
  { method: "POST", pattern: "/__dev/api/grounding/token", handler: handleGroundingToken },
@@ -24501,9 +24537,6 @@ var init_devAdmin = __esm({
24501
24537
  }
24502
24538
  res.json({ tool, status: "executed", message: `Tool '${tool}' executed (stub)`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
24503
24539
  };
24504
- handleChat = async (req2, res) => {
24505
- await proxyToSupervisor(req2, res, "/chat");
24506
- };
24507
24540
  DEFAULT_MCP_URL = "https://mcp.tina4.com";
24508
24541
  handleGroundingStatus = async (_req, res) => {
24509
24542
  const token = resolveDevEnvVar("TINA4_MCP_TOKEN");
@@ -24791,35 +24824,6 @@ var init_devAdmin = __esm({
24791
24824
  501
24792
24825
  );
24793
24826
  };
24794
- handleExecute = async (req2, res) => {
24795
- const port = parseInt(process.env.TINA4_PORT ?? process.env.PORT ?? "7148", 10);
24796
- const agentUrl = `http://127.0.0.1:${port + 2e3}/execute`;
24797
- try {
24798
- const upstream = await fetch(agentUrl, {
24799
- method: "POST",
24800
- headers: { "Content-Type": "application/json" },
24801
- body: JSON.stringify(req2.body ?? {})
24802
- });
24803
- if (!upstream.body) {
24804
- res.json({ error: "agent returned no body" }, 502);
24805
- return;
24806
- }
24807
- res.raw.writeHead(upstream.status || 200, {
24808
- "Content-Type": upstream.headers.get("content-type") || "text/event-stream",
24809
- "Cache-Control": "no-cache",
24810
- Connection: "keep-alive"
24811
- });
24812
- const reader = upstream.body.getReader();
24813
- while (true) {
24814
- const { done, value } = await reader.read();
24815
- if (done) break;
24816
- if (value) res.raw.write(Buffer.from(value));
24817
- }
24818
- res.raw.end();
24819
- } catch (e) {
24820
- res.json({ error: `agent unreachable at ${agentUrl}: ${e.message}` }, 502);
24821
- }
24822
- };
24823
24827
  DEV_FILES_IGNORED = /* @__PURE__ */ new Set([
24824
24828
  "__pycache__",
24825
24829
  "node_modules",
@@ -25884,12 +25888,46 @@ function sanitizeSecurity(reqs, schemes) {
25884
25888
  });
25885
25889
  }
25886
25890
  function generate2(routes, models = []) {
25891
+ const schemes = resolveSecuritySchemes();
25892
+ const spec = {
25893
+ openapi: resolveOpenApiVersion(),
25894
+ info: buildInfo(),
25895
+ servers: resolveServers(),
25896
+ paths: {},
25897
+ components: {
25898
+ schemas: {},
25899
+ // Configurable security schemes (v3.13.42): bearerFormat via env, optional
25900
+ // apiKey scheme, plus any programmatically-registered schemes (which may
25901
+ // override bearerAuth — e.g. an oauth2 scheme with scopes).
25902
+ securitySchemes: schemes
25903
+ }
25904
+ };
25905
+ const tableToSchema = /* @__PURE__ */ new Map();
25906
+ buildComponentSchemas(models, spec, tableToSchema);
25907
+ const ctx = {
25908
+ models,
25909
+ tableToSchema,
25910
+ schemes,
25911
+ // Default scheme secured routes use when no explicit meta.security is set.
25912
+ defaultScheme: process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth",
25913
+ // Path filters (comma-separated raw-path prefixes).
25914
+ includePrefixes: csv(process.env.TINA4_SWAGGER_INCLUDE),
25915
+ excludePrefixes: csv(process.env.TINA4_SWAGGER_EXCLUDE),
25916
+ // Reusable custom schemas referenced by routes via meta.requestSchema/responseSchemas.
25917
+ refSchemas: /* @__PURE__ */ new Set(),
25918
+ usedTags: [],
25919
+ seenIds: /* @__PURE__ */ new Set()
25920
+ };
25921
+ for (const route of routes) {
25922
+ buildOperation(route, spec, ctx);
25923
+ }
25924
+ buildRefSchemas(spec, ctx.refSchemas);
25925
+ buildTags(spec, ctx.usedTags);
25926
+ return spec;
25927
+ }
25928
+ function buildInfo() {
25887
25929
  const info = {
25888
25930
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
25889
- // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
25890
- // 0.0.1). description defaults to the empty string, not a canned sentence.
25891
- // Both are the settled cross-framework defaults (parity with the Python
25892
- // master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
25893
25931
  version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
25894
25932
  description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
25895
25933
  };
@@ -25906,135 +25944,134 @@ function generate2(routes, models = []) {
25906
25944
  const [name, url] = licenseRaw.split("|").map((s) => s.trim());
25907
25945
  info.license = url ? { name, url } : { name };
25908
25946
  }
25909
- const schemes = resolveSecuritySchemes();
25910
- const spec = {
25911
- openapi: resolveOpenApiVersion(),
25912
- info,
25913
- servers: resolveServers(),
25914
- paths: {},
25915
- components: {
25916
- schemas: {},
25917
- // Configurable security schemes (v3.13.42): bearerFormat via env, optional
25918
- // apiKey scheme, plus any programmatically-registered schemes (which may
25919
- // override bearerAuth — e.g. an oauth2 scheme with scopes).
25920
- securitySchemes: schemes
25921
- }
25922
- };
25923
- const defaultScheme = process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth";
25924
- const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
25925
- const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
25926
- const refSchemas = /* @__PURE__ */ new Set();
25927
- const tableToSchema = /* @__PURE__ */ new Map();
25947
+ return info;
25948
+ }
25949
+ function buildComponentSchemas(models, spec, tableToSchema) {
25928
25950
  for (const model of models) {
25929
25951
  const schemaKey = schemaNameForModel(model);
25930
25952
  tableToSchema.set(model.tableName, schemaKey);
25931
25953
  spec.components.schemas[schemaKey] = modelToSchema(model);
25932
25954
  }
25933
- const usedTags = [];
25934
- const seenIds = /* @__PURE__ */ new Set();
25935
- for (const route of routes) {
25936
- if (!isIncludedPath(route.pattern, includePrefixes, excludePrefixes)) continue;
25937
- const openApiPath = patternToOpenAPI(route.pattern);
25938
- const method = route.method.toLowerCase();
25939
- if (!spec.paths[openApiPath]) {
25940
- spec.paths[openApiPath] = {};
25941
- }
25942
- const tags = route.meta?.tags ?? inferTags(route.pattern);
25943
- for (const t of tags) {
25944
- if (!usedTags.includes(t)) usedTags.push(t);
25945
- }
25946
- const operation = {
25947
- operationId: uniqueOperationId(method, openApiPath, seenIds),
25948
- summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
25949
- tags,
25950
- responses: route.meta?.responses ?? {
25951
- "200": { description: "Successful response" }
25952
- }
25953
- };
25954
- if (route.meta?.description) operation.description = route.meta.description;
25955
- if (route.meta?.deprecated) operation.deprecated = true;
25956
- const pathParams = extractPathParams(route.pattern);
25957
- if (pathParams.length > 0) {
25958
- operation.parameters = pathParams.map(({ name, schema }) => ({
25959
- name,
25960
- in: "path",
25961
- required: true,
25962
- schema
25963
- }));
25955
+ }
25956
+ function buildOperation(route, spec, ctx) {
25957
+ if (!isIncludedPath(route.pattern, ctx.includePrefixes, ctx.excludePrefixes)) return;
25958
+ const openApiPath = patternToOpenAPI(route.pattern);
25959
+ const method = route.method.toLowerCase();
25960
+ if (!spec.paths[openApiPath]) {
25961
+ spec.paths[openApiPath] = {};
25962
+ }
25963
+ const tags = route.meta?.tags ?? inferTags(route.pattern);
25964
+ for (const t of tags) {
25965
+ if (!ctx.usedTags.includes(t)) ctx.usedTags.push(t);
25966
+ }
25967
+ const operation = {
25968
+ operationId: uniqueOperationId(method, openApiPath, ctx.seenIds),
25969
+ summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
25970
+ tags,
25971
+ responses: route.meta?.responses ?? {
25972
+ "200": { description: "Successful response" }
25964
25973
  }
25965
- if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
25966
- const modelName = inferModelFromPath(route.pattern);
25967
- if (modelName && models.some((m) => m.tableName === modelName)) {
25968
- operation.parameters = [
25969
- ...operation.parameters ?? [],
25970
- { name: "page", in: "query", schema: { type: "integer", default: 1 } },
25971
- { name: "limit", in: "query", schema: { type: "integer", default: 20 } },
25972
- { name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
25973
- ];
25974
- }
25974
+ };
25975
+ if (route.meta?.description) operation.description = route.meta.description;
25976
+ if (route.meta?.deprecated) operation.deprecated = true;
25977
+ const parameters = operationParameters(route, method, ctx.models);
25978
+ if (parameters.length > 0) operation.parameters = parameters;
25979
+ operationRequestBody(route, method, operation, ctx);
25980
+ operationResponseSchemas(route, operation, ctx.refSchemas);
25981
+ operationSecurity(route, method, operation, ctx.schemes, ctx.defaultScheme);
25982
+ spec.paths[openApiPath][method] = operation;
25983
+ }
25984
+ function operationParameters(route, method, models) {
25985
+ let parameters = [];
25986
+ const pathParams = extractPathParams(route.pattern);
25987
+ if (pathParams.length > 0) {
25988
+ parameters = pathParams.map(({ name, schema }) => ({
25989
+ name,
25990
+ in: "path",
25991
+ required: true,
25992
+ schema
25993
+ }));
25994
+ }
25995
+ if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
25996
+ const modelName = inferModelFromPath(route.pattern);
25997
+ if (modelName && models.some((m) => m.tableName === modelName)) {
25998
+ parameters = [
25999
+ ...parameters,
26000
+ { name: "page", in: "query", schema: { type: "integer", default: 1 } },
26001
+ { name: "limit", in: "query", schema: { type: "integer", default: 20 } },
26002
+ { name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
26003
+ ];
25975
26004
  }
25976
- const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
25977
- if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
25978
- refSchemas.add(reqSchemaRef.name);
25979
- const media = {
25980
- schema: { $ref: `#/components/schemas/${reqSchemaRef.name}` }
25981
- };
25982
- if (route.meta?.example !== void 0) media.example = route.meta.example;
26005
+ }
26006
+ return parameters;
26007
+ }
26008
+ function mediaWithExample(schema, example) {
26009
+ const media = { schema };
26010
+ if (example !== void 0) media.example = example;
26011
+ return media;
26012
+ }
26013
+ function operationRequestBody(route, method, operation, ctx) {
26014
+ const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
26015
+ if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
26016
+ ctx.refSchemas.add(reqSchemaRef.name);
26017
+ const media = mediaWithExample({ $ref: `#/components/schemas/${reqSchemaRef.name}` }, route.meta?.example);
26018
+ operation.requestBody = {
26019
+ content: { [reqSchemaRef.contentType]: media }
26020
+ };
26021
+ } else if (method === "post" || method === "put") {
26022
+ const modelName = inferModelFromPath(route.pattern);
26023
+ const schemaKey = modelName ? ctx.tableToSchema.get(modelName) : void 0;
26024
+ if (schemaKey) {
26025
+ const sref = `#/components/schemas/${schemaKey}`;
25983
26026
  operation.requestBody = {
25984
- content: { [reqSchemaRef.contentType]: media }
26027
+ required: true,
26028
+ content: { "application/json": mediaWithExample({ $ref: sref }, route.meta?.example) }
25985
26029
  };
25986
- } else if (method === "post" || method === "put") {
25987
- const modelName = inferModelFromPath(route.pattern);
25988
- const schemaKey = modelName ? tableToSchema.get(modelName) : void 0;
25989
- if (schemaKey) {
25990
- const sref = `#/components/schemas/${schemaKey}`;
25991
- const media = { schema: { $ref: sref } };
25992
- if (route.meta?.example !== void 0) media.example = route.meta.example;
25993
- operation.requestBody = {
25994
- required: true,
25995
- content: { "application/json": media }
25996
- };
25997
- if (route.meta?.responses === void 0) {
25998
- operation.responses = {
25999
- "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
26000
- };
26001
- }
26002
- } else if (route.meta?.example !== void 0) {
26003
- operation.requestBody = {
26004
- content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
26030
+ if (route.meta?.responses === void 0) {
26031
+ operation.responses = {
26032
+ "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
26005
26033
  };
26006
26034
  }
26035
+ } else if (route.meta?.example !== void 0) {
26036
+ operation.requestBody = {
26037
+ content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
26038
+ };
26007
26039
  }
26008
- const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
26009
- if (respSchemas.length > 0) {
26010
- const responses = operation.responses;
26011
- for (const { status: status2, name, isList } of respSchemas) {
26012
- refSchemas.add(name);
26013
- const sref = `#/components/schemas/${name}`;
26014
- const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
26015
- responses[status2] = {
26016
- description: status2.startsWith("2") ? "Successful response" : "Response",
26017
- content: { "application/json": { schema } }
26018
- };
26019
- }
26040
+ }
26041
+ }
26042
+ function operationResponseSchemas(route, operation, refSchemas) {
26043
+ const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
26044
+ if (respSchemas.length > 0) {
26045
+ const responses = operation.responses;
26046
+ for (const { status: status2, name, isList } of respSchemas) {
26047
+ refSchemas.add(name);
26048
+ const sref = `#/components/schemas/${name}`;
26049
+ const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
26050
+ responses[status2] = {
26051
+ description: status2.startsWith("2") ? "Successful response" : "Response",
26052
+ content: { "application/json": { schema } }
26053
+ };
26020
26054
  }
26021
- const hasExplicitSecurity = route.meta?.security !== void 0 || route.meta?.scopes !== void 0 && route.meta.scopes.length > 0;
26022
- if (hasExplicitSecurity) {
26023
- const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
26024
- operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
26025
- if (normalized.length > 0) {
26026
- const responses = operation.responses;
26027
- if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
26028
- }
26029
- } else if (routeRequiresAuth(route, method)) {
26030
- const requirements = [{ [defaultScheme]: [] }];
26031
- if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
26032
- operation.security = sanitizeSecurity(requirements, schemes);
26055
+ }
26056
+ }
26057
+ function operationSecurity(route, method, operation, schemes, defaultScheme) {
26058
+ const hasExplicitSecurity = route.meta?.security !== void 0 || route.meta?.scopes !== void 0 && route.meta.scopes.length > 0;
26059
+ if (hasExplicitSecurity) {
26060
+ const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
26061
+ operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
26062
+ if (normalized.length > 0) {
26033
26063
  const responses = operation.responses;
26034
26064
  if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
26035
26065
  }
26036
- spec.paths[openApiPath][method] = operation;
26066
+ } else if (routeRequiresAuth(route, method)) {
26067
+ const requirements = [{ [defaultScheme]: [] }];
26068
+ if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
26069
+ operation.security = sanitizeSecurity(requirements, schemes);
26070
+ const responses = operation.responses;
26071
+ if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
26037
26072
  }
26073
+ }
26074
+ function buildRefSchemas(spec, refSchemas) {
26038
26075
  if (refSchemas.size > 0) {
26039
26076
  const schemas = spec.components.schemas;
26040
26077
  for (const name of refSchemas) {
@@ -26043,10 +26080,11 @@ function generate2(routes, models = []) {
26043
26080
  }
26044
26081
  }
26045
26082
  }
26083
+ }
26084
+ function buildTags(spec, usedTags) {
26046
26085
  if (usedTags.length > 0) {
26047
26086
  spec.tags = usedTags.map((name) => ({ name }));
26048
26087
  }
26049
- return spec;
26050
26088
  }
26051
26089
  function routeRequiresAuth(route, method) {
26052
26090
  if (route.noAuth) return false;
@@ -27297,7 +27335,9 @@ function injectIntoHtml(ctx, devToolbar, html) {
27297
27335
  // Suppress the live reloader on the AI/stable port (data-reload="0"); the
27298
27336
  // toolbar JS early-returns when data-reload !== "1". Mirrors PHP's
27299
27337
  // suppressReload flag.
27300
- reload: !ctx.isAiPortRequest
27338
+ // Also suppress on the dev-admin dashboard (any /__dev page): its SPA reloads
27339
+ // itself gently, so the toolbar's full-page reloader must not fire there.
27340
+ reload: !ctx.isAiPortRequest && !ctx.pathname.startsWith("/__dev")
27301
27341
  };
27302
27342
  return injectFeedbackWidget(ctx.req, injectDevToolbar(html, toolbarCtx));
27303
27343
  }
@@ -27552,6 +27592,51 @@ async function runDispatch(ctx, rawReq, rawRes) {
27552
27592
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
27553
27593
  return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
27554
27594
  }
27595
+ function loopbackBindHosts(host) {
27596
+ const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
27597
+ switch (normalized) {
27598
+ case "localhost":
27599
+ return ["127.0.0.1", "::1"];
27600
+ case "127.0.0.1":
27601
+ case "0.0.0.0":
27602
+ return ["::1"];
27603
+ case "::1":
27604
+ case "::":
27605
+ return ["127.0.0.1"];
27606
+ default:
27607
+ return [];
27608
+ }
27609
+ }
27610
+ function startLoopbackSiblings(port, host, dispatch) {
27611
+ const siblingHosts = loopbackBindHosts(host);
27612
+ if (siblingHosts.length === 0) return Promise.resolve([]);
27613
+ return Promise.all(
27614
+ siblingHosts.map(
27615
+ (siblingHost) => new Promise((resolveSibling) => {
27616
+ const sibling = createServer2(dispatch);
27617
+ let settled = false;
27618
+ sibling.on("error", (err) => {
27619
+ const expected = err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT";
27620
+ if (!expected) {
27621
+ Log.debug(
27622
+ `Loopback sibling ${siblingHost}:${port} not bound (${err.code ?? err.message}) \u2014 skipped, primary already serves`
27623
+ );
27624
+ }
27625
+ if (!settled) {
27626
+ settled = true;
27627
+ resolveSibling(null);
27628
+ }
27629
+ });
27630
+ sibling.listen(port, siblingHost, () => {
27631
+ settled = true;
27632
+ resolveSibling(sibling);
27633
+ });
27634
+ })
27635
+ )
27636
+ ).then(
27637
+ (results) => results.filter((s) => s !== null)
27638
+ );
27639
+ }
27555
27640
  async function startServer(config) {
27556
27641
  loadEnv(".env.local");
27557
27642
  loadEnv();
@@ -27820,8 +27905,9 @@ ${reset2}
27820
27905
  }
27821
27906
  });
27822
27907
  return new Promise((resolvePromise) => {
27823
- server.listen(port, host, () => {
27908
+ server.listen(port, host, async () => {
27824
27909
  if (!cluster.isWorker) writePidfile(port);
27910
+ const siblingServers = await startLoopbackSiblings(port, host, dispatch);
27825
27911
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
27826
27912
  const isDebug = isTruthy(process.env.TINA4_DEBUG);
27827
27913
  const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
@@ -27884,14 +27970,22 @@ ${reset2}
27884
27970
  }
27885
27971
  let shuttingDown = false;
27886
27972
  const closeListeners = () => new Promise((done) => {
27887
- let pending = aiServer ? 2 : 1;
27973
+ let pending = 1 + (aiServer ? 1 : 0) + siblingServers.length;
27888
27974
  const one = () => {
27889
27975
  if (--pending === 0) done();
27890
27976
  };
27891
27977
  server.close(one);
27892
27978
  if (aiServer) aiServer.close(one);
27979
+ for (const sibling of siblingServers) {
27980
+ try {
27981
+ sibling.close(one);
27982
+ } catch {
27983
+ one();
27984
+ }
27985
+ }
27893
27986
  server.closeIdleConnections();
27894
27987
  aiServer?.closeIdleConnections();
27988
+ for (const sibling of siblingServers) sibling.closeIdleConnections();
27895
27989
  });
27896
27990
  const gracefulShutdown = async (signal) => {
27897
27991
  if (shuttingDown) return;
@@ -27919,6 +28013,7 @@ ${reset2}
27919
28013
  );
27920
28014
  server.closeAllConnections();
27921
28015
  aiServer?.closeAllConnections();
28016
+ for (const sibling of siblingServers) sibling.closeAllConnections();
27922
28017
  }
27923
28018
  try {
27924
28019
  const orm = await Promise.resolve().then(() => (init_index(), index_exports));
@@ -27946,6 +28041,12 @@ ${reset2}
27946
28041
  stopAllBackgroundTasks();
27947
28042
  if (aiServer) aiServer.close();
27948
28043
  server.close();
28044
+ for (const sibling of siblingServers) {
28045
+ try {
28046
+ sibling.close();
28047
+ } catch {
28048
+ }
28049
+ }
27949
28050
  Promise.resolve().then(() => (init_index(), index_exports)).then((orm) => orm.closeDatabase()).catch(() => {
27950
28051
  });
27951
28052
  },
@@ -35662,6 +35763,7 @@ __export(src_exports2, {
35662
35763
  isValidSessionId: () => isValidSessionId,
35663
35764
  kafkaSecurityConfig: () => kafkaSecurityConfig,
35664
35765
  loadEnv: () => loadEnv,
35766
+ loopbackBindHosts: () => loopbackBindHosts,
35665
35767
  makeCaseInsensitiveHeaders: () => makeCaseInsensitiveHeaders,
35666
35768
  matchCronField: () => matchCronField,
35667
35769
  matchesCron: () => matchesCron,
@@ -43053,6 +43155,7 @@ var init_baseModel = __esm({
43053
43155
  "src/baseModel.ts"() {
43054
43156
  "use strict";
43055
43157
  init_database();
43158
+ init_modelCollection();
43056
43159
  init_validation();
43057
43160
  init_queryBuilder();
43058
43161
  init_sqlite();
@@ -43288,6 +43391,30 @@ var init_baseModel = __esm({
43288
43391
  static getPkColumn() {
43289
43392
  return this.getDbColumn(this.getPkField());
43290
43393
  }
43394
+ /**
43395
+ * Shared read tail for the collection-returning finders (where / all / select
43396
+ * / find filter-form / withTrashed). Runs the SAME two calls `db.fetch()`
43397
+ * makes — the page fetch AND the COUNT probe over the SAME base SQL — hydrates
43398
+ * the rows into model instances, and returns a ModelCollection carrying the
43399
+ * total (ADR-0064).
43400
+ *
43401
+ * The total is FREE: `probeTotal` is the exact `COUNT(*)` probe `db.fetch()`
43402
+ * already runs; the ORM used to discard it. ZERO extra queries beyond that one
43403
+ * probe. `sql` MUST NOT carry its own LIMIT/OFFSET — `adapterFetch` applies
43404
+ * limit/offset to the page, and the probe wraps the un-limited SQL so it counts
43405
+ * the WHOLE filtered set, not the page.
43406
+ */
43407
+ static async _collect(sql, params, limit, offset, include) {
43408
+ const db = this.getDb();
43409
+ const rows = await adapterFetch(db, sql, params, limit, offset);
43410
+ const data = Array.isArray(rows) ? rows : rows?.data ?? [];
43411
+ const total = await probeTotal(db, sql, params, limit) ?? data.length;
43412
+ const instances = data.map((row) => new this(row));
43413
+ if (include) {
43414
+ await this._eagerLoad(instances, include);
43415
+ }
43416
+ return new ModelCollection(instances, total, limit, offset);
43417
+ }
43291
43418
  /**
43292
43419
  * Find a record by primary key.
43293
43420
  * @param id Primary key value.
@@ -43340,7 +43467,6 @@ var init_baseModel = __esm({
43340
43467
  );
43341
43468
  }
43342
43469
  const lim = typeof limit === "number" ? limit : 100;
43343
- const db = ModelClass.getDb();
43344
43470
  const conditions = [];
43345
43471
  const params = [];
43346
43472
  if (filter) {
@@ -43360,16 +43486,7 @@ var init_baseModel = __esm({
43360
43486
  if (orderBy) {
43361
43487
  sql += ` ORDER BY ${orderBy}`;
43362
43488
  }
43363
- const rows = await adapterFetch(db, sql, params, lim, offset);
43364
- const data = rows?.data ?? rows;
43365
- const instances = (Array.isArray(data) ? data : []).map((row) => {
43366
- const inst = new this(row);
43367
- return inst;
43368
- });
43369
- if (include) {
43370
- await ModelClass._eagerLoad(instances, include);
43371
- }
43372
- return instances;
43489
+ return ModelClass._collect(sql, params, lim, offset, include);
43373
43490
  }
43374
43491
  /**
43375
43492
  * Load a record into this instance via selectOne.
@@ -43437,7 +43554,6 @@ var init_baseModel = __esm({
43437
43554
  */
43438
43555
  static async all(limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
43439
43556
  const ModelClass = this;
43440
- const db = ModelClass.getDb();
43441
43557
  const conditions = [];
43442
43558
  if (ModelClass.softDelete) {
43443
43559
  conditions.push("is_deleted = 0");
@@ -43447,13 +43563,8 @@ var init_baseModel = __esm({
43447
43563
  }
43448
43564
  const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
43449
43565
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
43450
- const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
43451
- const rows = await adapterQuery(db, sql, []);
43452
- const instances = rows.map((row) => new ModelClass(row));
43453
- if (include) {
43454
- await ModelClass._eagerLoad(instances, include);
43455
- }
43456
- return instances;
43566
+ const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`;
43567
+ return ModelClass._collect(sql, [], limit, offset, include);
43457
43568
  }
43458
43569
  /**
43459
43570
  * Query records with a WHERE clause.
@@ -43468,7 +43579,6 @@ var init_baseModel = __esm({
43468
43579
  */
43469
43580
  static async where(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
43470
43581
  const ModelClass = this;
43471
- const db = ModelClass.getDb();
43472
43582
  const parts = [];
43473
43583
  if (ModelClass.softDelete) {
43474
43584
  parts.push("is_deleted = 0");
@@ -43478,13 +43588,8 @@ var init_baseModel = __esm({
43478
43588
  }
43479
43589
  parts.push(`(${conditions})`);
43480
43590
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
43481
- const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
43482
- const rows = await adapterQuery(db, sql, params);
43483
- const instances = rows.map((row) => new ModelClass(row));
43484
- if (include) {
43485
- await ModelClass._eagerLoad(instances, include);
43486
- }
43487
- return instances;
43591
+ const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause}`;
43592
+ return ModelClass._collect(sql, params, limit, offset, include);
43488
43593
  }
43489
43594
  /**
43490
43595
  * Save this instance (insert or update). Returns this on success (fluent
@@ -43964,10 +44069,7 @@ var init_baseModel = __esm({
43964
44069
  */
43965
44070
  static async select(sql, params, limit = DEFAULT_ROW_CAP, offset = 0) {
43966
44071
  const ModelClass = this;
43967
- const db = ModelClass.getDb();
43968
- const paged = SQLTranslator.appendLimit(sql, limit, offset);
43969
- const rows = await adapterQuery(db, paged, params);
43970
- return rows.map((row) => new ModelClass(row));
44072
+ return ModelClass._collect(sql, params, limit, offset);
43971
44073
  }
43972
44074
  static async selectOne(sql, params, include) {
43973
44075
  const ModelClass = this;
@@ -44039,7 +44141,6 @@ var init_baseModel = __esm({
44039
44141
  */
44040
44142
  static async withTrashed(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0) {
44041
44143
  const ModelClass = this;
44042
- const db = ModelClass.getDb();
44043
44144
  const parts = [];
44044
44145
  if (ModelClass.tableFilter) {
44045
44146
  parts.push(ModelClass.tableFilter);
@@ -44051,14 +44152,7 @@ var init_baseModel = __esm({
44051
44152
  if (parts.length > 0) {
44052
44153
  sql += ` WHERE ${parts.join(" AND ")}`;
44053
44154
  }
44054
- if (limit !== void 0) {
44055
- sql += ` LIMIT ${limit}`;
44056
- }
44057
- if (offset !== void 0) {
44058
- sql += ` OFFSET ${offset}`;
44059
- }
44060
- const rows = await adapterQuery(db, sql, params);
44061
- return rows.map((row) => new ModelClass(row));
44155
+ return ModelClass._collect(sql, params, limit, offset);
44062
44156
  }
44063
44157
  /**
44064
44158
  * Count records matching conditions (respects soft delete and table filter).
@@ -46866,6 +46960,7 @@ __export(index_exports, {
46866
46960
  InvalidId: () => InvalidId,
46867
46961
  LocalStorage: () => LocalStorage,
46868
46962
  Migration: () => Migration,
46963
+ ModelCollection: () => ModelCollection,
46869
46964
  MongodbAdapter: () => MongodbAdapter,
46870
46965
  MssqlAdapter: () => MssqlAdapter,
46871
46966
  MysqlAdapter: () => MysqlAdapter,
@@ -46969,6 +47064,7 @@ var init_index = __esm({
46969
47064
  "src/index.ts"() {
46970
47065
  init_types();
46971
47066
  init_databaseResult();
47067
+ init_modelCollection();
46972
47068
  init_database();
46973
47069
  init_database();
46974
47070
  init_databaseUrl();
@@ -47028,6 +47124,7 @@ export {
47028
47124
  InvalidId,
47029
47125
  LocalStorage,
47030
47126
  Migration,
47127
+ ModelCollection,
47031
47128
  MongodbAdapter,
47032
47129
  MssqlAdapter,
47033
47130
  MysqlAdapter,