@palbase/backend 27.1.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 (52) hide show
  1. package/dist/bin/palbase-backend.cjs +284 -50
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +4 -4
  4. package/dist/{chunk-JVZQCC77.js → chunk-3TUJWHC2.js} +8 -3
  5. package/dist/chunk-3TUJWHC2.js.map +1 -0
  6. package/dist/{chunk-EB3TUX5J.js → chunk-DHJ3SYAE.js} +251 -50
  7. package/dist/chunk-DHJ3SYAE.js.map +1 -0
  8. package/dist/{chunk-XABHGMUT.js → chunk-ENZ2RFFJ.js} +2 -2
  9. package/dist/{chunk-BQN723PL.js → chunk-ESSQ3YML.js} +73 -7
  10. package/dist/chunk-ESSQ3YML.js.map +1 -0
  11. package/dist/{chunk-OZKSM3JW.js → chunk-SG4UTNOP.js} +7 -3
  12. package/dist/chunk-SG4UTNOP.js.map +1 -0
  13. package/dist/db/index.cjs +8 -2
  14. package/dist/db/index.cjs.map +1 -1
  15. package/dist/db/index.d.cts +1 -1
  16. package/dist/db/index.d.ts +1 -1
  17. package/dist/db/index.js +3 -3
  18. package/dist/engine/index.cjs +294 -50
  19. package/dist/engine/index.cjs.map +1 -1
  20. package/dist/engine/index.d.cts +3 -3
  21. package/dist/engine/index.d.ts +3 -3
  22. package/dist/engine/index.js +11 -4
  23. package/dist/{index-V7QRh1wg.d.ts → index-BOe82fsK.d.ts} +256 -19
  24. package/dist/{index-CAKOgAlP.d.ts → index-BYDUKexK.d.ts} +139 -10
  25. package/dist/{index-H-0qv5d4.d.cts → index-BZfmdRmh.d.cts} +139 -10
  26. package/dist/{index-CgE4sVhg.d.cts → index-ChvoVczS.d.cts} +256 -19
  27. package/dist/index.cjs +81 -6
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +6 -22
  30. package/dist/index.d.ts +6 -22
  31. package/dist/index.js +10 -4
  32. package/dist/index.js.map +1 -1
  33. package/dist/openapi/index.d.cts +2 -2
  34. package/dist/openapi/index.d.ts +2 -2
  35. package/dist/{registry-DHsPDY0_.d.cts → registry-B2KfKWv1.d.cts} +1 -1
  36. package/dist/{registry-4EI8aaFs.d.ts → registry-DaqHsOq8.d.ts} +1 -1
  37. package/dist/test/index.cjs +129 -6
  38. package/dist/test/index.cjs.map +1 -1
  39. package/dist/test/index.d.cts +1 -1
  40. package/dist/test/index.d.ts +1 -1
  41. package/dist/test/index.js +126 -6
  42. package/dist/test/index.js.map +1 -1
  43. package/docs/README.md +1 -1
  44. package/docs/database.md +118 -1
  45. package/docs/llms-full.txt +119 -2
  46. package/package.json +1 -1
  47. package/template/package.json +1 -1
  48. package/dist/chunk-BQN723PL.js.map +0 -1
  49. package/dist/chunk-EB3TUX5J.js.map +0 -1
  50. package/dist/chunk-JVZQCC77.js.map +0 -1
  51. package/dist/chunk-OZKSM3JW.js.map +0 -1
  52. /package/dist/{chunk-XABHGMUT.js.map → chunk-ENZ2RFFJ.js.map} +0 -0
@@ -1,14 +1,15 @@
1
1
  import {
2
+ DeclarationRefused,
2
3
  __requestALS,
3
4
  __runStartHooks,
4
5
  __runWithRuntime,
5
6
  assertNoOrphanEntryPoints,
6
7
  buildContainer,
7
8
  buildRelations
8
- } from "./chunk-BQN723PL.js";
9
+ } from "./chunk-ESSQ3YML.js";
9
10
  import {
10
11
  qualifiedTableKey
11
- } from "./chunk-OZKSM3JW.js";
12
+ } from "./chunk-SG4UTNOP.js";
12
13
  import {
13
14
  assertNoExpressionHandles,
14
15
  assertUsableFilter,
@@ -21,7 +22,7 @@ import {
21
22
  isSqlFragment,
22
23
  looksLikeUnbrandedPlanRef,
23
24
  looksLikeUnbrandedRef
24
- } from "./chunk-JVZQCC77.js";
25
+ } from "./chunk-3TUJWHC2.js";
25
26
  import {
26
27
  getRoutes,
27
28
  resolveEffectiveAuth
@@ -227,9 +228,11 @@ function effectiveAuth(routeAuth, controllerAuth) {
227
228
  };
228
229
  const o = spec;
229
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;
230
232
  return {
231
233
  required: o.required !== false,
232
234
  role,
235
+ permission,
233
236
  verifiedEmail: o.verifiedEmail === true
234
237
  };
235
238
  }
@@ -919,7 +922,7 @@ function compileWhereBare(table, colSet, where, add, caller = "search", alias =
919
922
  const rel = rels.find((r) => r.name === relName);
920
923
  if (rel === void 0) {
921
924
  const known = rels.map((r) => r.name).sort();
922
- throw new Error(`${caller}(${table}): has.${relName} diye bir ili\u015Fki yok${known.length > 0 ? ` (bilinenler: ${known.join(", ")})` : " \u2014 bu tablonun hi\xE7 ili\u015Fkisi yok"}. \u0130li\u015Fkiler yabanc\u0131 anahtarlardan T\xDCRET\u0130L\u0130R; ad\u0131n\u0131 de\u011Fi\u015Ftirmek i\xE7in references(..., { as, reverseAs }).`);
925
+ throw new Error(`${caller}(${table}): has.${relName} diye bir ili\u015Fki yok` + (known.length > 0 ? ` (bilinenler: ${known.join(", ")}). \u0130li\u015Fkiler yabanc\u0131 anahtarlardan T\xDCRET\u0130L\u0130R; ad\u0131n\u0131 de\u011Fi\u015Ftirmek i\xE7in references(..., { as, reverseAs }).` : `. Bu tablo H\u0130\xC7 yabanc\u0131 anahtar bildirmiyor, ve ili\u015Fkiler onlardan T\xDCRET\u0130L\u0130R. Var olan bir veritaban\u0131ndan introspect edilmi\u015F \u015Femalarda bu s\u0131k olur: kolon \`text()\`/\`uuid()\` olarak durur, \`references(() => <tablo>.<kolon>)\` yoktur. Bildirdi\u011Finiz anda ili\u015Fki iki tarafta da adland\u0131r\u0131l\u0131r \u2014 ama bu CANLI veritaban\u0131na giden bir k\u0131s\u0131tt\u0131r ve mevcut yetim sat\u0131rlar onu d\xFC\u015F\xFCr\xFCr; \xF6nce onlar\u0131 say\u0131n.`));
923
926
  }
924
927
  const ourCol = rel.kind === "many" ? primaryKeyOf(table) : rel.via;
925
928
  const theirCol = rel.kind === "many" ? rel.via : primaryKeyOf(rel.to);
@@ -1099,6 +1102,57 @@ async function embedQuery(embed, text) {
1099
1102
  }
1100
1103
  }
1101
1104
  __name(embedQuery, "embedQuery");
1105
+ function buildSetAssignments(table, cols, set, add, caller) {
1106
+ return cols.map((c) => {
1107
+ const expr = columnExprOf(set[c]);
1108
+ if (expr !== null && expr.fn === "now") {
1109
+ return `${quoteIdent(c)} = now()`;
1110
+ }
1111
+ if (expr !== null) {
1112
+ const operator = expr.fn === "inc" ? "+" : "-";
1113
+ const t = transformsOf(currentSchema, table).get(c);
1114
+ const amount = t?.toDb === void 0 || expr.by === null || expr.by === void 0 ? expr.by : t.toDb(expr.by);
1115
+ return `${quoteIdent(c)} = ${quoteIdent(c)} ${operator} ${add(amount)}`;
1116
+ }
1117
+ return `${quoteIdent(c)} = ${add(asBindParams(table, [
1118
+ c
1119
+ ], set, caller)[0])}`;
1120
+ }).join(", ");
1121
+ }
1122
+ __name(buildSetAssignments, "buildSetAssignments");
1123
+ function buildValueTuple(table, cols, data, add, caller) {
1124
+ return cols.map((c) => {
1125
+ const expr = columnExprOf(data[c]);
1126
+ if (expr !== null && expr.fn === "now") return "now()";
1127
+ if (expr !== null) {
1128
+ throw new Error(`${caller}(${table}): "${c}" ${expr.fn === "inc" ? "increment()" : "decrement()"} ald\u0131 \u2014 sat\u0131r HEN\xDCZ YOK, yani "kolonun \u015Fu anki de\u011Feri" diye bir \u015Fey yok. Ba\u015Flang\u0131\xE7 de\u011Ferini yaz\u0131n; art\u0131rmak i\xE7in sat\u0131r olu\u015Ftuktan sonra updateMany({ where, set: { ${c}: ${expr.fn === "inc" ? "increment" : "decrement"}(n) } }).`);
1129
+ }
1130
+ return add(asBindParams(table, [
1131
+ c
1132
+ ], data, caller)[0]);
1133
+ }).join(", ");
1134
+ }
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");
1102
1156
  function createOps(tx) {
1103
1157
  const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
1104
1158
  const ops = {
@@ -1112,9 +1166,11 @@ function createOps(tx) {
1112
1166
  async insert(table, data) {
1113
1167
  const cols = Object.keys(data);
1114
1168
  if (cols.length === 0) throw new Error(`insert into ${table}: no columns given`);
1115
- const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1116
- const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`;
1117
- const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data, "insert"));
1169
+ const params = [];
1170
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1171
+ const tuple = buildValueTuple(table, cols, data, add, "insert");
1172
+ const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`;
1173
+ const rows = await (await at()).unsafe(sql, params);
1118
1174
  const inserted = rows[0];
1119
1175
  if (!inserted) {
1120
1176
  throw new Error(`insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`);
@@ -1122,6 +1178,41 @@ function createOps(tx) {
1122
1178
  return asTableRow(table, inserted);
1123
1179
  },
1124
1180
  /**
1181
+ * Çok satırı TEK statement'la ekle.
1182
+ *
1183
+ * Motor bunu zaten yapıyordu ve yalnız PLAN yolundan erişilebiliyordu
1184
+ * (`tx.public.x.insertMany`). Dışarıda tek kapı `insert(row)` olduğu için
1185
+ * "şu 200 satırı yaz" ya 200 gidiş-dönüş ya da ham SQL oluyordu — ölçüldü:
1186
+ * bir müşteri projesinde bir seed rutini satırları tek tek siliyor
1187
+ * (`deleteMany` VARKEN) ve tek tek ekliyor.
1188
+ *
1189
+ * SATIRLARIN ANAHTAR KÜMESİ AYNI OLMAK ZORUNDA. Kolon listesi tek bir
1190
+ * statement'ta paylaşıldığı için ilk satırdan türer; farklı anahtar taşıyan
1191
+ * bir satırın fazla kolonu SESSİZCE YAZILMAZDI. Eksik kolon için `null`
1192
+ * yazın ya da varsayılanı istiyorsanız o satırları ayrı çağrıda ekleyin.
1193
+ */
1194
+ async insertMany(table, rows, opts) {
1195
+ if (rows.length === 0) return [];
1196
+ const first = rows[0];
1197
+ const cols = Object.keys(first);
1198
+ if (cols.length === 0) throw new Error(`insertMany into ${table}: no columns given`);
1199
+ for (let i = 1; i < rows.length; i++) {
1200
+ const keys = Object.keys(rows[i]);
1201
+ const missing = cols.filter((c) => !(c in rows[i]));
1202
+ const extra = keys.filter((k) => !cols.includes(k));
1203
+ if (missing.length > 0 || extra.length > 0) {
1204
+ throw new Error(`insertMany(${table}): ${i}. sat\u0131r\u0131n kolonlar\u0131 ilk sat\u0131rla ayn\u0131 de\u011Fil` + (missing.length > 0 ? ` (eksik: ${missing.join(", ")})` : "") + (extra.length > 0 ? ` (fazla: ${extra.join(", ")})` : "") + `. Tek statement kolon listesini PAYLA\u015EIR, yani fazla kolon sessizce yaz\u0131lmaz ve eksik kolon yanl\u0131\u015F s\xFCtuna kayar. Eksikler i\xE7in null yaz\u0131n, ya da farkl\u0131 \u015Fekilli sat\u0131rlar\u0131 ayr\u0131 \xE7a\u011Fr\u0131larda ekleyin.`);
1205
+ }
1206
+ }
1207
+ const params = [];
1208
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1209
+ const tuples = rows.map((r) => `(${buildValueTuple(table, cols, r, add, "insertMany")})`);
1210
+ const tail = opts !== void 0 && opts.onConflict.length > 0 ? ` ${onConflictTail(cols, opts.onConflict, opts.action ?? "ignore")}` : "";
1211
+ const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
1212
+ const out = await (await at()).unsafe(sql, params);
1213
+ return out.map((r) => asTableRow(table, r));
1214
+ },
1215
+ /**
1125
1216
  * Bir ADI kilitle — satırı değil (FR-054).
1126
1217
  *
1127
1218
  * "Aynı anda tek bir fatura kapanışı koşsun" gibi, kilitlenecek satırı
@@ -1207,8 +1298,10 @@ function createOps(tx) {
1207
1298
  const live = await at();
1208
1299
  try {
1209
1300
  return await live.savepoint(async (sp) => {
1210
- const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1211
- const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`, asBindParams(table, cols, data, "claim"));
1301
+ const params = [];
1302
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1303
+ const tuple = buildValueTuple(table, cols, data, add, "claim");
1304
+ const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`, params);
1212
1305
  if (!rows[0]) {
1213
1306
  throw new Error(`claim(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si).`);
1214
1307
  }
@@ -1259,10 +1352,11 @@ function createOps(tx) {
1259
1352
  const cols = Object.keys(data);
1260
1353
  if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
1261
1354
  if (searchConfigFor(table)?.validity) {
1262
- const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
1355
+ const bindData = [];
1356
+ const addPut = /* @__PURE__ */ __name((v) => `$${bindData.push(v)}`, "addPut");
1357
+ const ph = buildValueTuple(table, cols, data, addPut, "put");
1263
1358
  const insertSql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING *`;
1264
1359
  const live = await at();
1265
- const bindData = asBindParams(table, cols, data, "put");
1266
1360
  const first = await live.unsafe(insertSql, bindData);
1267
1361
  if (first[0]) return asTableRow(table, first[0]);
1268
1362
  const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
@@ -1287,12 +1381,14 @@ function createOps(tx) {
1287
1381
  ]);
1288
1382
  return asTableRow(table, second[0]);
1289
1383
  }
1290
- const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1384
+ const params = [];
1385
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1386
+ const placeholders = buildValueTuple(table, cols, data, add, "put");
1291
1387
  const conflictSet = new Set(conflict);
1292
1388
  const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
1293
1389
  const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
1294
1390
  const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
1295
- const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data, "put"));
1391
+ const rows = await (await at()).unsafe(sql, params);
1296
1392
  const row = rows[0];
1297
1393
  if (!row) {
1298
1394
  throw new Error(`upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`);
@@ -1303,12 +1399,14 @@ function createOps(tx) {
1303
1399
  assertNotAppendOnly("update", table);
1304
1400
  const cols = Object.keys(data);
1305
1401
  if (cols.length === 0) return ops.findById(table, id);
1306
- const assignments = cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(", ");
1307
- const sql = `UPDATE ${quoteTable(table)} SET ${assignments} WHERE id = $${cols.length + 1} RETURNING *`;
1308
- const rows = await (await at()).unsafe(sql, [
1309
- ...asBindParams(table, cols, data, "update"),
1310
- id
1311
- ]);
1402
+ const params = [];
1403
+ const add = /* @__PURE__ */ __name((v) => {
1404
+ params.push(v);
1405
+ return `$${params.length}`;
1406
+ }, "add");
1407
+ const assignments = buildSetAssignments(table, cols, data, add, "update");
1408
+ const sql = `UPDATE ${quoteTable(table)} SET ${assignments} WHERE id = ${add(id)} RETURNING *`;
1409
+ const rows = await (await at()).unsafe(sql, params);
1312
1410
  return rows[0] ? asTableRow(table, rows[0]) : null;
1313
1411
  },
1314
1412
  async delete(table, id) {
@@ -1335,7 +1433,7 @@ function createOps(tx) {
1335
1433
  * should not silently succeed. Callers who mean every row say so with a
1336
1434
  * predicate that is true for every row.
1337
1435
  */
1338
- async updateMany(table, where, set) {
1436
+ async updateMany(table, where, set, opts) {
1339
1437
  assertNotAppendOnly("updateMany", table);
1340
1438
  const cols = Object.keys(set);
1341
1439
  if (cols.length === 0) {
@@ -1354,26 +1452,19 @@ function createOps(tx) {
1354
1452
  params.push(v);
1355
1453
  return `$${params.length}`;
1356
1454
  }, "add");
1357
- const assignments = cols.map((c) => {
1358
- const expr = columnExprOf(set[c]);
1359
- if (expr !== null && expr.fn === "now") {
1360
- return `${quoteIdent(c)} = now()`;
1361
- }
1362
- if (expr !== null) {
1363
- const operator = expr.fn === "inc" ? "+" : "-";
1364
- const t = transformsOf(currentSchema, table).get(c);
1365
- const amount = t?.toDb === void 0 || expr.by === null || expr.by === void 0 ? expr.by : t.toDb(expr.by);
1366
- return `${quoteIdent(c)} = ${quoteIdent(c)} ${operator} ${add(amount)}`;
1367
- }
1368
- return `${quoteIdent(c)} = ${add(asBindParams(table, [
1369
- c
1370
- ], set, "updateMany")[0])}`;
1371
- }).join(", ");
1455
+ const assignments = buildSetAssignments(table, cols, set, add, "updateMany");
1372
1456
  const whereSql = compileWhere(table, known, where, add, "updateMany");
1373
1457
  assertHasPredicate(whereSql, "updateMany", table);
1374
1458
  const pk = primaryKeyOf(table);
1375
- 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.*`;
1376
- 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);
1377
1468
  },
1378
1469
  /**
1379
1470
  * Delete every row the filter matches, in ONE statement; resolves to how
@@ -1403,6 +1494,78 @@ function createOps(tx) {
1403
1494
  * An empty filter is legitimate HERE: counting a whole table is a read, and
1404
1495
  * reads do not destroy anything.
1405
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
+ },
1406
1569
  async count(table, where = {}) {
1407
1570
  const params = [];
1408
1571
  const add = /* @__PURE__ */ __name((v) => {
@@ -1826,8 +1989,10 @@ function createOps(tx) {
1826
1989
  [cfg.pk]: newId
1827
1990
  };
1828
1991
  const cols = Object.keys(data);
1829
- const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
1830
- const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`, asBindParams(table, cols, data, "supersede"));
1992
+ const params = [];
1993
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1994
+ const tuple = buildValueTuple(table, cols, data, add, "supersede");
1995
+ const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`, params);
1831
1996
  if (!rows[0]) {
1832
1997
  throw new Error(`supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`);
1833
1998
  }
@@ -2806,6 +2971,26 @@ var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
2806
2971
  409,
2807
2972
  429
2808
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");
2809
2994
  async function createApp(opts) {
2810
2995
  const { config, controllers } = opts;
2811
2996
  const container = buildContainer();
@@ -2823,7 +3008,7 @@ async function createApp(opts) {
2823
3008
  assertNoOrphanEntryPoints(controllers, container.owned);
2824
3009
  const routes = buildRouteTable(controllers, liveContainer);
2825
3010
  if (routes.length === 0) {
2826
- throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
3011
+ throw new DeclarationRefused("boot refused: zero endpoints collected \u2014 nothing would answer.");
2827
3012
  }
2828
3013
  const sql = opts.sql ?? await defaultSqlDriver(config);
2829
3014
  await sql.unsafe("select 1");
@@ -2897,8 +3082,23 @@ async function createApp(opts) {
2897
3082
  if (spec.required && !callerClaims) {
2898
3083
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
2899
3084
  }
2900
- if (callerClaims && spec.role && callerClaims.role !== spec.role) {
2901
- 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
+ }
2902
3102
  }
2903
3103
  const grant = grantFor(target?.entry, {
2904
3104
  userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
@@ -2931,8 +3131,14 @@ async function createApp(opts) {
2931
3131
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
2932
3132
  }
2933
3133
  const userId = typeof claims?.sub === "string" ? claims.sub : void 0;
2934
- if (claims && spec.role && claims.role !== spec.role) {
2935
- 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;
2936
3142
  }
2937
3143
  if (claims && spec.verifiedEmail && claims.email_verified !== true) {
2938
3144
  return envelope("email_not_verified", "A verified email address is required", 403, requestId);
@@ -3153,11 +3359,6 @@ async function createApp(opts) {
3153
3359
  args[p.index] = void 0;
3154
3360
  }
3155
3361
  }
3156
- const db = createRequestDatabase(sql, {
3157
- role: config.dbRole,
3158
- serviceRole: config.dbServiceRole,
3159
- claimsJson: JSON.stringify(claims ?? {})
3160
- });
3161
3362
  try {
3162
3363
  const services = buildServices(db);
3163
3364
  if (meta.options?.sseConfig !== void 0) {
@@ -3350,4 +3551,4 @@ export {
3350
3551
  installEgressFence,
3351
3552
  createApp
3352
3553
  };
3353
- //# sourceMappingURL=chunk-EB3TUX5J.js.map
3554
+ //# sourceMappingURL=chunk-DHJ3SYAE.js.map