@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
package/dist/engine/index.cjs
CHANGED
|
@@ -265,7 +265,7 @@ var TxPlanBuilder = class {
|
|
|
265
265
|
`${name}.upsert()`
|
|
266
266
|
);
|
|
267
267
|
},
|
|
268
|
-
insertMany: (rows) => {
|
|
268
|
+
insertMany: (rows, opts) => {
|
|
269
269
|
if (rows.length === 0) {
|
|
270
270
|
return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
|
|
271
271
|
}
|
|
@@ -276,7 +276,23 @@ var TxPlanBuilder = class {
|
|
|
276
276
|
}
|
|
277
277
|
const encoded = rows.map((row) => encodeMap(row, false));
|
|
278
278
|
assertUniformRows(encoded, name);
|
|
279
|
-
|
|
279
|
+
if (opts !== void 0 && opts.onConflict.length === 0) {
|
|
280
|
+
throw new TxPlanError(
|
|
281
|
+
`${name}.insertMany() was given a conflict action with no onConflict columns. Postgres matches a collision on columns, so name them.`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return this.push(
|
|
285
|
+
{
|
|
286
|
+
op: "insertMany",
|
|
287
|
+
table: name,
|
|
288
|
+
rows: encoded,
|
|
289
|
+
// Omitted entirely when no options were given, so the op a plain
|
|
290
|
+
// insertMany produces is byte-identical to the one it produced
|
|
291
|
+
// before this option existed.
|
|
292
|
+
...opts !== void 0 ? { onConflict: opts.onConflict, action: opts.action ?? "ignore" } : {}
|
|
293
|
+
},
|
|
294
|
+
`${name}.insertMany()`
|
|
295
|
+
);
|
|
280
296
|
},
|
|
281
297
|
updateWhere: (where, set) => {
|
|
282
298
|
const encodedWhere = encodeMap(where, false);
|
|
@@ -450,6 +466,42 @@ function __getRuntime() {
|
|
|
450
466
|
}
|
|
451
467
|
return runtime;
|
|
452
468
|
}
|
|
469
|
+
var LIFECYCLE = /* @__PURE__ */ Symbol.for("palbase.backend.lifecycleHooks");
|
|
470
|
+
function declaredLifecycle() {
|
|
471
|
+
const g = globalThis;
|
|
472
|
+
return g[LIFECYCLE] ??= { start: [], shutdown: [] };
|
|
473
|
+
}
|
|
474
|
+
function reason(err) {
|
|
475
|
+
return err instanceof Error ? err.message : String(err);
|
|
476
|
+
}
|
|
477
|
+
async function drain(hooks) {
|
|
478
|
+
for (const h of [...hooks].reverse()) {
|
|
479
|
+
try {
|
|
480
|
+
await h.run();
|
|
481
|
+
} catch (err) {
|
|
482
|
+
console.error(`[palbase] shutdown hook "${h.name}" failed: ${reason(err)}`, err);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function __runStartHooks() {
|
|
487
|
+
const slot = declaredLifecycle();
|
|
488
|
+
const start = slot.start.splice(0);
|
|
489
|
+
const shutdown = slot.shutdown.splice(0);
|
|
490
|
+
for (const h of start) {
|
|
491
|
+
try {
|
|
492
|
+
await h.run();
|
|
493
|
+
} catch (err) {
|
|
494
|
+
await drain(shutdown);
|
|
495
|
+
throw new Error(`[palbase] start hook "${h.name}" failed: ${reason(err)}`, { cause: err });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
let drained = false;
|
|
499
|
+
return async () => {
|
|
500
|
+
if (drained) return;
|
|
501
|
+
drained = true;
|
|
502
|
+
await drain(shutdown);
|
|
503
|
+
};
|
|
504
|
+
}
|
|
453
505
|
function makeServiceProxy(key) {
|
|
454
506
|
const handler = {
|
|
455
507
|
get(_target, prop, receiver) {
|
|
@@ -477,6 +529,7 @@ function makeTablesAccessor(ops) {
|
|
|
477
529
|
search: (params) => ops().search(name, params),
|
|
478
530
|
similar: (id, params) => ops().similar(name, id, params),
|
|
479
531
|
recommend: (params) => ops().recommend(name, params),
|
|
532
|
+
facets: (params) => ops().facets(name, params),
|
|
480
533
|
supersede: (id, row) => ops().supersede(name, id, row)
|
|
481
534
|
};
|
|
482
535
|
}
|
|
@@ -495,9 +548,13 @@ function makeTypedSurface(raw) {
|
|
|
495
548
|
findById: (table, id) => raw.findById(table, id),
|
|
496
549
|
findMany: (table, query, opts) => raw.findMany(table, query, opts),
|
|
497
550
|
upsert: (table, data, opts) => raw.upsert(table, data, opts),
|
|
551
|
+
updateMany: (table, where, set) => raw.updateMany(table, where, set),
|
|
552
|
+
deleteMany: (table, where) => raw.deleteMany(table, where),
|
|
553
|
+
count: (table, where) => raw.count(table, where),
|
|
498
554
|
search: (table, params) => raw.search(table, params),
|
|
499
555
|
similar: (table, id, params) => reco.similar(table, id, params),
|
|
500
556
|
recommend: (table, params) => reco.recommend(table, params),
|
|
557
|
+
facets: (table, params) => reco.facets(table, params),
|
|
501
558
|
supersede: (table, id, row) => raw.supersede(table, id, row)
|
|
502
559
|
};
|
|
503
560
|
return Object.assign(ops, {
|
|
@@ -655,6 +712,44 @@ var HttpError = class extends Error {
|
|
|
655
712
|
return result;
|
|
656
713
|
}
|
|
657
714
|
};
|
|
715
|
+
var NamedHttpError = class extends HttpError {
|
|
716
|
+
constructor(status, defaultCode, name, message, code, data) {
|
|
717
|
+
super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
|
|
718
|
+
this.name = name;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
function defaultMessage(name) {
|
|
722
|
+
const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
723
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
724
|
+
}
|
|
725
|
+
var Conflict = class extends NamedHttpError {
|
|
726
|
+
constructor(message, code, data) {
|
|
727
|
+
super(409, "conflict", "Conflict", message, code, data);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
var UniqueViolation = class extends Conflict {
|
|
731
|
+
/**
|
|
732
|
+
* Whether `e` is a unique violation — REGARDLESS of which copy of this SDK
|
|
733
|
+
* constructed it.
|
|
734
|
+
*
|
|
735
|
+
* Use this instead of `instanceof`. Measured on a live stack: a controller
|
|
736
|
+
* bundle INLINES its own copy of `@palbase/backend`, and the engine that
|
|
737
|
+
* raises this error is the runtime's copy. Two copies, two class identities,
|
|
738
|
+
* and `e instanceof UniqueViolation` is false in the one place a caller
|
|
739
|
+
* writes it — a check that reads as correct and silently never matches.
|
|
740
|
+
*/
|
|
741
|
+
static is(e) {
|
|
742
|
+
return typeof e === "object" && e !== null && e.name === "UniqueViolation" && typeof e.constraint === "string";
|
|
743
|
+
}
|
|
744
|
+
/** The unique constraint the statement violated, as Postgres named it.
|
|
745
|
+
* `""` when the driver did not say which — see `engine/db.ts`. */
|
|
746
|
+
constraint;
|
|
747
|
+
constructor(constraint, message, code, data) {
|
|
748
|
+
super(message ?? "Unique constraint violated", code ?? "unique_violation", data);
|
|
749
|
+
this.name = "UniqueViolation";
|
|
750
|
+
this.constraint = constraint;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
658
753
|
|
|
659
754
|
// src/engine/config.ts
|
|
660
755
|
var BootRefused = class extends Error {
|
|
@@ -1017,9 +1112,9 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
1017
1112
|
release();
|
|
1018
1113
|
await settled;
|
|
1019
1114
|
},
|
|
1020
|
-
async rollback(
|
|
1115
|
+
async rollback(reason2) {
|
|
1021
1116
|
if (!opening) return;
|
|
1022
|
-
fail(
|
|
1117
|
+
fail(reason2);
|
|
1023
1118
|
await settled?.catch(() => void 0);
|
|
1024
1119
|
}
|
|
1025
1120
|
};
|
|
@@ -1062,18 +1157,48 @@ function reviveVectors(row, vectorCols) {
|
|
|
1062
1157
|
}
|
|
1063
1158
|
return row;
|
|
1064
1159
|
}
|
|
1160
|
+
function transformsOf(schema, table) {
|
|
1161
|
+
const out = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const [key, def] of Object.entries(schema.tables ?? {})) {
|
|
1163
|
+
if ((def.name ?? key) !== table) continue;
|
|
1164
|
+
for (const [col, c] of Object.entries(def.columns ?? {})) {
|
|
1165
|
+
const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
|
|
1166
|
+
const t = d?.transform;
|
|
1167
|
+
if (t !== null && typeof t === "object") {
|
|
1168
|
+
out.set(col, t);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return out;
|
|
1173
|
+
}
|
|
1174
|
+
function applyFromDb(row, transforms) {
|
|
1175
|
+
if (transforms.size === 0) return row;
|
|
1176
|
+
for (const [col, t] of transforms) {
|
|
1177
|
+
if (t.fromDb === void 0) continue;
|
|
1178
|
+
const v = row[col];
|
|
1179
|
+
if (v === null || v === void 0) continue;
|
|
1180
|
+
row[col] = t.fromDb(v);
|
|
1181
|
+
}
|
|
1182
|
+
return row;
|
|
1183
|
+
}
|
|
1065
1184
|
function asTableRow(table, row) {
|
|
1066
|
-
|
|
1185
|
+
const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
|
|
1186
|
+
return applyFromDb(revived, transformsOf(currentSchema, table));
|
|
1067
1187
|
}
|
|
1068
1188
|
function asTableRows(table, rows) {
|
|
1069
1189
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1070
|
-
|
|
1190
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1191
|
+
return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
|
|
1071
1192
|
}
|
|
1072
1193
|
function asBindParams(table, cols, data) {
|
|
1073
1194
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1195
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1074
1196
|
return cols.map((c) => {
|
|
1075
1197
|
const v = data[c];
|
|
1076
|
-
|
|
1198
|
+
if (Array.isArray(v) && vectorCols.has(c)) return toVectorLiteral(v);
|
|
1199
|
+
const t = transforms.get(c);
|
|
1200
|
+
if (t?.toDb !== void 0 && v !== null && v !== void 0) return t.toDb(v);
|
|
1201
|
+
return v;
|
|
1077
1202
|
});
|
|
1078
1203
|
}
|
|
1079
1204
|
var SELECTIVITY_EXACT_THRESHOLD = 1e4;
|
|
@@ -1267,6 +1392,20 @@ function limitClause(limit) {
|
|
|
1267
1392
|
}
|
|
1268
1393
|
return ` LIMIT ${limit}`;
|
|
1269
1394
|
}
|
|
1395
|
+
function offsetClause(offset, limit) {
|
|
1396
|
+
if (offset === void 0) return "";
|
|
1397
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`findMany: offset bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(offset)})`
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
if (limit === void 0) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
"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"
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
return ` OFFSET ${offset}`;
|
|
1408
|
+
}
|
|
1270
1409
|
function compileWhere(table, colSet, where, add, caller = "search") {
|
|
1271
1410
|
const parts = [];
|
|
1272
1411
|
for (const [col, cond] of Object.entries(where)) {
|
|
@@ -1295,6 +1434,13 @@ function compileWhere(table, colSet, where, add, caller = "search") {
|
|
|
1295
1434
|
}
|
|
1296
1435
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1297
1436
|
}
|
|
1437
|
+
function onConflictTail(cols, conflict, action) {
|
|
1438
|
+
const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
|
|
1439
|
+
if (action === "ignore") return `${target} DO NOTHING`;
|
|
1440
|
+
const conflictSet = new Set(conflict);
|
|
1441
|
+
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1442
|
+
return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1443
|
+
}
|
|
1298
1444
|
var cachedVectorSchema = null;
|
|
1299
1445
|
var cachedTrgmSchema = null;
|
|
1300
1446
|
async function trgmSchemaOf(live) {
|
|
@@ -1404,6 +1550,39 @@ function createOps(tx) {
|
|
|
1404
1550
|
}
|
|
1405
1551
|
const cols = Object.keys(data);
|
|
1406
1552
|
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
1553
|
+
if (searchConfigFor(table)?.validity) {
|
|
1554
|
+
const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1555
|
+
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 *`;
|
|
1556
|
+
const live = await at();
|
|
1557
|
+
const bindData = asBindParams(table, cols, data);
|
|
1558
|
+
const first = await live.unsafe(insertSql, bindData);
|
|
1559
|
+
if (first[0]) return asTableRow(table, first[0]);
|
|
1560
|
+
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
1561
|
+
const condBind = conflict.map((c) => data[c]);
|
|
1562
|
+
const cur = await live.unsafe(
|
|
1563
|
+
`SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
|
|
1564
|
+
condBind
|
|
1565
|
+
);
|
|
1566
|
+
const old = cur[0];
|
|
1567
|
+
if (old === void 0) {
|
|
1568
|
+
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)`);
|
|
1569
|
+
}
|
|
1570
|
+
const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
|
|
1571
|
+
await live.unsafe(
|
|
1572
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
|
|
1573
|
+
[old[pk]]
|
|
1574
|
+
);
|
|
1575
|
+
const second = await live.unsafe(insertSql, bindData);
|
|
1576
|
+
if (!second[0]) {
|
|
1577
|
+
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)`);
|
|
1578
|
+
}
|
|
1579
|
+
const fresh = second[0];
|
|
1580
|
+
await live.unsafe(
|
|
1581
|
+
`UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
|
|
1582
|
+
[old[pk], fresh[pk]]
|
|
1583
|
+
);
|
|
1584
|
+
return asTableRow(table, second[0]);
|
|
1585
|
+
}
|
|
1407
1586
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1408
1587
|
const conflictSet = new Set(conflict);
|
|
1409
1588
|
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
@@ -1429,6 +1608,86 @@ function createOps(tx) {
|
|
|
1429
1608
|
async delete(table, id) {
|
|
1430
1609
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
1431
1610
|
},
|
|
1611
|
+
/**
|
|
1612
|
+
* Update every row the filter matches, in ONE statement.
|
|
1613
|
+
*
|
|
1614
|
+
* The capability was already here and only reachable from inside a
|
|
1615
|
+
* transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
|
|
1616
|
+
* only door was `update(id, …)`, so "mark every unapproved row in this
|
|
1617
|
+
* household" was an N+1 loop or hand-written SQL — and hand-written SQL is
|
|
1618
|
+
* where the typed surface and RLS both stop helping.
|
|
1619
|
+
*
|
|
1620
|
+
* The filter language is `findMany`'s, compiled by the same `compileWhere`:
|
|
1621
|
+
* one filter language, or the two spellings drift.
|
|
1622
|
+
*
|
|
1623
|
+
* AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
|
|
1624
|
+
* and the shape that produces it by accident — a filter object built from
|
|
1625
|
+
* request input that happened to come back empty — is exactly the shape that
|
|
1626
|
+
* should not silently succeed. Callers who mean every row say so with a
|
|
1627
|
+
* predicate that is true for every row.
|
|
1628
|
+
*/
|
|
1629
|
+
async updateMany(table, where, set) {
|
|
1630
|
+
const cols = Object.keys(set);
|
|
1631
|
+
if (cols.length === 0) return [];
|
|
1632
|
+
if (Object.keys(where).length === 0) {
|
|
1633
|
+
throw new Error(
|
|
1634
|
+
`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`
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const params = [];
|
|
1638
|
+
const add = (v) => {
|
|
1639
|
+
params.push(v);
|
|
1640
|
+
return `$${params.length}`;
|
|
1641
|
+
};
|
|
1642
|
+
const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
|
|
1643
|
+
const known = schemaColumns(table);
|
|
1644
|
+
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
1645
|
+
const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
|
|
1646
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1647
|
+
},
|
|
1648
|
+
/**
|
|
1649
|
+
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
1650
|
+
* many went. Same filter language, same empty-filter refusal as
|
|
1651
|
+
* {@link updateMany} — and here the accident is worse.
|
|
1652
|
+
*/
|
|
1653
|
+
async deleteMany(table, where) {
|
|
1654
|
+
if (Object.keys(where).length === 0) {
|
|
1655
|
+
throw new Error(
|
|
1656
|
+
`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`
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const params = [];
|
|
1660
|
+
const add = (v) => {
|
|
1661
|
+
params.push(v);
|
|
1662
|
+
return `$${params.length}`;
|
|
1663
|
+
};
|
|
1664
|
+
const known = schemaColumns(table);
|
|
1665
|
+
const whereSql = compileWhere(table, known, where, add, "deleteMany");
|
|
1666
|
+
const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
|
|
1667
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1668
|
+
return rows.length;
|
|
1669
|
+
},
|
|
1670
|
+
/**
|
|
1671
|
+
* How many rows match — the half of pagination `limit`/`offset` cannot
|
|
1672
|
+
* supply. Without it a page count is either a guess or "fetch everything and
|
|
1673
|
+
* read .length", and the second one is the scale risk this surface exists to
|
|
1674
|
+
* remove.
|
|
1675
|
+
*
|
|
1676
|
+
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
1677
|
+
* reads do not destroy anything.
|
|
1678
|
+
*/
|
|
1679
|
+
async count(table, where = {}) {
|
|
1680
|
+
const params = [];
|
|
1681
|
+
const add = (v) => {
|
|
1682
|
+
params.push(v);
|
|
1683
|
+
return `$${params.length}`;
|
|
1684
|
+
};
|
|
1685
|
+
const known = schemaColumns(table);
|
|
1686
|
+
const whereSql = compileWhere(table, known, where, add, "count");
|
|
1687
|
+
const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
|
|
1688
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1689
|
+
return Number(rows[0]?.n ?? 0);
|
|
1690
|
+
},
|
|
1432
1691
|
async findById(table, id) {
|
|
1433
1692
|
const rows = await (await at()).unsafe(
|
|
1434
1693
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
@@ -1446,7 +1705,8 @@ function createOps(tx) {
|
|
|
1446
1705
|
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
1447
1706
|
const order = orderClause(table, known, opts.orderBy);
|
|
1448
1707
|
const limit = limitClause(opts.limit);
|
|
1449
|
-
const
|
|
1708
|
+
const offset = offsetClause(opts.offset, opts.limit);
|
|
1709
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
|
|
1450
1710
|
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1451
1711
|
},
|
|
1452
1712
|
/**
|
|
@@ -1529,7 +1789,9 @@ function createOps(tx) {
|
|
|
1529
1789
|
bindC.push(v);
|
|
1530
1790
|
return `$${bindC.length}`;
|
|
1531
1791
|
};
|
|
1532
|
-
const
|
|
1792
|
+
const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
|
|
1793
|
+
const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1794
|
+
const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
|
|
1533
1795
|
const liveC = await at();
|
|
1534
1796
|
const K2 = 50;
|
|
1535
1797
|
const cpool = Math.max(limit * 9, 90);
|
|
@@ -1538,8 +1800,19 @@ function createOps(tx) {
|
|
|
1538
1800
|
if (qvC !== null) {
|
|
1539
1801
|
const sch = await vectorSchemaWithGuc(liveC);
|
|
1540
1802
|
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1803
|
+
let exactOrderC = "";
|
|
1804
|
+
if (userWhereC !== "") {
|
|
1805
|
+
const probeRows = await liveC.unsafe(
|
|
1806
|
+
`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`,
|
|
1807
|
+
bindC.slice()
|
|
1808
|
+
);
|
|
1809
|
+
const n = probeRows?.[0]?.n;
|
|
1810
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1811
|
+
exactOrderC = " + 0.0";
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1541
1814
|
const vp = addC(toVectorLiteral(qvC));
|
|
1542
|
-
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
|
|
1815
|
+
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}`;
|
|
1543
1816
|
}
|
|
1544
1817
|
let qpC = "";
|
|
1545
1818
|
let qpCIdx = -1;
|
|
@@ -1547,7 +1820,7 @@ function createOps(tx) {
|
|
|
1547
1820
|
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
1548
1821
|
qpC = addC(qTextC);
|
|
1549
1822
|
qpCIdx = bindC.length - 1;
|
|
1550
|
-
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
|
|
1823
|
+
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}`;
|
|
1551
1824
|
}
|
|
1552
1825
|
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
1553
1826
|
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
@@ -1562,13 +1835,13 @@ function createOps(tx) {
|
|
|
1562
1835
|
} else {
|
|
1563
1836
|
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
1564
1837
|
}
|
|
1565
|
-
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
|
|
1838
|
+
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`;
|
|
1566
1839
|
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}`;
|
|
1567
1840
|
};
|
|
1568
1841
|
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
1569
1842
|
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
1570
1843
|
const tsch = await trgmSchemaOf(liveC);
|
|
1571
|
-
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
|
|
1844
|
+
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}`;
|
|
1572
1845
|
const retryBindC = bindC.slice();
|
|
1573
1846
|
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
1574
1847
|
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
@@ -1664,7 +1937,7 @@ function createOps(tx) {
|
|
|
1664
1937
|
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
1665
1938
|
const tsch = await trgmSchemaOf(live);
|
|
1666
1939
|
const expr = ftsExprOf(cfg.ftsCols);
|
|
1667
|
-
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 ${
|
|
1940
|
+
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}`;
|
|
1668
1941
|
const retryBind = bind.slice();
|
|
1669
1942
|
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
1670
1943
|
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
@@ -1687,6 +1960,25 @@ function createOps(tx) {
|
|
|
1687
1960
|
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
1688
1961
|
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
1689
1962
|
*/
|
|
1963
|
+
/** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
|
|
1964
|
+
* `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
|
|
1965
|
+
* yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
|
|
1966
|
+
* search'teki alan geriye-uyum için DURUR. where + validity default'u
|
|
1967
|
+
* sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
|
|
1968
|
+
async facets(table, params) {
|
|
1969
|
+
const cfg = searchConfigFor(table);
|
|
1970
|
+
if (!cfg) {
|
|
1971
|
+
throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
|
|
1972
|
+
}
|
|
1973
|
+
for (const col of params.facets) {
|
|
1974
|
+
if (!cfg.colSet.has(col)) {
|
|
1975
|
+
throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const live = await at();
|
|
1979
|
+
const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
|
|
1980
|
+
return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
|
|
1981
|
+
},
|
|
1690
1982
|
async similar(table, id, opts = {}) {
|
|
1691
1983
|
const cfg = searchConfigFor(table);
|
|
1692
1984
|
if (!cfg) {
|
|
@@ -1918,17 +2210,35 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1918
2210
|
}
|
|
1919
2211
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1920
2212
|
function isAbortedTransaction(e) {
|
|
1921
|
-
const code = e?.code;
|
|
1922
2213
|
const message = String(e?.message ?? "");
|
|
1923
|
-
return
|
|
2214
|
+
return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
|
|
2215
|
+
}
|
|
2216
|
+
function sqlstateOf(e) {
|
|
2217
|
+
const err = e;
|
|
2218
|
+
const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
|
|
2219
|
+
if (isState(err?.code)) return err.code;
|
|
2220
|
+
if (isState(err?.errno)) return err.errno;
|
|
2221
|
+
if (typeof err?.errno === "number") {
|
|
2222
|
+
const asText = String(err.errno);
|
|
2223
|
+
if (isState(asText)) return asText;
|
|
2224
|
+
}
|
|
2225
|
+
return void 0;
|
|
1924
2226
|
}
|
|
1925
2227
|
function isForeignKeyViolation(e) {
|
|
1926
|
-
return e
|
|
2228
|
+
return sqlstateOf(e) === "23503";
|
|
2229
|
+
}
|
|
2230
|
+
function isUniqueViolation(e) {
|
|
2231
|
+
return sqlstateOf(e) === "23505";
|
|
2232
|
+
}
|
|
2233
|
+
function constraintOf(e) {
|
|
2234
|
+
const named = e?.constraint;
|
|
2235
|
+
if (typeof named === "string" && named.length > 0) return named;
|
|
2236
|
+
const message = String(e?.message ?? "");
|
|
2237
|
+
return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
|
|
1927
2238
|
}
|
|
1928
2239
|
function isLockTimeout(e) {
|
|
1929
|
-
const code = e?.code;
|
|
1930
2240
|
const message = String(e?.message ?? "");
|
|
1931
|
-
return
|
|
2241
|
+
return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
|
|
1932
2242
|
}
|
|
1933
2243
|
function diagnosingDriver(sql, surface = "service") {
|
|
1934
2244
|
const original = (e) => String(e?.message ?? e);
|
|
@@ -1938,6 +2248,9 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
1938
2248
|
`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)})`
|
|
1939
2249
|
);
|
|
1940
2250
|
}
|
|
2251
|
+
if (isUniqueViolation(e)) {
|
|
2252
|
+
return new UniqueViolation(constraintOf(e));
|
|
2253
|
+
}
|
|
1941
2254
|
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
1942
2255
|
return new Error(
|
|
1943
2256
|
`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)})`
|
|
@@ -1989,9 +2302,9 @@ function createRequestDatabase(sql, identity) {
|
|
|
1989
2302
|
await tx.commit();
|
|
1990
2303
|
await serviceTx?.commit();
|
|
1991
2304
|
},
|
|
1992
|
-
async rollback(
|
|
1993
|
-
await tx.rollback(
|
|
1994
|
-
await serviceTx?.rollback(
|
|
2305
|
+
async rollback(reason2) {
|
|
2306
|
+
await tx.rollback(reason2);
|
|
2307
|
+
await serviceTx?.rollback(reason2);
|
|
1995
2308
|
}
|
|
1996
2309
|
};
|
|
1997
2310
|
}
|
|
@@ -2066,10 +2379,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
2066
2379
|
const rendered = cols.map(
|
|
2067
2380
|
(c) => renderValue(op.values[c], c, args, results, opVectorCols)
|
|
2068
2381
|
);
|
|
2069
|
-
|
|
2070
|
-
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
2071
|
-
const action = sets.length ? `DO UPDATE SET ${sets.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
2072
|
-
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
2382
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
|
|
2073
2383
|
break;
|
|
2074
2384
|
}
|
|
2075
2385
|
case "insertMany": {
|
|
@@ -2079,7 +2389,9 @@ async function runPlanOp(sp, op, results) {
|
|
|
2079
2389
|
const tuples = rows.map(
|
|
2080
2390
|
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
2081
2391
|
);
|
|
2082
|
-
|
|
2392
|
+
const conflictCols = op.onConflict ?? [];
|
|
2393
|
+
const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
|
|
2394
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
2083
2395
|
break;
|
|
2084
2396
|
}
|
|
2085
2397
|
case "update": {
|
|
@@ -2154,6 +2466,26 @@ function getRoutes(ctor) {
|
|
|
2154
2466
|
}));
|
|
2155
2467
|
}
|
|
2156
2468
|
|
|
2469
|
+
// src/decorators/controller.ts
|
|
2470
|
+
var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
|
|
2471
|
+
function appAuthSlot() {
|
|
2472
|
+
return globalThis;
|
|
2473
|
+
}
|
|
2474
|
+
function getDefaultAuth() {
|
|
2475
|
+
return appAuthSlot()[APP_DEFAULT_AUTH];
|
|
2476
|
+
}
|
|
2477
|
+
function resolveEffectiveAuth(routeAuth, controllerAuth) {
|
|
2478
|
+
return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
|
|
2479
|
+
}
|
|
2480
|
+
function assertZeroArgConstructor(Ctrl, kind) {
|
|
2481
|
+
const arity = Ctrl.length ?? 0;
|
|
2482
|
+
if (arity === 0) return;
|
|
2483
|
+
const name = Ctrl.name ?? "<anonymous>";
|
|
2484
|
+
throw new Error(
|
|
2485
|
+
`${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)\`).`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2157
2489
|
// src/engine/router.ts
|
|
2158
2490
|
var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
|
|
2159
2491
|
function toSegments(path) {
|
|
@@ -2172,6 +2504,7 @@ function buildRouteTable(controllers) {
|
|
|
2172
2504
|
`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.`
|
|
2173
2505
|
);
|
|
2174
2506
|
}
|
|
2507
|
+
assertZeroArgConstructor(Ctrl, "controller");
|
|
2175
2508
|
const instance = new ctor();
|
|
2176
2509
|
for (const r of routes) {
|
|
2177
2510
|
const full = `${basePath}${r.subpath ?? ""}` || "/";
|
|
@@ -2181,7 +2514,10 @@ function buildRouteTable(controllers) {
|
|
|
2181
2514
|
meta: r,
|
|
2182
2515
|
instance,
|
|
2183
2516
|
id: `${r.method} ${full}`,
|
|
2184
|
-
|
|
2517
|
+
// The route's OWN spec is deliberately not folded in here: this field
|
|
2518
|
+
// is what applies when the route is silent, and `effectiveAuth` puts
|
|
2519
|
+
// the route level back on top per request.
|
|
2520
|
+
controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
|
|
2185
2521
|
});
|
|
2186
2522
|
}
|
|
2187
2523
|
}
|
|
@@ -2810,18 +3146,29 @@ async function createApp(opts) {
|
|
|
2810
3146
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
2811
3147
|
}
|
|
2812
3148
|
}
|
|
3149
|
+
let runShutdownHooks;
|
|
3150
|
+
try {
|
|
3151
|
+
runShutdownHooks = await __runStartHooks();
|
|
3152
|
+
} catch (err) {
|
|
3153
|
+
await closeDriver(sql);
|
|
3154
|
+
throw err;
|
|
3155
|
+
}
|
|
2813
3156
|
return {
|
|
2814
3157
|
handle,
|
|
2815
3158
|
routes,
|
|
2816
3159
|
config,
|
|
2817
3160
|
runInServiceScope,
|
|
2818
3161
|
async shutdown() {
|
|
2819
|
-
|
|
2820
|
-
await
|
|
2821
|
-
await closable.end?.();
|
|
3162
|
+
await runShutdownHooks();
|
|
3163
|
+
await closeDriver(sql);
|
|
2822
3164
|
}
|
|
2823
3165
|
};
|
|
2824
3166
|
}
|
|
3167
|
+
async function closeDriver(sql) {
|
|
3168
|
+
const closable = sql;
|
|
3169
|
+
await closable.close?.();
|
|
3170
|
+
await closable.end?.();
|
|
3171
|
+
}
|
|
2825
3172
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2826
3173
|
0 && (module.exports = {
|
|
2827
3174
|
AuthVerifier,
|