@palbase/backend 23.0.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 +695 -61
- 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-FSGSB42K.js → chunk-7Z6MGMXQ.js} +64 -4
- package/dist/chunk-7Z6MGMXQ.js.map +1 -0
- package/dist/{chunk-OMRTHM4X.js → chunk-H3JAISUY.js} +136 -1
- package/dist/chunk-H3JAISUY.js.map +1 -0
- package/dist/{chunk-REZU6UKT.js → chunk-NXDH6VQJ.js} +549 -42
- package/dist/chunk-NXDH6VQJ.js.map +1 -0
- package/dist/{chunk-W5ODXPY3.js → chunk-P2Q27SGP.js} +32 -3
- package/dist/chunk-P2Q27SGP.js.map +1 -0
- package/dist/{chunk-ZC6Q2BRD.js → chunk-T5IOSOE5.js} +7 -2
- package/dist/chunk-T5IOSOE5.js.map +1 -0
- package/dist/{chunk-HAF67F2H.js → chunk-ZUGY7RGS.js} +86 -3
- package/dist/chunk-ZUGY7RGS.js.map +1 -0
- package/dist/db/index.cjs +115 -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-BavvbW4P.d.ts → endpoint-0_DGBajf.d.ts} +168 -9
- package/dist/{endpoint-i8TTCohk.d.cts → endpoint-CcQ1a36a.d.cts} +168 -9
- package/dist/engine/index.cjs +684 -48
- 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-B3jmmItD.d.ts → index-CJiJU9ux.d.ts} +209 -36
- package/dist/{index-Bmvx1EvJ.d.cts → index-D-4-PNuQ.d.cts} +209 -36
- package/dist/{index-B7YBEG5w.d.ts → index-D17r-MKb.d.ts} +177 -7
- package/dist/{index-E7OscPJT.d.cts → index-DRFxf07H.d.cts} +177 -7
- package/dist/index.cjs +269 -7
- 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 +54 -11
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +45 -5
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +9 -4
- package/dist/openapi/index.d.ts +9 -4
- package/dist/openapi/index.js +35 -11
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-C3H2uPeZ.d.cts → registry-1X-skBNu.d.cts} +101 -7
- package/dist/{registry-DY3d9l1k.d.ts → registry-CEod_5sz.d.ts} +101 -7
- package/dist/test/index.cjs +509 -9
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +35 -3
- package/dist/test/index.d.ts +35 -3
- package/dist/test/index.js +507 -8
- 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 +3 -2
- package/stager/return_types.js +23 -0
- 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-FSGSB42K.js.map +0 -1
- package/dist/chunk-HAF67F2H.js.map +0 -1
- package/dist/chunk-OMRTHM4X.js.map +0 -1
- package/dist/chunk-REZU6UKT.js.map +0 -1
- package/dist/chunk-W5ODXPY3.js.map +0 -1
- package/dist/chunk-Y5HXVUMP.js +0 -90
- package/dist/chunk-Y5HXVUMP.js.map +0 -1
- package/dist/chunk-ZC6Q2BRD.js.map +0 -1
|
@@ -218,7 +218,20 @@ var TxPlanBuilder = class {
|
|
|
218
218
|
}
|
|
219
219
|
return this.push({ op: "insert", table: name, values: encoded }, `${name}.insert()`);
|
|
220
220
|
},
|
|
221
|
-
|
|
221
|
+
upsert: (values, options) => {
|
|
222
|
+
const encoded = encodeMap(values, false);
|
|
223
|
+
if (Object.keys(encoded).length === 0) {
|
|
224
|
+
throw new TxPlanError(`${name}.upsert() needs at least one column`);
|
|
225
|
+
}
|
|
226
|
+
if (options.onConflict.length === 0) {
|
|
227
|
+
throw new TxPlanError(`${name}.upsert() needs at least one onConflict column`);
|
|
228
|
+
}
|
|
229
|
+
return this.push(
|
|
230
|
+
{ op: "upsert", table: name, values: encoded, onConflict: options.onConflict },
|
|
231
|
+
`${name}.upsert()`
|
|
232
|
+
);
|
|
233
|
+
},
|
|
234
|
+
insertMany: (rows, opts) => {
|
|
222
235
|
if (rows.length === 0) {
|
|
223
236
|
return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
|
|
224
237
|
}
|
|
@@ -229,7 +242,23 @@ var TxPlanBuilder = class {
|
|
|
229
242
|
}
|
|
230
243
|
const encoded = rows.map((row) => encodeMap(row, false));
|
|
231
244
|
assertUniformRows(encoded, name);
|
|
232
|
-
|
|
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
|
+
);
|
|
233
262
|
},
|
|
234
263
|
updateWhere: (where, set) => {
|
|
235
264
|
const encodedWhere = encodeMap(where, false);
|
|
@@ -403,6 +432,42 @@ function __getRuntime() {
|
|
|
403
432
|
}
|
|
404
433
|
return runtime;
|
|
405
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
|
+
}
|
|
406
471
|
function makeServiceProxy(key) {
|
|
407
472
|
const handler = {
|
|
408
473
|
get(_target, prop, receiver) {
|
|
@@ -425,10 +490,12 @@ function makeTablesAccessor(ops) {
|
|
|
425
490
|
update: (id, data) => ops().update(name, id, data),
|
|
426
491
|
delete: (id) => ops().delete(name, id),
|
|
427
492
|
findById: (id) => ops().findById(name, id),
|
|
428
|
-
findMany: (query) => ops().findMany(name, query),
|
|
493
|
+
findMany: (query, opts) => ops().findMany(name, query, opts),
|
|
494
|
+
upsert: (data, opts) => ops().upsert(name, data, opts),
|
|
429
495
|
search: (params) => ops().search(name, params),
|
|
430
496
|
similar: (id, params) => ops().similar(name, id, params),
|
|
431
497
|
recommend: (params) => ops().recommend(name, params),
|
|
498
|
+
facets: (params) => ops().facets(name, params),
|
|
432
499
|
supersede: (id, row) => ops().supersede(name, id, row)
|
|
433
500
|
};
|
|
434
501
|
}
|
|
@@ -445,13 +512,21 @@ function makeTypedSurface(raw) {
|
|
|
445
512
|
update: (table, id, data) => raw.update(table, id, data),
|
|
446
513
|
delete: (table, id) => raw.delete(table, id),
|
|
447
514
|
findById: (table, id) => raw.findById(table, id),
|
|
448
|
-
findMany: (table, query) => raw.findMany(table, query),
|
|
515
|
+
findMany: (table, query, opts) => raw.findMany(table, query, opts),
|
|
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),
|
|
449
520
|
search: (table, params) => raw.search(table, params),
|
|
450
521
|
similar: (table, id, params) => reco.similar(table, id, params),
|
|
451
522
|
recommend: (table, params) => reco.recommend(table, params),
|
|
523
|
+
facets: (table, params) => reco.facets(table, params),
|
|
452
524
|
supersede: (table, id, row) => raw.supersede(table, id, row)
|
|
453
525
|
};
|
|
454
526
|
return Object.assign(ops, {
|
|
527
|
+
// Both surfaces get it: a savepoint on the service transaction is as useful
|
|
528
|
+
// as one on the request's, and each is bound to its own connection.
|
|
529
|
+
attempt: (fn) => raw.attempt(fn),
|
|
455
530
|
tables: makeTablesAccessor(() => reco),
|
|
456
531
|
transaction(fn) {
|
|
457
532
|
const builder = new TxPlanBuilder();
|
|
@@ -603,6 +678,44 @@ var HttpError = class extends Error {
|
|
|
603
678
|
return result;
|
|
604
679
|
}
|
|
605
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
|
+
};
|
|
606
719
|
|
|
607
720
|
// src/engine/config.ts
|
|
608
721
|
var BootRefused = class extends Error {
|
|
@@ -920,6 +1033,14 @@ function makeMemoryCache(opts = {}) {
|
|
|
920
1033
|
function quoteIdent(name) {
|
|
921
1034
|
return `"${name.replace(/"/g, '""')}"`;
|
|
922
1035
|
}
|
|
1036
|
+
function encodePgArray(value) {
|
|
1037
|
+
const element = (x) => {
|
|
1038
|
+
if (x === null || x === void 0) return "NULL";
|
|
1039
|
+
if (Array.isArray(x)) return encodePgArray(x);
|
|
1040
|
+
return `"${String(x).replace(/(["\\])/g, "\\$1")}"`;
|
|
1041
|
+
};
|
|
1042
|
+
return `{${value.map(element).join(",")}}`;
|
|
1043
|
+
}
|
|
923
1044
|
var BIND_SQL = "select set_config('role',$1,true), set_config('search_path','public',true), set_config('request.jwt.claims',$2,true)";
|
|
924
1045
|
function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
925
1046
|
const { lockTimeout } = options;
|
|
@@ -957,9 +1078,9 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
957
1078
|
release();
|
|
958
1079
|
await settled;
|
|
959
1080
|
},
|
|
960
|
-
async rollback(
|
|
1081
|
+
async rollback(reason2) {
|
|
961
1082
|
if (!opening) return;
|
|
962
|
-
fail(
|
|
1083
|
+
fail(reason2);
|
|
963
1084
|
await settled?.catch(() => void 0);
|
|
964
1085
|
}
|
|
965
1086
|
};
|
|
@@ -1002,18 +1123,48 @@ function reviveVectors(row, vectorCols) {
|
|
|
1002
1123
|
}
|
|
1003
1124
|
return row;
|
|
1004
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
|
+
}
|
|
1005
1150
|
function asTableRow(table, row) {
|
|
1006
|
-
|
|
1151
|
+
const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
|
|
1152
|
+
return applyFromDb(revived, transformsOf(currentSchema, table));
|
|
1007
1153
|
}
|
|
1008
1154
|
function asTableRows(table, rows) {
|
|
1009
1155
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1010
|
-
|
|
1156
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1157
|
+
return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
|
|
1011
1158
|
}
|
|
1012
1159
|
function asBindParams(table, cols, data) {
|
|
1013
1160
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1161
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1014
1162
|
return cols.map((c) => {
|
|
1015
1163
|
const v = data[c];
|
|
1016
|
-
|
|
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;
|
|
1017
1168
|
});
|
|
1018
1169
|
}
|
|
1019
1170
|
var SELECTIVITY_EXACT_THRESHOLD = 1e4;
|
|
@@ -1140,7 +1291,9 @@ function expandSynonyms(query, map) {
|
|
|
1140
1291
|
}).join("");
|
|
1141
1292
|
}).join("");
|
|
1142
1293
|
}
|
|
1143
|
-
var searchLogger =
|
|
1294
|
+
var searchLogger = (evt, fields) => {
|
|
1295
|
+
console.log(evt, JSON.stringify(fields));
|
|
1296
|
+
};
|
|
1144
1297
|
function logNoResults(table, query, mode, n) {
|
|
1145
1298
|
if (n === 0) {
|
|
1146
1299
|
searchLogger?.("palbase.search.no_results", {
|
|
@@ -1181,19 +1334,55 @@ function validitySql(v, add) {
|
|
|
1181
1334
|
function excludeSql(pk, ids, add) {
|
|
1182
1335
|
if (ids.length === 0) return "";
|
|
1183
1336
|
if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
|
|
1184
|
-
return ` AND t.${quoteIdent(pk)} <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
1337
|
+
return ` AND t.${quoteIdent(pk)}::text <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
1338
|
+
}
|
|
1339
|
+
function schemaColumns(table) {
|
|
1340
|
+
const t = Object.values(currentSchema.tables ?? {}).find((x) => x?.name === table);
|
|
1341
|
+
const cols = t?.columns ? Object.keys(t.columns) : [];
|
|
1342
|
+
return cols.length > 0 ? new Set(cols) : null;
|
|
1343
|
+
}
|
|
1344
|
+
function orderClause(table, known, orderBy) {
|
|
1345
|
+
if (!orderBy) return "";
|
|
1346
|
+
const col = orderBy.column;
|
|
1347
|
+
const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(col);
|
|
1348
|
+
if (known !== null ? !known.has(col) : !shapeOK) {
|
|
1349
|
+
throw new Error(`findMany(${table}): orderBy kolonu "${col}" tabloda yok`);
|
|
1350
|
+
}
|
|
1351
|
+
const dir = orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
1352
|
+
return ` ORDER BY ${quoteIdent(col)} ${dir}`;
|
|
1353
|
+
}
|
|
1354
|
+
function limitClause(limit) {
|
|
1355
|
+
if (limit === void 0) return "";
|
|
1356
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
1357
|
+
throw new Error(`findMany: limit bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(limit)})`);
|
|
1358
|
+
}
|
|
1359
|
+
return ` LIMIT ${limit}`;
|
|
1185
1360
|
}
|
|
1186
|
-
function
|
|
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
|
+
}
|
|
1375
|
+
function compileWhere(table, colSet, where, add, caller = "search") {
|
|
1187
1376
|
const parts = [];
|
|
1188
1377
|
for (const [col, cond] of Object.entries(where)) {
|
|
1189
|
-
if (!colSet.has(col)) {
|
|
1190
|
-
throw new Error(
|
|
1378
|
+
if (colSet !== null && !colSet.has(col)) {
|
|
1379
|
+
throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
1191
1380
|
}
|
|
1192
1381
|
const q = `t.${quoteIdent(col)}`;
|
|
1193
1382
|
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
1194
1383
|
for (const [op, v] of Object.entries(cond)) {
|
|
1195
1384
|
if (op === "in") {
|
|
1196
|
-
if (!Array.isArray(v)) throw new Error(
|
|
1385
|
+
if (!Array.isArray(v)) throw new Error(`${caller}(${table}): where.${col}.in bir dizi olmal\u0131`);
|
|
1197
1386
|
if (v.length === 0) {
|
|
1198
1387
|
parts.push("false");
|
|
1199
1388
|
continue;
|
|
@@ -1202,7 +1391,7 @@ function compileWhere(table, colSet, where, add) {
|
|
|
1202
1391
|
} else if (op in WHERE_OPS) {
|
|
1203
1392
|
parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
|
|
1204
1393
|
} else {
|
|
1205
|
-
throw new Error(
|
|
1394
|
+
throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
|
|
1206
1395
|
}
|
|
1207
1396
|
}
|
|
1208
1397
|
} else {
|
|
@@ -1211,6 +1400,13 @@ function compileWhere(table, colSet, where, add) {
|
|
|
1211
1400
|
}
|
|
1212
1401
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1213
1402
|
}
|
|
1403
|
+
function onConflictTail(cols, conflict, action) {
|
|
1404
|
+
const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
|
|
1405
|
+
if (action === "ignore") return `${target} DO NOTHING`;
|
|
1406
|
+
const conflictSet = new Set(conflict);
|
|
1407
|
+
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1408
|
+
return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1409
|
+
}
|
|
1214
1410
|
var cachedVectorSchema = null;
|
|
1215
1411
|
var cachedTrgmSchema = null;
|
|
1216
1412
|
async function trgmSchemaOf(live) {
|
|
@@ -1282,7 +1478,8 @@ function createOps(tx) {
|
|
|
1282
1478
|
const at = () => resolveTx(tx);
|
|
1283
1479
|
const ops = {
|
|
1284
1480
|
async query(sql, params = []) {
|
|
1285
|
-
|
|
1481
|
+
const bound = params.map((p) => Array.isArray(p) ? encodePgArray(p) : p);
|
|
1482
|
+
return asWireRows(await (await at()).unsafe(sql, bound));
|
|
1286
1483
|
},
|
|
1287
1484
|
async insert(table, data) {
|
|
1288
1485
|
const cols = Object.keys(data);
|
|
@@ -1298,6 +1495,74 @@ function createOps(tx) {
|
|
|
1298
1495
|
}
|
|
1299
1496
|
return asTableRow(table, inserted);
|
|
1300
1497
|
},
|
|
1498
|
+
/**
|
|
1499
|
+
* INSERT the row, or UPDATE it when it collides on `onConflict`.
|
|
1500
|
+
*
|
|
1501
|
+
* WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
|
|
1502
|
+
* unique violation, update instead" does not work here: a request runs in ONE
|
|
1503
|
+
* Postgres transaction, so the failed insert aborts it and every later
|
|
1504
|
+
* statement answers `current transaction is aborted`. A tenant measured that
|
|
1505
|
+
* as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
|
|
1506
|
+
* trigger create the row instead — a workaround that needs a new trigger for
|
|
1507
|
+
* every table with a unique row.
|
|
1508
|
+
*
|
|
1509
|
+
* The conflict columns are excluded from the SET list: they are what MATCHED,
|
|
1510
|
+
* so writing them back is at best a no-op and at worst a surprise.
|
|
1511
|
+
*/
|
|
1512
|
+
async upsert(table, data, opts) {
|
|
1513
|
+
const conflict = opts.onConflict;
|
|
1514
|
+
if (conflict.length === 0) {
|
|
1515
|
+
throw new Error(`upsert into ${table}: onConflict en az bir kolon ad\u0131 ister`);
|
|
1516
|
+
}
|
|
1517
|
+
const cols = Object.keys(data);
|
|
1518
|
+
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
1519
|
+
if (searchConfigFor(table)?.validity) {
|
|
1520
|
+
const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1521
|
+
const insertSql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING *`;
|
|
1522
|
+
const live = await at();
|
|
1523
|
+
const bindData = asBindParams(table, cols, data);
|
|
1524
|
+
const first = await live.unsafe(insertSql, bindData);
|
|
1525
|
+
if (first[0]) return asTableRow(table, first[0]);
|
|
1526
|
+
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
1527
|
+
const condBind = conflict.map((c) => data[c]);
|
|
1528
|
+
const cur = await live.unsafe(
|
|
1529
|
+
`SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
|
|
1530
|
+
condBind
|
|
1531
|
+
);
|
|
1532
|
+
const old = cur[0];
|
|
1533
|
+
if (old === void 0) {
|
|
1534
|
+
throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 ge\xE7erli sat\u0131r bulunamad\u0131, iste\u011Fi yineleyin (D-020)`);
|
|
1535
|
+
}
|
|
1536
|
+
const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
|
|
1537
|
+
await live.unsafe(
|
|
1538
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
|
|
1539
|
+
[old[pk]]
|
|
1540
|
+
);
|
|
1541
|
+
const second = await live.unsafe(insertSql, bindData);
|
|
1542
|
+
if (!second[0]) {
|
|
1543
|
+
throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 yeni sat\u0131r a\xE7\u0131lamad\u0131, iste\u011Fi yineleyin (D-020)`);
|
|
1544
|
+
}
|
|
1545
|
+
const fresh = second[0];
|
|
1546
|
+
await live.unsafe(
|
|
1547
|
+
`UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
|
|
1548
|
+
[old[pk], fresh[pk]]
|
|
1549
|
+
);
|
|
1550
|
+
return asTableRow(table, second[0]);
|
|
1551
|
+
}
|
|
1552
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1553
|
+
const conflictSet = new Set(conflict);
|
|
1554
|
+
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1555
|
+
const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1556
|
+
const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
1557
|
+
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
|
|
1558
|
+
const row = rows[0];
|
|
1559
|
+
if (!row) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
return asTableRow(table, row);
|
|
1565
|
+
},
|
|
1301
1566
|
async update(table, id, data) {
|
|
1302
1567
|
const cols = Object.keys(data);
|
|
1303
1568
|
if (cols.length === 0) return ops.findById(table, id);
|
|
@@ -1309,6 +1574,86 @@ function createOps(tx) {
|
|
|
1309
1574
|
async delete(table, id) {
|
|
1310
1575
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
1311
1576
|
},
|
|
1577
|
+
/**
|
|
1578
|
+
* Update every row the filter matches, in ONE statement.
|
|
1579
|
+
*
|
|
1580
|
+
* The capability was already here and only reachable from inside a
|
|
1581
|
+
* transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
|
|
1582
|
+
* only door was `update(id, …)`, so "mark every unapproved row in this
|
|
1583
|
+
* household" was an N+1 loop or hand-written SQL — and hand-written SQL is
|
|
1584
|
+
* where the typed surface and RLS both stop helping.
|
|
1585
|
+
*
|
|
1586
|
+
* The filter language is `findMany`'s, compiled by the same `compileWhere`:
|
|
1587
|
+
* one filter language, or the two spellings drift.
|
|
1588
|
+
*
|
|
1589
|
+
* AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
|
|
1590
|
+
* and the shape that produces it by accident — a filter object built from
|
|
1591
|
+
* request input that happened to come back empty — is exactly the shape that
|
|
1592
|
+
* should not silently succeed. Callers who mean every row say so with a
|
|
1593
|
+
* predicate that is true for every row.
|
|
1594
|
+
*/
|
|
1595
|
+
async updateMany(table, where, set) {
|
|
1596
|
+
const cols = Object.keys(set);
|
|
1597
|
+
if (cols.length === 0) return [];
|
|
1598
|
+
if (Object.keys(where).length === 0) {
|
|
1599
|
+
throw new Error(
|
|
1600
|
+
`updateMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 g\xFCncellerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const params = [];
|
|
1604
|
+
const add = (v) => {
|
|
1605
|
+
params.push(v);
|
|
1606
|
+
return `$${params.length}`;
|
|
1607
|
+
};
|
|
1608
|
+
const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
|
|
1609
|
+
const known = schemaColumns(table);
|
|
1610
|
+
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
1611
|
+
const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
|
|
1612
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1613
|
+
},
|
|
1614
|
+
/**
|
|
1615
|
+
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
1616
|
+
* many went. Same filter language, same empty-filter refusal as
|
|
1617
|
+
* {@link updateMany} — and here the accident is worse.
|
|
1618
|
+
*/
|
|
1619
|
+
async deleteMany(table, where) {
|
|
1620
|
+
if (Object.keys(where).length === 0) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`deleteMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 silerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
const params = [];
|
|
1626
|
+
const add = (v) => {
|
|
1627
|
+
params.push(v);
|
|
1628
|
+
return `$${params.length}`;
|
|
1629
|
+
};
|
|
1630
|
+
const known = schemaColumns(table);
|
|
1631
|
+
const whereSql = compileWhere(table, known, where, add, "deleteMany");
|
|
1632
|
+
const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
|
|
1633
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1634
|
+
return rows.length;
|
|
1635
|
+
},
|
|
1636
|
+
/**
|
|
1637
|
+
* How many rows match — the half of pagination `limit`/`offset` cannot
|
|
1638
|
+
* supply. Without it a page count is either a guess or "fetch everything and
|
|
1639
|
+
* read .length", and the second one is the scale risk this surface exists to
|
|
1640
|
+
* remove.
|
|
1641
|
+
*
|
|
1642
|
+
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
1643
|
+
* reads do not destroy anything.
|
|
1644
|
+
*/
|
|
1645
|
+
async count(table, where = {}) {
|
|
1646
|
+
const params = [];
|
|
1647
|
+
const add = (v) => {
|
|
1648
|
+
params.push(v);
|
|
1649
|
+
return `$${params.length}`;
|
|
1650
|
+
};
|
|
1651
|
+
const known = schemaColumns(table);
|
|
1652
|
+
const whereSql = compileWhere(table, known, where, add, "count");
|
|
1653
|
+
const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
|
|
1654
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1655
|
+
return Number(rows[0]?.n ?? 0);
|
|
1656
|
+
},
|
|
1312
1657
|
async findById(table, id) {
|
|
1313
1658
|
const rows = await (await at()).unsafe(
|
|
1314
1659
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
@@ -1316,13 +1661,19 @@ function createOps(tx) {
|
|
|
1316
1661
|
);
|
|
1317
1662
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
1318
1663
|
},
|
|
1319
|
-
async findMany(table, query = {}) {
|
|
1320
|
-
const
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
)
|
|
1664
|
+
async findMany(table, query = {}, opts = {}) {
|
|
1665
|
+
const params = [];
|
|
1666
|
+
const add = (v) => {
|
|
1667
|
+
params.push(v);
|
|
1668
|
+
return `$${params.length}`;
|
|
1669
|
+
};
|
|
1670
|
+
const known = schemaColumns(table);
|
|
1671
|
+
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
1672
|
+
const order = orderClause(table, known, opts.orderBy);
|
|
1673
|
+
const limit = limitClause(opts.limit);
|
|
1674
|
+
const offset = offsetClause(opts.offset, opts.limit);
|
|
1675
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
|
|
1676
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1326
1677
|
},
|
|
1327
1678
|
/**
|
|
1328
1679
|
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
@@ -1404,7 +1755,9 @@ function createOps(tx) {
|
|
|
1404
1755
|
bindC.push(v);
|
|
1405
1756
|
return `$${bindC.length}`;
|
|
1406
1757
|
};
|
|
1407
|
-
const
|
|
1758
|
+
const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
|
|
1759
|
+
const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1760
|
+
const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
|
|
1408
1761
|
const liveC = await at();
|
|
1409
1762
|
const K2 = 50;
|
|
1410
1763
|
const cpool = Math.max(limit * 9, 90);
|
|
@@ -1413,8 +1766,19 @@ function createOps(tx) {
|
|
|
1413
1766
|
if (qvC !== null) {
|
|
1414
1767
|
const sch = await vectorSchemaWithGuc(liveC);
|
|
1415
1768
|
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1769
|
+
let exactOrderC = "";
|
|
1770
|
+
if (userWhereC !== "") {
|
|
1771
|
+
const probeRows = await liveC.unsafe(
|
|
1772
|
+
`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
|
|
1773
|
+
bindC.slice()
|
|
1774
|
+
);
|
|
1775
|
+
const n = probeRows?.[0]?.n;
|
|
1776
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1777
|
+
exactOrderC = " + 0.0";
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1416
1780
|
const vp = addC(toVectorLiteral(qvC));
|
|
1417
|
-
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)) AS r FROM ${quoteIdent(ck.table)} c
|
|
1781
|
+
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)${exactOrderC}) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
|
|
1418
1782
|
}
|
|
1419
1783
|
let qpC = "";
|
|
1420
1784
|
let qpCIdx = -1;
|
|
@@ -1422,7 +1786,7 @@ function createOps(tx) {
|
|
|
1422
1786
|
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
1423
1787
|
qpC = addC(qTextC);
|
|
1424
1788
|
qpCIdx = bindC.length - 1;
|
|
1425
|
-
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c
|
|
1789
|
+
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
|
|
1426
1790
|
}
|
|
1427
1791
|
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
1428
1792
|
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
@@ -1437,13 +1801,13 @@ function createOps(tx) {
|
|
|
1437
1801
|
} else {
|
|
1438
1802
|
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
1439
1803
|
}
|
|
1440
|
-
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid
|
|
1804
|
+
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid`;
|
|
1441
1805
|
return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
1442
1806
|
};
|
|
1443
1807
|
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
1444
1808
|
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
1445
1809
|
const tsch = await trgmSchemaOf(liveC);
|
|
1446
|
-
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c
|
|
1810
|
+
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) > 0.3 ORDER BY r LIMIT ${cpool}`;
|
|
1447
1811
|
const retryBindC = bindC.slice();
|
|
1448
1812
|
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
1449
1813
|
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
@@ -1539,7 +1903,7 @@ function createOps(tx) {
|
|
|
1539
1903
|
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
1540
1904
|
const tsch = await trgmSchemaOf(live);
|
|
1541
1905
|
const expr = ftsExprOf(cfg.ftsCols);
|
|
1542
|
-
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${
|
|
1906
|
+
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) > 0.3${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
1543
1907
|
const retryBind = bind.slice();
|
|
1544
1908
|
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
1545
1909
|
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
@@ -1562,6 +1926,25 @@ function createOps(tx) {
|
|
|
1562
1926
|
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
1563
1927
|
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
1564
1928
|
*/
|
|
1929
|
+
/** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
|
|
1930
|
+
* `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
|
|
1931
|
+
* yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
|
|
1932
|
+
* search'teki alan geriye-uyum için DURUR. where + validity default'u
|
|
1933
|
+
* sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
|
|
1934
|
+
async facets(table, params) {
|
|
1935
|
+
const cfg = searchConfigFor(table);
|
|
1936
|
+
if (!cfg) {
|
|
1937
|
+
throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
|
|
1938
|
+
}
|
|
1939
|
+
for (const col of params.facets) {
|
|
1940
|
+
if (!cfg.colSet.has(col)) {
|
|
1941
|
+
throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const live = await at();
|
|
1945
|
+
const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
|
|
1946
|
+
return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
|
|
1947
|
+
},
|
|
1565
1948
|
async similar(table, id, opts = {}) {
|
|
1566
1949
|
const cfg = searchConfigFor(table);
|
|
1567
1950
|
if (!cfg) {
|
|
@@ -1713,6 +2096,27 @@ function createOps(tx) {
|
|
|
1713
2096
|
const live = await at();
|
|
1714
2097
|
return live.savepoint(async (sp) => cb(withTables(createOps(sp), currentSchema)));
|
|
1715
2098
|
},
|
|
2099
|
+
/**
|
|
2100
|
+
* Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
|
|
2101
|
+
* rolls back only what that handle wrote and the request can keep writing.
|
|
2102
|
+
*
|
|
2103
|
+
* WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
|
|
2104
|
+
* ... Database.insert(...) ... })`, with no parameter — would have to point
|
|
2105
|
+
* the ambient `Database` at the savepoint for the duration, and a request is
|
|
2106
|
+
* concurrent with itself: `Promise.all([Database.insert(a),
|
|
2107
|
+
* Database.attempt(...)])` would put `a` inside the savepoint and roll it
|
|
2108
|
+
* back with it. Silent data loss, and the same interleaving this file already
|
|
2109
|
+
* refuses for `asService()`. Passing the handle makes the boundary something
|
|
2110
|
+
* you can see in the code that crosses it.
|
|
2111
|
+
*
|
|
2112
|
+
* Postgres, not us: the savepoint is released on success and rolled back on
|
|
2113
|
+
* failure by the driver, so an aborted statement inside `fn` does not poison
|
|
2114
|
+
* the surrounding transaction.
|
|
2115
|
+
*/
|
|
2116
|
+
async attempt(fn) {
|
|
2117
|
+
const live = await at();
|
|
2118
|
+
return live.savepoint(async (sp) => fn(withTables(createOps(sp), currentSchema)));
|
|
2119
|
+
},
|
|
1716
2120
|
/**
|
|
1717
2121
|
* Execute a whole transaction plan — what `Database.transaction(fn)` builds.
|
|
1718
2122
|
*
|
|
@@ -1763,22 +2167,68 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1763
2167
|
update: (id, data) => ops.update(name, id, data),
|
|
1764
2168
|
delete: (id) => ops.delete(name, id),
|
|
1765
2169
|
findById: (id) => ops.findById(name, id),
|
|
1766
|
-
findMany: (query) => ops.findMany(name, query ?? {})
|
|
2170
|
+
findMany: (query, opts) => ops.findMany(name, query ?? {}, opts),
|
|
2171
|
+
upsert: (data, opts) => ops.upsert(name, data, opts)
|
|
1767
2172
|
};
|
|
1768
2173
|
}
|
|
1769
2174
|
const base = /* @__PURE__ */ Object.create(null);
|
|
1770
2175
|
return Object.assign(base, ops, { tables });
|
|
1771
2176
|
}
|
|
1772
2177
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1773
|
-
function
|
|
1774
|
-
const code = e?.code;
|
|
2178
|
+
function isAbortedTransaction(e) {
|
|
1775
2179
|
const message = String(e?.message ?? "");
|
|
1776
|
-
return
|
|
2180
|
+
return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
|
|
1777
2181
|
}
|
|
1778
|
-
function
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1781
|
-
)
|
|
2182
|
+
function sqlstateOf(e) {
|
|
2183
|
+
const err = e;
|
|
2184
|
+
const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
|
|
2185
|
+
if (isState(err?.code)) return err.code;
|
|
2186
|
+
if (isState(err?.errno)) return err.errno;
|
|
2187
|
+
if (typeof err?.errno === "number") {
|
|
2188
|
+
const asText = String(err.errno);
|
|
2189
|
+
if (isState(asText)) return asText;
|
|
2190
|
+
}
|
|
2191
|
+
return void 0;
|
|
2192
|
+
}
|
|
2193
|
+
function isForeignKeyViolation(e) {
|
|
2194
|
+
return sqlstateOf(e) === "23503";
|
|
2195
|
+
}
|
|
2196
|
+
function isUniqueViolation(e) {
|
|
2197
|
+
return sqlstateOf(e) === "23505";
|
|
2198
|
+
}
|
|
2199
|
+
function constraintOf(e) {
|
|
2200
|
+
const named = e?.constraint;
|
|
2201
|
+
if (typeof named === "string" && named.length > 0) return named;
|
|
2202
|
+
const message = String(e?.message ?? "");
|
|
2203
|
+
return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
|
|
2204
|
+
}
|
|
2205
|
+
function isLockTimeout(e) {
|
|
2206
|
+
const message = String(e?.message ?? "");
|
|
2207
|
+
return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
|
|
2208
|
+
}
|
|
2209
|
+
function diagnosingDriver(sql, surface = "service") {
|
|
2210
|
+
const original = (e) => String(e?.message ?? e);
|
|
2211
|
+
const explain = (e) => {
|
|
2212
|
+
if (isAbortedTransaction(e)) {
|
|
2213
|
+
return new Error(
|
|
2214
|
+
`This request cannot write any more: an earlier write in it failed, and a request runs in ONE Postgres transaction, so every statement after the failure is refused. Nothing here is retryable in place. Wrap a write you expect to fail in \`Database.attempt(async (tx) => \u2026)\` \u2014 it takes a SAVEPOINT, so only that write rolls back \u2014 or use \`Database.tables.<t>.upsert(data, { onConflict: [...] })\` when the failure you were bracing for is a duplicate row. (${original(e)})`
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
if (isUniqueViolation(e)) {
|
|
2218
|
+
return new UniqueViolation(constraintOf(e));
|
|
2219
|
+
}
|
|
2220
|
+
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
2221
|
+
return new Error(
|
|
2222
|
+
`Database.asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
if (isLockTimeout(e)) {
|
|
2226
|
+
return new Error(
|
|
2227
|
+
`Database.asService() waited too long for a row lock. It runs in its OWN transaction, so a row this request already wrote through Database.* is locked against it until the request commits \u2014 a wait that cannot end. Do that row's work on one surface or the other. (${original(e)})`
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
return e;
|
|
2231
|
+
};
|
|
1782
2232
|
const wrapTx = (tx) => ({
|
|
1783
2233
|
async unsafe(text, params) {
|
|
1784
2234
|
try {
|
|
@@ -1797,13 +2247,13 @@ function diagnosingDriver(sql) {
|
|
|
1797
2247
|
};
|
|
1798
2248
|
}
|
|
1799
2249
|
function createRequestDatabase(sql, identity) {
|
|
1800
|
-
const tx = createLazyTransaction(sql, identity.role, identity.claimsJson);
|
|
2250
|
+
const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson);
|
|
1801
2251
|
let serviceTx = null;
|
|
1802
2252
|
let serviceClient = null;
|
|
1803
2253
|
const asService = () => {
|
|
1804
2254
|
if (serviceClient === null) {
|
|
1805
2255
|
serviceTx = createLazyTransaction(
|
|
1806
|
-
diagnosingDriver(sql),
|
|
2256
|
+
diagnosingDriver(sql, "service"),
|
|
1807
2257
|
identity.serviceRole,
|
|
1808
2258
|
identity.claimsJson,
|
|
1809
2259
|
{ lockTimeout: SERVICE_LOCK_TIMEOUT }
|
|
@@ -1818,9 +2268,9 @@ function createRequestDatabase(sql, identity) {
|
|
|
1818
2268
|
await tx.commit();
|
|
1819
2269
|
await serviceTx?.commit();
|
|
1820
2270
|
},
|
|
1821
|
-
async rollback(
|
|
1822
|
-
await tx.rollback(
|
|
1823
|
-
await serviceTx?.rollback(
|
|
2271
|
+
async rollback(reason2) {
|
|
2272
|
+
await tx.rollback(reason2);
|
|
2273
|
+
await serviceTx?.rollback(reason2);
|
|
1824
2274
|
}
|
|
1825
2275
|
};
|
|
1826
2276
|
}
|
|
@@ -1884,6 +2334,20 @@ async function runPlanOp(sp, op, results) {
|
|
|
1884
2334
|
sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
|
|
1885
2335
|
break;
|
|
1886
2336
|
}
|
|
2337
|
+
case "upsert": {
|
|
2338
|
+
const cols = Object.keys(op.values ?? {});
|
|
2339
|
+
const conflict = op.onConflict ?? [];
|
|
2340
|
+
if (cols.length === 0 || conflict.length === 0) {
|
|
2341
|
+
throw Object.assign(new Error(`upsert on ${op.table} needs columns and onConflict`), {
|
|
2342
|
+
error_code: "tx_bad_upsert"
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
const rendered = cols.map(
|
|
2346
|
+
(c) => renderValue(op.values[c], c, args, results, opVectorCols)
|
|
2347
|
+
);
|
|
2348
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
|
|
2349
|
+
break;
|
|
2350
|
+
}
|
|
1887
2351
|
case "insertMany": {
|
|
1888
2352
|
const rows = op.rows ?? [];
|
|
1889
2353
|
if (rows.length === 0 || !rows[0]) return [];
|
|
@@ -1891,7 +2355,9 @@ async function runPlanOp(sp, op, results) {
|
|
|
1891
2355
|
const tuples = rows.map(
|
|
1892
2356
|
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
1893
2357
|
);
|
|
1894
|
-
|
|
2358
|
+
const conflictCols = op.onConflict ?? [];
|
|
2359
|
+
const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
|
|
2360
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
1895
2361
|
break;
|
|
1896
2362
|
}
|
|
1897
2363
|
case "update": {
|
|
@@ -1966,6 +2432,38 @@ function getRoutes(ctor) {
|
|
|
1966
2432
|
}));
|
|
1967
2433
|
}
|
|
1968
2434
|
|
|
2435
|
+
// src/decorators/controller.ts
|
|
2436
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("palbase.backend.allControllers");
|
|
2437
|
+
function registry() {
|
|
2438
|
+
const g = globalThis;
|
|
2439
|
+
const existing = g[REGISTRY];
|
|
2440
|
+
if (existing) return existing;
|
|
2441
|
+
const fresh = [];
|
|
2442
|
+
g[REGISTRY] = fresh;
|
|
2443
|
+
return fresh;
|
|
2444
|
+
}
|
|
2445
|
+
function getRegisteredControllers() {
|
|
2446
|
+
return registry().slice();
|
|
2447
|
+
}
|
|
2448
|
+
var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
|
|
2449
|
+
function appAuthSlot() {
|
|
2450
|
+
return globalThis;
|
|
2451
|
+
}
|
|
2452
|
+
function getDefaultAuth() {
|
|
2453
|
+
return appAuthSlot()[APP_DEFAULT_AUTH];
|
|
2454
|
+
}
|
|
2455
|
+
function resolveEffectiveAuth(routeAuth, controllerAuth) {
|
|
2456
|
+
return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
|
|
2457
|
+
}
|
|
2458
|
+
function assertZeroArgConstructor(Ctrl, kind) {
|
|
2459
|
+
const arity = Ctrl.length ?? 0;
|
|
2460
|
+
if (arity === 0) return;
|
|
2461
|
+
const name = Ctrl.name ?? "<anonymous>";
|
|
2462
|
+
throw new Error(
|
|
2463
|
+
`${kind} ${name} declares a constructor with ${arity} parameter(s). A ${kind} is constructed by the runtime with a zero-argument constructor \u2014 there is no injector to supply them, so every parameter would arrive as undefined. Hold the dependency as a module-level singleton the ${kind} imports (\`const repo = makeRepo()\` beside the class), and construct the service directly in tests (e.g. \`new TodoService(fakeDatabase().db)\`).`
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
1969
2467
|
// src/engine/router.ts
|
|
1970
2468
|
var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
|
|
1971
2469
|
function toSegments(path) {
|
|
@@ -1984,6 +2482,7 @@ function buildRouteTable(controllers) {
|
|
|
1984
2482
|
`controller ${name} collected zero routes. Either it declares no @Get/@Post/\u2026 , or its decorator metadata was erased at build time \u2014 check that the bundle was compiled with experimentalDecorators enabled.`
|
|
1985
2483
|
);
|
|
1986
2484
|
}
|
|
2485
|
+
assertZeroArgConstructor(Ctrl, "controller");
|
|
1987
2486
|
const instance = new ctor();
|
|
1988
2487
|
for (const r of routes) {
|
|
1989
2488
|
const full = `${basePath}${r.subpath ?? ""}` || "/";
|
|
@@ -1993,7 +2492,10 @@ function buildRouteTable(controllers) {
|
|
|
1993
2492
|
meta: r,
|
|
1994
2493
|
instance,
|
|
1995
2494
|
id: `${r.method} ${full}`,
|
|
1996
|
-
|
|
2495
|
+
// The route's OWN spec is deliberately not folded in here: this field
|
|
2496
|
+
// is what applies when the route is silent, and `effectiveAuth` puts
|
|
2497
|
+
// the route level back on top per request.
|
|
2498
|
+
controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
|
|
1997
2499
|
});
|
|
1998
2500
|
}
|
|
1999
2501
|
}
|
|
@@ -2080,6 +2582,41 @@ function verifySignature(presented, expected) {
|
|
|
2080
2582
|
return diff === 0;
|
|
2081
2583
|
}
|
|
2082
2584
|
|
|
2585
|
+
// src/engine/sse.ts
|
|
2586
|
+
var SSE_CONTENT_TYPE = "text/event-stream";
|
|
2587
|
+
function encodeFrame(value) {
|
|
2588
|
+
return `data: ${JSON.stringify(value)}
|
|
2589
|
+
|
|
2590
|
+
`;
|
|
2591
|
+
}
|
|
2592
|
+
function encodeErrorFrame(requestId) {
|
|
2593
|
+
return `event: error
|
|
2594
|
+
data: ${JSON.stringify({ requestId })}
|
|
2595
|
+
|
|
2596
|
+
`;
|
|
2597
|
+
}
|
|
2598
|
+
function makeSseWriter(opts) {
|
|
2599
|
+
let begun = false;
|
|
2600
|
+
let chain = Promise.resolve();
|
|
2601
|
+
return {
|
|
2602
|
+
write(value) {
|
|
2603
|
+
const frame = encodeFrame(value);
|
|
2604
|
+
if (!begun) {
|
|
2605
|
+
begun = true;
|
|
2606
|
+
chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
chain = chain.then(() => opts.enqueue(frame));
|
|
2610
|
+
},
|
|
2611
|
+
started() {
|
|
2612
|
+
return begun;
|
|
2613
|
+
},
|
|
2614
|
+
drained() {
|
|
2615
|
+
return chain;
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2083
2620
|
// src/engine/index.ts
|
|
2084
2621
|
var JSON_HEADERS = { "content-type": "application/json" };
|
|
2085
2622
|
function fieldErrors(err) {
|
|
@@ -2254,6 +2791,13 @@ async function createApp(opts) {
|
|
|
2254
2791
|
const args = [];
|
|
2255
2792
|
let parsedBody;
|
|
2256
2793
|
let bodyRead = false;
|
|
2794
|
+
let sseEnqueue = null;
|
|
2795
|
+
let sseSettle = async () => {
|
|
2796
|
+
};
|
|
2797
|
+
const sseWriter = makeSseWriter({
|
|
2798
|
+
enqueue: (chunk) => sseEnqueue?.(chunk),
|
|
2799
|
+
onFirstWrite: () => sseSettle()
|
|
2800
|
+
});
|
|
2257
2801
|
for (const p of meta.params ?? []) {
|
|
2258
2802
|
switch (p.kind) {
|
|
2259
2803
|
case "body": {
|
|
@@ -2357,6 +2901,21 @@ async function createApp(opts) {
|
|
|
2357
2901
|
case "req":
|
|
2358
2902
|
args[p.index] = req;
|
|
2359
2903
|
break;
|
|
2904
|
+
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
2905
|
+
// state when the client disconnects. Measured on Bun: an infinite
|
|
2906
|
+
// producer guarded by it stops within a few frames of the client dying.
|
|
2907
|
+
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
2908
|
+
// request-scoped data and no signal.
|
|
2909
|
+
case "signal":
|
|
2910
|
+
args[p.index] = req.signal;
|
|
2911
|
+
break;
|
|
2912
|
+
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
2913
|
+
// on any other route it is an inert writer whose frames go nowhere,
|
|
2914
|
+
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
2915
|
+
// route.
|
|
2916
|
+
case "sseOut":
|
|
2917
|
+
args[p.index] = sseWriter;
|
|
2918
|
+
break;
|
|
2360
2919
|
default:
|
|
2361
2920
|
args[p.index] = void 0;
|
|
2362
2921
|
}
|
|
@@ -2368,6 +2927,84 @@ async function createApp(opts) {
|
|
|
2368
2927
|
});
|
|
2369
2928
|
try {
|
|
2370
2929
|
const services = buildServices(db);
|
|
2930
|
+
if (meta.options?.sseConfig !== void 0) {
|
|
2931
|
+
let sseController;
|
|
2932
|
+
const sseEncoder = new TextEncoder();
|
|
2933
|
+
const stream = new ReadableStream({
|
|
2934
|
+
start(c) {
|
|
2935
|
+
sseController = c;
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
sseEnqueue = (chunk) => {
|
|
2939
|
+
try {
|
|
2940
|
+
sseController.enqueue(sseEncoder.encode(chunk));
|
|
2941
|
+
} catch {
|
|
2942
|
+
}
|
|
2943
|
+
};
|
|
2944
|
+
let sawFirstFrame;
|
|
2945
|
+
const firstFrame = new Promise((r) => sawFirstFrame = r);
|
|
2946
|
+
sseSettle = async () => {
|
|
2947
|
+
await db.commit();
|
|
2948
|
+
sawFirstFrame();
|
|
2949
|
+
};
|
|
2950
|
+
const guarded = {
|
|
2951
|
+
...services,
|
|
2952
|
+
Database: new Proxy(services.Database, {
|
|
2953
|
+
get(target, prop, recv) {
|
|
2954
|
+
if (sseWriter.started()) {
|
|
2955
|
+
throw new Error(
|
|
2956
|
+
`sse_db_after_write: ${hit.entry.id} touched the database after its first write(). A streaming response settles its transaction with the first frame \u2014 do the database work before the first write.`
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
return Reflect.get(target, prop, recv);
|
|
2960
|
+
}
|
|
2961
|
+
})
|
|
2962
|
+
};
|
|
2963
|
+
let thrown = null;
|
|
2964
|
+
const ran = Promise.resolve(runWithRuntime(guarded, () => {
|
|
2965
|
+
const box = requestALS.getStore();
|
|
2966
|
+
if (box) {
|
|
2967
|
+
box.userId = userId ?? null;
|
|
2968
|
+
box.requestId = requestId;
|
|
2969
|
+
box.idempotencyKey = req.headers.get("idempotency-key");
|
|
2970
|
+
}
|
|
2971
|
+
const method = hit.entry.instance[meta.fnName];
|
|
2972
|
+
if (typeof method !== "function") {
|
|
2973
|
+
throw new Error(
|
|
2974
|
+
`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
return method.apply(hit.entry.instance, args);
|
|
2978
|
+
})).then(
|
|
2979
|
+
() => void 0,
|
|
2980
|
+
(err) => {
|
|
2981
|
+
thrown = err;
|
|
2982
|
+
}
|
|
2983
|
+
);
|
|
2984
|
+
await Promise.race([firstFrame, ran]);
|
|
2985
|
+
if (!sseWriter.started()) {
|
|
2986
|
+
await ran;
|
|
2987
|
+
await db.commit();
|
|
2988
|
+
if (thrown !== null) throw thrown;
|
|
2989
|
+
return new Response(null, { status: 204 });
|
|
2990
|
+
}
|
|
2991
|
+
void (async () => {
|
|
2992
|
+
await ran;
|
|
2993
|
+
await sseWriter.drained();
|
|
2994
|
+
try {
|
|
2995
|
+
if (thrown !== null) {
|
|
2996
|
+
log.error(`[engine] ${hit.entry.id} threw mid-stream`, thrown);
|
|
2997
|
+
sseController.enqueue(sseEncoder.encode(encodeErrorFrame(requestId)));
|
|
2998
|
+
}
|
|
2999
|
+
sseController.close();
|
|
3000
|
+
} catch {
|
|
3001
|
+
}
|
|
3002
|
+
})();
|
|
3003
|
+
return new Response(stream, {
|
|
3004
|
+
status: 200,
|
|
3005
|
+
headers: { "content-type": SSE_CONTENT_TYPE, "cache-control": "no-cache" }
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
2371
3008
|
const result = await runWithRuntime(services, () => {
|
|
2372
3009
|
const box = requestALS.getStore();
|
|
2373
3010
|
if (box) {
|
|
@@ -2426,31 +3063,28 @@ async function createApp(opts) {
|
|
|
2426
3063
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
2427
3064
|
}
|
|
2428
3065
|
}
|
|
3066
|
+
let runShutdownHooks;
|
|
3067
|
+
try {
|
|
3068
|
+
runShutdownHooks = await __runStartHooks();
|
|
3069
|
+
} catch (err) {
|
|
3070
|
+
await closeDriver(sql);
|
|
3071
|
+
throw err;
|
|
3072
|
+
}
|
|
2429
3073
|
return {
|
|
2430
3074
|
handle,
|
|
2431
3075
|
routes,
|
|
2432
3076
|
config,
|
|
2433
3077
|
runInServiceScope,
|
|
2434
3078
|
async shutdown() {
|
|
2435
|
-
|
|
2436
|
-
await
|
|
2437
|
-
await closable.end?.();
|
|
3079
|
+
await runShutdownHooks();
|
|
3080
|
+
await closeDriver(sql);
|
|
2438
3081
|
}
|
|
2439
3082
|
};
|
|
2440
3083
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
const g = globalThis;
|
|
2446
|
-
const existing = g[REGISTRY];
|
|
2447
|
-
if (existing) return existing;
|
|
2448
|
-
const fresh = [];
|
|
2449
|
-
g[REGISTRY] = fresh;
|
|
2450
|
-
return fresh;
|
|
2451
|
-
}
|
|
2452
|
-
function getRegisteredControllers() {
|
|
2453
|
-
return registry().slice();
|
|
3084
|
+
async function closeDriver(sql) {
|
|
3085
|
+
const closable = sql;
|
|
3086
|
+
await closable.close?.();
|
|
3087
|
+
await closable.end?.();
|
|
2454
3088
|
}
|
|
2455
3089
|
|
|
2456
3090
|
// src/bin/palbase-backend.ts
|