@palbase/backend 28.0.0 → 29.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 (48) hide show
  1. package/dist/bin/palbase-backend.cjs +151 -12
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +3 -3
  4. package/dist/{chunk-75YROPRZ.js → chunk-DHJ3SYAE.js} +152 -15
  5. package/dist/chunk-DHJ3SYAE.js.map +1 -0
  6. package/dist/{chunk-IVZERLTM.js → chunk-ENZ2RFFJ.js} +2 -2
  7. package/dist/{chunk-RVP6BTEZ.js → chunk-ESSQ3YML.js} +13 -4
  8. package/dist/chunk-ESSQ3YML.js.map +1 -0
  9. package/dist/{chunk-SNDXY565.js → chunk-SG4UTNOP.js} +5 -2
  10. package/dist/chunk-SG4UTNOP.js.map +1 -0
  11. package/dist/db/index.cjs +4 -1
  12. package/dist/db/index.cjs.map +1 -1
  13. package/dist/db/index.d.cts +1 -1
  14. package/dist/db/index.d.ts +1 -1
  15. package/dist/db/index.js +2 -2
  16. package/dist/engine/index.cjs +151 -12
  17. package/dist/engine/index.cjs.map +1 -1
  18. package/dist/engine/index.d.cts +3 -3
  19. package/dist/engine/index.d.ts +3 -3
  20. package/dist/engine/index.js +3 -3
  21. package/dist/{index-dTTLlHIn.d.ts → index-BOe82fsK.d.ts} +162 -8
  22. package/dist/{index-BbvOoZFr.d.ts → index-BYDUKexK.d.ts} +29 -7
  23. package/dist/{index-9C3JHxg-.d.cts → index-BZfmdRmh.d.cts} +29 -7
  24. package/dist/{index-DtISj9QX.d.cts → index-ChvoVczS.d.cts} +162 -8
  25. package/dist/index.cjs +15 -3
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +6 -6
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +3 -3
  30. package/dist/openapi/index.d.cts +2 -2
  31. package/dist/openapi/index.d.ts +2 -2
  32. package/dist/{registry-qIPM5BQe.d.cts → registry-B2KfKWv1.d.cts} +1 -1
  33. package/dist/{registry-JQNIX-eA.d.ts → registry-DaqHsOq8.d.ts} +1 -1
  34. package/dist/test/index.cjs +56 -0
  35. package/dist/test/index.cjs.map +1 -1
  36. package/dist/test/index.d.cts +1 -1
  37. package/dist/test/index.d.ts +1 -1
  38. package/dist/test/index.js +56 -0
  39. package/dist/test/index.js.map +1 -1
  40. package/docs/README.md +1 -1
  41. package/docs/database.md +49 -0
  42. package/docs/llms-full.txt +50 -1
  43. package/package.json +1 -1
  44. package/template/package.json +1 -1
  45. package/dist/chunk-75YROPRZ.js.map +0 -1
  46. package/dist/chunk-RVP6BTEZ.js.map +0 -1
  47. package/dist/chunk-SNDXY565.js.map +0 -1
  48. /package/dist/{chunk-IVZERLTM.js.map → chunk-ENZ2RFFJ.js.map} +0 -0
@@ -3,9 +3,9 @@ import {
3
3
  BootRefused,
4
4
  createApp,
5
5
  loadConfig
6
- } from "../chunk-75YROPRZ.js";
7
- import "../chunk-RVP6BTEZ.js";
8
- import "../chunk-SNDXY565.js";
6
+ } from "../chunk-DHJ3SYAE.js";
7
+ import "../chunk-ESSQ3YML.js";
8
+ import "../chunk-SG4UTNOP.js";
9
9
  import "../chunk-3TUJWHC2.js";
10
10
  import {
11
11
  getRegisteredControllers
@@ -6,10 +6,10 @@ import {
6
6
  assertNoOrphanEntryPoints,
7
7
  buildContainer,
8
8
  buildRelations
9
- } from "./chunk-RVP6BTEZ.js";
9
+ } from "./chunk-ESSQ3YML.js";
10
10
  import {
11
11
  qualifiedTableKey
12
- } from "./chunk-SNDXY565.js";
12
+ } from "./chunk-SG4UTNOP.js";
13
13
  import {
14
14
  assertNoExpressionHandles,
15
15
  assertUsableFilter,
@@ -228,9 +228,11 @@ function effectiveAuth(routeAuth, controllerAuth) {
228
228
  };
229
229
  const o = spec;
230
230
  const role = typeof o.role === "string" && o.role.trim() !== "" ? o.role.trim() : void 0;
231
+ const permission = typeof o.permission === "string" && o.permission.trim() !== "" ? o.permission.trim() : void 0;
231
232
  return {
232
233
  required: o.required !== false,
233
234
  role,
235
+ permission,
234
236
  verifiedEmail: o.verifiedEmail === true
235
237
  };
236
238
  }
@@ -1131,6 +1133,26 @@ function buildValueTuple(table, cols, data, add, caller) {
1131
1133
  }).join(", ");
1132
1134
  }
1133
1135
  __name(buildValueTuple, "buildValueTuple");
1136
+ function columnTypeOf(table, col) {
1137
+ const def = tableDefOf(table);
1138
+ const c = def?.columns?.[col];
1139
+ if (c === void 0 || c === null) return null;
1140
+ const d = typeof c === "object" && "_def" in c ? c._def : c;
1141
+ return d !== null && typeof d === "object" && typeof d.type === "string" ? d.type : null;
1142
+ }
1143
+ __name(columnTypeOf, "columnTypeOf");
1144
+ var NUMERIC_PG_TYPES = /* @__PURE__ */ new Set([
1145
+ "integer",
1146
+ "bigint",
1147
+ "numeric"
1148
+ ]);
1149
+ function aggregateExpr(table, fn, col, alias) {
1150
+ const q = `t.${quoteIdent(col)}`;
1151
+ const t = columnTypeOf(table, col);
1152
+ if (fn === "sum" || fn === "avg") return `${fn}(${q})::text AS ${quoteIdent(alias)}`;
1153
+ return t !== null && NUMERIC_PG_TYPES.has(t) && t !== "integer" ? `${fn}(${q})::text AS ${quoteIdent(alias)}` : `${fn}(${q}) AS ${quoteIdent(alias)}`;
1154
+ }
1155
+ __name(aggregateExpr, "aggregateExpr");
1134
1156
  function createOps(tx) {
1135
1157
  const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
1136
1158
  const ops = {
@@ -1411,7 +1433,7 @@ function createOps(tx) {
1411
1433
  * should not silently succeed. Callers who mean every row say so with a
1412
1434
  * predicate that is true for every row.
1413
1435
  */
1414
- async updateMany(table, where, set) {
1436
+ async updateMany(table, where, set, opts) {
1415
1437
  assertNotAppendOnly("updateMany", table);
1416
1438
  const cols = Object.keys(set);
1417
1439
  if (cols.length === 0) {
@@ -1434,8 +1456,15 @@ function createOps(tx) {
1434
1456
  const whereSql = compileWhere(table, known, where, add, "updateMany");
1435
1457
  assertHasPredicate(whereSql, "updateMany", table);
1436
1458
  const pk = primaryKeyOf(table);
1437
- const sql = isSinglePkEquality(where, pk) || pk === null ? `UPDATE ${quoteTable(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *` : `WITH locked AS (SELECT t.${quoteIdent(pk)} FROM ${quoteTable(table)} t WHERE true${whereSql} ORDER BY t.${quoteIdent(pk)} FOR NO KEY UPDATE) UPDATE ${quoteTable(table)} AS t SET ${assignments} FROM locked WHERE t.${quoteIdent(pk)} = locked.${quoteIdent(pk)} RETURNING t.*`;
1438
- return asTableRows(table, await (await at()).unsafe(sql, params));
1459
+ const wantRows = opts?.returning !== false;
1460
+ const tail = wantRows ? "RETURNING *" : "RETURNING 1";
1461
+ const simple = isSinglePkEquality(where, pk) || pk === null;
1462
+ const body = simple ? `UPDATE ${quoteTable(table)} AS t SET ${assignments} WHERE true${whereSql} ${tail}` : `UPDATE ${quoteTable(table)} AS t SET ${assignments} FROM locked WHERE t.${quoteIdent(pk)} = locked.${quoteIdent(pk)} ` + (wantRows ? "RETURNING t.*" : "RETURNING 1");
1463
+ const lockedCte = simple ? "" : `WITH locked AS (SELECT t.${quoteIdent(pk)} FROM ${quoteTable(table)} t WHERE true${whereSql} ORDER BY t.${quoteIdent(pk)} FOR NO KEY UPDATE) `;
1464
+ const sql = wantRows ? `${lockedCte}${body}` : simple ? `WITH upd AS (${body}) SELECT count(*)::int AS n FROM upd` : `${lockedCte.replace(/ $/, "")}, upd AS (${body}) SELECT count(*)::int AS n FROM upd`;
1465
+ const out = await (await at()).unsafe(sql, params);
1466
+ if (!wantRows) return Number(out[0]?.["n"] ?? 0);
1467
+ return asTableRows(table, out);
1439
1468
  },
1440
1469
  /**
1441
1470
  * Delete every row the filter matches, in ONE statement; resolves to how
@@ -1465,6 +1494,78 @@ function createOps(tx) {
1465
1494
  * An empty filter is legitimate HERE: counting a whole table is a read, and
1466
1495
  * reads do not destroy anything.
1467
1496
  */
1497
+ /**
1498
+ * Toplama — `count`'un yanına `sum`/`avg`/`min`/`max` ve `groupBy`.
1499
+ *
1500
+ * Var olma nedeni: bunlar olmadan "bu hesabın toplamı" ya `$query`'ye ya da
1501
+ * TÜM satırları çekip JS'te toplamaya düşüyor. İkincisi hem N+1 hem de para
1502
+ * için YANLIŞ — `numeric` JS `number`'ına uğradığı anda kesinliğini
1503
+ * kaybeder.
1504
+ *
1505
+ * `groupBy` verilirse SATIRLAR döner, verilmezse TEK sonuç. Boş kümede
1506
+ * Postgres NULL döndürür, 0 değil — ve bu SDK onu `null` olarak geçiriyor:
1507
+ * "hiç satır yoktu" ile "toplam sıfırdı" farklı şeyler.
1508
+ */
1509
+ async aggregate(table, q = {}) {
1510
+ const known = schemaColumns(table);
1511
+ const check = /* @__PURE__ */ __name((col, where) => {
1512
+ if (known !== null && !known.has(col)) {
1513
+ throw new Error(`aggregate(${table}): ${where} kolonu "${col}" tabloda yok (FR-016)`);
1514
+ }
1515
+ }, "check");
1516
+ const parts = [];
1517
+ const wants = [];
1518
+ for (const fn of [
1519
+ "sum",
1520
+ "avg",
1521
+ "min",
1522
+ "max"
1523
+ ]) {
1524
+ for (const col of q[fn] ?? []) {
1525
+ check(col, fn);
1526
+ if (fn === "sum" || fn === "avg") {
1527
+ const t = columnTypeOf(table, col);
1528
+ if (t !== null && !NUMERIC_PG_TYPES.has(t)) {
1529
+ throw new Error(`aggregate(${table}): ${fn}("${col}") say\u0131sal olmayan bir kolonda \u2014 "${col}" bir ${t} kolonu. Toplama yaln\u0131z integer/bigint/numeric kolonlarda anlaml\u0131; ka\xE7 sat\u0131r oldu\u011Funu saymak i\xE7in count: true kullan\u0131n.`);
1530
+ }
1531
+ }
1532
+ const alias = `${fn}__${col}`;
1533
+ wants.push({
1534
+ fn,
1535
+ col,
1536
+ alias
1537
+ });
1538
+ parts.push(aggregateExpr(table, fn, col, alias));
1539
+ }
1540
+ }
1541
+ if (q.count === true) parts.push('count(*) AS "_count"');
1542
+ if (parts.length === 0) {
1543
+ throw new Error(`aggregate(${table}): hi\xE7bir toplama istenmedi \u2014 sonu\xE7 bo\u015F bir nesne olurdu. sum/avg/min/max kolonlar\u0131n\u0131 ya da count: true'yu verin.`);
1544
+ }
1545
+ const groups = q.groupBy ?? [];
1546
+ for (const col of groups) check(col, "groupBy");
1547
+ const groupSel = groups.map((c) => `t.${quoteIdent(c)} AS ${quoteIdent(`g__${c}`)}`);
1548
+ const params = [];
1549
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1550
+ const whereSql = compileWhere(table, known, q.where ?? {}, add, "aggregate");
1551
+ const groupSql = groups.length > 0 ? ` GROUP BY ${groups.map((c) => `t.${quoteIdent(c)}`).join(", ")}` : "";
1552
+ const sql = `SELECT ${[
1553
+ ...groupSel,
1554
+ ...parts
1555
+ ].join(", ")} FROM ${quoteTable(table)} t WHERE true${whereSql}${groupSql}`;
1556
+ const rows = await (await at()).unsafe(sql, params);
1557
+ const shape = /* @__PURE__ */ __name((r) => {
1558
+ const out = {};
1559
+ for (const c of groups) out[c] = r[`g__${c}`];
1560
+ for (const w of wants) {
1561
+ const bucket = out[w.fn] ??= {};
1562
+ bucket[w.col] = r[w.alias] ?? null;
1563
+ }
1564
+ if (q.count === true) out["count"] = Number(r["_count"] ?? 0);
1565
+ return out;
1566
+ }, "shape");
1567
+ return groups.length > 0 ? rows.map(shape) : shape(rows[0] ?? {});
1568
+ },
1468
1569
  async count(table, where = {}) {
1469
1570
  const params = [];
1470
1571
  const add = /* @__PURE__ */ __name((v) => {
@@ -2870,6 +2971,26 @@ var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
2870
2971
  409,
2871
2972
  429
2872
2973
  ]);
2974
+ async function refuseUnlessAuthorized(client, spec, requestId) {
2975
+ if (spec.permission !== void 0) {
2976
+ const [row] = await client.query("select auth.has_permission($1) as ok", [
2977
+ spec.permission
2978
+ ]);
2979
+ if (row?.ok !== true) {
2980
+ return envelope("forbidden", `This endpoint requires the "${spec.permission}" permission`, 403, requestId);
2981
+ }
2982
+ }
2983
+ if (spec.role !== void 0) {
2984
+ const [row] = await client.query("select $1 = any(auth.app_roles()) as ok", [
2985
+ spec.role
2986
+ ]);
2987
+ if (row?.ok !== true) {
2988
+ return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
2989
+ }
2990
+ }
2991
+ return null;
2992
+ }
2993
+ __name(refuseUnlessAuthorized, "refuseUnlessAuthorized");
2873
2994
  async function createApp(opts) {
2874
2995
  const { config, controllers } = opts;
2875
2996
  const container = buildContainer();
@@ -2961,8 +3082,23 @@ async function createApp(opts) {
2961
3082
  if (spec.required && !callerClaims) {
2962
3083
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
2963
3084
  }
2964
- if (callerClaims && spec.role && callerClaims.role !== spec.role) {
2965
- return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
3085
+ if (callerClaims) {
3086
+ const uploadDb = createRequestDatabase(sql, {
3087
+ role: config.dbRole,
3088
+ serviceRole: config.dbServiceRole,
3089
+ claimsJson: JSON.stringify(callerClaims)
3090
+ });
3091
+ try {
3092
+ const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId);
3093
+ if (refusal) {
3094
+ await uploadDb.commit();
3095
+ return refusal;
3096
+ }
3097
+ await uploadDb.commit();
3098
+ } catch (err) {
3099
+ await uploadDb.rollback(err);
3100
+ throw err;
3101
+ }
2966
3102
  }
2967
3103
  const grant = grantFor(target?.entry, {
2968
3104
  userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
@@ -2995,8 +3131,14 @@ async function createApp(opts) {
2995
3131
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
2996
3132
  }
2997
3133
  const userId = typeof claims?.sub === "string" ? claims.sub : void 0;
2998
- if (claims && spec.role && claims.role !== spec.role) {
2999
- return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
3134
+ const db = createRequestDatabase(sql, {
3135
+ role: config.dbRole,
3136
+ serviceRole: config.dbServiceRole,
3137
+ claimsJson: JSON.stringify(claims ?? {})
3138
+ });
3139
+ if (claims) {
3140
+ const refusal = await refuseUnlessAuthorized(db.client, spec, requestId);
3141
+ if (refusal) return refusal;
3000
3142
  }
3001
3143
  if (claims && spec.verifiedEmail && claims.email_verified !== true) {
3002
3144
  return envelope("email_not_verified", "A verified email address is required", 403, requestId);
@@ -3217,11 +3359,6 @@ async function createApp(opts) {
3217
3359
  args[p.index] = void 0;
3218
3360
  }
3219
3361
  }
3220
- const db = createRequestDatabase(sql, {
3221
- role: config.dbRole,
3222
- serviceRole: config.dbServiceRole,
3223
- claimsJson: JSON.stringify(claims ?? {})
3224
- });
3225
3362
  try {
3226
3363
  const services = buildServices(db);
3227
3364
  if (meta.options?.sseConfig !== void 0) {
@@ -3414,4 +3551,4 @@ export {
3414
3551
  installEgressFence,
3415
3552
  createApp
3416
3553
  };
3417
- //# sourceMappingURL=chunk-75YROPRZ.js.map
3554
+ //# sourceMappingURL=chunk-DHJ3SYAE.js.map