@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.
- package/dist/bin/palbase-backend.cjs +284 -50
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +4 -4
- package/dist/{chunk-JVZQCC77.js → chunk-3TUJWHC2.js} +8 -3
- package/dist/chunk-3TUJWHC2.js.map +1 -0
- package/dist/{chunk-EB3TUX5J.js → chunk-DHJ3SYAE.js} +251 -50
- package/dist/chunk-DHJ3SYAE.js.map +1 -0
- package/dist/{chunk-XABHGMUT.js → chunk-ENZ2RFFJ.js} +2 -2
- package/dist/{chunk-BQN723PL.js → chunk-ESSQ3YML.js} +73 -7
- package/dist/chunk-ESSQ3YML.js.map +1 -0
- package/dist/{chunk-OZKSM3JW.js → chunk-SG4UTNOP.js} +7 -3
- package/dist/chunk-SG4UTNOP.js.map +1 -0
- package/dist/db/index.cjs +8 -2
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +3 -3
- package/dist/engine/index.cjs +294 -50
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +3 -3
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +11 -4
- package/dist/{index-V7QRh1wg.d.ts → index-BOe82fsK.d.ts} +256 -19
- package/dist/{index-CAKOgAlP.d.ts → index-BYDUKexK.d.ts} +139 -10
- package/dist/{index-H-0qv5d4.d.cts → index-BZfmdRmh.d.cts} +139 -10
- package/dist/{index-CgE4sVhg.d.cts → index-ChvoVczS.d.cts} +256 -19
- package/dist/index.cjs +81 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -22
- package/dist/index.d.ts +6 -22
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.d.cts +2 -2
- package/dist/openapi/index.d.ts +2 -2
- package/dist/{registry-DHsPDY0_.d.cts → registry-B2KfKWv1.d.cts} +1 -1
- package/dist/{registry-4EI8aaFs.d.ts → registry-DaqHsOq8.d.ts} +1 -1
- package/dist/test/index.cjs +129 -6
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +1 -1
- package/dist/test/index.d.ts +1 -1
- package/dist/test/index.js +126 -6
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +1 -1
- package/docs/database.md +118 -1
- package/docs/llms-full.txt +119 -2
- package/package.json +1 -1
- package/template/package.json +1 -1
- package/dist/chunk-BQN723PL.js.map +0 -1
- package/dist/chunk-EB3TUX5J.js.map +0 -1
- package/dist/chunk-JVZQCC77.js.map +0 -1
- package/dist/chunk-OZKSM3JW.js.map +0 -1
- /package/dist/{chunk-XABHGMUT.js.map → chunk-ENZ2RFFJ.js.map} +0 -0
|
@@ -83,8 +83,11 @@ __name(isColumnExpr, "isColumnExpr");
|
|
|
83
83
|
function assertNoExpressionHandles(caller, table, cols, data) {
|
|
84
84
|
for (const c of cols) {
|
|
85
85
|
const v = data[c];
|
|
86
|
+
if (isNowExpr(v)) {
|
|
87
|
+
throw new Error(`${caller}(${table}): "${c}" now() ald\u0131 \u2014 bu yolda de\u011Fer beklenir. Sat\u0131r EKLEN\u0130RKEN sunucu saatini yazman\u0131n yolu kolonu defaultNow() ile bildirmek (varsay\u0131lan kolonun yan\u0131nda durur, her \xE7a\u011Fr\u0131da tekrarlanmaz); var olan bir sat\u0131r\u0131 damgalamak i\xE7in updateMany({ where, set: { ${c}: now() } }) ya da $transaction i\xE7inde tx.public.${table}.updateWhere(where, { ${c}: now() }).`);
|
|
88
|
+
}
|
|
86
89
|
if (isColumnExpr(v)) {
|
|
87
|
-
throw new Error(`${caller}(${table}): "${c}" bir ifade tutama\u011F\u0131 ald\u0131 (increment()/decrement()
|
|
90
|
+
throw new Error(`${caller}(${table}): "${c}" bir ifade tutama\u011F\u0131 ald\u0131 (increment()/decrement()). Bu yolda de\u011Fer beklenir. Saya\xE7 art\u0131\u015F\u0131 i\xE7in updateMany({ where, set: { ${c}: increment(n) } }) ya da $transaction i\xE7inde tx.public.${table}.updateWhere(where, { ${c}: increment(n) }) kullan\u0131n.`);
|
|
88
91
|
}
|
|
89
92
|
if (isColRef(v)) {
|
|
90
93
|
throw new Error(`${caller}(${table}): "${c}" bir col() ald\u0131. Kolon referans\u0131 yaln\u0131z F\u0130LTREDE durabilir; bir kolonun de\u011Ferini ba\u015Fka bir kolona yazmak i\xE7in $query kullan\u0131n.`);
|
|
@@ -376,7 +379,9 @@ function encodeMap(map, allowColumnExpr) {
|
|
|
376
379
|
const out = {};
|
|
377
380
|
for (const key of Object.keys(map).sort()) {
|
|
378
381
|
const value = map[key];
|
|
379
|
-
if (value === void 0)
|
|
382
|
+
if (value === void 0) {
|
|
383
|
+
throw new TxPlanError(`${key} de\u011Feri undefined \u2014 bu bir yazma de\u011Feri de\u011Fil. Anahtar duruyor ama de\u011Feri yok, yani kolon sessizce YAZILMAZDI (istek g\xF6vdesinden gelen bir alan\u0131n eksik olmas\u0131 bu \u015Fekilde g\xF6r\xFCn\xFCr). Kolon varsay\u0131lan\u0131n\u0131 istiyorsan\u0131z anahtar\u0131 hi\xE7 koymay\u0131n; NULL yazmak istiyorsan\u0131z a\xE7\u0131k\xE7a null yaz\u0131n.`);
|
|
384
|
+
}
|
|
380
385
|
out[key] = encodeValue(value, key, allowColumnExpr);
|
|
381
386
|
}
|
|
382
387
|
return out;
|
|
@@ -904,6 +909,11 @@ function makeTableProxy(ops, prefix) {
|
|
|
904
909
|
const name = `${prefix}${prop}`;
|
|
905
910
|
return {
|
|
906
911
|
insert: /* @__PURE__ */ __name((data) => ops().insert(name, data), "insert"),
|
|
912
|
+
// DÖRDÜNCÜ FİİL, aynı gerekçeyle: tip söz veriyor, ops katmanı
|
|
913
|
+
// uyguluyor, ve bir handler'ın gerçekten dokunduğu yer BURASI.
|
|
914
|
+
// `runtime-table-verbs` kapısı bunu adıyla saydı.
|
|
915
|
+
aggregate: /* @__PURE__ */ __name((q) => ops().aggregate(name, q), "aggregate"),
|
|
916
|
+
insertMany: /* @__PURE__ */ __name((rows, opts) => ops().insertMany(name, rows, opts), "insertMany"),
|
|
907
917
|
update: /* @__PURE__ */ __name((q) => ops().update(name, q.where.id, q.set), "update"),
|
|
908
918
|
delete: /* @__PURE__ */ __name((id) => ops().delete(name, id), "delete"),
|
|
909
919
|
findById: /* @__PURE__ */ __name((id) => ops().findById(name, id), "findById"),
|
|
@@ -960,6 +970,8 @@ function makeTypedSurface(raw) {
|
|
|
960
970
|
$claim: /* @__PURE__ */ __name((table, unique, extra) => reco.claim(table, unique, extra), "$claim"),
|
|
961
971
|
$lockRows: /* @__PURE__ */ __name((table, ids) => reco.lockRows(table, ids), "$lockRows"),
|
|
962
972
|
$advisoryXactLock: /* @__PURE__ */ __name((key) => reco.advisoryXactLock(key), "$advisoryXactLock"),
|
|
973
|
+
$aggregate: /* @__PURE__ */ __name((table, q) => raw.aggregate(table, q), "$aggregate"),
|
|
974
|
+
$insertMany: /* @__PURE__ */ __name((table, rows, opts) => raw.insertMany(table, rows, opts), "$insertMany"),
|
|
963
975
|
$supersede: /* @__PURE__ */ __name((table, id, row) => raw.supersede(table, id, row), "$supersede")
|
|
964
976
|
};
|
|
965
977
|
const base = Object.assign(ops, {
|
|
@@ -1149,6 +1161,20 @@ ${detail}`);
|
|
|
1149
1161
|
}
|
|
1150
1162
|
__name(loadConfig, "loadConfig");
|
|
1151
1163
|
|
|
1164
|
+
// src/refusals.ts
|
|
1165
|
+
var DECLARATION_REFUSAL = /* @__PURE__ */ Symbol.for("palbase.backend.declarationRefusal");
|
|
1166
|
+
var DeclarationRefused = class extends Error {
|
|
1167
|
+
static {
|
|
1168
|
+
__name(this, "DeclarationRefused");
|
|
1169
|
+
}
|
|
1170
|
+
/** @see DECLARATION_REFUSAL — realm-safe, unlike `instanceof`. */
|
|
1171
|
+
[DECLARATION_REFUSAL] = true;
|
|
1172
|
+
constructor(message) {
|
|
1173
|
+
super(message);
|
|
1174
|
+
this.name = "DeclarationRefused";
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1152
1178
|
// src/engine/auth.ts
|
|
1153
1179
|
function b64urlToBytes(s) {
|
|
1154
1180
|
const pad = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -1275,9 +1301,11 @@ function effectiveAuth(routeAuth, controllerAuth) {
|
|
|
1275
1301
|
};
|
|
1276
1302
|
const o = spec;
|
|
1277
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;
|
|
1278
1305
|
return {
|
|
1279
1306
|
required: o.required !== false,
|
|
1280
1307
|
role,
|
|
1308
|
+
permission,
|
|
1281
1309
|
verifiedEmail: o.verifiedEmail === true
|
|
1282
1310
|
};
|
|
1283
1311
|
}
|
|
@@ -1471,12 +1499,12 @@ function buildRelations(schemas) {
|
|
|
1471
1499
|
const held = names.get(name);
|
|
1472
1500
|
if (held !== void 0) {
|
|
1473
1501
|
if (held.def.owns === true && origin.def.owns === true) {
|
|
1474
|
-
throw new
|
|
1502
|
+
throw new DeclarationRefused(`table "${tableKey}" declares TWO owner columns (${held.column}, ${origin.column}) \u2014 a table has at most ONE ownedByUser(). If the other column only POINTS at a user, declare it userRef({ onDelete: \u2026 }) instead.`);
|
|
1475
1503
|
}
|
|
1476
1504
|
const heldFix = renameCall(held);
|
|
1477
1505
|
const originFix = renameCall(origin);
|
|
1478
1506
|
const remedy = heldFix === originFix ? `rename one of them: ${heldFix}` : `rename one of them \u2014 "${held.table}.${held.column}": ${heldFix}; "${origin.table}.${origin.column}": ${originFix}`;
|
|
1479
|
-
throw new
|
|
1507
|
+
throw new DeclarationRefused(`table "${tableKey}": ${describeOrigin(held)} and ${describeOrigin(origin)} both resolve to the relation name "${name}" \u2014 ${remedy}.`);
|
|
1480
1508
|
}
|
|
1481
1509
|
names.set(name, origin);
|
|
1482
1510
|
out.get(tableKey)?.push(edge);
|
|
@@ -2054,7 +2082,7 @@ function compileWhereBare(table, colSet, where, add, caller = "search", alias =
|
|
|
2054
2082
|
const rel = rels.find((r) => r.name === relName);
|
|
2055
2083
|
if (rel === void 0) {
|
|
2056
2084
|
const known = rels.map((r) => r.name).sort();
|
|
2057
|
-
throw new Error(`${caller}(${table}): has.${relName} diye bir ili\u015Fki yok
|
|
2085
|
+
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.`));
|
|
2058
2086
|
}
|
|
2059
2087
|
const ourCol = rel.kind === "many" ? primaryKeyOf(table) : rel.via;
|
|
2060
2088
|
const theirCol = rel.kind === "many" ? rel.via : primaryKeyOf(rel.to);
|
|
@@ -2234,6 +2262,57 @@ async function embedQuery(embed, text) {
|
|
|
2234
2262
|
}
|
|
2235
2263
|
}
|
|
2236
2264
|
__name(embedQuery, "embedQuery");
|
|
2265
|
+
function buildSetAssignments(table, cols, set, add, caller) {
|
|
2266
|
+
return cols.map((c) => {
|
|
2267
|
+
const expr = columnExprOf(set[c]);
|
|
2268
|
+
if (expr !== null && expr.fn === "now") {
|
|
2269
|
+
return `${quoteIdent(c)} = now()`;
|
|
2270
|
+
}
|
|
2271
|
+
if (expr !== null) {
|
|
2272
|
+
const operator = expr.fn === "inc" ? "+" : "-";
|
|
2273
|
+
const t = transformsOf(currentSchema, table).get(c);
|
|
2274
|
+
const amount = t?.toDb === void 0 || expr.by === null || expr.by === void 0 ? expr.by : t.toDb(expr.by);
|
|
2275
|
+
return `${quoteIdent(c)} = ${quoteIdent(c)} ${operator} ${add(amount)}`;
|
|
2276
|
+
}
|
|
2277
|
+
return `${quoteIdent(c)} = ${add(asBindParams(table, [
|
|
2278
|
+
c
|
|
2279
|
+
], set, caller)[0])}`;
|
|
2280
|
+
}).join(", ");
|
|
2281
|
+
}
|
|
2282
|
+
__name(buildSetAssignments, "buildSetAssignments");
|
|
2283
|
+
function buildValueTuple(table, cols, data, add, caller) {
|
|
2284
|
+
return cols.map((c) => {
|
|
2285
|
+
const expr = columnExprOf(data[c]);
|
|
2286
|
+
if (expr !== null && expr.fn === "now") return "now()";
|
|
2287
|
+
if (expr !== null) {
|
|
2288
|
+
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) } }).`);
|
|
2289
|
+
}
|
|
2290
|
+
return add(asBindParams(table, [
|
|
2291
|
+
c
|
|
2292
|
+
], data, caller)[0]);
|
|
2293
|
+
}).join(", ");
|
|
2294
|
+
}
|
|
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");
|
|
2237
2316
|
function createOps(tx) {
|
|
2238
2317
|
const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
|
|
2239
2318
|
const ops = {
|
|
@@ -2247,9 +2326,11 @@ function createOps(tx) {
|
|
|
2247
2326
|
async insert(table, data) {
|
|
2248
2327
|
const cols = Object.keys(data);
|
|
2249
2328
|
if (cols.length === 0) throw new Error(`insert into ${table}: no columns given`);
|
|
2250
|
-
const
|
|
2251
|
-
const
|
|
2252
|
-
const
|
|
2329
|
+
const params = [];
|
|
2330
|
+
const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
|
|
2331
|
+
const tuple = buildValueTuple(table, cols, data, add, "insert");
|
|
2332
|
+
const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`;
|
|
2333
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
2253
2334
|
const inserted = rows[0];
|
|
2254
2335
|
if (!inserted) {
|
|
2255
2336
|
throw new Error(`insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`);
|
|
@@ -2257,6 +2338,41 @@ function createOps(tx) {
|
|
|
2257
2338
|
return asTableRow(table, inserted);
|
|
2258
2339
|
},
|
|
2259
2340
|
/**
|
|
2341
|
+
* Çok satırı TEK statement'la ekle.
|
|
2342
|
+
*
|
|
2343
|
+
* Motor bunu zaten yapıyordu ve yalnız PLAN yolundan erişilebiliyordu
|
|
2344
|
+
* (`tx.public.x.insertMany`). Dışarıda tek kapı `insert(row)` olduğu için
|
|
2345
|
+
* "şu 200 satırı yaz" ya 200 gidiş-dönüş ya da ham SQL oluyordu — ölçüldü:
|
|
2346
|
+
* bir müşteri projesinde bir seed rutini satırları tek tek siliyor
|
|
2347
|
+
* (`deleteMany` VARKEN) ve tek tek ekliyor.
|
|
2348
|
+
*
|
|
2349
|
+
* SATIRLARIN ANAHTAR KÜMESİ AYNI OLMAK ZORUNDA. Kolon listesi tek bir
|
|
2350
|
+
* statement'ta paylaşıldığı için ilk satırdan türer; farklı anahtar taşıyan
|
|
2351
|
+
* bir satırın fazla kolonu SESSİZCE YAZILMAZDI. Eksik kolon için `null`
|
|
2352
|
+
* yazın ya da varsayılanı istiyorsanız o satırları ayrı çağrıda ekleyin.
|
|
2353
|
+
*/
|
|
2354
|
+
async insertMany(table, rows, opts) {
|
|
2355
|
+
if (rows.length === 0) return [];
|
|
2356
|
+
const first = rows[0];
|
|
2357
|
+
const cols = Object.keys(first);
|
|
2358
|
+
if (cols.length === 0) throw new Error(`insertMany into ${table}: no columns given`);
|
|
2359
|
+
for (let i = 1; i < rows.length; i++) {
|
|
2360
|
+
const keys = Object.keys(rows[i]);
|
|
2361
|
+
const missing = cols.filter((c) => !(c in rows[i]));
|
|
2362
|
+
const extra = keys.filter((k) => !cols.includes(k));
|
|
2363
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
2364
|
+
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.`);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
const params = [];
|
|
2368
|
+
const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
|
|
2369
|
+
const tuples = rows.map((r) => `(${buildValueTuple(table, cols, r, add, "insertMany")})`);
|
|
2370
|
+
const tail = opts !== void 0 && opts.onConflict.length > 0 ? ` ${onConflictTail(cols, opts.onConflict, opts.action ?? "ignore")}` : "";
|
|
2371
|
+
const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
2372
|
+
const out = await (await at()).unsafe(sql, params);
|
|
2373
|
+
return out.map((r) => asTableRow(table, r));
|
|
2374
|
+
},
|
|
2375
|
+
/**
|
|
2260
2376
|
* Bir ADI kilitle — satırı değil (FR-054).
|
|
2261
2377
|
*
|
|
2262
2378
|
* "Aynı anda tek bir fatura kapanışı koşsun" gibi, kilitlenecek satırı
|
|
@@ -2342,8 +2458,10 @@ function createOps(tx) {
|
|
|
2342
2458
|
const live = await at();
|
|
2343
2459
|
try {
|
|
2344
2460
|
return await live.savepoint(async (sp) => {
|
|
2345
|
-
const
|
|
2346
|
-
const
|
|
2461
|
+
const params = [];
|
|
2462
|
+
const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
|
|
2463
|
+
const tuple = buildValueTuple(table, cols, data, add, "claim");
|
|
2464
|
+
const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`, params);
|
|
2347
2465
|
if (!rows[0]) {
|
|
2348
2466
|
throw new Error(`claim(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si).`);
|
|
2349
2467
|
}
|
|
@@ -2394,10 +2512,11 @@ function createOps(tx) {
|
|
|
2394
2512
|
const cols = Object.keys(data);
|
|
2395
2513
|
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
2396
2514
|
if (searchConfigFor(table)?.validity) {
|
|
2397
|
-
const
|
|
2515
|
+
const bindData = [];
|
|
2516
|
+
const addPut = /* @__PURE__ */ __name((v) => `$${bindData.push(v)}`, "addPut");
|
|
2517
|
+
const ph = buildValueTuple(table, cols, data, addPut, "put");
|
|
2398
2518
|
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 *`;
|
|
2399
2519
|
const live = await at();
|
|
2400
|
-
const bindData = asBindParams(table, cols, data, "put");
|
|
2401
2520
|
const first = await live.unsafe(insertSql, bindData);
|
|
2402
2521
|
if (first[0]) return asTableRow(table, first[0]);
|
|
2403
2522
|
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
@@ -2422,12 +2541,14 @@ function createOps(tx) {
|
|
|
2422
2541
|
]);
|
|
2423
2542
|
return asTableRow(table, second[0]);
|
|
2424
2543
|
}
|
|
2425
|
-
const
|
|
2544
|
+
const params = [];
|
|
2545
|
+
const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
|
|
2546
|
+
const placeholders = buildValueTuple(table, cols, data, add, "put");
|
|
2426
2547
|
const conflictSet = new Set(conflict);
|
|
2427
2548
|
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
2428
2549
|
const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
2429
2550
|
const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
2430
|
-
const rows = await (await at()).unsafe(sql,
|
|
2551
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
2431
2552
|
const row = rows[0];
|
|
2432
2553
|
if (!row) {
|
|
2433
2554
|
throw new Error(`upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`);
|
|
@@ -2438,12 +2559,14 @@ function createOps(tx) {
|
|
|
2438
2559
|
assertNotAppendOnly("update", table);
|
|
2439
2560
|
const cols = Object.keys(data);
|
|
2440
2561
|
if (cols.length === 0) return ops.findById(table, id);
|
|
2441
|
-
const
|
|
2442
|
-
const
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2562
|
+
const params = [];
|
|
2563
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
2564
|
+
params.push(v);
|
|
2565
|
+
return `$${params.length}`;
|
|
2566
|
+
}, "add");
|
|
2567
|
+
const assignments = buildSetAssignments(table, cols, data, add, "update");
|
|
2568
|
+
const sql = `UPDATE ${quoteTable(table)} SET ${assignments} WHERE id = ${add(id)} RETURNING *`;
|
|
2569
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
2447
2570
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
2448
2571
|
},
|
|
2449
2572
|
async delete(table, id) {
|
|
@@ -2470,7 +2593,7 @@ function createOps(tx) {
|
|
|
2470
2593
|
* should not silently succeed. Callers who mean every row say so with a
|
|
2471
2594
|
* predicate that is true for every row.
|
|
2472
2595
|
*/
|
|
2473
|
-
async updateMany(table, where, set) {
|
|
2596
|
+
async updateMany(table, where, set, opts) {
|
|
2474
2597
|
assertNotAppendOnly("updateMany", table);
|
|
2475
2598
|
const cols = Object.keys(set);
|
|
2476
2599
|
if (cols.length === 0) {
|
|
@@ -2489,26 +2612,19 @@ function createOps(tx) {
|
|
|
2489
2612
|
params.push(v);
|
|
2490
2613
|
return `$${params.length}`;
|
|
2491
2614
|
}, "add");
|
|
2492
|
-
const assignments =
|
|
2493
|
-
const expr = columnExprOf(set[c]);
|
|
2494
|
-
if (expr !== null && expr.fn === "now") {
|
|
2495
|
-
return `${quoteIdent(c)} = now()`;
|
|
2496
|
-
}
|
|
2497
|
-
if (expr !== null) {
|
|
2498
|
-
const operator = expr.fn === "inc" ? "+" : "-";
|
|
2499
|
-
const t = transformsOf(currentSchema, table).get(c);
|
|
2500
|
-
const amount = t?.toDb === void 0 || expr.by === null || expr.by === void 0 ? expr.by : t.toDb(expr.by);
|
|
2501
|
-
return `${quoteIdent(c)} = ${quoteIdent(c)} ${operator} ${add(amount)}`;
|
|
2502
|
-
}
|
|
2503
|
-
return `${quoteIdent(c)} = ${add(asBindParams(table, [
|
|
2504
|
-
c
|
|
2505
|
-
], set, "updateMany")[0])}`;
|
|
2506
|
-
}).join(", ");
|
|
2615
|
+
const assignments = buildSetAssignments(table, cols, set, add, "updateMany");
|
|
2507
2616
|
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
2508
2617
|
assertHasPredicate(whereSql, "updateMany", table);
|
|
2509
2618
|
const pk = primaryKeyOf(table);
|
|
2510
|
-
const
|
|
2511
|
-
|
|
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);
|
|
2512
2628
|
},
|
|
2513
2629
|
/**
|
|
2514
2630
|
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
@@ -2538,6 +2654,78 @@ function createOps(tx) {
|
|
|
2538
2654
|
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
2539
2655
|
* reads do not destroy anything.
|
|
2540
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
|
+
},
|
|
2541
2729
|
async count(table, where = {}) {
|
|
2542
2730
|
const params = [];
|
|
2543
2731
|
const add = /* @__PURE__ */ __name((v) => {
|
|
@@ -2961,8 +3149,10 @@ function createOps(tx) {
|
|
|
2961
3149
|
[cfg.pk]: newId
|
|
2962
3150
|
};
|
|
2963
3151
|
const cols = Object.keys(data);
|
|
2964
|
-
const
|
|
2965
|
-
const
|
|
3152
|
+
const params = [];
|
|
3153
|
+
const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
|
|
3154
|
+
const tuple = buildValueTuple(table, cols, data, add, "supersede");
|
|
3155
|
+
const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`, params);
|
|
2966
3156
|
if (!rows[0]) {
|
|
2967
3157
|
throw new Error(`supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`);
|
|
2968
3158
|
}
|
|
@@ -3918,6 +4108,14 @@ var DiError = class extends Error {
|
|
|
3918
4108
|
path;
|
|
3919
4109
|
at;
|
|
3920
4110
|
fixes;
|
|
4111
|
+
/**
|
|
4112
|
+
* EVERY `DiKind` IS A FACT ABOUT THE AUTHOR'S DECLARATIONS — an unresolvable
|
|
4113
|
+
* dependency, a cycle, a class no module owns. Not one of them is something
|
|
4114
|
+
* the environment could change, so restarting re-reads the same bytes and
|
|
4115
|
+
* fails identically. The cure is a new artifact, and the tag is how the
|
|
4116
|
+
* runtime learns that without matching on wording.
|
|
4117
|
+
*/
|
|
4118
|
+
[DECLARATION_REFUSAL] = true;
|
|
3921
4119
|
constructor(kind, path, at, detail, fixes) {
|
|
3922
4120
|
super(`
|
|
3923
4121
|
Kind: ${kind}
|
|
@@ -4316,6 +4514,26 @@ var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
|
|
|
4316
4514
|
409,
|
|
4317
4515
|
429
|
|
4318
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");
|
|
4319
4537
|
async function createApp(opts) {
|
|
4320
4538
|
const { config, controllers } = opts;
|
|
4321
4539
|
const container = buildContainer();
|
|
@@ -4333,7 +4551,7 @@ async function createApp(opts) {
|
|
|
4333
4551
|
assertNoOrphanEntryPoints(controllers, container.owned);
|
|
4334
4552
|
const routes = buildRouteTable(controllers, liveContainer);
|
|
4335
4553
|
if (routes.length === 0) {
|
|
4336
|
-
throw new
|
|
4554
|
+
throw new DeclarationRefused("boot refused: zero endpoints collected \u2014 nothing would answer.");
|
|
4337
4555
|
}
|
|
4338
4556
|
const sql = opts.sql ?? await defaultSqlDriver(config);
|
|
4339
4557
|
await sql.unsafe("select 1");
|
|
@@ -4407,8 +4625,23 @@ async function createApp(opts) {
|
|
|
4407
4625
|
if (spec.required && !callerClaims) {
|
|
4408
4626
|
return envelope("unauthorized", "A valid access token is required", 401, requestId);
|
|
4409
4627
|
}
|
|
4410
|
-
if (callerClaims
|
|
4411
|
-
|
|
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
|
+
}
|
|
4412
4645
|
}
|
|
4413
4646
|
const grant = grantFor(target?.entry, {
|
|
4414
4647
|
userId: typeof callerClaims?.sub === "string" ? callerClaims.sub : null,
|
|
@@ -4441,8 +4674,14 @@ async function createApp(opts) {
|
|
|
4441
4674
|
return envelope("unauthorized", "A valid access token is required", 401, requestId);
|
|
4442
4675
|
}
|
|
4443
4676
|
const userId = typeof claims?.sub === "string" ? claims.sub : void 0;
|
|
4444
|
-
|
|
4445
|
-
|
|
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;
|
|
4446
4685
|
}
|
|
4447
4686
|
if (claims && spec.verifiedEmail && claims.email_verified !== true) {
|
|
4448
4687
|
return envelope("email_not_verified", "A verified email address is required", 403, requestId);
|
|
@@ -4663,11 +4902,6 @@ async function createApp(opts) {
|
|
|
4663
4902
|
args[p.index] = void 0;
|
|
4664
4903
|
}
|
|
4665
4904
|
}
|
|
4666
|
-
const db = createRequestDatabase(sql, {
|
|
4667
|
-
role: config.dbRole,
|
|
4668
|
-
serviceRole: config.dbServiceRole,
|
|
4669
|
-
claimsJson: JSON.stringify(claims ?? {})
|
|
4670
|
-
});
|
|
4671
4905
|
try {
|
|
4672
4906
|
const services = buildServices(db);
|
|
4673
4907
|
if (meta.options?.sseConfig !== void 0) {
|