@palbase/backend 23.1.0 → 24.0.1
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 +400 -47
- 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-PY7YJDCT.js → chunk-LCL7TUAI.js} +32 -3
- package/dist/chunk-LCL7TUAI.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-M5MCBWJI.js → chunk-YSQBC2VL.js} +275 -31
- package/dist/chunk-YSQBC2VL.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 +389 -34
- 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-BOS_rFBO.d.ts} +125 -21
- package/dist/{index-CxeQSfJP.d.cts → index-C84bLgeO.d.cts} +134 -9
- package/dist/{index-By8Dle5U.d.cts → index-MoQ31B6M.d.cts} +125 -21
- package/dist/{index-BZrJXnVh.d.ts → index-dJNhDZ7j.d.ts} +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,13 +1392,33 @@ 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 = [];
|
|
1411
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1412
|
+
const bindFor = (col) => {
|
|
1413
|
+
const t = transforms.get(col);
|
|
1414
|
+
return t?.toDb === void 0 ? add : (v) => add(v === null || v === void 0 ? v : t.toDb(v));
|
|
1415
|
+
};
|
|
1272
1416
|
for (const [col, cond] of Object.entries(where)) {
|
|
1273
1417
|
if (colSet !== null && !colSet.has(col)) {
|
|
1274
1418
|
throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
1275
1419
|
}
|
|
1276
1420
|
const q = `t.${quoteIdent(col)}`;
|
|
1421
|
+
const bind = bindFor(col);
|
|
1277
1422
|
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
1278
1423
|
for (const [op, v] of Object.entries(cond)) {
|
|
1279
1424
|
if (op === "in") {
|
|
@@ -1282,19 +1427,26 @@ function compileWhere(table, colSet, where, add, caller = "search") {
|
|
|
1282
1427
|
parts.push("false");
|
|
1283
1428
|
continue;
|
|
1284
1429
|
}
|
|
1285
|
-
parts.push(`${q} IN (${v.map((x) =>
|
|
1430
|
+
parts.push(`${q} IN (${v.map((x) => bind(x)).join(", ")})`);
|
|
1286
1431
|
} else if (op in WHERE_OPS) {
|
|
1287
|
-
parts.push(`${q} ${WHERE_OPS[op]} ${
|
|
1432
|
+
parts.push(`${q} ${WHERE_OPS[op]} ${bind(v)}`);
|
|
1288
1433
|
} else {
|
|
1289
1434
|
throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
|
|
1290
1435
|
}
|
|
1291
1436
|
}
|
|
1292
1437
|
} else {
|
|
1293
|
-
parts.push(`${q} = ${
|
|
1438
|
+
parts.push(`${q} = ${bind(cond)}`);
|
|
1294
1439
|
}
|
|
1295
1440
|
}
|
|
1296
1441
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1297
1442
|
}
|
|
1443
|
+
function onConflictTail(cols, conflict, action) {
|
|
1444
|
+
const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
|
|
1445
|
+
if (action === "ignore") return `${target} DO NOTHING`;
|
|
1446
|
+
const conflictSet = new Set(conflict);
|
|
1447
|
+
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1448
|
+
return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1449
|
+
}
|
|
1298
1450
|
var cachedVectorSchema = null;
|
|
1299
1451
|
var cachedTrgmSchema = null;
|
|
1300
1452
|
async function trgmSchemaOf(live) {
|
|
@@ -1404,6 +1556,39 @@ function createOps(tx) {
|
|
|
1404
1556
|
}
|
|
1405
1557
|
const cols = Object.keys(data);
|
|
1406
1558
|
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
1559
|
+
if (searchConfigFor(table)?.validity) {
|
|
1560
|
+
const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1561
|
+
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 *`;
|
|
1562
|
+
const live = await at();
|
|
1563
|
+
const bindData = asBindParams(table, cols, data);
|
|
1564
|
+
const first = await live.unsafe(insertSql, bindData);
|
|
1565
|
+
if (first[0]) return asTableRow(table, first[0]);
|
|
1566
|
+
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
1567
|
+
const condBind = conflict.map((c) => data[c]);
|
|
1568
|
+
const cur = await live.unsafe(
|
|
1569
|
+
`SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
|
|
1570
|
+
condBind
|
|
1571
|
+
);
|
|
1572
|
+
const old = cur[0];
|
|
1573
|
+
if (old === void 0) {
|
|
1574
|
+
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)`);
|
|
1575
|
+
}
|
|
1576
|
+
const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
|
|
1577
|
+
await live.unsafe(
|
|
1578
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
|
|
1579
|
+
[old[pk]]
|
|
1580
|
+
);
|
|
1581
|
+
const second = await live.unsafe(insertSql, bindData);
|
|
1582
|
+
if (!second[0]) {
|
|
1583
|
+
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)`);
|
|
1584
|
+
}
|
|
1585
|
+
const fresh = second[0];
|
|
1586
|
+
await live.unsafe(
|
|
1587
|
+
`UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
|
|
1588
|
+
[old[pk], fresh[pk]]
|
|
1589
|
+
);
|
|
1590
|
+
return asTableRow(table, second[0]);
|
|
1591
|
+
}
|
|
1407
1592
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1408
1593
|
const conflictSet = new Set(conflict);
|
|
1409
1594
|
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
@@ -1429,6 +1614,82 @@ function createOps(tx) {
|
|
|
1429
1614
|
async delete(table, id) {
|
|
1430
1615
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
1431
1616
|
},
|
|
1617
|
+
/**
|
|
1618
|
+
* Update every row the filter matches, in ONE statement.
|
|
1619
|
+
*
|
|
1620
|
+
* The capability was already here and only reachable from inside a
|
|
1621
|
+
* transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
|
|
1622
|
+
* only door was `update(id, …)`, so "mark every unapproved row in this
|
|
1623
|
+
* household" was an N+1 loop or hand-written SQL — and hand-written SQL is
|
|
1624
|
+
* where the typed surface and RLS both stop helping.
|
|
1625
|
+
*
|
|
1626
|
+
* The filter language is `findMany`'s, compiled by the same `compileWhere`:
|
|
1627
|
+
* one filter language, or the two spellings drift.
|
|
1628
|
+
*
|
|
1629
|
+
* AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
|
|
1630
|
+
* and the shape that produces it by accident — a filter object built from
|
|
1631
|
+
* request input that happened to come back empty — is exactly the shape that
|
|
1632
|
+
* should not silently succeed. Callers who mean every row say so with a
|
|
1633
|
+
* predicate that is true for every row.
|
|
1634
|
+
*/
|
|
1635
|
+
async updateMany(table, where, set) {
|
|
1636
|
+
const cols = Object.keys(set);
|
|
1637
|
+
if (cols.length === 0) {
|
|
1638
|
+
throw new Error(
|
|
1639
|
+
`updateMany(${table}): nothing to set. An update with no columns is not a no-op worth pretending happened \u2014 pass the columns to write.`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
const params = [];
|
|
1643
|
+
const add = (v) => {
|
|
1644
|
+
params.push(v);
|
|
1645
|
+
return `$${params.length}`;
|
|
1646
|
+
};
|
|
1647
|
+
const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
|
|
1648
|
+
const known = schemaColumns(table);
|
|
1649
|
+
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
1650
|
+
assertHasPredicate(whereSql, "updateMany", table);
|
|
1651
|
+
const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
|
|
1652
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1653
|
+
},
|
|
1654
|
+
/**
|
|
1655
|
+
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
1656
|
+
* many went. Same filter language, same empty-filter refusal as
|
|
1657
|
+
* {@link updateMany} — and here the accident is worse.
|
|
1658
|
+
*/
|
|
1659
|
+
async deleteMany(table, where) {
|
|
1660
|
+
const params = [];
|
|
1661
|
+
const add = (v) => {
|
|
1662
|
+
params.push(v);
|
|
1663
|
+
return `$${params.length}`;
|
|
1664
|
+
};
|
|
1665
|
+
const known = schemaColumns(table);
|
|
1666
|
+
const whereSql = compileWhere(table, known, where, add, "deleteMany");
|
|
1667
|
+
assertHasPredicate(whereSql, "deleteMany", table);
|
|
1668
|
+
const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
|
|
1669
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1670
|
+
return rows.length;
|
|
1671
|
+
},
|
|
1672
|
+
/**
|
|
1673
|
+
* How many rows match — the half of pagination `limit`/`offset` cannot
|
|
1674
|
+
* supply. Without it a page count is either a guess or "fetch everything and
|
|
1675
|
+
* read .length", and the second one is the scale risk this surface exists to
|
|
1676
|
+
* remove.
|
|
1677
|
+
*
|
|
1678
|
+
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
1679
|
+
* reads do not destroy anything.
|
|
1680
|
+
*/
|
|
1681
|
+
async count(table, where = {}) {
|
|
1682
|
+
const params = [];
|
|
1683
|
+
const add = (v) => {
|
|
1684
|
+
params.push(v);
|
|
1685
|
+
return `$${params.length}`;
|
|
1686
|
+
};
|
|
1687
|
+
const known = schemaColumns(table);
|
|
1688
|
+
const whereSql = compileWhere(table, known, where, add, "count");
|
|
1689
|
+
const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
|
|
1690
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1691
|
+
return Number(rows[0]?.n ?? 0);
|
|
1692
|
+
},
|
|
1432
1693
|
async findById(table, id) {
|
|
1433
1694
|
const rows = await (await at()).unsafe(
|
|
1434
1695
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
@@ -1446,7 +1707,8 @@ function createOps(tx) {
|
|
|
1446
1707
|
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
1447
1708
|
const order = orderClause(table, known, opts.orderBy);
|
|
1448
1709
|
const limit = limitClause(opts.limit);
|
|
1449
|
-
const
|
|
1710
|
+
const offset = offsetClause(opts.offset, opts.limit);
|
|
1711
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
|
|
1450
1712
|
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1451
1713
|
},
|
|
1452
1714
|
/**
|
|
@@ -1529,7 +1791,9 @@ function createOps(tx) {
|
|
|
1529
1791
|
bindC.push(v);
|
|
1530
1792
|
return `$${bindC.length}`;
|
|
1531
1793
|
};
|
|
1532
|
-
const
|
|
1794
|
+
const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
|
|
1795
|
+
const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1796
|
+
const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
|
|
1533
1797
|
const liveC = await at();
|
|
1534
1798
|
const K2 = 50;
|
|
1535
1799
|
const cpool = Math.max(limit * 9, 90);
|
|
@@ -1538,8 +1802,19 @@ function createOps(tx) {
|
|
|
1538
1802
|
if (qvC !== null) {
|
|
1539
1803
|
const sch = await vectorSchemaWithGuc(liveC);
|
|
1540
1804
|
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1805
|
+
let exactOrderC = "";
|
|
1806
|
+
if (userWhereC !== "") {
|
|
1807
|
+
const probeRows = await liveC.unsafe(
|
|
1808
|
+
`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`,
|
|
1809
|
+
bindC.slice()
|
|
1810
|
+
);
|
|
1811
|
+
const n = probeRows?.[0]?.n;
|
|
1812
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1813
|
+
exactOrderC = " + 0.0";
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1541
1816
|
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
|
|
1817
|
+
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
1818
|
}
|
|
1544
1819
|
let qpC = "";
|
|
1545
1820
|
let qpCIdx = -1;
|
|
@@ -1547,7 +1822,7 @@ function createOps(tx) {
|
|
|
1547
1822
|
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
1548
1823
|
qpC = addC(qTextC);
|
|
1549
1824
|
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
|
|
1825
|
+
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
1826
|
}
|
|
1552
1827
|
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
1553
1828
|
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
@@ -1562,13 +1837,13 @@ function createOps(tx) {
|
|
|
1562
1837
|
} else {
|
|
1563
1838
|
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
1564
1839
|
}
|
|
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
|
|
1840
|
+
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
1841
|
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
1842
|
};
|
|
1568
1843
|
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
1569
1844
|
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
1570
1845
|
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
|
|
1846
|
+
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
1847
|
const retryBindC = bindC.slice();
|
|
1573
1848
|
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
1574
1849
|
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
@@ -1664,7 +1939,7 @@ function createOps(tx) {
|
|
|
1664
1939
|
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
1665
1940
|
const tsch = await trgmSchemaOf(live);
|
|
1666
1941
|
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 ${
|
|
1942
|
+
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
1943
|
const retryBind = bind.slice();
|
|
1669
1944
|
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
1670
1945
|
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
@@ -1687,6 +1962,25 @@ function createOps(tx) {
|
|
|
1687
1962
|
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
1688
1963
|
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
1689
1964
|
*/
|
|
1965
|
+
/** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
|
|
1966
|
+
* `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
|
|
1967
|
+
* yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
|
|
1968
|
+
* search'teki alan geriye-uyum için DURUR. where + validity default'u
|
|
1969
|
+
* sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
|
|
1970
|
+
async facets(table, params) {
|
|
1971
|
+
const cfg = searchConfigFor(table);
|
|
1972
|
+
if (!cfg) {
|
|
1973
|
+
throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
|
|
1974
|
+
}
|
|
1975
|
+
for (const col of params.facets) {
|
|
1976
|
+
if (!cfg.colSet.has(col)) {
|
|
1977
|
+
throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
const live = await at();
|
|
1981
|
+
const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
|
|
1982
|
+
return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
|
|
1983
|
+
},
|
|
1690
1984
|
async similar(table, id, opts = {}) {
|
|
1691
1985
|
const cfg = searchConfigFor(table);
|
|
1692
1986
|
if (!cfg) {
|
|
@@ -1918,17 +2212,41 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1918
2212
|
}
|
|
1919
2213
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1920
2214
|
function isAbortedTransaction(e) {
|
|
1921
|
-
const code = e?.code;
|
|
1922
2215
|
const message = String(e?.message ?? "");
|
|
1923
|
-
return
|
|
2216
|
+
return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
|
|
2217
|
+
}
|
|
2218
|
+
function assertHasPredicate(whereSql, op, table) {
|
|
2219
|
+
if (whereSql.trim().length > 0) return;
|
|
2220
|
+
throw new Error(
|
|
2221
|
+
`${op}(${table}): the filter compiled to no condition, so this would have written EVERY row. An empty filter \u2014 or an operator object with no operators in it, like { col: {} } \u2014 is refused. Name a condition.`
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
function sqlstateOf(e) {
|
|
2225
|
+
const err = e;
|
|
2226
|
+
const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
|
|
2227
|
+
if (isState(err?.code)) return err.code;
|
|
2228
|
+
if (isState(err?.errno)) return err.errno;
|
|
2229
|
+
if (typeof err?.errno === "number") {
|
|
2230
|
+
const asText = String(err.errno);
|
|
2231
|
+
if (isState(asText)) return asText;
|
|
2232
|
+
}
|
|
2233
|
+
return void 0;
|
|
1924
2234
|
}
|
|
1925
2235
|
function isForeignKeyViolation(e) {
|
|
1926
|
-
return e
|
|
2236
|
+
return sqlstateOf(e) === "23503";
|
|
2237
|
+
}
|
|
2238
|
+
function isUniqueViolation(e) {
|
|
2239
|
+
return sqlstateOf(e) === "23505";
|
|
2240
|
+
}
|
|
2241
|
+
function constraintOf(e) {
|
|
2242
|
+
const named = e?.constraint;
|
|
2243
|
+
if (typeof named === "string" && named.length > 0) return named;
|
|
2244
|
+
const message = String(e?.message ?? "");
|
|
2245
|
+
return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
|
|
1927
2246
|
}
|
|
1928
2247
|
function isLockTimeout(e) {
|
|
1929
|
-
const code = e?.code;
|
|
1930
2248
|
const message = String(e?.message ?? "");
|
|
1931
|
-
return
|
|
2249
|
+
return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
|
|
1932
2250
|
}
|
|
1933
2251
|
function diagnosingDriver(sql, surface = "service") {
|
|
1934
2252
|
const original = (e) => String(e?.message ?? e);
|
|
@@ -1938,6 +2256,9 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
1938
2256
|
`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
2257
|
);
|
|
1940
2258
|
}
|
|
2259
|
+
if (isUniqueViolation(e)) {
|
|
2260
|
+
return new UniqueViolation(constraintOf(e));
|
|
2261
|
+
}
|
|
1941
2262
|
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
1942
2263
|
return new Error(
|
|
1943
2264
|
`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 +2310,9 @@ function createRequestDatabase(sql, identity) {
|
|
|
1989
2310
|
await tx.commit();
|
|
1990
2311
|
await serviceTx?.commit();
|
|
1991
2312
|
},
|
|
1992
|
-
async rollback(
|
|
1993
|
-
await tx.rollback(
|
|
1994
|
-
await serviceTx?.rollback(
|
|
2313
|
+
async rollback(reason2) {
|
|
2314
|
+
await tx.rollback(reason2);
|
|
2315
|
+
await serviceTx?.rollback(reason2);
|
|
1995
2316
|
}
|
|
1996
2317
|
};
|
|
1997
2318
|
}
|
|
@@ -2066,10 +2387,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
2066
2387
|
const rendered = cols.map(
|
|
2067
2388
|
(c) => renderValue(op.values[c], c, args, results, opVectorCols)
|
|
2068
2389
|
);
|
|
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 *`;
|
|
2390
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
|
|
2073
2391
|
break;
|
|
2074
2392
|
}
|
|
2075
2393
|
case "insertMany": {
|
|
@@ -2079,7 +2397,9 @@ async function runPlanOp(sp, op, results) {
|
|
|
2079
2397
|
const tuples = rows.map(
|
|
2080
2398
|
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
2081
2399
|
);
|
|
2082
|
-
|
|
2400
|
+
const conflictCols = op.onConflict ?? [];
|
|
2401
|
+
const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
|
|
2402
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
2083
2403
|
break;
|
|
2084
2404
|
}
|
|
2085
2405
|
case "update": {
|
|
@@ -2154,6 +2474,26 @@ function getRoutes(ctor) {
|
|
|
2154
2474
|
}));
|
|
2155
2475
|
}
|
|
2156
2476
|
|
|
2477
|
+
// src/decorators/controller.ts
|
|
2478
|
+
var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
|
|
2479
|
+
function appAuthSlot() {
|
|
2480
|
+
return globalThis;
|
|
2481
|
+
}
|
|
2482
|
+
function getDefaultAuth() {
|
|
2483
|
+
return appAuthSlot()[APP_DEFAULT_AUTH];
|
|
2484
|
+
}
|
|
2485
|
+
function resolveEffectiveAuth(routeAuth, controllerAuth) {
|
|
2486
|
+
return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
|
|
2487
|
+
}
|
|
2488
|
+
function assertZeroArgConstructor(Ctrl, kind) {
|
|
2489
|
+
const arity = Ctrl.length ?? 0;
|
|
2490
|
+
if (arity === 0) return;
|
|
2491
|
+
const name = Ctrl.name ?? "<anonymous>";
|
|
2492
|
+
throw new Error(
|
|
2493
|
+
`${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)\`).`
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2157
2497
|
// src/engine/router.ts
|
|
2158
2498
|
var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
|
|
2159
2499
|
function toSegments(path) {
|
|
@@ -2172,6 +2512,7 @@ function buildRouteTable(controllers) {
|
|
|
2172
2512
|
`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
2513
|
);
|
|
2174
2514
|
}
|
|
2515
|
+
assertZeroArgConstructor(Ctrl, "controller");
|
|
2175
2516
|
const instance = new ctor();
|
|
2176
2517
|
for (const r of routes) {
|
|
2177
2518
|
const full = `${basePath}${r.subpath ?? ""}` || "/";
|
|
@@ -2181,7 +2522,10 @@ function buildRouteTable(controllers) {
|
|
|
2181
2522
|
meta: r,
|
|
2182
2523
|
instance,
|
|
2183
2524
|
id: `${r.method} ${full}`,
|
|
2184
|
-
|
|
2525
|
+
// The route's OWN spec is deliberately not folded in here: this field
|
|
2526
|
+
// is what applies when the route is silent, and `effectiveAuth` puts
|
|
2527
|
+
// the route level back on top per request.
|
|
2528
|
+
controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
|
|
2185
2529
|
});
|
|
2186
2530
|
}
|
|
2187
2531
|
}
|
|
@@ -2810,18 +3154,29 @@ async function createApp(opts) {
|
|
|
2810
3154
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
2811
3155
|
}
|
|
2812
3156
|
}
|
|
3157
|
+
let runShutdownHooks;
|
|
3158
|
+
try {
|
|
3159
|
+
runShutdownHooks = await __runStartHooks();
|
|
3160
|
+
} catch (err) {
|
|
3161
|
+
await closeDriver(sql);
|
|
3162
|
+
throw err;
|
|
3163
|
+
}
|
|
2813
3164
|
return {
|
|
2814
3165
|
handle,
|
|
2815
3166
|
routes,
|
|
2816
3167
|
config,
|
|
2817
3168
|
runInServiceScope,
|
|
2818
3169
|
async shutdown() {
|
|
2819
|
-
|
|
2820
|
-
await
|
|
2821
|
-
await closable.end?.();
|
|
3170
|
+
await runShutdownHooks();
|
|
3171
|
+
await closeDriver(sql);
|
|
2822
3172
|
}
|
|
2823
3173
|
};
|
|
2824
3174
|
}
|
|
3175
|
+
async function closeDriver(sql) {
|
|
3176
|
+
const closable = sql;
|
|
3177
|
+
await closable.close?.();
|
|
3178
|
+
await closable.end?.();
|
|
3179
|
+
}
|
|
2825
3180
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2826
3181
|
0 && (module.exports = {
|
|
2827
3182
|
AuthVerifier,
|