@palbase/backend 23.1.0 → 24.0.1

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 +400 -47
  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-PY7YJDCT.js → chunk-LCL7TUAI.js} +32 -3
  10. package/dist/chunk-LCL7TUAI.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-M5MCBWJI.js → chunk-YSQBC2VL.js} +275 -31
  16. package/dist/chunk-YSQBC2VL.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 +389 -34
  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-BOS_rFBO.d.ts} +125 -21
  30. package/dist/{index-CxeQSfJP.d.cts → index-C84bLgeO.d.cts} +134 -9
  31. package/dist/{index-By8Dle5U.d.cts → index-MoQ31B6M.d.cts} +125 -21
  32. package/dist/{index-BZrJXnVh.d.ts → index-dJNhDZ7j.d.ts} +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,13 +1392,33 @@ 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 = [];
1411
+ const transforms = transformsOf(currentSchema, table);
1412
+ const bindFor = (col) => {
1413
+ const t = transforms.get(col);
1414
+ return t?.toDb === void 0 ? add : (v) => add(v === null || v === void 0 ? v : t.toDb(v));
1415
+ };
1272
1416
  for (const [col, cond] of Object.entries(where)) {
1273
1417
  if (colSet !== null && !colSet.has(col)) {
1274
1418
  throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
1275
1419
  }
1276
1420
  const q = `t.${quoteIdent(col)}`;
1421
+ const bind = bindFor(col);
1277
1422
  if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
1278
1423
  for (const [op, v] of Object.entries(cond)) {
1279
1424
  if (op === "in") {
@@ -1282,19 +1427,26 @@ function compileWhere(table, colSet, where, add, caller = "search") {
1282
1427
  parts.push("false");
1283
1428
  continue;
1284
1429
  }
1285
- parts.push(`${q} IN (${v.map((x) => add(x)).join(", ")})`);
1430
+ parts.push(`${q} IN (${v.map((x) => bind(x)).join(", ")})`);
1286
1431
  } else if (op in WHERE_OPS) {
1287
- parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
1432
+ parts.push(`${q} ${WHERE_OPS[op]} ${bind(v)}`);
1288
1433
  } else {
1289
1434
  throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
1290
1435
  }
1291
1436
  }
1292
1437
  } else {
1293
- parts.push(`${q} = ${add(cond)}`);
1438
+ parts.push(`${q} = ${bind(cond)}`);
1294
1439
  }
1295
1440
  }
1296
1441
  return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
1297
1442
  }
1443
+ function onConflictTail(cols, conflict, action) {
1444
+ const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
1445
+ if (action === "ignore") return `${target} DO NOTHING`;
1446
+ const conflictSet = new Set(conflict);
1447
+ const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
1448
+ return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
1449
+ }
1298
1450
  var cachedVectorSchema = null;
1299
1451
  var cachedTrgmSchema = null;
1300
1452
  async function trgmSchemaOf(live) {
@@ -1404,6 +1556,39 @@ function createOps(tx) {
1404
1556
  }
1405
1557
  const cols = Object.keys(data);
1406
1558
  if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
1559
+ if (searchConfigFor(table)?.validity) {
1560
+ const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
1561
+ 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 *`;
1562
+ const live = await at();
1563
+ const bindData = asBindParams(table, cols, data);
1564
+ const first = await live.unsafe(insertSql, bindData);
1565
+ if (first[0]) return asTableRow(table, first[0]);
1566
+ const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
1567
+ const condBind = conflict.map((c) => data[c]);
1568
+ const cur = await live.unsafe(
1569
+ `SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
1570
+ condBind
1571
+ );
1572
+ const old = cur[0];
1573
+ if (old === void 0) {
1574
+ 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)`);
1575
+ }
1576
+ const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
1577
+ await live.unsafe(
1578
+ `UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
1579
+ [old[pk]]
1580
+ );
1581
+ const second = await live.unsafe(insertSql, bindData);
1582
+ if (!second[0]) {
1583
+ 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)`);
1584
+ }
1585
+ const fresh = second[0];
1586
+ await live.unsafe(
1587
+ `UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
1588
+ [old[pk], fresh[pk]]
1589
+ );
1590
+ return asTableRow(table, second[0]);
1591
+ }
1407
1592
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1408
1593
  const conflictSet = new Set(conflict);
1409
1594
  const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
@@ -1429,6 +1614,82 @@ function createOps(tx) {
1429
1614
  async delete(table, id) {
1430
1615
  await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
1431
1616
  },
1617
+ /**
1618
+ * Update every row the filter matches, in ONE statement.
1619
+ *
1620
+ * The capability was already here and only reachable from inside a
1621
+ * transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
1622
+ * only door was `update(id, …)`, so "mark every unapproved row in this
1623
+ * household" was an N+1 loop or hand-written SQL — and hand-written SQL is
1624
+ * where the typed surface and RLS both stop helping.
1625
+ *
1626
+ * The filter language is `findMany`'s, compiled by the same `compileWhere`:
1627
+ * one filter language, or the two spellings drift.
1628
+ *
1629
+ * AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
1630
+ * and the shape that produces it by accident — a filter object built from
1631
+ * request input that happened to come back empty — is exactly the shape that
1632
+ * should not silently succeed. Callers who mean every row say so with a
1633
+ * predicate that is true for every row.
1634
+ */
1635
+ async updateMany(table, where, set) {
1636
+ const cols = Object.keys(set);
1637
+ if (cols.length === 0) {
1638
+ throw new Error(
1639
+ `updateMany(${table}): nothing to set. An update with no columns is not a no-op worth pretending happened \u2014 pass the columns to write.`
1640
+ );
1641
+ }
1642
+ const params = [];
1643
+ const add = (v) => {
1644
+ params.push(v);
1645
+ return `$${params.length}`;
1646
+ };
1647
+ const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
1648
+ const known = schemaColumns(table);
1649
+ const whereSql = compileWhere(table, known, where, add, "updateMany");
1650
+ assertHasPredicate(whereSql, "updateMany", table);
1651
+ const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
1652
+ return asTableRows(table, await (await at()).unsafe(sql, params));
1653
+ },
1654
+ /**
1655
+ * Delete every row the filter matches, in ONE statement; resolves to how
1656
+ * many went. Same filter language, same empty-filter refusal as
1657
+ * {@link updateMany} — and here the accident is worse.
1658
+ */
1659
+ async deleteMany(table, where) {
1660
+ const params = [];
1661
+ const add = (v) => {
1662
+ params.push(v);
1663
+ return `$${params.length}`;
1664
+ };
1665
+ const known = schemaColumns(table);
1666
+ const whereSql = compileWhere(table, known, where, add, "deleteMany");
1667
+ assertHasPredicate(whereSql, "deleteMany", table);
1668
+ const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
1669
+ const rows = await (await at()).unsafe(sql, params);
1670
+ return rows.length;
1671
+ },
1672
+ /**
1673
+ * How many rows match — the half of pagination `limit`/`offset` cannot
1674
+ * supply. Without it a page count is either a guess or "fetch everything and
1675
+ * read .length", and the second one is the scale risk this surface exists to
1676
+ * remove.
1677
+ *
1678
+ * An empty filter is legitimate HERE: counting a whole table is a read, and
1679
+ * reads do not destroy anything.
1680
+ */
1681
+ async count(table, where = {}) {
1682
+ const params = [];
1683
+ const add = (v) => {
1684
+ params.push(v);
1685
+ return `$${params.length}`;
1686
+ };
1687
+ const known = schemaColumns(table);
1688
+ const whereSql = compileWhere(table, known, where, add, "count");
1689
+ const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
1690
+ const rows = await (await at()).unsafe(sql, params);
1691
+ return Number(rows[0]?.n ?? 0);
1692
+ },
1432
1693
  async findById(table, id) {
1433
1694
  const rows = await (await at()).unsafe(
1434
1695
  `SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
@@ -1446,7 +1707,8 @@ function createOps(tx) {
1446
1707
  const whereSql = compileWhere(table, known, query, add, "findMany");
1447
1708
  const order = orderClause(table, known, opts.orderBy);
1448
1709
  const limit = limitClause(opts.limit);
1449
- const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}`;
1710
+ const offset = offsetClause(opts.offset, opts.limit);
1711
+ const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
1450
1712
  return asTableRows(table, await (await at()).unsafe(sql, params));
1451
1713
  },
1452
1714
  /**
@@ -1529,7 +1791,9 @@ function createOps(tx) {
1529
1791
  bindC.push(v);
1530
1792
  return `$${bindC.length}`;
1531
1793
  };
1532
- const whereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC) + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1794
+ const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
1795
+ const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
1796
+ const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
1533
1797
  const liveC = await at();
1534
1798
  const K2 = 50;
1535
1799
  const cpool = Math.max(limit * 9, 90);
@@ -1538,8 +1802,19 @@ function createOps(tx) {
1538
1802
  if (qvC !== null) {
1539
1803
  const sch = await vectorSchemaWithGuc(liveC);
1540
1804
  const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
1805
+ let exactOrderC = "";
1806
+ if (userWhereC !== "") {
1807
+ const probeRows = await liveC.unsafe(
1808
+ `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`,
1809
+ bindC.slice()
1810
+ );
1811
+ const n = probeRows?.[0]?.n;
1812
+ if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
1813
+ exactOrderC = " + 0.0";
1814
+ }
1815
+ }
1541
1816
  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}`;
1817
+ 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
1818
  }
1544
1819
  let qpC = "";
1545
1820
  let qpCIdx = -1;
@@ -1547,7 +1822,7 @@ function createOps(tx) {
1547
1822
  const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
1548
1823
  qpC = addC(qTextC);
1549
1824
  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}`;
1825
+ 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
1826
  }
1552
1827
  const msC = params.minScore === void 0 ? "" : addC(params.minScore);
1553
1828
  const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
@@ -1562,13 +1837,13 @@ function createOps(tx) {
1562
1837
  } else {
1563
1838
  fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
1564
1839
  }
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}`;
1840
+ 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
1841
  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
1842
  };
1568
1843
  let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
1569
1844
  if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
1570
1845
  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}`;
1846
+ 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
1847
  const retryBindC = bindC.slice();
1573
1848
  if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
1574
1849
  rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
@@ -1664,7 +1939,7 @@ function createOps(tx) {
1664
1939
  if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
1665
1940
  const tsch = await trgmSchemaOf(live);
1666
1941
  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}`;
1942
+ 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
1943
  const retryBind = bind.slice();
1669
1944
  if (qpIdx >= 0) retryBind[qpIdx] = params.query;
1670
1945
  rows = await live.unsafe(assemble(trgmKw, false), retryBind);
@@ -1687,6 +1962,25 @@ function createOps(tx) {
1687
1962
  * tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
1688
1963
  * adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
1689
1964
  */
1965
+ /** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
1966
+ * `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
1967
+ * yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
1968
+ * search'teki alan geriye-uyum için DURUR. where + validity default'u
1969
+ * sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
1970
+ async facets(table, params) {
1971
+ const cfg = searchConfigFor(table);
1972
+ if (!cfg) {
1973
+ throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
1974
+ }
1975
+ for (const col of params.facets) {
1976
+ if (!cfg.colSet.has(col)) {
1977
+ throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
1978
+ }
1979
+ }
1980
+ const live = await at();
1981
+ const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
1982
+ return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
1983
+ },
1690
1984
  async similar(table, id, opts = {}) {
1691
1985
  const cfg = searchConfigFor(table);
1692
1986
  if (!cfg) {
@@ -1918,17 +2212,41 @@ function withTables(ops, schema = currentSchema) {
1918
2212
  }
1919
2213
  var SERVICE_LOCK_TIMEOUT = "5s";
1920
2214
  function isAbortedTransaction(e) {
1921
- const code = e?.code;
1922
2215
  const message = String(e?.message ?? "");
1923
- return code === "25P02" || /current transaction is aborted/i.test(message);
2216
+ return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
2217
+ }
2218
+ function assertHasPredicate(whereSql, op, table) {
2219
+ if (whereSql.trim().length > 0) return;
2220
+ throw new Error(
2221
+ `${op}(${table}): the filter compiled to no condition, so this would have written EVERY row. An empty filter \u2014 or an operator object with no operators in it, like { col: {} } \u2014 is refused. Name a condition.`
2222
+ );
2223
+ }
2224
+ function sqlstateOf(e) {
2225
+ const err = e;
2226
+ const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
2227
+ if (isState(err?.code)) return err.code;
2228
+ if (isState(err?.errno)) return err.errno;
2229
+ if (typeof err?.errno === "number") {
2230
+ const asText = String(err.errno);
2231
+ if (isState(asText)) return asText;
2232
+ }
2233
+ return void 0;
1924
2234
  }
1925
2235
  function isForeignKeyViolation(e) {
1926
- return e?.code === "23503";
2236
+ return sqlstateOf(e) === "23503";
2237
+ }
2238
+ function isUniqueViolation(e) {
2239
+ return sqlstateOf(e) === "23505";
2240
+ }
2241
+ function constraintOf(e) {
2242
+ const named = e?.constraint;
2243
+ if (typeof named === "string" && named.length > 0) return named;
2244
+ const message = String(e?.message ?? "");
2245
+ return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
1927
2246
  }
1928
2247
  function isLockTimeout(e) {
1929
- const code = e?.code;
1930
2248
  const message = String(e?.message ?? "");
1931
- return code === "55P03" || /lock timeout/i.test(message);
2249
+ return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
1932
2250
  }
1933
2251
  function diagnosingDriver(sql, surface = "service") {
1934
2252
  const original = (e) => String(e?.message ?? e);
@@ -1938,6 +2256,9 @@ function diagnosingDriver(sql, surface = "service") {
1938
2256
  `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
2257
  );
1940
2258
  }
2259
+ if (isUniqueViolation(e)) {
2260
+ return new UniqueViolation(constraintOf(e));
2261
+ }
1941
2262
  if (surface === "service" && isForeignKeyViolation(e)) {
1942
2263
  return new Error(
1943
2264
  `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 +2310,9 @@ function createRequestDatabase(sql, identity) {
1989
2310
  await tx.commit();
1990
2311
  await serviceTx?.commit();
1991
2312
  },
1992
- async rollback(reason) {
1993
- await tx.rollback(reason);
1994
- await serviceTx?.rollback(reason);
2313
+ async rollback(reason2) {
2314
+ await tx.rollback(reason2);
2315
+ await serviceTx?.rollback(reason2);
1995
2316
  }
1996
2317
  };
1997
2318
  }
@@ -2066,10 +2387,7 @@ async function runPlanOp(sp, op, results) {
2066
2387
  const rendered = cols.map(
2067
2388
  (c) => renderValue(op.values[c], c, args, results, opVectorCols)
2068
2389
  );
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 *`;
2390
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
2073
2391
  break;
2074
2392
  }
2075
2393
  case "insertMany": {
@@ -2079,7 +2397,9 @@ async function runPlanOp(sp, op, results) {
2079
2397
  const tuples = rows.map(
2080
2398
  (r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
2081
2399
  );
2082
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")} RETURNING *`;
2400
+ const conflictCols = op.onConflict ?? [];
2401
+ const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
2402
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
2083
2403
  break;
2084
2404
  }
2085
2405
  case "update": {
@@ -2154,6 +2474,26 @@ function getRoutes(ctor) {
2154
2474
  }));
2155
2475
  }
2156
2476
 
2477
+ // src/decorators/controller.ts
2478
+ var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
2479
+ function appAuthSlot() {
2480
+ return globalThis;
2481
+ }
2482
+ function getDefaultAuth() {
2483
+ return appAuthSlot()[APP_DEFAULT_AUTH];
2484
+ }
2485
+ function resolveEffectiveAuth(routeAuth, controllerAuth) {
2486
+ return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
2487
+ }
2488
+ function assertZeroArgConstructor(Ctrl, kind) {
2489
+ const arity = Ctrl.length ?? 0;
2490
+ if (arity === 0) return;
2491
+ const name = Ctrl.name ?? "<anonymous>";
2492
+ throw new Error(
2493
+ `${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)\`).`
2494
+ );
2495
+ }
2496
+
2157
2497
  // src/engine/router.ts
2158
2498
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
2159
2499
  function toSegments(path) {
@@ -2172,6 +2512,7 @@ function buildRouteTable(controllers) {
2172
2512
  `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
2513
  );
2174
2514
  }
2515
+ assertZeroArgConstructor(Ctrl, "controller");
2175
2516
  const instance = new ctor();
2176
2517
  for (const r of routes) {
2177
2518
  const full = `${basePath}${r.subpath ?? ""}` || "/";
@@ -2181,7 +2522,10 @@ function buildRouteTable(controllers) {
2181
2522
  meta: r,
2182
2523
  instance,
2183
2524
  id: `${r.method} ${full}`,
2184
- controllerAuth: meta?.defaultAuth
2525
+ // The route's OWN spec is deliberately not folded in here: this field
2526
+ // is what applies when the route is silent, and `effectiveAuth` puts
2527
+ // the route level back on top per request.
2528
+ controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
2185
2529
  });
2186
2530
  }
2187
2531
  }
@@ -2810,18 +3154,29 @@ async function createApp(opts) {
2810
3154
  return envelope("internal_error", "The request could not be completed", 500, requestId);
2811
3155
  }
2812
3156
  }
3157
+ let runShutdownHooks;
3158
+ try {
3159
+ runShutdownHooks = await __runStartHooks();
3160
+ } catch (err) {
3161
+ await closeDriver(sql);
3162
+ throw err;
3163
+ }
2813
3164
  return {
2814
3165
  handle,
2815
3166
  routes,
2816
3167
  config,
2817
3168
  runInServiceScope,
2818
3169
  async shutdown() {
2819
- const closable = sql;
2820
- await closable.close?.();
2821
- await closable.end?.();
3170
+ await runShutdownHooks();
3171
+ await closeDriver(sql);
2822
3172
  }
2823
3173
  };
2824
3174
  }
3175
+ async function closeDriver(sql) {
3176
+ const closable = sql;
3177
+ await closable.close?.();
3178
+ await closable.end?.();
3179
+ }
2825
3180
  // Annotate the CommonJS export names for ESM import in node:
2826
3181
  0 && (module.exports = {
2827
3182
  AuthVerifier,