@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.
- package/dist/bin/palbase-backend.cjs +166 -12
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +3 -3
- package/dist/{chunk-RVP6BTEZ.js → chunk-5J5W75A6.js} +15 -4
- package/dist/chunk-5J5W75A6.js.map +1 -0
- package/dist/{chunk-IVZERLTM.js → chunk-ENZ2RFFJ.js} +2 -2
- package/dist/{chunk-SNDXY565.js → chunk-SG4UTNOP.js} +5 -2
- package/dist/chunk-SG4UTNOP.js.map +1 -0
- package/dist/{chunk-75YROPRZ.js → chunk-VN7NMDUH.js} +166 -15
- package/dist/chunk-VN7NMDUH.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 +166 -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-8qy3kIuA.d.ts} +228 -12
- package/dist/{index-DtISj9QX.d.cts → index-DtCgaZAg.d.cts} +228 -12
- package/dist/{index-9C3JHxg-.d.cts → index-Zi7MptvP.d.cts} +30 -8
- package/dist/{index-BbvOoZFr.d.ts → index-b-Q3l7W5.d.ts} +30 -8
- package/dist/index.cjs +66 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -11
- package/dist/index.d.ts +51 -11
- package/dist/index.js +52 -3
- 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-JQNIX-eA.d.ts → registry-BGJ-Al6F.d.ts} +1 -1
- package/dist/{registry-qIPM5BQe.d.cts → registry-CEYLH-Iz.d.cts} +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
|
};
|
|
@@ -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
|
|
2596
|
-
|
|
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
|
|
4506
|
-
|
|
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
|
-
|
|
4540
|
-
|
|
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) {
|