@palbase/backend 28.0.0 → 30.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 (49) hide show
  1. package/dist/bin/palbase-backend.cjs +166 -12
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +3 -3
  4. package/dist/{chunk-RVP6BTEZ.js → chunk-5J5W75A6.js} +15 -4
  5. package/dist/chunk-5J5W75A6.js.map +1 -0
  6. package/dist/{chunk-IVZERLTM.js → chunk-ENZ2RFFJ.js} +2 -2
  7. package/dist/{chunk-SNDXY565.js → chunk-SG4UTNOP.js} +5 -2
  8. package/dist/chunk-SG4UTNOP.js.map +1 -0
  9. package/dist/{chunk-75YROPRZ.js → chunk-VN7NMDUH.js} +166 -15
  10. package/dist/chunk-VN7NMDUH.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 +166 -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-8qy3kIuA.d.ts} +228 -12
  22. package/dist/{index-DtISj9QX.d.cts → index-DtCgaZAg.d.cts} +228 -12
  23. package/dist/{index-9C3JHxg-.d.cts → index-Zi7MptvP.d.cts} +30 -8
  24. package/dist/{index-BbvOoZFr.d.ts → index-b-Q3l7W5.d.ts} +30 -8
  25. package/dist/index.cjs +66 -3
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +51 -11
  28. package/dist/index.d.ts +51 -11
  29. package/dist/index.js +52 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/openapi/index.d.cts +2 -2
  32. package/dist/openapi/index.d.ts +2 -2
  33. package/dist/{registry-JQNIX-eA.d.ts → registry-BGJ-Al6F.d.ts} +1 -1
  34. package/dist/{registry-qIPM5BQe.d.cts → registry-CEYLH-Iz.d.cts} +1 -1
  35. package/dist/test/index.cjs +56 -0
  36. package/dist/test/index.cjs.map +1 -1
  37. package/dist/test/index.d.cts +1 -1
  38. package/dist/test/index.d.ts +1 -1
  39. package/dist/test/index.js +56 -0
  40. package/dist/test/index.js.map +1 -1
  41. package/docs/README.md +1 -1
  42. package/docs/database.md +49 -0
  43. package/docs/llms-full.txt +50 -1
  44. package/package.json +1 -1
  45. package/template/package.json +1 -1
  46. package/dist/chunk-75YROPRZ.js.map +0 -1
  47. package/dist/chunk-RVP6BTEZ.js.map +0 -1
  48. package/dist/chunk-SNDXY565.js.map +0 -1
  49. /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
  };
@@ -1054,6 +1056,7 @@ var Storage = Object.assign({
1054
1056
  });
1055
1057
  var Cache = makeServiceProxy("Cache");
1056
1058
  var Secrets = makeServiceProxy("Secrets");
1059
+ var Auth = makeServiceProxy("Auth");
1057
1060
  var Log = makeServiceProxy("Log");
1058
1061
  var Notifications = makeServiceProxy("Notifications");
1059
1062
  var rawFlags = makeServiceProxy("Flags");
@@ -1299,9 +1302,11 @@ function effectiveAuth(routeAuth, controllerAuth) {
1299
1302
  };
1300
1303
  const o = spec;
1301
1304
  const role = typeof o.role === "string" && o.role.trim() !== "" ? o.role.trim() : void 0;
1305
+ const permission = typeof o.permission === "string" && o.permission.trim() !== "" ? o.permission.trim() : void 0;
1302
1306
  return {
1303
1307
  required: o.required !== false,
1304
1308
  role,
1309
+ permission,
1305
1310
  verifiedEmail: o.verifiedEmail === true
1306
1311
  };
1307
1312
  }
@@ -2289,6 +2294,26 @@ function buildValueTuple(table, cols, data, add, caller) {
2289
2294
  }).join(", ");
2290
2295
  }
2291
2296
  __name(buildValueTuple, "buildValueTuple");
2297
+ function columnTypeOf(table, col) {
2298
+ const def = tableDefOf(table);
2299
+ const c = def?.columns?.[col];
2300
+ if (c === void 0 || c === null) return null;
2301
+ const d = typeof c === "object" && "_def" in c ? c._def : c;
2302
+ return d !== null && typeof d === "object" && typeof d.type === "string" ? d.type : null;
2303
+ }
2304
+ __name(columnTypeOf, "columnTypeOf");
2305
+ var NUMERIC_PG_TYPES = /* @__PURE__ */ new Set([
2306
+ "integer",
2307
+ "bigint",
2308
+ "numeric"
2309
+ ]);
2310
+ function aggregateExpr(table, fn, col, alias) {
2311
+ const q = `t.${quoteIdent(col)}`;
2312
+ const t = columnTypeOf(table, col);
2313
+ if (fn === "sum" || fn === "avg") return `${fn}(${q})::text AS ${quoteIdent(alias)}`;
2314
+ return t !== null && NUMERIC_PG_TYPES.has(t) && t !== "integer" ? `${fn}(${q})::text AS ${quoteIdent(alias)}` : `${fn}(${q}) AS ${quoteIdent(alias)}`;
2315
+ }
2316
+ __name(aggregateExpr, "aggregateExpr");
2292
2317
  function createOps(tx) {
2293
2318
  const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
2294
2319
  const ops = {
@@ -2569,7 +2594,7 @@ function createOps(tx) {
2569
2594
  * should not silently succeed. Callers who mean every row say so with a
2570
2595
  * predicate that is true for every row.
2571
2596
  */
2572
- async updateMany(table, where, set) {
2597
+ async updateMany(table, where, set, opts) {
2573
2598
  assertNotAppendOnly("updateMany", table);
2574
2599
  const cols = Object.keys(set);
2575
2600
  if (cols.length === 0) {
@@ -2592,8 +2617,15 @@ function createOps(tx) {
2592
2617
  const whereSql = compileWhere(table, known, where, add, "updateMany");
2593
2618
  assertHasPredicate(whereSql, "updateMany", table);
2594
2619
  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));
2620
+ const wantRows = opts?.returning !== false;
2621
+ const tail = wantRows ? "RETURNING *" : "RETURNING 1";
2622
+ const simple = isSinglePkEquality(where, pk) || pk === null;
2623
+ 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");
2624
+ 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) `;
2625
+ 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`;
2626
+ const out = await (await at()).unsafe(sql, params);
2627
+ if (!wantRows) return Number(out[0]?.["n"] ?? 0);
2628
+ return asTableRows(table, out);
2597
2629
  },
2598
2630
  /**
2599
2631
  * Delete every row the filter matches, in ONE statement; resolves to how
@@ -2623,6 +2655,78 @@ function createOps(tx) {
2623
2655
  * An empty filter is legitimate HERE: counting a whole table is a read, and
2624
2656
  * reads do not destroy anything.
2625
2657
  */
2658
+ /**
2659
+ * Toplama — `count`'un yanına `sum`/`avg`/`min`/`max` ve `groupBy`.
2660
+ *
2661
+ * Var olma nedeni: bunlar olmadan "bu hesabın toplamı" ya `$query`'ye ya da
2662
+ * TÜM satırları çekip JS'te toplamaya düşüyor. İkincisi hem N+1 hem de para
2663
+ * için YANLIŞ — `numeric` JS `number`'ına uğradığı anda kesinliğini
2664
+ * kaybeder.
2665
+ *
2666
+ * `groupBy` verilirse SATIRLAR döner, verilmezse TEK sonuç. Boş kümede
2667
+ * Postgres NULL döndürür, 0 değil — ve bu SDK onu `null` olarak geçiriyor:
2668
+ * "hiç satır yoktu" ile "toplam sıfırdı" farklı şeyler.
2669
+ */
2670
+ async aggregate(table, q = {}) {
2671
+ const known = schemaColumns(table);
2672
+ const check = /* @__PURE__ */ __name((col, where) => {
2673
+ if (known !== null && !known.has(col)) {
2674
+ throw new Error(`aggregate(${table}): ${where} kolonu "${col}" tabloda yok (FR-016)`);
2675
+ }
2676
+ }, "check");
2677
+ const parts = [];
2678
+ const wants = [];
2679
+ for (const fn of [
2680
+ "sum",
2681
+ "avg",
2682
+ "min",
2683
+ "max"
2684
+ ]) {
2685
+ for (const col of q[fn] ?? []) {
2686
+ check(col, fn);
2687
+ if (fn === "sum" || fn === "avg") {
2688
+ const t = columnTypeOf(table, col);
2689
+ if (t !== null && !NUMERIC_PG_TYPES.has(t)) {
2690
+ 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.`);
2691
+ }
2692
+ }
2693
+ const alias = `${fn}__${col}`;
2694
+ wants.push({
2695
+ fn,
2696
+ col,
2697
+ alias
2698
+ });
2699
+ parts.push(aggregateExpr(table, fn, col, alias));
2700
+ }
2701
+ }
2702
+ if (q.count === true) parts.push('count(*) AS "_count"');
2703
+ if (parts.length === 0) {
2704
+ 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.`);
2705
+ }
2706
+ const groups = q.groupBy ?? [];
2707
+ for (const col of groups) check(col, "groupBy");
2708
+ const groupSel = groups.map((c) => `t.${quoteIdent(c)} AS ${quoteIdent(`g__${c}`)}`);
2709
+ const params = [];
2710
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
2711
+ const whereSql = compileWhere(table, known, q.where ?? {}, add, "aggregate");
2712
+ const groupSql = groups.length > 0 ? ` GROUP BY ${groups.map((c) => `t.${quoteIdent(c)}`).join(", ")}` : "";
2713
+ const sql = `SELECT ${[
2714
+ ...groupSel,
2715
+ ...parts
2716
+ ].join(", ")} FROM ${quoteTable(table)} t WHERE true${whereSql}${groupSql}`;
2717
+ const rows = await (await at()).unsafe(sql, params);
2718
+ const shape = /* @__PURE__ */ __name((r) => {
2719
+ const out = {};
2720
+ for (const c of groups) out[c] = r[`g__${c}`];
2721
+ for (const w of wants) {
2722
+ const bucket = out[w.fn] ??= {};
2723
+ bucket[w.col] = r[w.alias] ?? null;
2724
+ }
2725
+ if (q.count === true) out["count"] = Number(r["_count"] ?? 0);
2726
+ return out;
2727
+ }, "shape");
2728
+ return groups.length > 0 ? rows.map(shape) : shape(rows[0] ?? {});
2729
+ },
2626
2730
  async count(table, where = {}) {
2627
2731
  const params = [];
2628
2732
  const add = /* @__PURE__ */ __name((v) => {
@@ -4411,6 +4515,32 @@ var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
4411
4515
  409,
4412
4516
  429
4413
4517
  ]);
4518
+ async function refuseUnlessAuthorized(client, spec, requestId, rolesOfCaller) {
4519
+ if (spec.permission !== void 0) {
4520
+ const [row] = await client.query("select auth.has_permission($1) as ok", [
4521
+ spec.permission
4522
+ ]);
4523
+ if (row?.ok !== true) {
4524
+ return envelope("forbidden", `This endpoint requires the "${spec.permission}" permission`, 403, requestId);
4525
+ }
4526
+ }
4527
+ if (spec.role !== void 0) {
4528
+ const held = await rolesOfCaller();
4529
+ if (!held.includes(spec.role)) {
4530
+ return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4531
+ }
4532
+ }
4533
+ return null;
4534
+ }
4535
+ __name(refuseUnlessAuthorized, "refuseUnlessAuthorized");
4536
+ function makeRoleResolver(client) {
4537
+ let pending = null;
4538
+ return () => {
4539
+ pending ??= client.query("select unnest(auth.app_roles()) as role").then((rows) => rows.map((r) => r.role));
4540
+ return pending;
4541
+ };
4542
+ }
4543
+ __name(makeRoleResolver, "makeRoleResolver");
4414
4544
  async function createApp(opts) {
4415
4545
  const { config, controllers } = opts;
4416
4546
  const container = buildContainer();
@@ -4459,6 +4589,7 @@ async function createApp(opts) {
4459
4589
  Database: db?.client ?? stubModule("Database (a start hook runs before any request \u2014 open your own connection here)"),
4460
4590
  Cache: cache,
4461
4591
  Log: log,
4592
+ Auth: modules.Auth ?? stubModule("Auth"),
4462
4593
  Documents: modules.Documents ?? stubModule("Documents"),
4463
4594
  Storage: modules.Storage ?? stubModule("Storage"),
4464
4595
  Notifications: modules.Notifications ?? stubModule("Notifications"),
@@ -4502,8 +4633,23 @@ async function createApp(opts) {
4502
4633
  if (spec.required && !callerClaims) {
4503
4634
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
4504
4635
  }
4505
- if (callerClaims && spec.role && callerClaims.role !== spec.role) {
4506
- return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4636
+ if (callerClaims) {
4637
+ const uploadDb = createRequestDatabase(sql, {
4638
+ role: config.dbRole,
4639
+ serviceRole: config.dbServiceRole,
4640
+ claimsJson: JSON.stringify(callerClaims)
4641
+ });
4642
+ try {
4643
+ const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
4644
+ if (refusal) {
4645
+ await uploadDb.commit();
4646
+ return refusal;
4647
+ }
4648
+ await uploadDb.commit();
4649
+ } catch (err) {
4650
+ await uploadDb.rollback(err);
4651
+ throw err;
4652
+ }
4507
4653
  }
4508
4654
  const grant = grantFor(target?.entry, {
4509
4655
  userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
@@ -4536,8 +4682,15 @@ async function createApp(opts) {
4536
4682
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
4537
4683
  }
4538
4684
  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);
4685
+ const db = createRequestDatabase(sql, {
4686
+ role: config.dbRole,
4687
+ serviceRole: config.dbServiceRole,
4688
+ claimsJson: JSON.stringify(claims ?? {})
4689
+ });
4690
+ const rolesOfCaller = makeRoleResolver(db.client);
4691
+ if (claims) {
4692
+ const refusal = await refuseUnlessAuthorized(db.client, spec, requestId, rolesOfCaller);
4693
+ if (refusal) return refusal;
4541
4694
  }
4542
4695
  if (claims && spec.verifiedEmail && claims.email_verified !== true) {
4543
4696
  return envelope("email_not_verified", "A verified email address is required", 403, requestId);
@@ -4689,6 +4842,12 @@ async function createApp(opts) {
4689
4842
  role: claims.role,
4690
4843
  emailVerified: claims.email_verified === true,
4691
4844
  metadata: claims.metadata ?? {},
4845
+ // The caller's APPLICATION roles, from `auth.user_roles` — the
4846
+ // same resolution the gate used, so the handler cannot read a
4847
+ // different set than the one it was admitted on. Never from a
4848
+ // claim: a revoked role has to bite on the next request, and a
4849
+ // token says only what was true when it was minted.
4850
+ roles: await rolesOfCaller(),
4692
4851
  // Server-owned and only ever filled from a verdict palauth
4693
4852
  // returned. Never from a raw header, never from JWT metadata —
4694
4853
  // the type has said so since it was written and this is the
@@ -4758,11 +4917,6 @@ async function createApp(opts) {
4758
4917
  args[p.index] = void 0;
4759
4918
  }
4760
4919
  }
4761
- const db = createRequestDatabase(sql, {
4762
- role: config.dbRole,
4763
- serviceRole: config.dbServiceRole,
4764
- claimsJson: JSON.stringify(claims ?? {})
4765
- });
4766
4920
  try {
4767
4921
  const services = buildServices(db);
4768
4922
  if (meta.options?.sseConfig !== void 0) {