@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
@@ -912,6 +912,7 @@ function makeTableProxy(ops, prefix) {
912
912
  // DÖRDÜNCÜ FİİL, aynı gerekçeyle: tip söz veriyor, ops katmanı
913
913
  // uyguluyor, ve bir handler'ın gerçekten dokunduğu yer BURASI.
914
914
  // `runtime-table-verbs` kapısı bunu adıyla saydı.
915
+ aggregate: /* @__PURE__ */ __name((q) => ops().aggregate(name, q), "aggregate"),
915
916
  insertMany: /* @__PURE__ */ __name((rows, opts) => ops().insertMany(name, rows, opts), "insertMany"),
916
917
  update: /* @__PURE__ */ __name((q) => ops().update(name, q.where.id, q.set), "update"),
917
918
  delete: /* @__PURE__ */ __name((id) => ops().delete(name, id), "delete"),
@@ -969,6 +970,7 @@ function makeTypedSurface(raw) {
969
970
  $claim: /* @__PURE__ */ __name((table, unique, extra) => reco.claim(table, unique, extra), "$claim"),
970
971
  $lockRows: /* @__PURE__ */ __name((table, ids) => reco.lockRows(table, ids), "$lockRows"),
971
972
  $advisoryXactLock: /* @__PURE__ */ __name((key) => reco.advisoryXactLock(key), "$advisoryXactLock"),
973
+ $aggregate: /* @__PURE__ */ __name((table, q) => raw.aggregate(table, q), "$aggregate"),
972
974
  $insertMany: /* @__PURE__ */ __name((table, rows, opts) => raw.insertMany(table, rows, opts), "$insertMany"),
973
975
  $supersede: /* @__PURE__ */ __name((table, id, row) => raw.supersede(table, id, row), "$supersede")
974
976
  };
@@ -1299,9 +1301,11 @@ function effectiveAuth(routeAuth, controllerAuth) {
1299
1301
  };
1300
1302
  const o = spec;
1301
1303
  const role = typeof o.role === "string" && o.role.trim() !== "" ? o.role.trim() : void 0;
1304
+ const permission = typeof o.permission === "string" && o.permission.trim() !== "" ? o.permission.trim() : void 0;
1302
1305
  return {
1303
1306
  required: o.required !== false,
1304
1307
  role,
1308
+ permission,
1305
1309
  verifiedEmail: o.verifiedEmail === true
1306
1310
  };
1307
1311
  }
@@ -2289,6 +2293,26 @@ function buildValueTuple(table, cols, data, add, caller) {
2289
2293
  }).join(", ");
2290
2294
  }
2291
2295
  __name(buildValueTuple, "buildValueTuple");
2296
+ function columnTypeOf(table, col) {
2297
+ const def = tableDefOf(table);
2298
+ const c = def?.columns?.[col];
2299
+ if (c === void 0 || c === null) return null;
2300
+ const d = typeof c === "object" && "_def" in c ? c._def : c;
2301
+ return d !== null && typeof d === "object" && typeof d.type === "string" ? d.type : null;
2302
+ }
2303
+ __name(columnTypeOf, "columnTypeOf");
2304
+ var NUMERIC_PG_TYPES = /* @__PURE__ */ new Set([
2305
+ "integer",
2306
+ "bigint",
2307
+ "numeric"
2308
+ ]);
2309
+ function aggregateExpr(table, fn, col, alias) {
2310
+ const q = `t.${quoteIdent(col)}`;
2311
+ const t = columnTypeOf(table, col);
2312
+ if (fn === "sum" || fn === "avg") return `${fn}(${q})::text AS ${quoteIdent(alias)}`;
2313
+ return t !== null && NUMERIC_PG_TYPES.has(t) && t !== "integer" ? `${fn}(${q})::text AS ${quoteIdent(alias)}` : `${fn}(${q}) AS ${quoteIdent(alias)}`;
2314
+ }
2315
+ __name(aggregateExpr, "aggregateExpr");
2292
2316
  function createOps(tx) {
2293
2317
  const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
2294
2318
  const ops = {
@@ -2569,7 +2593,7 @@ function createOps(tx) {
2569
2593
  * should not silently succeed. Callers who mean every row say so with a
2570
2594
  * predicate that is true for every row.
2571
2595
  */
2572
- async updateMany(table, where, set) {
2596
+ async updateMany(table, where, set, opts) {
2573
2597
  assertNotAppendOnly("updateMany", table);
2574
2598
  const cols = Object.keys(set);
2575
2599
  if (cols.length === 0) {
@@ -2592,8 +2616,15 @@ function createOps(tx) {
2592
2616
  const whereSql = compileWhere(table, known, where, add, "updateMany");
2593
2617
  assertHasPredicate(whereSql, "updateMany", table);
2594
2618
  const pk = primaryKeyOf(table);
2595
- 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.*`;
2596
- return asTableRows(table, await (await at()).unsafe(sql, params));
2619
+ const wantRows = opts?.returning !== false;
2620
+ const tail = wantRows ? "RETURNING *" : "RETURNING 1";
2621
+ const simple = isSinglePkEquality(where, pk) || pk === null;
2622
+ 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");
2623
+ 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) `;
2624
+ 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`;
2625
+ const out = await (await at()).unsafe(sql, params);
2626
+ if (!wantRows) return Number(out[0]?.["n"] ?? 0);
2627
+ return asTableRows(table, out);
2597
2628
  },
2598
2629
  /**
2599
2630
  * Delete every row the filter matches, in ONE statement; resolves to how
@@ -2623,6 +2654,78 @@ function createOps(tx) {
2623
2654
  * An empty filter is legitimate HERE: counting a whole table is a read, and
2624
2655
  * reads do not destroy anything.
2625
2656
  */
2657
+ /**
2658
+ * Toplama — `count`'un yanına `sum`/`avg`/`min`/`max` ve `groupBy`.
2659
+ *
2660
+ * Var olma nedeni: bunlar olmadan "bu hesabın toplamı" ya `$query`'ye ya da
2661
+ * TÜM satırları çekip JS'te toplamaya düşüyor. İkincisi hem N+1 hem de para
2662
+ * için YANLIŞ — `numeric` JS `number`'ına uğradığı anda kesinliğini
2663
+ * kaybeder.
2664
+ *
2665
+ * `groupBy` verilirse SATIRLAR döner, verilmezse TEK sonuç. Boş kümede
2666
+ * Postgres NULL döndürür, 0 değil — ve bu SDK onu `null` olarak geçiriyor:
2667
+ * "hiç satır yoktu" ile "toplam sıfırdı" farklı şeyler.
2668
+ */
2669
+ async aggregate(table, q = {}) {
2670
+ const known = schemaColumns(table);
2671
+ const check = /* @__PURE__ */ __name((col, where) => {
2672
+ if (known !== null && !known.has(col)) {
2673
+ throw new Error(`aggregate(${table}): ${where} kolonu "${col}" tabloda yok (FR-016)`);
2674
+ }
2675
+ }, "check");
2676
+ const parts = [];
2677
+ const wants = [];
2678
+ for (const fn of [
2679
+ "sum",
2680
+ "avg",
2681
+ "min",
2682
+ "max"
2683
+ ]) {
2684
+ for (const col of q[fn] ?? []) {
2685
+ check(col, fn);
2686
+ if (fn === "sum" || fn === "avg") {
2687
+ const t = columnTypeOf(table, col);
2688
+ if (t !== null && !NUMERIC_PG_TYPES.has(t)) {
2689
+ 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.`);
2690
+ }
2691
+ }
2692
+ const alias = `${fn}__${col}`;
2693
+ wants.push({
2694
+ fn,
2695
+ col,
2696
+ alias
2697
+ });
2698
+ parts.push(aggregateExpr(table, fn, col, alias));
2699
+ }
2700
+ }
2701
+ if (q.count === true) parts.push('count(*) AS "_count"');
2702
+ if (parts.length === 0) {
2703
+ 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.`);
2704
+ }
2705
+ const groups = q.groupBy ?? [];
2706
+ for (const col of groups) check(col, "groupBy");
2707
+ const groupSel = groups.map((c) => `t.${quoteIdent(c)} AS ${quoteIdent(`g__${c}`)}`);
2708
+ const params = [];
2709
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
2710
+ const whereSql = compileWhere(table, known, q.where ?? {}, add, "aggregate");
2711
+ const groupSql = groups.length > 0 ? ` GROUP BY ${groups.map((c) => `t.${quoteIdent(c)}`).join(", ")}` : "";
2712
+ const sql = `SELECT ${[
2713
+ ...groupSel,
2714
+ ...parts
2715
+ ].join(", ")} FROM ${quoteTable(table)} t WHERE true${whereSql}${groupSql}`;
2716
+ const rows = await (await at()).unsafe(sql, params);
2717
+ const shape = /* @__PURE__ */ __name((r) => {
2718
+ const out = {};
2719
+ for (const c of groups) out[c] = r[`g__${c}`];
2720
+ for (const w of wants) {
2721
+ const bucket = out[w.fn] ??= {};
2722
+ bucket[w.col] = r[w.alias] ?? null;
2723
+ }
2724
+ if (q.count === true) out["count"] = Number(r["_count"] ?? 0);
2725
+ return out;
2726
+ }, "shape");
2727
+ return groups.length > 0 ? rows.map(shape) : shape(rows[0] ?? {});
2728
+ },
2626
2729
  async count(table, where = {}) {
2627
2730
  const params = [];
2628
2731
  const add = /* @__PURE__ */ __name((v) => {
@@ -4411,6 +4514,26 @@ var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
4411
4514
  409,
4412
4515
  429
4413
4516
  ]);
4517
+ async function refuseUnlessAuthorized(client, spec, requestId) {
4518
+ if (spec.permission !== void 0) {
4519
+ const [row] = await client.query("select auth.has_permission($1) as ok", [
4520
+ spec.permission
4521
+ ]);
4522
+ if (row?.ok !== true) {
4523
+ return envelope("forbidden", `This endpoint requires the "${spec.permission}" permission`, 403, requestId);
4524
+ }
4525
+ }
4526
+ if (spec.role !== void 0) {
4527
+ const [row] = await client.query("select $1 = any(auth.app_roles()) as ok", [
4528
+ spec.role
4529
+ ]);
4530
+ if (row?.ok !== true) {
4531
+ return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4532
+ }
4533
+ }
4534
+ return null;
4535
+ }
4536
+ __name(refuseUnlessAuthorized, "refuseUnlessAuthorized");
4414
4537
  async function createApp(opts) {
4415
4538
  const { config, controllers } = opts;
4416
4539
  const container = buildContainer();
@@ -4502,8 +4625,23 @@ async function createApp(opts) {
4502
4625
  if (spec.required && !callerClaims) {
4503
4626
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
4504
4627
  }
4505
- if (callerClaims && spec.role && callerClaims.role !== spec.role) {
4506
- return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4628
+ if (callerClaims) {
4629
+ const uploadDb = createRequestDatabase(sql, {
4630
+ role: config.dbRole,
4631
+ serviceRole: config.dbServiceRole,
4632
+ claimsJson: JSON.stringify(callerClaims)
4633
+ });
4634
+ try {
4635
+ const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId);
4636
+ if (refusal) {
4637
+ await uploadDb.commit();
4638
+ return refusal;
4639
+ }
4640
+ await uploadDb.commit();
4641
+ } catch (err) {
4642
+ await uploadDb.rollback(err);
4643
+ throw err;
4644
+ }
4507
4645
  }
4508
4646
  const grant = grantFor(target?.entry, {
4509
4647
  userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
@@ -4536,8 +4674,14 @@ async function createApp(opts) {
4536
4674
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
4537
4675
  }
4538
4676
  const userId = typeof claims?.sub === "string" ? claims.sub : void 0;
4539
- if (claims && spec.role && claims.role !== spec.role) {
4540
- return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4677
+ const db = createRequestDatabase(sql, {
4678
+ role: config.dbRole,
4679
+ serviceRole: config.dbServiceRole,
4680
+ claimsJson: JSON.stringify(claims ?? {})
4681
+ });
4682
+ if (claims) {
4683
+ const refusal = await refuseUnlessAuthorized(db.client, spec, requestId);
4684
+ if (refusal) return refusal;
4541
4685
  }
4542
4686
  if (claims && spec.verifiedEmail && claims.email_verified !== true) {
4543
4687
  return envelope("email_not_verified", "A verified email address is required", 403, requestId);
@@ -4758,11 +4902,6 @@ async function createApp(opts) {
4758
4902
  args[p.index] = void 0;
4759
4903
  }
4760
4904
  }
4761
- const db = createRequestDatabase(sql, {
4762
- role: config.dbRole,
4763
- serviceRole: config.dbServiceRole,
4764
- claimsJson: JSON.stringify(claims ?? {})
4765
- });
4766
4905
  try {
4767
4906
  const services = buildServices(db);
4768
4907
  if (meta.options?.sseConfig !== void 0) {