@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
|
@@ -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,13 +1358,33 @@ 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 = [];
|
|
1377
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1378
|
+
const bindFor = (col) => {
|
|
1379
|
+
const t = transforms.get(col);
|
|
1380
|
+
return t?.toDb === void 0 ? add : (v) => add(v === null || v === void 0 ? v : t.toDb(v));
|
|
1381
|
+
};
|
|
1238
1382
|
for (const [col, cond] of Object.entries(where)) {
|
|
1239
1383
|
if (colSet !== null && !colSet.has(col)) {
|
|
1240
1384
|
throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
1241
1385
|
}
|
|
1242
1386
|
const q = `t.${quoteIdent(col)}`;
|
|
1387
|
+
const bind = bindFor(col);
|
|
1243
1388
|
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
1244
1389
|
for (const [op, v] of Object.entries(cond)) {
|
|
1245
1390
|
if (op === "in") {
|
|
@@ -1248,19 +1393,26 @@ function compileWhere(table, colSet, where, add, caller = "search") {
|
|
|
1248
1393
|
parts.push("false");
|
|
1249
1394
|
continue;
|
|
1250
1395
|
}
|
|
1251
|
-
parts.push(`${q} IN (${v.map((x) =>
|
|
1396
|
+
parts.push(`${q} IN (${v.map((x) => bind(x)).join(", ")})`);
|
|
1252
1397
|
} else if (op in WHERE_OPS) {
|
|
1253
|
-
parts.push(`${q} ${WHERE_OPS[op]} ${
|
|
1398
|
+
parts.push(`${q} ${WHERE_OPS[op]} ${bind(v)}`);
|
|
1254
1399
|
} else {
|
|
1255
1400
|
throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
|
|
1256
1401
|
}
|
|
1257
1402
|
}
|
|
1258
1403
|
} else {
|
|
1259
|
-
parts.push(`${q} = ${
|
|
1404
|
+
parts.push(`${q} = ${bind(cond)}`);
|
|
1260
1405
|
}
|
|
1261
1406
|
}
|
|
1262
1407
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1263
1408
|
}
|
|
1409
|
+
function onConflictTail(cols, conflict, action) {
|
|
1410
|
+
const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
|
|
1411
|
+
if (action === "ignore") return `${target} DO NOTHING`;
|
|
1412
|
+
const conflictSet = new Set(conflict);
|
|
1413
|
+
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1414
|
+
return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1415
|
+
}
|
|
1264
1416
|
var cachedVectorSchema = null;
|
|
1265
1417
|
var cachedTrgmSchema = null;
|
|
1266
1418
|
async function trgmSchemaOf(live) {
|
|
@@ -1370,6 +1522,39 @@ function createOps(tx) {
|
|
|
1370
1522
|
}
|
|
1371
1523
|
const cols = Object.keys(data);
|
|
1372
1524
|
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
1525
|
+
if (searchConfigFor(table)?.validity) {
|
|
1526
|
+
const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1527
|
+
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 *`;
|
|
1528
|
+
const live = await at();
|
|
1529
|
+
const bindData = asBindParams(table, cols, data);
|
|
1530
|
+
const first = await live.unsafe(insertSql, bindData);
|
|
1531
|
+
if (first[0]) return asTableRow(table, first[0]);
|
|
1532
|
+
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
1533
|
+
const condBind = conflict.map((c) => data[c]);
|
|
1534
|
+
const cur = await live.unsafe(
|
|
1535
|
+
`SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
|
|
1536
|
+
condBind
|
|
1537
|
+
);
|
|
1538
|
+
const old = cur[0];
|
|
1539
|
+
if (old === void 0) {
|
|
1540
|
+
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)`);
|
|
1541
|
+
}
|
|
1542
|
+
const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
|
|
1543
|
+
await live.unsafe(
|
|
1544
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
|
|
1545
|
+
[old[pk]]
|
|
1546
|
+
);
|
|
1547
|
+
const second = await live.unsafe(insertSql, bindData);
|
|
1548
|
+
if (!second[0]) {
|
|
1549
|
+
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)`);
|
|
1550
|
+
}
|
|
1551
|
+
const fresh = second[0];
|
|
1552
|
+
await live.unsafe(
|
|
1553
|
+
`UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
|
|
1554
|
+
[old[pk], fresh[pk]]
|
|
1555
|
+
);
|
|
1556
|
+
return asTableRow(table, second[0]);
|
|
1557
|
+
}
|
|
1373
1558
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1374
1559
|
const conflictSet = new Set(conflict);
|
|
1375
1560
|
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
@@ -1395,6 +1580,82 @@ function createOps(tx) {
|
|
|
1395
1580
|
async delete(table, id) {
|
|
1396
1581
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
1397
1582
|
},
|
|
1583
|
+
/**
|
|
1584
|
+
* Update every row the filter matches, in ONE statement.
|
|
1585
|
+
*
|
|
1586
|
+
* The capability was already here and only reachable from inside a
|
|
1587
|
+
* transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
|
|
1588
|
+
* only door was `update(id, …)`, so "mark every unapproved row in this
|
|
1589
|
+
* household" was an N+1 loop or hand-written SQL — and hand-written SQL is
|
|
1590
|
+
* where the typed surface and RLS both stop helping.
|
|
1591
|
+
*
|
|
1592
|
+
* The filter language is `findMany`'s, compiled by the same `compileWhere`:
|
|
1593
|
+
* one filter language, or the two spellings drift.
|
|
1594
|
+
*
|
|
1595
|
+
* AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
|
|
1596
|
+
* and the shape that produces it by accident — a filter object built from
|
|
1597
|
+
* request input that happened to come back empty — is exactly the shape that
|
|
1598
|
+
* should not silently succeed. Callers who mean every row say so with a
|
|
1599
|
+
* predicate that is true for every row.
|
|
1600
|
+
*/
|
|
1601
|
+
async updateMany(table, where, set) {
|
|
1602
|
+
const cols = Object.keys(set);
|
|
1603
|
+
if (cols.length === 0) {
|
|
1604
|
+
throw new Error(
|
|
1605
|
+
`updateMany(${table}): nothing to set. An update with no columns is not a no-op worth pretending happened \u2014 pass the columns to write.`
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
const params = [];
|
|
1609
|
+
const add = (v) => {
|
|
1610
|
+
params.push(v);
|
|
1611
|
+
return `$${params.length}`;
|
|
1612
|
+
};
|
|
1613
|
+
const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
|
|
1614
|
+
const known = schemaColumns(table);
|
|
1615
|
+
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
1616
|
+
assertHasPredicate(whereSql, "updateMany", table);
|
|
1617
|
+
const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
|
|
1618
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1619
|
+
},
|
|
1620
|
+
/**
|
|
1621
|
+
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
1622
|
+
* many went. Same filter language, same empty-filter refusal as
|
|
1623
|
+
* {@link updateMany} — and here the accident is worse.
|
|
1624
|
+
*/
|
|
1625
|
+
async deleteMany(table, where) {
|
|
1626
|
+
const params = [];
|
|
1627
|
+
const add = (v) => {
|
|
1628
|
+
params.push(v);
|
|
1629
|
+
return `$${params.length}`;
|
|
1630
|
+
};
|
|
1631
|
+
const known = schemaColumns(table);
|
|
1632
|
+
const whereSql = compileWhere(table, known, where, add, "deleteMany");
|
|
1633
|
+
assertHasPredicate(whereSql, "deleteMany", table);
|
|
1634
|
+
const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
|
|
1635
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1636
|
+
return rows.length;
|
|
1637
|
+
},
|
|
1638
|
+
/**
|
|
1639
|
+
* How many rows match — the half of pagination `limit`/`offset` cannot
|
|
1640
|
+
* supply. Without it a page count is either a guess or "fetch everything and
|
|
1641
|
+
* read .length", and the second one is the scale risk this surface exists to
|
|
1642
|
+
* remove.
|
|
1643
|
+
*
|
|
1644
|
+
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
1645
|
+
* reads do not destroy anything.
|
|
1646
|
+
*/
|
|
1647
|
+
async count(table, where = {}) {
|
|
1648
|
+
const params = [];
|
|
1649
|
+
const add = (v) => {
|
|
1650
|
+
params.push(v);
|
|
1651
|
+
return `$${params.length}`;
|
|
1652
|
+
};
|
|
1653
|
+
const known = schemaColumns(table);
|
|
1654
|
+
const whereSql = compileWhere(table, known, where, add, "count");
|
|
1655
|
+
const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
|
|
1656
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1657
|
+
return Number(rows[0]?.n ?? 0);
|
|
1658
|
+
},
|
|
1398
1659
|
async findById(table, id) {
|
|
1399
1660
|
const rows = await (await at()).unsafe(
|
|
1400
1661
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
@@ -1412,7 +1673,8 @@ function createOps(tx) {
|
|
|
1412
1673
|
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
1413
1674
|
const order = orderClause(table, known, opts.orderBy);
|
|
1414
1675
|
const limit = limitClause(opts.limit);
|
|
1415
|
-
const
|
|
1676
|
+
const offset = offsetClause(opts.offset, opts.limit);
|
|
1677
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
|
|
1416
1678
|
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1417
1679
|
},
|
|
1418
1680
|
/**
|
|
@@ -1495,7 +1757,9 @@ function createOps(tx) {
|
|
|
1495
1757
|
bindC.push(v);
|
|
1496
1758
|
return `$${bindC.length}`;
|
|
1497
1759
|
};
|
|
1498
|
-
const
|
|
1760
|
+
const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
|
|
1761
|
+
const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1762
|
+
const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
|
|
1499
1763
|
const liveC = await at();
|
|
1500
1764
|
const K2 = 50;
|
|
1501
1765
|
const cpool = Math.max(limit * 9, 90);
|
|
@@ -1504,8 +1768,19 @@ function createOps(tx) {
|
|
|
1504
1768
|
if (qvC !== null) {
|
|
1505
1769
|
const sch = await vectorSchemaWithGuc(liveC);
|
|
1506
1770
|
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1771
|
+
let exactOrderC = "";
|
|
1772
|
+
if (userWhereC !== "") {
|
|
1773
|
+
const probeRows = await liveC.unsafe(
|
|
1774
|
+
`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`,
|
|
1775
|
+
bindC.slice()
|
|
1776
|
+
);
|
|
1777
|
+
const n = probeRows?.[0]?.n;
|
|
1778
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1779
|
+
exactOrderC = " + 0.0";
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1507
1782
|
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
|
|
1783
|
+
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
1784
|
}
|
|
1510
1785
|
let qpC = "";
|
|
1511
1786
|
let qpCIdx = -1;
|
|
@@ -1513,7 +1788,7 @@ function createOps(tx) {
|
|
|
1513
1788
|
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
1514
1789
|
qpC = addC(qTextC);
|
|
1515
1790
|
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
|
|
1791
|
+
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
1792
|
}
|
|
1518
1793
|
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
1519
1794
|
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
@@ -1528,13 +1803,13 @@ function createOps(tx) {
|
|
|
1528
1803
|
} else {
|
|
1529
1804
|
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
1530
1805
|
}
|
|
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
|
|
1806
|
+
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
1807
|
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
1808
|
};
|
|
1534
1809
|
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
1535
1810
|
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
1536
1811
|
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
|
|
1812
|
+
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
1813
|
const retryBindC = bindC.slice();
|
|
1539
1814
|
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
1540
1815
|
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
@@ -1630,7 +1905,7 @@ function createOps(tx) {
|
|
|
1630
1905
|
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
1631
1906
|
const tsch = await trgmSchemaOf(live);
|
|
1632
1907
|
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 ${
|
|
1908
|
+
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
1909
|
const retryBind = bind.slice();
|
|
1635
1910
|
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
1636
1911
|
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
@@ -1653,6 +1928,25 @@ function createOps(tx) {
|
|
|
1653
1928
|
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
1654
1929
|
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
1655
1930
|
*/
|
|
1931
|
+
/** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
|
|
1932
|
+
* `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
|
|
1933
|
+
* yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
|
|
1934
|
+
* search'teki alan geriye-uyum için DURUR. where + validity default'u
|
|
1935
|
+
* sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
|
|
1936
|
+
async facets(table, params) {
|
|
1937
|
+
const cfg = searchConfigFor(table);
|
|
1938
|
+
if (!cfg) {
|
|
1939
|
+
throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
|
|
1940
|
+
}
|
|
1941
|
+
for (const col of params.facets) {
|
|
1942
|
+
if (!cfg.colSet.has(col)) {
|
|
1943
|
+
throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
const live = await at();
|
|
1947
|
+
const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
|
|
1948
|
+
return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
|
|
1949
|
+
},
|
|
1656
1950
|
async similar(table, id, opts = {}) {
|
|
1657
1951
|
const cfg = searchConfigFor(table);
|
|
1658
1952
|
if (!cfg) {
|
|
@@ -1884,17 +2178,41 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1884
2178
|
}
|
|
1885
2179
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1886
2180
|
function isAbortedTransaction(e) {
|
|
1887
|
-
const code = e?.code;
|
|
1888
2181
|
const message = String(e?.message ?? "");
|
|
1889
|
-
return
|
|
2182
|
+
return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
|
|
2183
|
+
}
|
|
2184
|
+
function assertHasPredicate(whereSql, op, table) {
|
|
2185
|
+
if (whereSql.trim().length > 0) return;
|
|
2186
|
+
throw new Error(
|
|
2187
|
+
`${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.`
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
function sqlstateOf(e) {
|
|
2191
|
+
const err = e;
|
|
2192
|
+
const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
|
|
2193
|
+
if (isState(err?.code)) return err.code;
|
|
2194
|
+
if (isState(err?.errno)) return err.errno;
|
|
2195
|
+
if (typeof err?.errno === "number") {
|
|
2196
|
+
const asText = String(err.errno);
|
|
2197
|
+
if (isState(asText)) return asText;
|
|
2198
|
+
}
|
|
2199
|
+
return void 0;
|
|
1890
2200
|
}
|
|
1891
2201
|
function isForeignKeyViolation(e) {
|
|
1892
|
-
return e
|
|
2202
|
+
return sqlstateOf(e) === "23503";
|
|
2203
|
+
}
|
|
2204
|
+
function isUniqueViolation(e) {
|
|
2205
|
+
return sqlstateOf(e) === "23505";
|
|
2206
|
+
}
|
|
2207
|
+
function constraintOf(e) {
|
|
2208
|
+
const named = e?.constraint;
|
|
2209
|
+
if (typeof named === "string" && named.length > 0) return named;
|
|
2210
|
+
const message = String(e?.message ?? "");
|
|
2211
|
+
return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
|
|
1893
2212
|
}
|
|
1894
2213
|
function isLockTimeout(e) {
|
|
1895
|
-
const code = e?.code;
|
|
1896
2214
|
const message = String(e?.message ?? "");
|
|
1897
|
-
return
|
|
2215
|
+
return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
|
|
1898
2216
|
}
|
|
1899
2217
|
function diagnosingDriver(sql, surface = "service") {
|
|
1900
2218
|
const original = (e) => String(e?.message ?? e);
|
|
@@ -1904,6 +2222,9 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
1904
2222
|
`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
2223
|
);
|
|
1906
2224
|
}
|
|
2225
|
+
if (isUniqueViolation(e)) {
|
|
2226
|
+
return new UniqueViolation(constraintOf(e));
|
|
2227
|
+
}
|
|
1907
2228
|
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
1908
2229
|
return new Error(
|
|
1909
2230
|
`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 +2276,9 @@ function createRequestDatabase(sql, identity) {
|
|
|
1955
2276
|
await tx.commit();
|
|
1956
2277
|
await serviceTx?.commit();
|
|
1957
2278
|
},
|
|
1958
|
-
async rollback(
|
|
1959
|
-
await tx.rollback(
|
|
1960
|
-
await serviceTx?.rollback(
|
|
2279
|
+
async rollback(reason2) {
|
|
2280
|
+
await tx.rollback(reason2);
|
|
2281
|
+
await serviceTx?.rollback(reason2);
|
|
1961
2282
|
}
|
|
1962
2283
|
};
|
|
1963
2284
|
}
|
|
@@ -2032,10 +2353,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
2032
2353
|
const rendered = cols.map(
|
|
2033
2354
|
(c) => renderValue(op.values[c], c, args, results, opVectorCols)
|
|
2034
2355
|
);
|
|
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 *`;
|
|
2356
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
|
|
2039
2357
|
break;
|
|
2040
2358
|
}
|
|
2041
2359
|
case "insertMany": {
|
|
@@ -2045,7 +2363,9 @@ async function runPlanOp(sp, op, results) {
|
|
|
2045
2363
|
const tuples = rows.map(
|
|
2046
2364
|
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
2047
2365
|
);
|
|
2048
|
-
|
|
2366
|
+
const conflictCols = op.onConflict ?? [];
|
|
2367
|
+
const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
|
|
2368
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
2049
2369
|
break;
|
|
2050
2370
|
}
|
|
2051
2371
|
case "update": {
|
|
@@ -2120,6 +2440,38 @@ function getRoutes(ctor) {
|
|
|
2120
2440
|
}));
|
|
2121
2441
|
}
|
|
2122
2442
|
|
|
2443
|
+
// src/decorators/controller.ts
|
|
2444
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("palbase.backend.allControllers");
|
|
2445
|
+
function registry() {
|
|
2446
|
+
const g = globalThis;
|
|
2447
|
+
const existing = g[REGISTRY];
|
|
2448
|
+
if (existing) return existing;
|
|
2449
|
+
const fresh = [];
|
|
2450
|
+
g[REGISTRY] = fresh;
|
|
2451
|
+
return fresh;
|
|
2452
|
+
}
|
|
2453
|
+
function getRegisteredControllers() {
|
|
2454
|
+
return registry().slice();
|
|
2455
|
+
}
|
|
2456
|
+
var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
|
|
2457
|
+
function appAuthSlot() {
|
|
2458
|
+
return globalThis;
|
|
2459
|
+
}
|
|
2460
|
+
function getDefaultAuth() {
|
|
2461
|
+
return appAuthSlot()[APP_DEFAULT_AUTH];
|
|
2462
|
+
}
|
|
2463
|
+
function resolveEffectiveAuth(routeAuth, controllerAuth) {
|
|
2464
|
+
return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
|
|
2465
|
+
}
|
|
2466
|
+
function assertZeroArgConstructor(Ctrl, kind) {
|
|
2467
|
+
const arity = Ctrl.length ?? 0;
|
|
2468
|
+
if (arity === 0) return;
|
|
2469
|
+
const name = Ctrl.name ?? "<anonymous>";
|
|
2470
|
+
throw new Error(
|
|
2471
|
+
`${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)\`).`
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2123
2475
|
// src/engine/router.ts
|
|
2124
2476
|
var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
|
|
2125
2477
|
function toSegments(path) {
|
|
@@ -2138,6 +2490,7 @@ function buildRouteTable(controllers) {
|
|
|
2138
2490
|
`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
2491
|
);
|
|
2140
2492
|
}
|
|
2493
|
+
assertZeroArgConstructor(Ctrl, "controller");
|
|
2141
2494
|
const instance = new ctor();
|
|
2142
2495
|
for (const r of routes) {
|
|
2143
2496
|
const full = `${basePath}${r.subpath ?? ""}` || "/";
|
|
@@ -2147,7 +2500,10 @@ function buildRouteTable(controllers) {
|
|
|
2147
2500
|
meta: r,
|
|
2148
2501
|
instance,
|
|
2149
2502
|
id: `${r.method} ${full}`,
|
|
2150
|
-
|
|
2503
|
+
// The route's OWN spec is deliberately not folded in here: this field
|
|
2504
|
+
// is what applies when the route is silent, and `effectiveAuth` puts
|
|
2505
|
+
// the route level back on top per request.
|
|
2506
|
+
controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
|
|
2151
2507
|
});
|
|
2152
2508
|
}
|
|
2153
2509
|
}
|
|
@@ -2715,31 +3071,28 @@ async function createApp(opts) {
|
|
|
2715
3071
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
2716
3072
|
}
|
|
2717
3073
|
}
|
|
3074
|
+
let runShutdownHooks;
|
|
3075
|
+
try {
|
|
3076
|
+
runShutdownHooks = await __runStartHooks();
|
|
3077
|
+
} catch (err) {
|
|
3078
|
+
await closeDriver(sql);
|
|
3079
|
+
throw err;
|
|
3080
|
+
}
|
|
2718
3081
|
return {
|
|
2719
3082
|
handle,
|
|
2720
3083
|
routes,
|
|
2721
3084
|
config,
|
|
2722
3085
|
runInServiceScope,
|
|
2723
3086
|
async shutdown() {
|
|
2724
|
-
|
|
2725
|
-
await
|
|
2726
|
-
await closable.end?.();
|
|
3087
|
+
await runShutdownHooks();
|
|
3088
|
+
await closeDriver(sql);
|
|
2727
3089
|
}
|
|
2728
3090
|
};
|
|
2729
3091
|
}
|
|
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();
|
|
3092
|
+
async function closeDriver(sql) {
|
|
3093
|
+
const closable = sql;
|
|
3094
|
+
await closable.close?.();
|
|
3095
|
+
await closable.end?.();
|
|
2743
3096
|
}
|
|
2744
3097
|
|
|
2745
3098
|
// src/bin/palbase-backend.ts
|