@palbase/backend 23.1.0 → 24.0.0

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 (73) hide show
  1. package/dist/bin/palbase-backend.cjs +389 -44
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +4 -5
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/{chunk-OHALWEOG.js → chunk-7Z6MGMXQ.js} +57 -2
  6. package/dist/chunk-7Z6MGMXQ.js.map +1 -0
  7. package/dist/{chunk-RCLNBJCM.js → chunk-H3JAISUY.js} +136 -1
  8. package/dist/chunk-H3JAISUY.js.map +1 -0
  9. package/dist/{chunk-M5MCBWJI.js → chunk-NXDH6VQJ.js} +264 -28
  10. package/dist/chunk-NXDH6VQJ.js.map +1 -0
  11. package/dist/{chunk-NS5V43YQ.js → chunk-P2Q27SGP.js} +19 -3
  12. package/dist/chunk-P2Q27SGP.js.map +1 -0
  13. package/dist/{chunk-R3KN6RHD.js → chunk-T5IOSOE5.js} +7 -2
  14. package/dist/chunk-T5IOSOE5.js.map +1 -0
  15. package/dist/{chunk-PY7YJDCT.js → chunk-ZUGY7RGS.js} +32 -3
  16. package/dist/chunk-ZUGY7RGS.js.map +1 -0
  17. package/dist/db/index.cjs +48 -3
  18. package/dist/db/index.cjs.map +1 -1
  19. package/dist/db/index.d.cts +2 -2
  20. package/dist/db/index.d.ts +2 -2
  21. package/dist/db/index.js +2 -2
  22. package/dist/{endpoint-CVWXh6oG.d.ts → endpoint-0_DGBajf.d.ts} +100 -3
  23. package/dist/{endpoint-c9h5jriX.d.cts → endpoint-CcQ1a36a.d.cts} +100 -3
  24. package/dist/engine/index.cjs +378 -31
  25. package/dist/engine/index.cjs.map +1 -1
  26. package/dist/engine/index.d.cts +4 -4
  27. package/dist/engine/index.d.ts +4 -4
  28. package/dist/engine/index.js +4 -4
  29. package/dist/{index-CwAJ7HEe.d.ts → index-CJiJU9ux.d.ts} +125 -21
  30. package/dist/{index-By8Dle5U.d.cts → index-D-4-PNuQ.d.cts} +125 -21
  31. package/dist/{index-BZrJXnVh.d.ts → index-D17r-MKb.d.ts} +134 -9
  32. package/dist/{index-CxeQSfJP.d.cts → index-DRFxf07H.d.cts} +134 -9
  33. package/dist/index.cjs +166 -5
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +54 -12
  36. package/dist/index.d.ts +54 -12
  37. package/dist/index.js +28 -11
  38. package/dist/index.js.map +1 -1
  39. package/dist/openapi/index.cjs +16 -1
  40. package/dist/openapi/index.cjs.map +1 -1
  41. package/dist/openapi/index.d.cts +6 -4
  42. package/dist/openapi/index.d.ts +6 -4
  43. package/dist/openapi/index.js +6 -7
  44. package/dist/openapi/index.js.map +1 -1
  45. package/dist/{registry-CqPK2Qby.d.cts → registry-1X-skBNu.d.cts} +1 -1
  46. package/dist/{registry-B3niOVYp.d.ts → registry-CEod_5sz.d.ts} +1 -1
  47. package/dist/test/index.cjs +27 -0
  48. package/dist/test/index.cjs.map +1 -1
  49. package/dist/test/index.d.cts +1 -1
  50. package/dist/test/index.d.ts +1 -1
  51. package/dist/test/index.js +27 -0
  52. package/dist/test/index.js.map +1 -1
  53. package/docs/README.md +4 -4
  54. package/docs/database.md +115 -11
  55. package/docs/getting-started.md +5 -4
  56. package/docs/llms-full.txt +385 -89
  57. package/docs/migrations.md +81 -59
  58. package/docs/schema.md +82 -2
  59. package/docs/services.md +98 -9
  60. package/package.json +2 -2
  61. package/template/AGENTS.md +121 -41
  62. package/template/controllers/notes.controller.ts +64 -0
  63. package/template/package.json +1 -1
  64. package/template/services/note.service.ts +74 -0
  65. package/template/tsconfig.json +11 -1
  66. package/dist/chunk-HQRJDARQ.js +0 -90
  67. package/dist/chunk-HQRJDARQ.js.map +0 -1
  68. package/dist/chunk-M5MCBWJI.js.map +0 -1
  69. package/dist/chunk-NS5V43YQ.js.map +0 -1
  70. package/dist/chunk-OHALWEOG.js.map +0 -1
  71. package/dist/chunk-PY7YJDCT.js.map +0 -1
  72. package/dist/chunk-R3KN6RHD.js.map +0 -1
  73. package/dist/chunk-RCLNBJCM.js.map +0 -1
@@ -265,7 +265,7 @@ var TxPlanBuilder = class {
265
265
  `${name}.upsert()`
266
266
  );
267
267
  },
268
- insertMany: (rows) => {
268
+ insertMany: (rows, opts) => {
269
269
  if (rows.length === 0) {
270
270
  return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
271
271
  }
@@ -276,7 +276,23 @@ var TxPlanBuilder = class {
276
276
  }
277
277
  const encoded = rows.map((row) => encodeMap(row, false));
278
278
  assertUniformRows(encoded, name);
279
- return this.push({ op: "insertMany", table: name, rows: encoded }, `${name}.insertMany()`);
279
+ if (opts !== void 0 && opts.onConflict.length === 0) {
280
+ throw new TxPlanError(
281
+ `${name}.insertMany() was given a conflict action with no onConflict columns. Postgres matches a collision on columns, so name them.`
282
+ );
283
+ }
284
+ return this.push(
285
+ {
286
+ op: "insertMany",
287
+ table: name,
288
+ rows: encoded,
289
+ // Omitted entirely when no options were given, so the op a plain
290
+ // insertMany produces is byte-identical to the one it produced
291
+ // before this option existed.
292
+ ...opts !== void 0 ? { onConflict: opts.onConflict, action: opts.action ?? "ignore" } : {}
293
+ },
294
+ `${name}.insertMany()`
295
+ );
280
296
  },
281
297
  updateWhere: (where, set) => {
282
298
  const encodedWhere = encodeMap(where, false);
@@ -450,6 +466,42 @@ function __getRuntime() {
450
466
  }
451
467
  return runtime;
452
468
  }
469
+ var LIFECYCLE = /* @__PURE__ */ Symbol.for("palbase.backend.lifecycleHooks");
470
+ function declaredLifecycle() {
471
+ const g = globalThis;
472
+ return g[LIFECYCLE] ??= { start: [], shutdown: [] };
473
+ }
474
+ function reason(err) {
475
+ return err instanceof Error ? err.message : String(err);
476
+ }
477
+ async function drain(hooks) {
478
+ for (const h of [...hooks].reverse()) {
479
+ try {
480
+ await h.run();
481
+ } catch (err) {
482
+ console.error(`[palbase] shutdown hook "${h.name}" failed: ${reason(err)}`, err);
483
+ }
484
+ }
485
+ }
486
+ async function __runStartHooks() {
487
+ const slot = declaredLifecycle();
488
+ const start = slot.start.splice(0);
489
+ const shutdown = slot.shutdown.splice(0);
490
+ for (const h of start) {
491
+ try {
492
+ await h.run();
493
+ } catch (err) {
494
+ await drain(shutdown);
495
+ throw new Error(`[palbase] start hook "${h.name}" failed: ${reason(err)}`, { cause: err });
496
+ }
497
+ }
498
+ let drained = false;
499
+ return async () => {
500
+ if (drained) return;
501
+ drained = true;
502
+ await drain(shutdown);
503
+ };
504
+ }
453
505
  function makeServiceProxy(key) {
454
506
  const handler = {
455
507
  get(_target, prop, receiver) {
@@ -477,6 +529,7 @@ function makeTablesAccessor(ops) {
477
529
  search: (params) => ops().search(name, params),
478
530
  similar: (id, params) => ops().similar(name, id, params),
479
531
  recommend: (params) => ops().recommend(name, params),
532
+ facets: (params) => ops().facets(name, params),
480
533
  supersede: (id, row) => ops().supersede(name, id, row)
481
534
  };
482
535
  }
@@ -495,9 +548,13 @@ function makeTypedSurface(raw) {
495
548
  findById: (table, id) => raw.findById(table, id),
496
549
  findMany: (table, query, opts) => raw.findMany(table, query, opts),
497
550
  upsert: (table, data, opts) => raw.upsert(table, data, opts),
551
+ updateMany: (table, where, set) => raw.updateMany(table, where, set),
552
+ deleteMany: (table, where) => raw.deleteMany(table, where),
553
+ count: (table, where) => raw.count(table, where),
498
554
  search: (table, params) => raw.search(table, params),
499
555
  similar: (table, id, params) => reco.similar(table, id, params),
500
556
  recommend: (table, params) => reco.recommend(table, params),
557
+ facets: (table, params) => reco.facets(table, params),
501
558
  supersede: (table, id, row) => raw.supersede(table, id, row)
502
559
  };
503
560
  return Object.assign(ops, {
@@ -655,6 +712,44 @@ var HttpError = class extends Error {
655
712
  return result;
656
713
  }
657
714
  };
715
+ var NamedHttpError = class extends HttpError {
716
+ constructor(status, defaultCode, name, message, code, data) {
717
+ super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
718
+ this.name = name;
719
+ }
720
+ };
721
+ function defaultMessage(name) {
722
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
723
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
724
+ }
725
+ var Conflict = class extends NamedHttpError {
726
+ constructor(message, code, data) {
727
+ super(409, "conflict", "Conflict", message, code, data);
728
+ }
729
+ };
730
+ var UniqueViolation = class extends Conflict {
731
+ /**
732
+ * Whether `e` is a unique violation — REGARDLESS of which copy of this SDK
733
+ * constructed it.
734
+ *
735
+ * Use this instead of `instanceof`. Measured on a live stack: a controller
736
+ * bundle INLINES its own copy of `@palbase/backend`, and the engine that
737
+ * raises this error is the runtime's copy. Two copies, two class identities,
738
+ * and `e instanceof UniqueViolation` is false in the one place a caller
739
+ * writes it — a check that reads as correct and silently never matches.
740
+ */
741
+ static is(e) {
742
+ return typeof e === "object" && e !== null && e.name === "UniqueViolation" && typeof e.constraint === "string";
743
+ }
744
+ /** The unique constraint the statement violated, as Postgres named it.
745
+ * `""` when the driver did not say which — see `engine/db.ts`. */
746
+ constraint;
747
+ constructor(constraint, message, code, data) {
748
+ super(message ?? "Unique constraint violated", code ?? "unique_violation", data);
749
+ this.name = "UniqueViolation";
750
+ this.constraint = constraint;
751
+ }
752
+ };
658
753
 
659
754
  // src/engine/config.ts
660
755
  var BootRefused = class extends Error {
@@ -1017,9 +1112,9 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
1017
1112
  release();
1018
1113
  await settled;
1019
1114
  },
1020
- async rollback(reason) {
1115
+ async rollback(reason2) {
1021
1116
  if (!opening) return;
1022
- fail(reason);
1117
+ fail(reason2);
1023
1118
  await settled?.catch(() => void 0);
1024
1119
  }
1025
1120
  };
@@ -1062,18 +1157,48 @@ function reviveVectors(row, vectorCols) {
1062
1157
  }
1063
1158
  return row;
1064
1159
  }
1160
+ function transformsOf(schema, table) {
1161
+ const out = /* @__PURE__ */ new Map();
1162
+ for (const [key, def] of Object.entries(schema.tables ?? {})) {
1163
+ if ((def.name ?? key) !== table) continue;
1164
+ for (const [col, c] of Object.entries(def.columns ?? {})) {
1165
+ const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
1166
+ const t = d?.transform;
1167
+ if (t !== null && typeof t === "object") {
1168
+ out.set(col, t);
1169
+ }
1170
+ }
1171
+ }
1172
+ return out;
1173
+ }
1174
+ function applyFromDb(row, transforms) {
1175
+ if (transforms.size === 0) return row;
1176
+ for (const [col, t] of transforms) {
1177
+ if (t.fromDb === void 0) continue;
1178
+ const v = row[col];
1179
+ if (v === null || v === void 0) continue;
1180
+ row[col] = t.fromDb(v);
1181
+ }
1182
+ return row;
1183
+ }
1065
1184
  function asTableRow(table, row) {
1066
- return reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
1185
+ const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
1186
+ return applyFromDb(revived, transformsOf(currentSchema, table));
1067
1187
  }
1068
1188
  function asTableRows(table, rows) {
1069
1189
  const vectorCols = vectorColumnsOf(currentSchema, table);
1070
- return rows.map((row) => reviveVectors(asWireRow(row), vectorCols));
1190
+ const transforms = transformsOf(currentSchema, table);
1191
+ return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
1071
1192
  }
1072
1193
  function asBindParams(table, cols, data) {
1073
1194
  const vectorCols = vectorColumnsOf(currentSchema, table);
1195
+ const transforms = transformsOf(currentSchema, table);
1074
1196
  return cols.map((c) => {
1075
1197
  const v = data[c];
1076
- return Array.isArray(v) && vectorCols.has(c) ? toVectorLiteral(v) : v;
1198
+ if (Array.isArray(v) && vectorCols.has(c)) return toVectorLiteral(v);
1199
+ const t = transforms.get(c);
1200
+ if (t?.toDb !== void 0 && v !== null && v !== void 0) return t.toDb(v);
1201
+ return v;
1077
1202
  });
1078
1203
  }
1079
1204
  var SELECTIVITY_EXACT_THRESHOLD = 1e4;
@@ -1267,6 +1392,20 @@ function limitClause(limit) {
1267
1392
  }
1268
1393
  return ` LIMIT ${limit}`;
1269
1394
  }
1395
+ function offsetClause(offset, limit) {
1396
+ if (offset === void 0) return "";
1397
+ if (!Number.isInteger(offset) || offset < 0) {
1398
+ throw new Error(
1399
+ `findMany: offset bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(offset)})`
1400
+ );
1401
+ }
1402
+ if (limit === void 0) {
1403
+ throw new Error(
1404
+ "findMany: offset yaln\u0131z limit ile birlikte verilir \u2014 limitsiz offset bir sayfa de\u011Fil, s\u0131n\u0131rs\u0131z bir kuyru\u011Fun kayd\u0131r\u0131lm\u0131\u015F\u0131d\u0131r"
1405
+ );
1406
+ }
1407
+ return ` OFFSET ${offset}`;
1408
+ }
1270
1409
  function compileWhere(table, colSet, where, add, caller = "search") {
1271
1410
  const parts = [];
1272
1411
  for (const [col, cond] of Object.entries(where)) {
@@ -1295,6 +1434,13 @@ function compileWhere(table, colSet, where, add, caller = "search") {
1295
1434
  }
1296
1435
  return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
1297
1436
  }
1437
+ function onConflictTail(cols, conflict, action) {
1438
+ const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
1439
+ if (action === "ignore") return `${target} DO NOTHING`;
1440
+ const conflictSet = new Set(conflict);
1441
+ const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
1442
+ return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
1443
+ }
1298
1444
  var cachedVectorSchema = null;
1299
1445
  var cachedTrgmSchema = null;
1300
1446
  async function trgmSchemaOf(live) {
@@ -1404,6 +1550,39 @@ function createOps(tx) {
1404
1550
  }
1405
1551
  const cols = Object.keys(data);
1406
1552
  if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
1553
+ if (searchConfigFor(table)?.validity) {
1554
+ const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
1555
+ const insertSql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING *`;
1556
+ const live = await at();
1557
+ const bindData = asBindParams(table, cols, data);
1558
+ const first = await live.unsafe(insertSql, bindData);
1559
+ if (first[0]) return asTableRow(table, first[0]);
1560
+ const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
1561
+ const condBind = conflict.map((c) => data[c]);
1562
+ const cur = await live.unsafe(
1563
+ `SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
1564
+ condBind
1565
+ );
1566
+ const old = cur[0];
1567
+ if (old === void 0) {
1568
+ throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 ge\xE7erli sat\u0131r bulunamad\u0131, iste\u011Fi yineleyin (D-020)`);
1569
+ }
1570
+ const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
1571
+ await live.unsafe(
1572
+ `UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
1573
+ [old[pk]]
1574
+ );
1575
+ const second = await live.unsafe(insertSql, bindData);
1576
+ if (!second[0]) {
1577
+ throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 yeni sat\u0131r a\xE7\u0131lamad\u0131, iste\u011Fi yineleyin (D-020)`);
1578
+ }
1579
+ const fresh = second[0];
1580
+ await live.unsafe(
1581
+ `UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
1582
+ [old[pk], fresh[pk]]
1583
+ );
1584
+ return asTableRow(table, second[0]);
1585
+ }
1407
1586
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1408
1587
  const conflictSet = new Set(conflict);
1409
1588
  const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
@@ -1429,6 +1608,86 @@ function createOps(tx) {
1429
1608
  async delete(table, id) {
1430
1609
  await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
1431
1610
  },
1611
+ /**
1612
+ * Update every row the filter matches, in ONE statement.
1613
+ *
1614
+ * The capability was already here and only reachable from inside a
1615
+ * transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
1616
+ * only door was `update(id, …)`, so "mark every unapproved row in this
1617
+ * household" was an N+1 loop or hand-written SQL — and hand-written SQL is
1618
+ * where the typed surface and RLS both stop helping.
1619
+ *
1620
+ * The filter language is `findMany`'s, compiled by the same `compileWhere`:
1621
+ * one filter language, or the two spellings drift.
1622
+ *
1623
+ * AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
1624
+ * and the shape that produces it by accident — a filter object built from
1625
+ * request input that happened to come back empty — is exactly the shape that
1626
+ * should not silently succeed. Callers who mean every row say so with a
1627
+ * predicate that is true for every row.
1628
+ */
1629
+ async updateMany(table, where, set) {
1630
+ const cols = Object.keys(set);
1631
+ if (cols.length === 0) return [];
1632
+ if (Object.keys(where).length === 0) {
1633
+ throw new Error(
1634
+ `updateMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 g\xFCncellerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
1635
+ );
1636
+ }
1637
+ const params = [];
1638
+ const add = (v) => {
1639
+ params.push(v);
1640
+ return `$${params.length}`;
1641
+ };
1642
+ const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
1643
+ const known = schemaColumns(table);
1644
+ const whereSql = compileWhere(table, known, where, add, "updateMany");
1645
+ const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
1646
+ return asTableRows(table, await (await at()).unsafe(sql, params));
1647
+ },
1648
+ /**
1649
+ * Delete every row the filter matches, in ONE statement; resolves to how
1650
+ * many went. Same filter language, same empty-filter refusal as
1651
+ * {@link updateMany} — and here the accident is worse.
1652
+ */
1653
+ async deleteMany(table, where) {
1654
+ if (Object.keys(where).length === 0) {
1655
+ throw new Error(
1656
+ `deleteMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 silerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
1657
+ );
1658
+ }
1659
+ const params = [];
1660
+ const add = (v) => {
1661
+ params.push(v);
1662
+ return `$${params.length}`;
1663
+ };
1664
+ const known = schemaColumns(table);
1665
+ const whereSql = compileWhere(table, known, where, add, "deleteMany");
1666
+ const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
1667
+ const rows = await (await at()).unsafe(sql, params);
1668
+ return rows.length;
1669
+ },
1670
+ /**
1671
+ * How many rows match — the half of pagination `limit`/`offset` cannot
1672
+ * supply. Without it a page count is either a guess or "fetch everything and
1673
+ * read .length", and the second one is the scale risk this surface exists to
1674
+ * remove.
1675
+ *
1676
+ * An empty filter is legitimate HERE: counting a whole table is a read, and
1677
+ * reads do not destroy anything.
1678
+ */
1679
+ async count(table, where = {}) {
1680
+ const params = [];
1681
+ const add = (v) => {
1682
+ params.push(v);
1683
+ return `$${params.length}`;
1684
+ };
1685
+ const known = schemaColumns(table);
1686
+ const whereSql = compileWhere(table, known, where, add, "count");
1687
+ const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
1688
+ const rows = await (await at()).unsafe(sql, params);
1689
+ return Number(rows[0]?.n ?? 0);
1690
+ },
1432
1691
  async findById(table, id) {
1433
1692
  const rows = await (await at()).unsafe(
1434
1693
  `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
@@ -1446,7 +1705,8 @@ function createOps(tx) {
1446
1705
  const whereSql = compileWhere(table, known, query, add, "findMany");
1447
1706
  const order = orderClause(table, known, opts.orderBy);
1448
1707
  const limit = limitClause(opts.limit);
1449
- const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}`;
1708
+ const offset = offsetClause(opts.offset, opts.limit);
1709
+ const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
1450
1710
  return asTableRows(table, await (await at()).unsafe(sql, params));
1451
1711
  },
1452
1712
  /**
@@ -1529,7 +1789,9 @@ function createOps(tx) {
1529
1789
  bindC.push(v);
1530
1790
  return `$${bindC.length}`;
1531
1791
  };
1532
- const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1792
+ const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
1793
+ const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1794
+ const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
1533
1795
  const liveC = await at();
1534
1796
  const K2 = 50;
1535
1797
  const cpool = Math.max(limit * 9, 90);
@@ -1538,8 +1800,19 @@ function createOps(tx) {
1538
1800
  if (qvC !== null) {
1539
1801
  const sch = await vectorSchemaWithGuc(liveC);
1540
1802
  const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
1803
+ let exactOrderC = "";
1804
+ if (userWhereC !== "") {
1805
+ const probeRows = await liveC.unsafe(
1806
+ `SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
1807
+ bindC.slice()
1808
+ );
1809
+ const n = probeRows?.[0]?.n;
1810
+ if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
1811
+ exactOrderC = " + 0.0";
1812
+ }
1813
+ }
1541
1814
  const vp = addC(toVectorLiteral(qvC));
1542
- semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)) AS r FROM ${quoteIdent(ck.table)} c WHERE c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
1815
+ semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)${exactOrderC}) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
1543
1816
  }
1544
1817
  let qpC = "";
1545
1818
  let qpCIdx = -1;
@@ -1547,7 +1820,7 @@ function createOps(tx) {
1547
1820
  const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
1548
1821
  qpC = addC(qTextC);
1549
1822
  qpCIdx = bindC.length - 1;
1550
- kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c WHERE c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
1823
+ kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
1551
1824
  }
1552
1825
  const msC = params.minScore === void 0 ? "" : addC(params.minScore);
1553
1826
  const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
@@ -1562,13 +1835,13 @@ function createOps(tx) {
1562
1835
  } else {
1563
1836
  fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
1564
1837
  }
1565
- const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid WHERE true${whereC}`;
1838
+ const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid`;
1566
1839
  return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
1567
1840
  };
1568
1841
  let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
1569
1842
  if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
1570
1843
  const tsch = await trgmSchemaOf(liveC);
1571
- const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c WHERE ${qpC} OPERATOR(${quoteIdent(tsch)}.<%) coalesce(c.content,'') ORDER BY r LIMIT ${cpool}`;
1844
+ const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) > 0.3 ORDER BY r LIMIT ${cpool}`;
1572
1845
  const retryBindC = bindC.slice();
1573
1846
  if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
1574
1847
  rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
@@ -1664,7 +1937,7 @@ function createOps(tx) {
1664
1937
  if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
1665
1938
  const tsch = await trgmSchemaOf(live);
1666
1939
  const expr = ftsExprOf(cfg.ftsCols);
1667
- const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${qpRef} OPERATOR(${quoteIdent(tsch)}.<%) ${expr}${whereSql} ORDER BY r LIMIT ${pool}`;
1940
+ const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) > 0.3${whereSql} ORDER BY r LIMIT ${pool}`;
1668
1941
  const retryBind = bind.slice();
1669
1942
  if (qpIdx >= 0) retryBind[qpIdx] = params.query;
1670
1943
  rows = await live.unsafe(assemble(trgmKw, false), retryBind);
@@ -1687,6 +1960,25 @@ function createOps(tx) {
1687
1960
  * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
1688
1961
  * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
1689
1962
  */
1963
+ /** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
1964
+ * `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
1965
+ * yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
1966
+ * search'teki alan geriye-uyum için DURUR. where + validity default'u
1967
+ * sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
1968
+ async facets(table, params) {
1969
+ const cfg = searchConfigFor(table);
1970
+ if (!cfg) {
1971
+ throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
1972
+ }
1973
+ for (const col of params.facets) {
1974
+ if (!cfg.colSet.has(col)) {
1975
+ throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
1976
+ }
1977
+ }
1978
+ const live = await at();
1979
+ const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
1980
+ return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
1981
+ },
1690
1982
  async similar(table, id, opts = {}) {
1691
1983
  const cfg = searchConfigFor(table);
1692
1984
  if (!cfg) {
@@ -1918,17 +2210,35 @@ function withTables(ops, schema = currentSchema) {
1918
2210
  }
1919
2211
  var SERVICE_LOCK_TIMEOUT = "5s";
1920
2212
  function isAbortedTransaction(e) {
1921
- const code = e?.code;
1922
2213
  const message = String(e?.message ?? "");
1923
- return code === "25P02" || /current transaction is aborted/i.test(message);
2214
+ return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
2215
+ }
2216
+ function sqlstateOf(e) {
2217
+ const err = e;
2218
+ const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
2219
+ if (isState(err?.code)) return err.code;
2220
+ if (isState(err?.errno)) return err.errno;
2221
+ if (typeof err?.errno === "number") {
2222
+ const asText = String(err.errno);
2223
+ if (isState(asText)) return asText;
2224
+ }
2225
+ return void 0;
1924
2226
  }
1925
2227
  function isForeignKeyViolation(e) {
1926
- return e?.code === "23503";
2228
+ return sqlstateOf(e) === "23503";
2229
+ }
2230
+ function isUniqueViolation(e) {
2231
+ return sqlstateOf(e) === "23505";
2232
+ }
2233
+ function constraintOf(e) {
2234
+ const named = e?.constraint;
2235
+ if (typeof named === "string" && named.length > 0) return named;
2236
+ const message = String(e?.message ?? "");
2237
+ return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
1927
2238
  }
1928
2239
  function isLockTimeout(e) {
1929
- const code = e?.code;
1930
2240
  const message = String(e?.message ?? "");
1931
- return code === "55P03" || /lock timeout/i.test(message);
2241
+ return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
1932
2242
  }
1933
2243
  function diagnosingDriver(sql, surface = "service") {
1934
2244
  const original = (e) => String(e?.message ?? e);
@@ -1938,6 +2248,9 @@ function diagnosingDriver(sql, surface = "service") {
1938
2248
  `This request cannot write any more: an earlier write in it failed, and a request runs in ONE Postgres transaction, so every statement after the failure is refused. Nothing here is retryable in place. Wrap a write you expect to fail in \`Database.attempt(async (tx) => \u2026)\` \u2014 it takes a SAVEPOINT, so only that write rolls back \u2014 or use \`Database.tables.<t>.upsert(data, { onConflict: [...] })\` when the failure you were bracing for is a duplicate row. (${original(e)})`
1939
2249
  );
1940
2250
  }
2251
+ if (isUniqueViolation(e)) {
2252
+ return new UniqueViolation(constraintOf(e));
2253
+ }
1941
2254
  if (surface === "service" && isForeignKeyViolation(e)) {
1942
2255
  return new Error(
1943
2256
  `Database.asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`
@@ -1989,9 +2302,9 @@ function createRequestDatabase(sql, identity) {
1989
2302
  await tx.commit();
1990
2303
  await serviceTx?.commit();
1991
2304
  },
1992
- async rollback(reason) {
1993
- await tx.rollback(reason);
1994
- await serviceTx?.rollback(reason);
2305
+ async rollback(reason2) {
2306
+ await tx.rollback(reason2);
2307
+ await serviceTx?.rollback(reason2);
1995
2308
  }
1996
2309
  };
1997
2310
  }
@@ -2066,10 +2379,7 @@ async function runPlanOp(sp, op, results) {
2066
2379
  const rendered = cols.map(
2067
2380
  (c) => renderValue(op.values[c], c, args, results, opVectorCols)
2068
2381
  );
2069
- const conflictSet = new Set(conflict);
2070
- const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
2071
- const action = sets.length ? `DO UPDATE SET ${sets.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
2072
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
2382
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
2073
2383
  break;
2074
2384
  }
2075
2385
  case "insertMany": {
@@ -2079,7 +2389,9 @@ async function runPlanOp(sp, op, results) {
2079
2389
  const tuples = rows.map(
2080
2390
  (r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
2081
2391
  );
2082
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
2392
+ const conflictCols = op.onConflict ?? [];
2393
+ const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
2394
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
2083
2395
  break;
2084
2396
  }
2085
2397
  case "update": {
@@ -2154,6 +2466,26 @@ function getRoutes(ctor) {
2154
2466
  }));
2155
2467
  }
2156
2468
 
2469
+ // src/decorators/controller.ts
2470
+ var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
2471
+ function appAuthSlot() {
2472
+ return globalThis;
2473
+ }
2474
+ function getDefaultAuth() {
2475
+ return appAuthSlot()[APP_DEFAULT_AUTH];
2476
+ }
2477
+ function resolveEffectiveAuth(routeAuth, controllerAuth) {
2478
+ return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
2479
+ }
2480
+ function assertZeroArgConstructor(Ctrl, kind) {
2481
+ const arity = Ctrl.length ?? 0;
2482
+ if (arity === 0) return;
2483
+ const name = Ctrl.name ?? "<anonymous>";
2484
+ throw new Error(
2485
+ `${kind} ${name} declares a constructor with ${arity} parameter(s). A ${kind} is constructed by the runtime with a zero-argument constructor \u2014 there is no injector to supply them, so every parameter would arrive as undefined. Hold the dependency as a module-level singleton the ${kind} imports (\`const repo = makeRepo()\` beside the class), and construct the service directly in tests (e.g. \`new TodoService(fakeDatabase().db)\`).`
2486
+ );
2487
+ }
2488
+
2157
2489
  // src/engine/router.ts
2158
2490
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
2159
2491
  function toSegments(path) {
@@ -2172,6 +2504,7 @@ function buildRouteTable(controllers) {
2172
2504
  `controller ${name} collected zero routes. Either it declares no @Get/@Post/\u2026 , or its decorator metadata was erased at build time \u2014 check that the bundle was compiled with experimentalDecorators enabled.`
2173
2505
  );
2174
2506
  }
2507
+ assertZeroArgConstructor(Ctrl, "controller");
2175
2508
  const instance = new ctor();
2176
2509
  for (const r of routes) {
2177
2510
  const full = `${basePath}${r.subpath ?? ""}` || "/";
@@ -2181,7 +2514,10 @@ function buildRouteTable(controllers) {
2181
2514
  meta: r,
2182
2515
  instance,
2183
2516
  id: `${r.method} ${full}`,
2184
- controllerAuth: meta?.defaultAuth
2517
+ // The route's OWN spec is deliberately not folded in here: this field
2518
+ // is what applies when the route is silent, and `effectiveAuth` puts
2519
+ // the route level back on top per request.
2520
+ controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
2185
2521
  });
2186
2522
  }
2187
2523
  }
@@ -2810,18 +3146,29 @@ async function createApp(opts) {
2810
3146
  return envelope("internal_error", "The request could not be completed", 500, requestId);
2811
3147
  }
2812
3148
  }
3149
+ let runShutdownHooks;
3150
+ try {
3151
+ runShutdownHooks = await __runStartHooks();
3152
+ } catch (err) {
3153
+ await closeDriver(sql);
3154
+ throw err;
3155
+ }
2813
3156
  return {
2814
3157
  handle,
2815
3158
  routes,
2816
3159
  config,
2817
3160
  runInServiceScope,
2818
3161
  async shutdown() {
2819
- const closable = sql;
2820
- await closable.close?.();
2821
- await closable.end?.();
3162
+ await runShutdownHooks();
3163
+ await closeDriver(sql);
2822
3164
  }
2823
3165
  };
2824
3166
  }
3167
+ async function closeDriver(sql) {
3168
+ const closable = sql;
3169
+ await closable.close?.();
3170
+ await closable.end?.();
3171
+ }
2825
3172
  // Annotate the CommonJS export names for ESM import in node:
2826
3173
  0 && (module.exports = {
2827
3174
  AuthVerifier,