@palbase/backend 23.1.0 → 24.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 +389 -44
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +4 -5
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/{chunk-OHALWEOG.js → chunk-7Z6MGMXQ.js} +57 -2
- package/dist/chunk-7Z6MGMXQ.js.map +1 -0
- package/dist/{chunk-RCLNBJCM.js → chunk-H3JAISUY.js} +136 -1
- package/dist/chunk-H3JAISUY.js.map +1 -0
- package/dist/{chunk-M5MCBWJI.js → chunk-NXDH6VQJ.js} +264 -28
- package/dist/chunk-NXDH6VQJ.js.map +1 -0
- package/dist/{chunk-NS5V43YQ.js → chunk-P2Q27SGP.js} +19 -3
- package/dist/chunk-P2Q27SGP.js.map +1 -0
- package/dist/{chunk-R3KN6RHD.js → chunk-T5IOSOE5.js} +7 -2
- package/dist/chunk-T5IOSOE5.js.map +1 -0
- package/dist/{chunk-PY7YJDCT.js → chunk-ZUGY7RGS.js} +32 -3
- package/dist/chunk-ZUGY7RGS.js.map +1 -0
- package/dist/db/index.cjs +48 -3
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +2 -2
- package/dist/{endpoint-CVWXh6oG.d.ts → endpoint-0_DGBajf.d.ts} +100 -3
- package/dist/{endpoint-c9h5jriX.d.cts → endpoint-CcQ1a36a.d.cts} +100 -3
- package/dist/engine/index.cjs +378 -31
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +4 -4
- package/dist/engine/index.d.ts +4 -4
- package/dist/engine/index.js +4 -4
- package/dist/{index-CwAJ7HEe.d.ts → index-CJiJU9ux.d.ts} +125 -21
- package/dist/{index-By8Dle5U.d.cts → index-D-4-PNuQ.d.cts} +125 -21
- package/dist/{index-BZrJXnVh.d.ts → index-D17r-MKb.d.ts} +134 -9
- package/dist/{index-CxeQSfJP.d.cts → index-DRFxf07H.d.cts} +134 -9
- package/dist/index.cjs +166 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -12
- package/dist/index.d.ts +54 -12
- package/dist/index.js +28 -11
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +16 -1
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +6 -4
- package/dist/openapi/index.d.ts +6 -4
- package/dist/openapi/index.js +6 -7
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-CqPK2Qby.d.cts → registry-1X-skBNu.d.cts} +1 -1
- package/dist/{registry-B3niOVYp.d.ts → registry-CEod_5sz.d.ts} +1 -1
- package/dist/test/index.cjs +27 -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 +27 -0
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +4 -4
- package/docs/database.md +115 -11
- package/docs/getting-started.md +5 -4
- package/docs/llms-full.txt +385 -89
- package/docs/migrations.md +81 -59
- package/docs/schema.md +82 -2
- package/docs/services.md +98 -9
- package/package.json +2 -2
- package/template/AGENTS.md +121 -41
- package/template/controllers/notes.controller.ts +64 -0
- package/template/package.json +1 -1
- package/template/services/note.service.ts +74 -0
- package/template/tsconfig.json +11 -1
- package/dist/chunk-HQRJDARQ.js +0 -90
- package/dist/chunk-HQRJDARQ.js.map +0 -1
- package/dist/chunk-M5MCBWJI.js.map +0 -1
- package/dist/chunk-NS5V43YQ.js.map +0 -1
- package/dist/chunk-OHALWEOG.js.map +0 -1
- package/dist/chunk-PY7YJDCT.js.map +0 -1
- package/dist/chunk-R3KN6RHD.js.map +0 -1
- package/dist/chunk-RCLNBJCM.js.map +0 -1
|
@@ -231,7 +231,7 @@ var TxPlanBuilder = class {
|
|
|
231
231
|
`${name}.upsert()`
|
|
232
232
|
);
|
|
233
233
|
},
|
|
234
|
-
insertMany: (rows) => {
|
|
234
|
+
insertMany: (rows, opts) => {
|
|
235
235
|
if (rows.length === 0) {
|
|
236
236
|
return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
|
|
237
237
|
}
|
|
@@ -242,7 +242,23 @@ var TxPlanBuilder = class {
|
|
|
242
242
|
}
|
|
243
243
|
const encoded = rows.map((row) => encodeMap(row, false));
|
|
244
244
|
assertUniformRows(encoded, name);
|
|
245
|
-
|
|
245
|
+
if (opts !== void 0 && opts.onConflict.length === 0) {
|
|
246
|
+
throw new TxPlanError(
|
|
247
|
+
`${name}.insertMany() was given a conflict action with no onConflict columns. Postgres matches a collision on columns, so name them.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return this.push(
|
|
251
|
+
{
|
|
252
|
+
op: "insertMany",
|
|
253
|
+
table: name,
|
|
254
|
+
rows: encoded,
|
|
255
|
+
// Omitted entirely when no options were given, so the op a plain
|
|
256
|
+
// insertMany produces is byte-identical to the one it produced
|
|
257
|
+
// before this option existed.
|
|
258
|
+
...opts !== void 0 ? { onConflict: opts.onConflict, action: opts.action ?? "ignore" } : {}
|
|
259
|
+
},
|
|
260
|
+
`${name}.insertMany()`
|
|
261
|
+
);
|
|
246
262
|
},
|
|
247
263
|
updateWhere: (where, set) => {
|
|
248
264
|
const encodedWhere = encodeMap(where, false);
|
|
@@ -416,6 +432,42 @@ function __getRuntime() {
|
|
|
416
432
|
}
|
|
417
433
|
return runtime;
|
|
418
434
|
}
|
|
435
|
+
var LIFECYCLE = /* @__PURE__ */ Symbol.for("palbase.backend.lifecycleHooks");
|
|
436
|
+
function declaredLifecycle() {
|
|
437
|
+
const g = globalThis;
|
|
438
|
+
return g[LIFECYCLE] ??= { start: [], shutdown: [] };
|
|
439
|
+
}
|
|
440
|
+
function reason(err) {
|
|
441
|
+
return err instanceof Error ? err.message : String(err);
|
|
442
|
+
}
|
|
443
|
+
async function drain(hooks) {
|
|
444
|
+
for (const h of [...hooks].reverse()) {
|
|
445
|
+
try {
|
|
446
|
+
await h.run();
|
|
447
|
+
} catch (err) {
|
|
448
|
+
console.error(`[palbase] shutdown hook "${h.name}" failed: ${reason(err)}`, err);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function __runStartHooks() {
|
|
453
|
+
const slot = declaredLifecycle();
|
|
454
|
+
const start = slot.start.splice(0);
|
|
455
|
+
const shutdown = slot.shutdown.splice(0);
|
|
456
|
+
for (const h of start) {
|
|
457
|
+
try {
|
|
458
|
+
await h.run();
|
|
459
|
+
} catch (err) {
|
|
460
|
+
await drain(shutdown);
|
|
461
|
+
throw new Error(`[palbase] start hook "${h.name}" failed: ${reason(err)}`, { cause: err });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
let drained = false;
|
|
465
|
+
return async () => {
|
|
466
|
+
if (drained) return;
|
|
467
|
+
drained = true;
|
|
468
|
+
await drain(shutdown);
|
|
469
|
+
};
|
|
470
|
+
}
|
|
419
471
|
function makeServiceProxy(key) {
|
|
420
472
|
const handler = {
|
|
421
473
|
get(_target, prop, receiver) {
|
|
@@ -443,6 +495,7 @@ function makeTablesAccessor(ops) {
|
|
|
443
495
|
search: (params) => ops().search(name, params),
|
|
444
496
|
similar: (id, params) => ops().similar(name, id, params),
|
|
445
497
|
recommend: (params) => ops().recommend(name, params),
|
|
498
|
+
facets: (params) => ops().facets(name, params),
|
|
446
499
|
supersede: (id, row) => ops().supersede(name, id, row)
|
|
447
500
|
};
|
|
448
501
|
}
|
|
@@ -461,9 +514,13 @@ function makeTypedSurface(raw) {
|
|
|
461
514
|
findById: (table, id) => raw.findById(table, id),
|
|
462
515
|
findMany: (table, query, opts) => raw.findMany(table, query, opts),
|
|
463
516
|
upsert: (table, data, opts) => raw.upsert(table, data, opts),
|
|
517
|
+
updateMany: (table, where, set) => raw.updateMany(table, where, set),
|
|
518
|
+
deleteMany: (table, where) => raw.deleteMany(table, where),
|
|
519
|
+
count: (table, where) => raw.count(table, where),
|
|
464
520
|
search: (table, params) => raw.search(table, params),
|
|
465
521
|
similar: (table, id, params) => reco.similar(table, id, params),
|
|
466
522
|
recommend: (table, params) => reco.recommend(table, params),
|
|
523
|
+
facets: (table, params) => reco.facets(table, params),
|
|
467
524
|
supersede: (table, id, row) => raw.supersede(table, id, row)
|
|
468
525
|
};
|
|
469
526
|
return Object.assign(ops, {
|
|
@@ -621,6 +678,44 @@ var HttpError = class extends Error {
|
|
|
621
678
|
return result;
|
|
622
679
|
}
|
|
623
680
|
};
|
|
681
|
+
var NamedHttpError = class extends HttpError {
|
|
682
|
+
constructor(status, defaultCode, name, message, code, data) {
|
|
683
|
+
super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
|
|
684
|
+
this.name = name;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
function defaultMessage(name) {
|
|
688
|
+
const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
689
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
690
|
+
}
|
|
691
|
+
var Conflict = class extends NamedHttpError {
|
|
692
|
+
constructor(message, code, data) {
|
|
693
|
+
super(409, "conflict", "Conflict", message, code, data);
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
var UniqueViolation = class extends Conflict {
|
|
697
|
+
/**
|
|
698
|
+
* Whether `e` is a unique violation — REGARDLESS of which copy of this SDK
|
|
699
|
+
* constructed it.
|
|
700
|
+
*
|
|
701
|
+
* Use this instead of `instanceof`. Measured on a live stack: a controller
|
|
702
|
+
* bundle INLINES its own copy of `@palbase/backend`, and the engine that
|
|
703
|
+
* raises this error is the runtime's copy. Two copies, two class identities,
|
|
704
|
+
* and `e instanceof UniqueViolation` is false in the one place a caller
|
|
705
|
+
* writes it — a check that reads as correct and silently never matches.
|
|
706
|
+
*/
|
|
707
|
+
static is(e) {
|
|
708
|
+
return typeof e === "object" && e !== null && e.name === "UniqueViolation" && typeof e.constraint === "string";
|
|
709
|
+
}
|
|
710
|
+
/** The unique constraint the statement violated, as Postgres named it.
|
|
711
|
+
* `""` when the driver did not say which — see `engine/db.ts`. */
|
|
712
|
+
constraint;
|
|
713
|
+
constructor(constraint, message, code, data) {
|
|
714
|
+
super(message ?? "Unique constraint violated", code ?? "unique_violation", data);
|
|
715
|
+
this.name = "UniqueViolation";
|
|
716
|
+
this.constraint = constraint;
|
|
717
|
+
}
|
|
718
|
+
};
|
|
624
719
|
|
|
625
720
|
// src/engine/config.ts
|
|
626
721
|
var BootRefused = class extends Error {
|
|
@@ -983,9 +1078,9 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
983
1078
|
release();
|
|
984
1079
|
await settled;
|
|
985
1080
|
},
|
|
986
|
-
async rollback(
|
|
1081
|
+
async rollback(reason2) {
|
|
987
1082
|
if (!opening) return;
|
|
988
|
-
fail(
|
|
1083
|
+
fail(reason2);
|
|
989
1084
|
await settled?.catch(() => void 0);
|
|
990
1085
|
}
|
|
991
1086
|
};
|
|
@@ -1028,18 +1123,48 @@ function reviveVectors(row, vectorCols) {
|
|
|
1028
1123
|
}
|
|
1029
1124
|
return row;
|
|
1030
1125
|
}
|
|
1126
|
+
function transformsOf(schema, table) {
|
|
1127
|
+
const out = /* @__PURE__ */ new Map();
|
|
1128
|
+
for (const [key, def] of Object.entries(schema.tables ?? {})) {
|
|
1129
|
+
if ((def.name ?? key) !== table) continue;
|
|
1130
|
+
for (const [col, c] of Object.entries(def.columns ?? {})) {
|
|
1131
|
+
const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
|
|
1132
|
+
const t = d?.transform;
|
|
1133
|
+
if (t !== null && typeof t === "object") {
|
|
1134
|
+
out.set(col, t);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return out;
|
|
1139
|
+
}
|
|
1140
|
+
function applyFromDb(row, transforms) {
|
|
1141
|
+
if (transforms.size === 0) return row;
|
|
1142
|
+
for (const [col, t] of transforms) {
|
|
1143
|
+
if (t.fromDb === void 0) continue;
|
|
1144
|
+
const v = row[col];
|
|
1145
|
+
if (v === null || v === void 0) continue;
|
|
1146
|
+
row[col] = t.fromDb(v);
|
|
1147
|
+
}
|
|
1148
|
+
return row;
|
|
1149
|
+
}
|
|
1031
1150
|
function asTableRow(table, row) {
|
|
1032
|
-
|
|
1151
|
+
const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
|
|
1152
|
+
return applyFromDb(revived, transformsOf(currentSchema, table));
|
|
1033
1153
|
}
|
|
1034
1154
|
function asTableRows(table, rows) {
|
|
1035
1155
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1036
|
-
|
|
1156
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1157
|
+
return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
|
|
1037
1158
|
}
|
|
1038
1159
|
function asBindParams(table, cols, data) {
|
|
1039
1160
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1161
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1040
1162
|
return cols.map((c) => {
|
|
1041
1163
|
const v = data[c];
|
|
1042
|
-
|
|
1164
|
+
if (Array.isArray(v) && vectorCols.has(c)) return toVectorLiteral(v);
|
|
1165
|
+
const t = transforms.get(c);
|
|
1166
|
+
if (t?.toDb !== void 0 && v !== null && v !== void 0) return t.toDb(v);
|
|
1167
|
+
return v;
|
|
1043
1168
|
});
|
|
1044
1169
|
}
|
|
1045
1170
|
var SELECTIVITY_EXACT_THRESHOLD = 1e4;
|
|
@@ -1233,6 +1358,20 @@ function limitClause(limit) {
|
|
|
1233
1358
|
}
|
|
1234
1359
|
return ` LIMIT ${limit}`;
|
|
1235
1360
|
}
|
|
1361
|
+
function offsetClause(offset, limit) {
|
|
1362
|
+
if (offset === void 0) return "";
|
|
1363
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
1364
|
+
throw new Error(
|
|
1365
|
+
`findMany: offset bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(offset)})`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
if (limit === void 0) {
|
|
1369
|
+
throw new Error(
|
|
1370
|
+
"findMany: offset yaln\u0131z limit ile birlikte verilir \u2014 limitsiz offset bir sayfa de\u011Fil, s\u0131n\u0131rs\u0131z bir kuyru\u011Fun kayd\u0131r\u0131lm\u0131\u015F\u0131d\u0131r"
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
return ` OFFSET ${offset}`;
|
|
1374
|
+
}
|
|
1236
1375
|
function compileWhere(table, colSet, where, add, caller = "search") {
|
|
1237
1376
|
const parts = [];
|
|
1238
1377
|
for (const [col, cond] of Object.entries(where)) {
|
|
@@ -1261,6 +1400,13 @@ function compileWhere(table, colSet, where, add, caller = "search") {
|
|
|
1261
1400
|
}
|
|
1262
1401
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1263
1402
|
}
|
|
1403
|
+
function onConflictTail(cols, conflict, action) {
|
|
1404
|
+
const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
|
|
1405
|
+
if (action === "ignore") return `${target} DO NOTHING`;
|
|
1406
|
+
const conflictSet = new Set(conflict);
|
|
1407
|
+
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1408
|
+
return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1409
|
+
}
|
|
1264
1410
|
var cachedVectorSchema = null;
|
|
1265
1411
|
var cachedTrgmSchema = null;
|
|
1266
1412
|
async function trgmSchemaOf(live) {
|
|
@@ -1370,6 +1516,39 @@ function createOps(tx) {
|
|
|
1370
1516
|
}
|
|
1371
1517
|
const cols = Object.keys(data);
|
|
1372
1518
|
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
1519
|
+
if (searchConfigFor(table)?.validity) {
|
|
1520
|
+
const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1521
|
+
const insertSql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING *`;
|
|
1522
|
+
const live = await at();
|
|
1523
|
+
const bindData = asBindParams(table, cols, data);
|
|
1524
|
+
const first = await live.unsafe(insertSql, bindData);
|
|
1525
|
+
if (first[0]) return asTableRow(table, first[0]);
|
|
1526
|
+
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
1527
|
+
const condBind = conflict.map((c) => data[c]);
|
|
1528
|
+
const cur = await live.unsafe(
|
|
1529
|
+
`SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
|
|
1530
|
+
condBind
|
|
1531
|
+
);
|
|
1532
|
+
const old = cur[0];
|
|
1533
|
+
if (old === void 0) {
|
|
1534
|
+
throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 ge\xE7erli sat\u0131r bulunamad\u0131, iste\u011Fi yineleyin (D-020)`);
|
|
1535
|
+
}
|
|
1536
|
+
const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
|
|
1537
|
+
await live.unsafe(
|
|
1538
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
|
|
1539
|
+
[old[pk]]
|
|
1540
|
+
);
|
|
1541
|
+
const second = await live.unsafe(insertSql, bindData);
|
|
1542
|
+
if (!second[0]) {
|
|
1543
|
+
throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 yeni sat\u0131r a\xE7\u0131lamad\u0131, iste\u011Fi yineleyin (D-020)`);
|
|
1544
|
+
}
|
|
1545
|
+
const fresh = second[0];
|
|
1546
|
+
await live.unsafe(
|
|
1547
|
+
`UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
|
|
1548
|
+
[old[pk], fresh[pk]]
|
|
1549
|
+
);
|
|
1550
|
+
return asTableRow(table, second[0]);
|
|
1551
|
+
}
|
|
1373
1552
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1374
1553
|
const conflictSet = new Set(conflict);
|
|
1375
1554
|
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
@@ -1395,6 +1574,86 @@ function createOps(tx) {
|
|
|
1395
1574
|
async delete(table, id) {
|
|
1396
1575
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
1397
1576
|
},
|
|
1577
|
+
/**
|
|
1578
|
+
* Update every row the filter matches, in ONE statement.
|
|
1579
|
+
*
|
|
1580
|
+
* The capability was already here and only reachable from inside a
|
|
1581
|
+
* transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
|
|
1582
|
+
* only door was `update(id, …)`, so "mark every unapproved row in this
|
|
1583
|
+
* household" was an N+1 loop or hand-written SQL — and hand-written SQL is
|
|
1584
|
+
* where the typed surface and RLS both stop helping.
|
|
1585
|
+
*
|
|
1586
|
+
* The filter language is `findMany`'s, compiled by the same `compileWhere`:
|
|
1587
|
+
* one filter language, or the two spellings drift.
|
|
1588
|
+
*
|
|
1589
|
+
* AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
|
|
1590
|
+
* and the shape that produces it by accident — a filter object built from
|
|
1591
|
+
* request input that happened to come back empty — is exactly the shape that
|
|
1592
|
+
* should not silently succeed. Callers who mean every row say so with a
|
|
1593
|
+
* predicate that is true for every row.
|
|
1594
|
+
*/
|
|
1595
|
+
async updateMany(table, where, set) {
|
|
1596
|
+
const cols = Object.keys(set);
|
|
1597
|
+
if (cols.length === 0) return [];
|
|
1598
|
+
if (Object.keys(where).length === 0) {
|
|
1599
|
+
throw new Error(
|
|
1600
|
+
`updateMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 g\xFCncellerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const params = [];
|
|
1604
|
+
const add = (v) => {
|
|
1605
|
+
params.push(v);
|
|
1606
|
+
return `$${params.length}`;
|
|
1607
|
+
};
|
|
1608
|
+
const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
|
|
1609
|
+
const known = schemaColumns(table);
|
|
1610
|
+
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
1611
|
+
const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
|
|
1612
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1613
|
+
},
|
|
1614
|
+
/**
|
|
1615
|
+
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
1616
|
+
* many went. Same filter language, same empty-filter refusal as
|
|
1617
|
+
* {@link updateMany} — and here the accident is worse.
|
|
1618
|
+
*/
|
|
1619
|
+
async deleteMany(table, where) {
|
|
1620
|
+
if (Object.keys(where).length === 0) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`deleteMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 silerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
const params = [];
|
|
1626
|
+
const add = (v) => {
|
|
1627
|
+
params.push(v);
|
|
1628
|
+
return `$${params.length}`;
|
|
1629
|
+
};
|
|
1630
|
+
const known = schemaColumns(table);
|
|
1631
|
+
const whereSql = compileWhere(table, known, where, add, "deleteMany");
|
|
1632
|
+
const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
|
|
1633
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1634
|
+
return rows.length;
|
|
1635
|
+
},
|
|
1636
|
+
/**
|
|
1637
|
+
* How many rows match — the half of pagination `limit`/`offset` cannot
|
|
1638
|
+
* supply. Without it a page count is either a guess or "fetch everything and
|
|
1639
|
+
* read .length", and the second one is the scale risk this surface exists to
|
|
1640
|
+
* remove.
|
|
1641
|
+
*
|
|
1642
|
+
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
1643
|
+
* reads do not destroy anything.
|
|
1644
|
+
*/
|
|
1645
|
+
async count(table, where = {}) {
|
|
1646
|
+
const params = [];
|
|
1647
|
+
const add = (v) => {
|
|
1648
|
+
params.push(v);
|
|
1649
|
+
return `$${params.length}`;
|
|
1650
|
+
};
|
|
1651
|
+
const known = schemaColumns(table);
|
|
1652
|
+
const whereSql = compileWhere(table, known, where, add, "count");
|
|
1653
|
+
const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
|
|
1654
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1655
|
+
return Number(rows[0]?.n ?? 0);
|
|
1656
|
+
},
|
|
1398
1657
|
async findById(table, id) {
|
|
1399
1658
|
const rows = await (await at()).unsafe(
|
|
1400
1659
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
@@ -1412,7 +1671,8 @@ function createOps(tx) {
|
|
|
1412
1671
|
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
1413
1672
|
const order = orderClause(table, known, opts.orderBy);
|
|
1414
1673
|
const limit = limitClause(opts.limit);
|
|
1415
|
-
const
|
|
1674
|
+
const offset = offsetClause(opts.offset, opts.limit);
|
|
1675
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
|
|
1416
1676
|
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1417
1677
|
},
|
|
1418
1678
|
/**
|
|
@@ -1495,7 +1755,9 @@ function createOps(tx) {
|
|
|
1495
1755
|
bindC.push(v);
|
|
1496
1756
|
return `$${bindC.length}`;
|
|
1497
1757
|
};
|
|
1498
|
-
const
|
|
1758
|
+
const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
|
|
1759
|
+
const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1760
|
+
const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
|
|
1499
1761
|
const liveC = await at();
|
|
1500
1762
|
const K2 = 50;
|
|
1501
1763
|
const cpool = Math.max(limit * 9, 90);
|
|
@@ -1504,8 +1766,19 @@ function createOps(tx) {
|
|
|
1504
1766
|
if (qvC !== null) {
|
|
1505
1767
|
const sch = await vectorSchemaWithGuc(liveC);
|
|
1506
1768
|
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1769
|
+
let exactOrderC = "";
|
|
1770
|
+
if (userWhereC !== "") {
|
|
1771
|
+
const probeRows = await liveC.unsafe(
|
|
1772
|
+
`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
|
|
1773
|
+
bindC.slice()
|
|
1774
|
+
);
|
|
1775
|
+
const n = probeRows?.[0]?.n;
|
|
1776
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1777
|
+
exactOrderC = " + 0.0";
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1507
1780
|
const vp = addC(toVectorLiteral(qvC));
|
|
1508
|
-
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)) AS r FROM ${quoteIdent(ck.table)} c
|
|
1781
|
+
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)${exactOrderC}) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
|
|
1509
1782
|
}
|
|
1510
1783
|
let qpC = "";
|
|
1511
1784
|
let qpCIdx = -1;
|
|
@@ -1513,7 +1786,7 @@ function createOps(tx) {
|
|
|
1513
1786
|
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
1514
1787
|
qpC = addC(qTextC);
|
|
1515
1788
|
qpCIdx = bindC.length - 1;
|
|
1516
|
-
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c
|
|
1789
|
+
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
|
|
1517
1790
|
}
|
|
1518
1791
|
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
1519
1792
|
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
@@ -1528,13 +1801,13 @@ function createOps(tx) {
|
|
|
1528
1801
|
} else {
|
|
1529
1802
|
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
1530
1803
|
}
|
|
1531
|
-
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid
|
|
1804
|
+
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid`;
|
|
1532
1805
|
return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
1533
1806
|
};
|
|
1534
1807
|
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
1535
1808
|
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
1536
1809
|
const tsch = await trgmSchemaOf(liveC);
|
|
1537
|
-
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c
|
|
1810
|
+
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) > 0.3 ORDER BY r LIMIT ${cpool}`;
|
|
1538
1811
|
const retryBindC = bindC.slice();
|
|
1539
1812
|
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
1540
1813
|
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
@@ -1630,7 +1903,7 @@ function createOps(tx) {
|
|
|
1630
1903
|
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
1631
1904
|
const tsch = await trgmSchemaOf(live);
|
|
1632
1905
|
const expr = ftsExprOf(cfg.ftsCols);
|
|
1633
|
-
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${
|
|
1906
|
+
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) > 0.3${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
1634
1907
|
const retryBind = bind.slice();
|
|
1635
1908
|
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
1636
1909
|
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
@@ -1653,6 +1926,25 @@ function createOps(tx) {
|
|
|
1653
1926
|
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
1654
1927
|
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
1655
1928
|
*/
|
|
1929
|
+
/** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
|
|
1930
|
+
* `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
|
|
1931
|
+
* yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
|
|
1932
|
+
* search'teki alan geriye-uyum için DURUR. where + validity default'u
|
|
1933
|
+
* sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
|
|
1934
|
+
async facets(table, params) {
|
|
1935
|
+
const cfg = searchConfigFor(table);
|
|
1936
|
+
if (!cfg) {
|
|
1937
|
+
throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
|
|
1938
|
+
}
|
|
1939
|
+
for (const col of params.facets) {
|
|
1940
|
+
if (!cfg.colSet.has(col)) {
|
|
1941
|
+
throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const live = await at();
|
|
1945
|
+
const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
|
|
1946
|
+
return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
|
|
1947
|
+
},
|
|
1656
1948
|
async similar(table, id, opts = {}) {
|
|
1657
1949
|
const cfg = searchConfigFor(table);
|
|
1658
1950
|
if (!cfg) {
|
|
@@ -1884,17 +2176,35 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1884
2176
|
}
|
|
1885
2177
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1886
2178
|
function isAbortedTransaction(e) {
|
|
1887
|
-
const code = e?.code;
|
|
1888
2179
|
const message = String(e?.message ?? "");
|
|
1889
|
-
return
|
|
2180
|
+
return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
|
|
2181
|
+
}
|
|
2182
|
+
function sqlstateOf(e) {
|
|
2183
|
+
const err = e;
|
|
2184
|
+
const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
|
|
2185
|
+
if (isState(err?.code)) return err.code;
|
|
2186
|
+
if (isState(err?.errno)) return err.errno;
|
|
2187
|
+
if (typeof err?.errno === "number") {
|
|
2188
|
+
const asText = String(err.errno);
|
|
2189
|
+
if (isState(asText)) return asText;
|
|
2190
|
+
}
|
|
2191
|
+
return void 0;
|
|
1890
2192
|
}
|
|
1891
2193
|
function isForeignKeyViolation(e) {
|
|
1892
|
-
return e
|
|
2194
|
+
return sqlstateOf(e) === "23503";
|
|
2195
|
+
}
|
|
2196
|
+
function isUniqueViolation(e) {
|
|
2197
|
+
return sqlstateOf(e) === "23505";
|
|
2198
|
+
}
|
|
2199
|
+
function constraintOf(e) {
|
|
2200
|
+
const named = e?.constraint;
|
|
2201
|
+
if (typeof named === "string" && named.length > 0) return named;
|
|
2202
|
+
const message = String(e?.message ?? "");
|
|
2203
|
+
return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
|
|
1893
2204
|
}
|
|
1894
2205
|
function isLockTimeout(e) {
|
|
1895
|
-
const code = e?.code;
|
|
1896
2206
|
const message = String(e?.message ?? "");
|
|
1897
|
-
return
|
|
2207
|
+
return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
|
|
1898
2208
|
}
|
|
1899
2209
|
function diagnosingDriver(sql, surface = "service") {
|
|
1900
2210
|
const original = (e) => String(e?.message ?? e);
|
|
@@ -1904,6 +2214,9 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
1904
2214
|
`This request cannot write any more: an earlier write in it failed, and a request runs in ONE Postgres transaction, so every statement after the failure is refused. Nothing here is retryable in place. Wrap a write you expect to fail in \`Database.attempt(async (tx) => \u2026)\` \u2014 it takes a SAVEPOINT, so only that write rolls back \u2014 or use \`Database.tables.<t>.upsert(data, { onConflict: [...] })\` when the failure you were bracing for is a duplicate row. (${original(e)})`
|
|
1905
2215
|
);
|
|
1906
2216
|
}
|
|
2217
|
+
if (isUniqueViolation(e)) {
|
|
2218
|
+
return new UniqueViolation(constraintOf(e));
|
|
2219
|
+
}
|
|
1907
2220
|
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
1908
2221
|
return new Error(
|
|
1909
2222
|
`Database.asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`
|
|
@@ -1955,9 +2268,9 @@ function createRequestDatabase(sql, identity) {
|
|
|
1955
2268
|
await tx.commit();
|
|
1956
2269
|
await serviceTx?.commit();
|
|
1957
2270
|
},
|
|
1958
|
-
async rollback(
|
|
1959
|
-
await tx.rollback(
|
|
1960
|
-
await serviceTx?.rollback(
|
|
2271
|
+
async rollback(reason2) {
|
|
2272
|
+
await tx.rollback(reason2);
|
|
2273
|
+
await serviceTx?.rollback(reason2);
|
|
1961
2274
|
}
|
|
1962
2275
|
};
|
|
1963
2276
|
}
|
|
@@ -2032,10 +2345,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
2032
2345
|
const rendered = cols.map(
|
|
2033
2346
|
(c) => renderValue(op.values[c], c, args, results, opVectorCols)
|
|
2034
2347
|
);
|
|
2035
|
-
|
|
2036
|
-
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
2037
|
-
const action = sets.length ? `DO UPDATE SET ${sets.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
2038
|
-
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
2348
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
|
|
2039
2349
|
break;
|
|
2040
2350
|
}
|
|
2041
2351
|
case "insertMany": {
|
|
@@ -2045,7 +2355,9 @@ async function runPlanOp(sp, op, results) {
|
|
|
2045
2355
|
const tuples = rows.map(
|
|
2046
2356
|
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
2047
2357
|
);
|
|
2048
|
-
|
|
2358
|
+
const conflictCols = op.onConflict ?? [];
|
|
2359
|
+
const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
|
|
2360
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
2049
2361
|
break;
|
|
2050
2362
|
}
|
|
2051
2363
|
case "update": {
|
|
@@ -2120,6 +2432,38 @@ function getRoutes(ctor) {
|
|
|
2120
2432
|
}));
|
|
2121
2433
|
}
|
|
2122
2434
|
|
|
2435
|
+
// src/decorators/controller.ts
|
|
2436
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("palbase.backend.allControllers");
|
|
2437
|
+
function registry() {
|
|
2438
|
+
const g = globalThis;
|
|
2439
|
+
const existing = g[REGISTRY];
|
|
2440
|
+
if (existing) return existing;
|
|
2441
|
+
const fresh = [];
|
|
2442
|
+
g[REGISTRY] = fresh;
|
|
2443
|
+
return fresh;
|
|
2444
|
+
}
|
|
2445
|
+
function getRegisteredControllers() {
|
|
2446
|
+
return registry().slice();
|
|
2447
|
+
}
|
|
2448
|
+
var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
|
|
2449
|
+
function appAuthSlot() {
|
|
2450
|
+
return globalThis;
|
|
2451
|
+
}
|
|
2452
|
+
function getDefaultAuth() {
|
|
2453
|
+
return appAuthSlot()[APP_DEFAULT_AUTH];
|
|
2454
|
+
}
|
|
2455
|
+
function resolveEffectiveAuth(routeAuth, controllerAuth) {
|
|
2456
|
+
return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
|
|
2457
|
+
}
|
|
2458
|
+
function assertZeroArgConstructor(Ctrl, kind) {
|
|
2459
|
+
const arity = Ctrl.length ?? 0;
|
|
2460
|
+
if (arity === 0) return;
|
|
2461
|
+
const name = Ctrl.name ?? "<anonymous>";
|
|
2462
|
+
throw new Error(
|
|
2463
|
+
`${kind} ${name} declares a constructor with ${arity} parameter(s). A ${kind} is constructed by the runtime with a zero-argument constructor \u2014 there is no injector to supply them, so every parameter would arrive as undefined. Hold the dependency as a module-level singleton the ${kind} imports (\`const repo = makeRepo()\` beside the class), and construct the service directly in tests (e.g. \`new TodoService(fakeDatabase().db)\`).`
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2123
2467
|
// src/engine/router.ts
|
|
2124
2468
|
var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
|
|
2125
2469
|
function toSegments(path) {
|
|
@@ -2138,6 +2482,7 @@ function buildRouteTable(controllers) {
|
|
|
2138
2482
|
`controller ${name} collected zero routes. Either it declares no @Get/@Post/\u2026 , or its decorator metadata was erased at build time \u2014 check that the bundle was compiled with experimentalDecorators enabled.`
|
|
2139
2483
|
);
|
|
2140
2484
|
}
|
|
2485
|
+
assertZeroArgConstructor(Ctrl, "controller");
|
|
2141
2486
|
const instance = new ctor();
|
|
2142
2487
|
for (const r of routes) {
|
|
2143
2488
|
const full = `${basePath}${r.subpath ?? ""}` || "/";
|
|
@@ -2147,7 +2492,10 @@ function buildRouteTable(controllers) {
|
|
|
2147
2492
|
meta: r,
|
|
2148
2493
|
instance,
|
|
2149
2494
|
id: `${r.method} ${full}`,
|
|
2150
|
-
|
|
2495
|
+
// The route's OWN spec is deliberately not folded in here: this field
|
|
2496
|
+
// is what applies when the route is silent, and `effectiveAuth` puts
|
|
2497
|
+
// the route level back on top per request.
|
|
2498
|
+
controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
|
|
2151
2499
|
});
|
|
2152
2500
|
}
|
|
2153
2501
|
}
|
|
@@ -2715,31 +3063,28 @@ async function createApp(opts) {
|
|
|
2715
3063
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
2716
3064
|
}
|
|
2717
3065
|
}
|
|
3066
|
+
let runShutdownHooks;
|
|
3067
|
+
try {
|
|
3068
|
+
runShutdownHooks = await __runStartHooks();
|
|
3069
|
+
} catch (err) {
|
|
3070
|
+
await closeDriver(sql);
|
|
3071
|
+
throw err;
|
|
3072
|
+
}
|
|
2718
3073
|
return {
|
|
2719
3074
|
handle,
|
|
2720
3075
|
routes,
|
|
2721
3076
|
config,
|
|
2722
3077
|
runInServiceScope,
|
|
2723
3078
|
async shutdown() {
|
|
2724
|
-
|
|
2725
|
-
await
|
|
2726
|
-
await closable.end?.();
|
|
3079
|
+
await runShutdownHooks();
|
|
3080
|
+
await closeDriver(sql);
|
|
2727
3081
|
}
|
|
2728
3082
|
};
|
|
2729
3083
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
const g = globalThis;
|
|
2735
|
-
const existing = g[REGISTRY];
|
|
2736
|
-
if (existing) return existing;
|
|
2737
|
-
const fresh = [];
|
|
2738
|
-
g[REGISTRY] = fresh;
|
|
2739
|
-
return fresh;
|
|
2740
|
-
}
|
|
2741
|
-
function getRegisteredControllers() {
|
|
2742
|
-
return registry().slice();
|
|
3084
|
+
async function closeDriver(sql) {
|
|
3085
|
+
const closable = sql;
|
|
3086
|
+
await closable.close?.();
|
|
3087
|
+
await closable.end?.();
|
|
2743
3088
|
}
|
|
2744
3089
|
|
|
2745
3090
|
// src/bin/palbase-backend.ts
|