@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.
- package/dist/bin/palbase-backend.cjs +151 -12
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +3 -3
- package/dist/{chunk-75YROPRZ.js → chunk-DHJ3SYAE.js} +152 -15
- package/dist/chunk-DHJ3SYAE.js.map +1 -0
- package/dist/{chunk-IVZERLTM.js → chunk-ENZ2RFFJ.js} +2 -2
- package/dist/{chunk-RVP6BTEZ.js → chunk-ESSQ3YML.js} +13 -4
- package/dist/chunk-ESSQ3YML.js.map +1 -0
- package/dist/{chunk-SNDXY565.js → chunk-SG4UTNOP.js} +5 -2
- package/dist/chunk-SG4UTNOP.js.map +1 -0
- package/dist/db/index.cjs +4 -1
- 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 +2 -2
- package/dist/engine/index.cjs +151 -12
- 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 +3 -3
- package/dist/{index-dTTLlHIn.d.ts → index-BOe82fsK.d.ts} +162 -8
- package/dist/{index-BbvOoZFr.d.ts → index-BYDUKexK.d.ts} +29 -7
- package/dist/{index-9C3JHxg-.d.cts → index-BZfmdRmh.d.cts} +29 -7
- package/dist/{index-DtISj9QX.d.cts → index-ChvoVczS.d.cts} +162 -8
- package/dist/index.cjs +15 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +3 -3
- package/dist/openapi/index.d.cts +2 -2
- package/dist/openapi/index.d.ts +2 -2
- package/dist/{registry-qIPM5BQe.d.cts → registry-B2KfKWv1.d.cts} +1 -1
- package/dist/{registry-JQNIX-eA.d.ts → registry-DaqHsOq8.d.ts} +1 -1
- package/dist/test/index.cjs +56 -0
- 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 +56 -0
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +1 -1
- package/docs/database.md +49 -0
- package/docs/llms-full.txt +50 -1
- package/package.json +1 -1
- package/template/package.json +1 -1
- package/dist/chunk-75YROPRZ.js.map +0 -1
- package/dist/chunk-RVP6BTEZ.js.map +0 -1
- package/dist/chunk-SNDXY565.js.map +0 -1
- /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
|
|
2596
|
-
|
|
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
|
|
4506
|
-
|
|
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
|
-
|
|
4540
|
-
|
|
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) {
|