@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
@@ -231,7 +231,7 @@ var TxPlanBuilder = class {
231
231
  `${name}.upsert()`
232
232
  );
233
233
  },
234
- insertMany: (rows) => {
234
+ insertMany: (rows, opts) => {
235
235
  if (rows.length === 0) {
236
236
  return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
237
237
  }
@@ -242,7 +242,23 @@ var TxPlanBuilder = class {
242
242
  }
243
243
  const encoded = rows.map((row) => encodeMap(row, false));
244
244
  assertUniformRows(encoded, name);
245
- return this.push({ op: "insertMany", table: name, rows: encoded }, `${name}.insertMany()`);
245
+ if (opts !== void 0 && opts.onConflict.length === 0) {
246
+ throw new TxPlanError(
247
+ `${name}.insertMany() was given a conflict action with no onConflict columns. Postgres matches a collision on columns, so name them.`
248
+ );
249
+ }
250
+ return this.push(
251
+ {
252
+ op: "insertMany",
253
+ table: name,
254
+ rows: encoded,
255
+ // Omitted entirely when no options were given, so the op a plain
256
+ // insertMany produces is byte-identical to the one it produced
257
+ // before this option existed.
258
+ ...opts !== void 0 ? { onConflict: opts.onConflict, action: opts.action ?? "ignore" } : {}
259
+ },
260
+ `${name}.insertMany()`
261
+ );
246
262
  },
247
263
  updateWhere: (where, set) => {
248
264
  const encodedWhere = encodeMap(where, false);
@@ -416,6 +432,42 @@ function __getRuntime() {
416
432
  }
417
433
  return runtime;
418
434
  }
435
+ var LIFECYCLE = /* @__PURE__ */ Symbol.for("palbase.backend.lifecycleHooks");
436
+ function declaredLifecycle() {
437
+ const g = globalThis;
438
+ return g[LIFECYCLE] ??= { start: [], shutdown: [] };
439
+ }
440
+ function reason(err) {
441
+ return err instanceof Error ? err.message : String(err);
442
+ }
443
+ async function drain(hooks) {
444
+ for (const h of [...hooks].reverse()) {
445
+ try {
446
+ await h.run();
447
+ } catch (err) {
448
+ console.error(`[palbase] shutdown hook "${h.name}" failed: ${reason(err)}`, err);
449
+ }
450
+ }
451
+ }
452
+ async function __runStartHooks() {
453
+ const slot = declaredLifecycle();
454
+ const start = slot.start.splice(0);
455
+ const shutdown = slot.shutdown.splice(0);
456
+ for (const h of start) {
457
+ try {
458
+ await h.run();
459
+ } catch (err) {
460
+ await drain(shutdown);
461
+ throw new Error(`[palbase] start hook "${h.name}" failed: ${reason(err)}`, { cause: err });
462
+ }
463
+ }
464
+ let drained = false;
465
+ return async () => {
466
+ if (drained) return;
467
+ drained = true;
468
+ await drain(shutdown);
469
+ };
470
+ }
419
471
  function makeServiceProxy(key) {
420
472
  const handler = {
421
473
  get(_target, prop, receiver) {
@@ -443,6 +495,7 @@ function makeTablesAccessor(ops) {
443
495
  search: (params) => ops().search(name, params),
444
496
  similar: (id, params) => ops().similar(name, id, params),
445
497
  recommend: (params) => ops().recommend(name, params),
498
+ facets: (params) => ops().facets(name, params),
446
499
  supersede: (id, row) => ops().supersede(name, id, row)
447
500
  };
448
501
  }
@@ -461,9 +514,13 @@ function makeTypedSurface(raw) {
461
514
  findById: (table, id) => raw.findById(table, id),
462
515
  findMany: (table, query, opts) => raw.findMany(table, query, opts),
463
516
  upsert: (table, data, opts) => raw.upsert(table, data, opts),
517
+ updateMany: (table, where, set) => raw.updateMany(table, where, set),
518
+ deleteMany: (table, where) => raw.deleteMany(table, where),
519
+ count: (table, where) => raw.count(table, where),
464
520
  search: (table, params) => raw.search(table, params),
465
521
  similar: (table, id, params) => reco.similar(table, id, params),
466
522
  recommend: (table, params) => reco.recommend(table, params),
523
+ facets: (table, params) => reco.facets(table, params),
467
524
  supersede: (table, id, row) => raw.supersede(table, id, row)
468
525
  };
469
526
  return Object.assign(ops, {
@@ -621,6 +678,44 @@ var HttpError = class extends Error {
621
678
  return result;
622
679
  }
623
680
  };
681
+ var NamedHttpError = class extends HttpError {
682
+ constructor(status, defaultCode, name, message, code, data) {
683
+ super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
684
+ this.name = name;
685
+ }
686
+ };
687
+ function defaultMessage(name) {
688
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
689
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
690
+ }
691
+ var Conflict = class extends NamedHttpError {
692
+ constructor(message, code, data) {
693
+ super(409, "conflict", "Conflict", message, code, data);
694
+ }
695
+ };
696
+ var UniqueViolation = class extends Conflict {
697
+ /**
698
+ * Whether `e` is a unique violation — REGARDLESS of which copy of this SDK
699
+ * constructed it.
700
+ *
701
+ * Use this instead of `instanceof`. Measured on a live stack: a controller
702
+ * bundle INLINES its own copy of `@palbase/backend`, and the engine that
703
+ * raises this error is the runtime's copy. Two copies, two class identities,
704
+ * and `e instanceof UniqueViolation` is false in the one place a caller
705
+ * writes it — a check that reads as correct and silently never matches.
706
+ */
707
+ static is(e) {
708
+ return typeof e === "object" && e !== null && e.name === "UniqueViolation" && typeof e.constraint === "string";
709
+ }
710
+ /** The unique constraint the statement violated, as Postgres named it.
711
+ * `""` when the driver did not say which — see `engine/db.ts`. */
712
+ constraint;
713
+ constructor(constraint, message, code, data) {
714
+ super(message ?? "Unique constraint violated", code ?? "unique_violation", data);
715
+ this.name = "UniqueViolation";
716
+ this.constraint = constraint;
717
+ }
718
+ };
624
719
 
625
720
  // src/engine/config.ts
626
721
  var BootRefused = class extends Error {
@@ -983,9 +1078,9 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
983
1078
  release();
984
1079
  await settled;
985
1080
  },
986
- async rollback(reason) {
1081
+ async rollback(reason2) {
987
1082
  if (!opening) return;
988
- fail(reason);
1083
+ fail(reason2);
989
1084
  await settled?.catch(() => void 0);
990
1085
  }
991
1086
  };
@@ -1028,18 +1123,48 @@ function reviveVectors(row, vectorCols) {
1028
1123
  }
1029
1124
  return row;
1030
1125
  }
1126
+ function transformsOf(schema, table) {
1127
+ const out = /* @__PURE__ */ new Map();
1128
+ for (const [key, def] of Object.entries(schema.tables ?? {})) {
1129
+ if ((def.name ?? key) !== table) continue;
1130
+ for (const [col, c] of Object.entries(def.columns ?? {})) {
1131
+ const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
1132
+ const t = d?.transform;
1133
+ if (t !== null && typeof t === "object") {
1134
+ out.set(col, t);
1135
+ }
1136
+ }
1137
+ }
1138
+ return out;
1139
+ }
1140
+ function applyFromDb(row, transforms) {
1141
+ if (transforms.size === 0) return row;
1142
+ for (const [col, t] of transforms) {
1143
+ if (t.fromDb === void 0) continue;
1144
+ const v = row[col];
1145
+ if (v === null || v === void 0) continue;
1146
+ row[col] = t.fromDb(v);
1147
+ }
1148
+ return row;
1149
+ }
1031
1150
  function asTableRow(table, row) {
1032
- return reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
1151
+ const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
1152
+ return applyFromDb(revived, transformsOf(currentSchema, table));
1033
1153
  }
1034
1154
  function asTableRows(table, rows) {
1035
1155
  const vectorCols = vectorColumnsOf(currentSchema, table);
1036
- return rows.map((row) => reviveVectors(asWireRow(row), vectorCols));
1156
+ const transforms = transformsOf(currentSchema, table);
1157
+ return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
1037
1158
  }
1038
1159
  function asBindParams(table, cols, data) {
1039
1160
  const vectorCols = vectorColumnsOf(currentSchema, table);
1161
+ const transforms = transformsOf(currentSchema, table);
1040
1162
  return cols.map((c) => {
1041
1163
  const v = data[c];
1042
- return Array.isArray(v) && vectorCols.has(c) ? toVectorLiteral(v) : v;
1164
+ if (Array.isArray(v) && vectorCols.has(c)) return toVectorLiteral(v);
1165
+ const t = transforms.get(c);
1166
+ if (t?.toDb !== void 0 && v !== null && v !== void 0) return t.toDb(v);
1167
+ return v;
1043
1168
  });
1044
1169
  }
1045
1170
  var SELECTIVITY_EXACT_THRESHOLD = 1e4;
@@ -1233,6 +1358,20 @@ function limitClause(limit) {
1233
1358
  }
1234
1359
  return ` LIMIT ${limit}`;
1235
1360
  }
1361
+ function offsetClause(offset, limit) {
1362
+ if (offset === void 0) return "";
1363
+ if (!Number.isInteger(offset) || offset < 0) {
1364
+ throw new Error(
1365
+ `findMany: offset bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(offset)})`
1366
+ );
1367
+ }
1368
+ if (limit === void 0) {
1369
+ throw new Error(
1370
+ "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"
1371
+ );
1372
+ }
1373
+ return ` OFFSET ${offset}`;
1374
+ }
1236
1375
  function compileWhere(table, colSet, where, add, caller = "search") {
1237
1376
  const parts = [];
1238
1377
  for (const [col, cond] of Object.entries(where)) {
@@ -1261,6 +1400,13 @@ function compileWhere(table, colSet, where, add, caller = "search") {
1261
1400
  }
1262
1401
  return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
1263
1402
  }
1403
+ function onConflictTail(cols, conflict, action) {
1404
+ const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
1405
+ if (action === "ignore") return `${target} DO NOTHING`;
1406
+ const conflictSet = new Set(conflict);
1407
+ const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
1408
+ return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
1409
+ }
1264
1410
  var cachedVectorSchema = null;
1265
1411
  var cachedTrgmSchema = null;
1266
1412
  async function trgmSchemaOf(live) {
@@ -1370,6 +1516,39 @@ function createOps(tx) {
1370
1516
  }
1371
1517
  const cols = Object.keys(data);
1372
1518
  if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
1519
+ if (searchConfigFor(table)?.validity) {
1520
+ const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
1521
+ 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 *`;
1522
+ const live = await at();
1523
+ const bindData = asBindParams(table, cols, data);
1524
+ const first = await live.unsafe(insertSql, bindData);
1525
+ if (first[0]) return asTableRow(table, first[0]);
1526
+ const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
1527
+ const condBind = conflict.map((c) => data[c]);
1528
+ const cur = await live.unsafe(
1529
+ `SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
1530
+ condBind
1531
+ );
1532
+ const old = cur[0];
1533
+ if (old === void 0) {
1534
+ 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)`);
1535
+ }
1536
+ const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
1537
+ await live.unsafe(
1538
+ `UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
1539
+ [old[pk]]
1540
+ );
1541
+ const second = await live.unsafe(insertSql, bindData);
1542
+ if (!second[0]) {
1543
+ 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)`);
1544
+ }
1545
+ const fresh = second[0];
1546
+ await live.unsafe(
1547
+ `UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
1548
+ [old[pk], fresh[pk]]
1549
+ );
1550
+ return asTableRow(table, second[0]);
1551
+ }
1373
1552
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1374
1553
  const conflictSet = new Set(conflict);
1375
1554
  const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
@@ -1395,6 +1574,86 @@ function createOps(tx) {
1395
1574
  async delete(table, id) {
1396
1575
  await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
1397
1576
  },
1577
+ /**
1578
+ * Update every row the filter matches, in ONE statement.
1579
+ *
1580
+ * The capability was already here and only reachable from inside a
1581
+ * transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
1582
+ * only door was `update(id, …)`, so "mark every unapproved row in this
1583
+ * household" was an N+1 loop or hand-written SQL — and hand-written SQL is
1584
+ * where the typed surface and RLS both stop helping.
1585
+ *
1586
+ * The filter language is `findMany`'s, compiled by the same `compileWhere`:
1587
+ * one filter language, or the two spellings drift.
1588
+ *
1589
+ * AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
1590
+ * and the shape that produces it by accident — a filter object built from
1591
+ * request input that happened to come back empty — is exactly the shape that
1592
+ * should not silently succeed. Callers who mean every row say so with a
1593
+ * predicate that is true for every row.
1594
+ */
1595
+ async updateMany(table, where, set) {
1596
+ const cols = Object.keys(set);
1597
+ if (cols.length === 0) return [];
1598
+ if (Object.keys(where).length === 0) {
1599
+ throw new Error(
1600
+ `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`
1601
+ );
1602
+ }
1603
+ const params = [];
1604
+ const add = (v) => {
1605
+ params.push(v);
1606
+ return `$${params.length}`;
1607
+ };
1608
+ const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
1609
+ const known = schemaColumns(table);
1610
+ const whereSql = compileWhere(table, known, where, add, "updateMany");
1611
+ const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
1612
+ return asTableRows(table, await (await at()).unsafe(sql, params));
1613
+ },
1614
+ /**
1615
+ * Delete every row the filter matches, in ONE statement; resolves to how
1616
+ * many went. Same filter language, same empty-filter refusal as
1617
+ * {@link updateMany} — and here the accident is worse.
1618
+ */
1619
+ async deleteMany(table, where) {
1620
+ if (Object.keys(where).length === 0) {
1621
+ throw new Error(
1622
+ `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`
1623
+ );
1624
+ }
1625
+ const params = [];
1626
+ const add = (v) => {
1627
+ params.push(v);
1628
+ return `$${params.length}`;
1629
+ };
1630
+ const known = schemaColumns(table);
1631
+ const whereSql = compileWhere(table, known, where, add, "deleteMany");
1632
+ const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
1633
+ const rows = await (await at()).unsafe(sql, params);
1634
+ return rows.length;
1635
+ },
1636
+ /**
1637
+ * How many rows match — the half of pagination `limit`/`offset` cannot
1638
+ * supply. Without it a page count is either a guess or "fetch everything and
1639
+ * read .length", and the second one is the scale risk this surface exists to
1640
+ * remove.
1641
+ *
1642
+ * An empty filter is legitimate HERE: counting a whole table is a read, and
1643
+ * reads do not destroy anything.
1644
+ */
1645
+ async count(table, where = {}) {
1646
+ const params = [];
1647
+ const add = (v) => {
1648
+ params.push(v);
1649
+ return `$${params.length}`;
1650
+ };
1651
+ const known = schemaColumns(table);
1652
+ const whereSql = compileWhere(table, known, where, add, "count");
1653
+ const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
1654
+ const rows = await (await at()).unsafe(sql, params);
1655
+ return Number(rows[0]?.n ?? 0);
1656
+ },
1398
1657
  async findById(table, id) {
1399
1658
  const rows = await (await at()).unsafe(
1400
1659
  `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
@@ -1412,7 +1671,8 @@ function createOps(tx) {
1412
1671
  const whereSql = compileWhere(table, known, query, add, "findMany");
1413
1672
  const order = orderClause(table, known, opts.orderBy);
1414
1673
  const limit = limitClause(opts.limit);
1415
- const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}`;
1674
+ const offset = offsetClause(opts.offset, opts.limit);
1675
+ const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
1416
1676
  return asTableRows(table, await (await at()).unsafe(sql, params));
1417
1677
  },
1418
1678
  /**
@@ -1495,7 +1755,9 @@ function createOps(tx) {
1495
1755
  bindC.push(v);
1496
1756
  return `$${bindC.length}`;
1497
1757
  };
1498
- const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1758
+ const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
1759
+ const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1760
+ const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
1499
1761
  const liveC = await at();
1500
1762
  const K2 = 50;
1501
1763
  const cpool = Math.max(limit * 9, 90);
@@ -1504,8 +1766,19 @@ function createOps(tx) {
1504
1766
  if (qvC !== null) {
1505
1767
  const sch = await vectorSchemaWithGuc(liveC);
1506
1768
  const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
1769
+ let exactOrderC = "";
1770
+ if (userWhereC !== "") {
1771
+ const probeRows = await liveC.unsafe(
1772
+ `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`,
1773
+ bindC.slice()
1774
+ );
1775
+ const n = probeRows?.[0]?.n;
1776
+ if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
1777
+ exactOrderC = " + 0.0";
1778
+ }
1779
+ }
1507
1780
  const vp = addC(toVectorLiteral(qvC));
1508
- 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}`;
1781
+ 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}`;
1509
1782
  }
1510
1783
  let qpC = "";
1511
1784
  let qpCIdx = -1;
@@ -1513,7 +1786,7 @@ function createOps(tx) {
1513
1786
  const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
1514
1787
  qpC = addC(qTextC);
1515
1788
  qpCIdx = bindC.length - 1;
1516
- 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}`;
1789
+ 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}`;
1517
1790
  }
1518
1791
  const msC = params.minScore === void 0 ? "" : addC(params.minScore);
1519
1792
  const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
@@ -1528,13 +1801,13 @@ function createOps(tx) {
1528
1801
  } else {
1529
1802
  fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
1530
1803
  }
1531
- 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}`;
1804
+ 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`;
1532
1805
  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}`;
1533
1806
  };
1534
1807
  let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
1535
1808
  if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
1536
1809
  const tsch = await trgmSchemaOf(liveC);
1537
- 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}`;
1810
+ 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}`;
1538
1811
  const retryBindC = bindC.slice();
1539
1812
  if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
1540
1813
  rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
@@ -1630,7 +1903,7 @@ function createOps(tx) {
1630
1903
  if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
1631
1904
  const tsch = await trgmSchemaOf(live);
1632
1905
  const expr = ftsExprOf(cfg.ftsCols);
1633
- 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}`;
1906
+ 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}`;
1634
1907
  const retryBind = bind.slice();
1635
1908
  if (qpIdx >= 0) retryBind[qpIdx] = params.query;
1636
1909
  rows = await live.unsafe(assemble(trgmKw, false), retryBind);
@@ -1653,6 +1926,25 @@ function createOps(tx) {
1653
1926
  * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
1654
1927
  * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
1655
1928
  */
1929
+ /** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
1930
+ * `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
1931
+ * yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
1932
+ * search'teki alan geriye-uyum için DURUR. where + validity default'u
1933
+ * sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
1934
+ async facets(table, params) {
1935
+ const cfg = searchConfigFor(table);
1936
+ if (!cfg) {
1937
+ throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
1938
+ }
1939
+ for (const col of params.facets) {
1940
+ if (!cfg.colSet.has(col)) {
1941
+ throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
1942
+ }
1943
+ }
1944
+ const live = await at();
1945
+ const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
1946
+ return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
1947
+ },
1656
1948
  async similar(table, id, opts = {}) {
1657
1949
  const cfg = searchConfigFor(table);
1658
1950
  if (!cfg) {
@@ -1884,17 +2176,35 @@ function withTables(ops, schema = currentSchema) {
1884
2176
  }
1885
2177
  var SERVICE_LOCK_TIMEOUT = "5s";
1886
2178
  function isAbortedTransaction(e) {
1887
- const code = e?.code;
1888
2179
  const message = String(e?.message ?? "");
1889
- return code === "25P02" || /current transaction is aborted/i.test(message);
2180
+ return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
2181
+ }
2182
+ function sqlstateOf(e) {
2183
+ const err = e;
2184
+ const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
2185
+ if (isState(err?.code)) return err.code;
2186
+ if (isState(err?.errno)) return err.errno;
2187
+ if (typeof err?.errno === "number") {
2188
+ const asText = String(err.errno);
2189
+ if (isState(asText)) return asText;
2190
+ }
2191
+ return void 0;
1890
2192
  }
1891
2193
  function isForeignKeyViolation(e) {
1892
- return e?.code === "23503";
2194
+ return sqlstateOf(e) === "23503";
2195
+ }
2196
+ function isUniqueViolation(e) {
2197
+ return sqlstateOf(e) === "23505";
2198
+ }
2199
+ function constraintOf(e) {
2200
+ const named = e?.constraint;
2201
+ if (typeof named === "string" && named.length > 0) return named;
2202
+ const message = String(e?.message ?? "");
2203
+ return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
1893
2204
  }
1894
2205
  function isLockTimeout(e) {
1895
- const code = e?.code;
1896
2206
  const message = String(e?.message ?? "");
1897
- return code === "55P03" || /lock timeout/i.test(message);
2207
+ return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
1898
2208
  }
1899
2209
  function diagnosingDriver(sql, surface = "service") {
1900
2210
  const original = (e) => String(e?.message ?? e);
@@ -1904,6 +2214,9 @@ function diagnosingDriver(sql, surface = "service") {
1904
2214
  `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)})`
1905
2215
  );
1906
2216
  }
2217
+ if (isUniqueViolation(e)) {
2218
+ return new UniqueViolation(constraintOf(e));
2219
+ }
1907
2220
  if (surface === "service" && isForeignKeyViolation(e)) {
1908
2221
  return new Error(
1909
2222
  `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)})`
@@ -1955,9 +2268,9 @@ function createRequestDatabase(sql, identity) {
1955
2268
  await tx.commit();
1956
2269
  await serviceTx?.commit();
1957
2270
  },
1958
- async rollback(reason) {
1959
- await tx.rollback(reason);
1960
- await serviceTx?.rollback(reason);
2271
+ async rollback(reason2) {
2272
+ await tx.rollback(reason2);
2273
+ await serviceTx?.rollback(reason2);
1961
2274
  }
1962
2275
  };
1963
2276
  }
@@ -2032,10 +2345,7 @@ async function runPlanOp(sp, op, results) {
2032
2345
  const rendered = cols.map(
2033
2346
  (c) => renderValue(op.values[c], c, args, results, opVectorCols)
2034
2347
  );
2035
- const conflictSet = new Set(conflict);
2036
- const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
2037
- const action = sets.length ? `DO UPDATE SET ${sets.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
2038
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
2348
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
2039
2349
  break;
2040
2350
  }
2041
2351
  case "insertMany": {
@@ -2045,7 +2355,9 @@ async function runPlanOp(sp, op, results) {
2045
2355
  const tuples = rows.map(
2046
2356
  (r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
2047
2357
  );
2048
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
2358
+ const conflictCols = op.onConflict ?? [];
2359
+ const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
2360
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
2049
2361
  break;
2050
2362
  }
2051
2363
  case "update": {
@@ -2120,6 +2432,38 @@ function getRoutes(ctor) {
2120
2432
  }));
2121
2433
  }
2122
2434
 
2435
+ // src/decorators/controller.ts
2436
+ var REGISTRY = /* @__PURE__ */ Symbol.for("palbase.backend.allControllers");
2437
+ function registry() {
2438
+ const g = globalThis;
2439
+ const existing = g[REGISTRY];
2440
+ if (existing) return existing;
2441
+ const fresh = [];
2442
+ g[REGISTRY] = fresh;
2443
+ return fresh;
2444
+ }
2445
+ function getRegisteredControllers() {
2446
+ return registry().slice();
2447
+ }
2448
+ var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
2449
+ function appAuthSlot() {
2450
+ return globalThis;
2451
+ }
2452
+ function getDefaultAuth() {
2453
+ return appAuthSlot()[APP_DEFAULT_AUTH];
2454
+ }
2455
+ function resolveEffectiveAuth(routeAuth, controllerAuth) {
2456
+ return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
2457
+ }
2458
+ function assertZeroArgConstructor(Ctrl, kind) {
2459
+ const arity = Ctrl.length ?? 0;
2460
+ if (arity === 0) return;
2461
+ const name = Ctrl.name ?? "<anonymous>";
2462
+ throw new Error(
2463
+ `${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)\`).`
2464
+ );
2465
+ }
2466
+
2123
2467
  // src/engine/router.ts
2124
2468
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
2125
2469
  function toSegments(path) {
@@ -2138,6 +2482,7 @@ function buildRouteTable(controllers) {
2138
2482
  `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.`
2139
2483
  );
2140
2484
  }
2485
+ assertZeroArgConstructor(Ctrl, "controller");
2141
2486
  const instance = new ctor();
2142
2487
  for (const r of routes) {
2143
2488
  const full = `${basePath}${r.subpath ?? ""}` || "/";
@@ -2147,7 +2492,10 @@ function buildRouteTable(controllers) {
2147
2492
  meta: r,
2148
2493
  instance,
2149
2494
  id: `${r.method} ${full}`,
2150
- controllerAuth: meta?.defaultAuth
2495
+ // The route's OWN spec is deliberately not folded in here: this field
2496
+ // is what applies when the route is silent, and `effectiveAuth` puts
2497
+ // the route level back on top per request.
2498
+ controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
2151
2499
  });
2152
2500
  }
2153
2501
  }
@@ -2715,31 +3063,28 @@ async function createApp(opts) {
2715
3063
  return envelope("internal_error", "The request could not be completed", 500, requestId);
2716
3064
  }
2717
3065
  }
3066
+ let runShutdownHooks;
3067
+ try {
3068
+ runShutdownHooks = await __runStartHooks();
3069
+ } catch (err) {
3070
+ await closeDriver(sql);
3071
+ throw err;
3072
+ }
2718
3073
  return {
2719
3074
  handle,
2720
3075
  routes,
2721
3076
  config,
2722
3077
  runInServiceScope,
2723
3078
  async shutdown() {
2724
- const closable = sql;
2725
- await closable.close?.();
2726
- await closable.end?.();
3079
+ await runShutdownHooks();
3080
+ await closeDriver(sql);
2727
3081
  }
2728
3082
  };
2729
3083
  }
2730
-
2731
- // src/decorators/controller.ts
2732
- var REGISTRY = /* @__PURE__ */ Symbol.for("palbase.backend.allControllers");
2733
- function registry() {
2734
- const g = globalThis;
2735
- const existing = g[REGISTRY];
2736
- if (existing) return existing;
2737
- const fresh = [];
2738
- g[REGISTRY] = fresh;
2739
- return fresh;
2740
- }
2741
- function getRegisteredControllers() {
2742
- return registry().slice();
3084
+ async function closeDriver(sql) {
3085
+ const closable = sql;
3086
+ await closable.close?.();
3087
+ await closable.end?.();
2743
3088
  }
2744
3089
 
2745
3090
  // src/bin/palbase-backend.ts