@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
package/dist/engine/index.cjs
CHANGED
|
@@ -252,7 +252,20 @@ var TxPlanBuilder = class {
|
|
|
252
252
|
}
|
|
253
253
|
return this.push({ op: "insert", table: name, values: encoded }, `${name}.insert()`);
|
|
254
254
|
},
|
|
255
|
-
|
|
255
|
+
upsert: (values, options) => {
|
|
256
|
+
const encoded = encodeMap(values, false);
|
|
257
|
+
if (Object.keys(encoded).length === 0) {
|
|
258
|
+
throw new TxPlanError(`${name}.upsert() needs at least one column`);
|
|
259
|
+
}
|
|
260
|
+
if (options.onConflict.length === 0) {
|
|
261
|
+
throw new TxPlanError(`${name}.upsert() needs at least one onConflict column`);
|
|
262
|
+
}
|
|
263
|
+
return this.push(
|
|
264
|
+
{ op: "upsert", table: name, values: encoded, onConflict: options.onConflict },
|
|
265
|
+
`${name}.upsert()`
|
|
266
|
+
);
|
|
267
|
+
},
|
|
268
|
+
insertMany: (rows, opts) => {
|
|
256
269
|
if (rows.length === 0) {
|
|
257
270
|
return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);
|
|
258
271
|
}
|
|
@@ -263,7 +276,23 @@ var TxPlanBuilder = class {
|
|
|
263
276
|
}
|
|
264
277
|
const encoded = rows.map((row) => encodeMap(row, false));
|
|
265
278
|
assertUniformRows(encoded, name);
|
|
266
|
-
|
|
279
|
+
if (opts !== void 0 && opts.onConflict.length === 0) {
|
|
280
|
+
throw new TxPlanError(
|
|
281
|
+
`${name}.insertMany() was given a conflict action with no onConflict columns. Postgres matches a collision on columns, so name them.`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return this.push(
|
|
285
|
+
{
|
|
286
|
+
op: "insertMany",
|
|
287
|
+
table: name,
|
|
288
|
+
rows: encoded,
|
|
289
|
+
// Omitted entirely when no options were given, so the op a plain
|
|
290
|
+
// insertMany produces is byte-identical to the one it produced
|
|
291
|
+
// before this option existed.
|
|
292
|
+
...opts !== void 0 ? { onConflict: opts.onConflict, action: opts.action ?? "ignore" } : {}
|
|
293
|
+
},
|
|
294
|
+
`${name}.insertMany()`
|
|
295
|
+
);
|
|
267
296
|
},
|
|
268
297
|
updateWhere: (where, set) => {
|
|
269
298
|
const encodedWhere = encodeMap(where, false);
|
|
@@ -437,6 +466,42 @@ function __getRuntime() {
|
|
|
437
466
|
}
|
|
438
467
|
return runtime;
|
|
439
468
|
}
|
|
469
|
+
var LIFECYCLE = /* @__PURE__ */ Symbol.for("palbase.backend.lifecycleHooks");
|
|
470
|
+
function declaredLifecycle() {
|
|
471
|
+
const g = globalThis;
|
|
472
|
+
return g[LIFECYCLE] ??= { start: [], shutdown: [] };
|
|
473
|
+
}
|
|
474
|
+
function reason(err) {
|
|
475
|
+
return err instanceof Error ? err.message : String(err);
|
|
476
|
+
}
|
|
477
|
+
async function drain(hooks) {
|
|
478
|
+
for (const h of [...hooks].reverse()) {
|
|
479
|
+
try {
|
|
480
|
+
await h.run();
|
|
481
|
+
} catch (err) {
|
|
482
|
+
console.error(`[palbase] shutdown hook "${h.name}" failed: ${reason(err)}`, err);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function __runStartHooks() {
|
|
487
|
+
const slot = declaredLifecycle();
|
|
488
|
+
const start = slot.start.splice(0);
|
|
489
|
+
const shutdown = slot.shutdown.splice(0);
|
|
490
|
+
for (const h of start) {
|
|
491
|
+
try {
|
|
492
|
+
await h.run();
|
|
493
|
+
} catch (err) {
|
|
494
|
+
await drain(shutdown);
|
|
495
|
+
throw new Error(`[palbase] start hook "${h.name}" failed: ${reason(err)}`, { cause: err });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
let drained = false;
|
|
499
|
+
return async () => {
|
|
500
|
+
if (drained) return;
|
|
501
|
+
drained = true;
|
|
502
|
+
await drain(shutdown);
|
|
503
|
+
};
|
|
504
|
+
}
|
|
440
505
|
function makeServiceProxy(key) {
|
|
441
506
|
const handler = {
|
|
442
507
|
get(_target, prop, receiver) {
|
|
@@ -459,10 +524,12 @@ function makeTablesAccessor(ops) {
|
|
|
459
524
|
update: (id, data) => ops().update(name, id, data),
|
|
460
525
|
delete: (id) => ops().delete(name, id),
|
|
461
526
|
findById: (id) => ops().findById(name, id),
|
|
462
|
-
findMany: (query) => ops().findMany(name, query),
|
|
527
|
+
findMany: (query, opts) => ops().findMany(name, query, opts),
|
|
528
|
+
upsert: (data, opts) => ops().upsert(name, data, opts),
|
|
463
529
|
search: (params) => ops().search(name, params),
|
|
464
530
|
similar: (id, params) => ops().similar(name, id, params),
|
|
465
531
|
recommend: (params) => ops().recommend(name, params),
|
|
532
|
+
facets: (params) => ops().facets(name, params),
|
|
466
533
|
supersede: (id, row) => ops().supersede(name, id, row)
|
|
467
534
|
};
|
|
468
535
|
}
|
|
@@ -479,13 +546,21 @@ function makeTypedSurface(raw) {
|
|
|
479
546
|
update: (table, id, data) => raw.update(table, id, data),
|
|
480
547
|
delete: (table, id) => raw.delete(table, id),
|
|
481
548
|
findById: (table, id) => raw.findById(table, id),
|
|
482
|
-
findMany: (table, query) => raw.findMany(table, query),
|
|
549
|
+
findMany: (table, query, opts) => raw.findMany(table, query, opts),
|
|
550
|
+
upsert: (table, data, opts) => raw.upsert(table, data, opts),
|
|
551
|
+
updateMany: (table, where, set) => raw.updateMany(table, where, set),
|
|
552
|
+
deleteMany: (table, where) => raw.deleteMany(table, where),
|
|
553
|
+
count: (table, where) => raw.count(table, where),
|
|
483
554
|
search: (table, params) => raw.search(table, params),
|
|
484
555
|
similar: (table, id, params) => reco.similar(table, id, params),
|
|
485
556
|
recommend: (table, params) => reco.recommend(table, params),
|
|
557
|
+
facets: (table, params) => reco.facets(table, params),
|
|
486
558
|
supersede: (table, id, row) => raw.supersede(table, id, row)
|
|
487
559
|
};
|
|
488
560
|
return Object.assign(ops, {
|
|
561
|
+
// Both surfaces get it: a savepoint on the service transaction is as useful
|
|
562
|
+
// as one on the request's, and each is bound to its own connection.
|
|
563
|
+
attempt: (fn) => raw.attempt(fn),
|
|
489
564
|
tables: makeTablesAccessor(() => reco),
|
|
490
565
|
transaction(fn) {
|
|
491
566
|
const builder = new TxPlanBuilder();
|
|
@@ -637,6 +712,44 @@ var HttpError = class extends Error {
|
|
|
637
712
|
return result;
|
|
638
713
|
}
|
|
639
714
|
};
|
|
715
|
+
var NamedHttpError = class extends HttpError {
|
|
716
|
+
constructor(status, defaultCode, name, message, code, data) {
|
|
717
|
+
super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
|
|
718
|
+
this.name = name;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
function defaultMessage(name) {
|
|
722
|
+
const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
723
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
724
|
+
}
|
|
725
|
+
var Conflict = class extends NamedHttpError {
|
|
726
|
+
constructor(message, code, data) {
|
|
727
|
+
super(409, "conflict", "Conflict", message, code, data);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
var UniqueViolation = class extends Conflict {
|
|
731
|
+
/**
|
|
732
|
+
* Whether `e` is a unique violation — REGARDLESS of which copy of this SDK
|
|
733
|
+
* constructed it.
|
|
734
|
+
*
|
|
735
|
+
* Use this instead of `instanceof`. Measured on a live stack: a controller
|
|
736
|
+
* bundle INLINES its own copy of `@palbase/backend`, and the engine that
|
|
737
|
+
* raises this error is the runtime's copy. Two copies, two class identities,
|
|
738
|
+
* and `e instanceof UniqueViolation` is false in the one place a caller
|
|
739
|
+
* writes it — a check that reads as correct and silently never matches.
|
|
740
|
+
*/
|
|
741
|
+
static is(e) {
|
|
742
|
+
return typeof e === "object" && e !== null && e.name === "UniqueViolation" && typeof e.constraint === "string";
|
|
743
|
+
}
|
|
744
|
+
/** The unique constraint the statement violated, as Postgres named it.
|
|
745
|
+
* `""` when the driver did not say which — see `engine/db.ts`. */
|
|
746
|
+
constraint;
|
|
747
|
+
constructor(constraint, message, code, data) {
|
|
748
|
+
super(message ?? "Unique constraint violated", code ?? "unique_violation", data);
|
|
749
|
+
this.name = "UniqueViolation";
|
|
750
|
+
this.constraint = constraint;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
640
753
|
|
|
641
754
|
// src/engine/config.ts
|
|
642
755
|
var BootRefused = class extends Error {
|
|
@@ -954,6 +1067,14 @@ function makeMemoryCache(opts = {}) {
|
|
|
954
1067
|
function quoteIdent(name) {
|
|
955
1068
|
return `"${name.replace(/"/g, '""')}"`;
|
|
956
1069
|
}
|
|
1070
|
+
function encodePgArray(value) {
|
|
1071
|
+
const element = (x) => {
|
|
1072
|
+
if (x === null || x === void 0) return "NULL";
|
|
1073
|
+
if (Array.isArray(x)) return encodePgArray(x);
|
|
1074
|
+
return `"${String(x).replace(/(["\\])/g, "\\$1")}"`;
|
|
1075
|
+
};
|
|
1076
|
+
return `{${value.map(element).join(",")}}`;
|
|
1077
|
+
}
|
|
957
1078
|
var BIND_SQL = "select set_config('role',$1,true), set_config('search_path','public',true), set_config('request.jwt.claims',$2,true)";
|
|
958
1079
|
function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
959
1080
|
const { lockTimeout } = options;
|
|
@@ -991,9 +1112,9 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
991
1112
|
release();
|
|
992
1113
|
await settled;
|
|
993
1114
|
},
|
|
994
|
-
async rollback(
|
|
1115
|
+
async rollback(reason2) {
|
|
995
1116
|
if (!opening) return;
|
|
996
|
-
fail(
|
|
1117
|
+
fail(reason2);
|
|
997
1118
|
await settled?.catch(() => void 0);
|
|
998
1119
|
}
|
|
999
1120
|
};
|
|
@@ -1036,18 +1157,48 @@ function reviveVectors(row, vectorCols) {
|
|
|
1036
1157
|
}
|
|
1037
1158
|
return row;
|
|
1038
1159
|
}
|
|
1160
|
+
function transformsOf(schema, table) {
|
|
1161
|
+
const out = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const [key, def] of Object.entries(schema.tables ?? {})) {
|
|
1163
|
+
if ((def.name ?? key) !== table) continue;
|
|
1164
|
+
for (const [col, c] of Object.entries(def.columns ?? {})) {
|
|
1165
|
+
const d = c !== null && typeof c === "object" && "_def" in c ? c._def : c;
|
|
1166
|
+
const t = d?.transform;
|
|
1167
|
+
if (t !== null && typeof t === "object") {
|
|
1168
|
+
out.set(col, t);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return out;
|
|
1173
|
+
}
|
|
1174
|
+
function applyFromDb(row, transforms) {
|
|
1175
|
+
if (transforms.size === 0) return row;
|
|
1176
|
+
for (const [col, t] of transforms) {
|
|
1177
|
+
if (t.fromDb === void 0) continue;
|
|
1178
|
+
const v = row[col];
|
|
1179
|
+
if (v === null || v === void 0) continue;
|
|
1180
|
+
row[col] = t.fromDb(v);
|
|
1181
|
+
}
|
|
1182
|
+
return row;
|
|
1183
|
+
}
|
|
1039
1184
|
function asTableRow(table, row) {
|
|
1040
|
-
|
|
1185
|
+
const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
|
|
1186
|
+
return applyFromDb(revived, transformsOf(currentSchema, table));
|
|
1041
1187
|
}
|
|
1042
1188
|
function asTableRows(table, rows) {
|
|
1043
1189
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1044
|
-
|
|
1190
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1191
|
+
return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
|
|
1045
1192
|
}
|
|
1046
1193
|
function asBindParams(table, cols, data) {
|
|
1047
1194
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
1195
|
+
const transforms = transformsOf(currentSchema, table);
|
|
1048
1196
|
return cols.map((c) => {
|
|
1049
1197
|
const v = data[c];
|
|
1050
|
-
|
|
1198
|
+
if (Array.isArray(v) && vectorCols.has(c)) return toVectorLiteral(v);
|
|
1199
|
+
const t = transforms.get(c);
|
|
1200
|
+
if (t?.toDb !== void 0 && v !== null && v !== void 0) return t.toDb(v);
|
|
1201
|
+
return v;
|
|
1051
1202
|
});
|
|
1052
1203
|
}
|
|
1053
1204
|
var SELECTIVITY_EXACT_THRESHOLD = 1e4;
|
|
@@ -1174,7 +1325,9 @@ function expandSynonyms(query, map) {
|
|
|
1174
1325
|
}).join("");
|
|
1175
1326
|
}).join("");
|
|
1176
1327
|
}
|
|
1177
|
-
var searchLogger =
|
|
1328
|
+
var searchLogger = (evt, fields) => {
|
|
1329
|
+
console.log(evt, JSON.stringify(fields));
|
|
1330
|
+
};
|
|
1178
1331
|
function logNoResults(table, query, mode, n) {
|
|
1179
1332
|
if (n === 0) {
|
|
1180
1333
|
searchLogger?.("palbase.search.no_results", {
|
|
@@ -1215,19 +1368,55 @@ function validitySql(v, add) {
|
|
|
1215
1368
|
function excludeSql(pk, ids, add) {
|
|
1216
1369
|
if (ids.length === 0) return "";
|
|
1217
1370
|
if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
|
|
1218
|
-
return ` AND t.${quoteIdent(pk)} <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
1371
|
+
return ` AND t.${quoteIdent(pk)}::text <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
1372
|
+
}
|
|
1373
|
+
function schemaColumns(table) {
|
|
1374
|
+
const t = Object.values(currentSchema.tables ?? {}).find((x) => x?.name === table);
|
|
1375
|
+
const cols = t?.columns ? Object.keys(t.columns) : [];
|
|
1376
|
+
return cols.length > 0 ? new Set(cols) : null;
|
|
1377
|
+
}
|
|
1378
|
+
function orderClause(table, known, orderBy) {
|
|
1379
|
+
if (!orderBy) return "";
|
|
1380
|
+
const col = orderBy.column;
|
|
1381
|
+
const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(col);
|
|
1382
|
+
if (known !== null ? !known.has(col) : !shapeOK) {
|
|
1383
|
+
throw new Error(`findMany(${table}): orderBy kolonu "${col}" tabloda yok`);
|
|
1384
|
+
}
|
|
1385
|
+
const dir = orderBy.direction === "desc" ? "DESC" : "ASC";
|
|
1386
|
+
return ` ORDER BY ${quoteIdent(col)} ${dir}`;
|
|
1387
|
+
}
|
|
1388
|
+
function limitClause(limit) {
|
|
1389
|
+
if (limit === void 0) return "";
|
|
1390
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
1391
|
+
throw new Error(`findMany: limit bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(limit)})`);
|
|
1392
|
+
}
|
|
1393
|
+
return ` LIMIT ${limit}`;
|
|
1219
1394
|
}
|
|
1220
|
-
function
|
|
1395
|
+
function offsetClause(offset, limit) {
|
|
1396
|
+
if (offset === void 0) return "";
|
|
1397
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`findMany: offset bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(offset)})`
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
if (limit === void 0) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
"findMany: offset yaln\u0131z limit ile birlikte verilir \u2014 limitsiz offset bir sayfa de\u011Fil, s\u0131n\u0131rs\u0131z bir kuyru\u011Fun kayd\u0131r\u0131lm\u0131\u015F\u0131d\u0131r"
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
return ` OFFSET ${offset}`;
|
|
1408
|
+
}
|
|
1409
|
+
function compileWhere(table, colSet, where, add, caller = "search") {
|
|
1221
1410
|
const parts = [];
|
|
1222
1411
|
for (const [col, cond] of Object.entries(where)) {
|
|
1223
|
-
if (!colSet.has(col)) {
|
|
1224
|
-
throw new Error(
|
|
1412
|
+
if (colSet !== null && !colSet.has(col)) {
|
|
1413
|
+
throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
1225
1414
|
}
|
|
1226
1415
|
const q = `t.${quoteIdent(col)}`;
|
|
1227
1416
|
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
1228
1417
|
for (const [op, v] of Object.entries(cond)) {
|
|
1229
1418
|
if (op === "in") {
|
|
1230
|
-
if (!Array.isArray(v)) throw new Error(
|
|
1419
|
+
if (!Array.isArray(v)) throw new Error(`${caller}(${table}): where.${col}.in bir dizi olmal\u0131`);
|
|
1231
1420
|
if (v.length === 0) {
|
|
1232
1421
|
parts.push("false");
|
|
1233
1422
|
continue;
|
|
@@ -1236,7 +1425,7 @@ function compileWhere(table, colSet, where, add) {
|
|
|
1236
1425
|
} else if (op in WHERE_OPS) {
|
|
1237
1426
|
parts.push(`${q} ${WHERE_OPS[op]} ${add(v)}`);
|
|
1238
1427
|
} else {
|
|
1239
|
-
throw new Error(
|
|
1428
|
+
throw new Error(`${caller}(${table}): where.${col} bilinmeyen operat\xF6r "${op}" (gt/gte/lt/lte/neq/in)`);
|
|
1240
1429
|
}
|
|
1241
1430
|
}
|
|
1242
1431
|
} else {
|
|
@@ -1245,6 +1434,13 @@ function compileWhere(table, colSet, where, add) {
|
|
|
1245
1434
|
}
|
|
1246
1435
|
return parts.length === 0 ? "" : ` AND ${parts.join(" AND ")}`;
|
|
1247
1436
|
}
|
|
1437
|
+
function onConflictTail(cols, conflict, action) {
|
|
1438
|
+
const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
|
|
1439
|
+
if (action === "ignore") return `${target} DO NOTHING`;
|
|
1440
|
+
const conflictSet = new Set(conflict);
|
|
1441
|
+
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1442
|
+
return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1443
|
+
}
|
|
1248
1444
|
var cachedVectorSchema = null;
|
|
1249
1445
|
var cachedTrgmSchema = null;
|
|
1250
1446
|
async function trgmSchemaOf(live) {
|
|
@@ -1316,7 +1512,8 @@ function createOps(tx) {
|
|
|
1316
1512
|
const at = () => resolveTx(tx);
|
|
1317
1513
|
const ops = {
|
|
1318
1514
|
async query(sql, params = []) {
|
|
1319
|
-
|
|
1515
|
+
const bound = params.map((p) => Array.isArray(p) ? encodePgArray(p) : p);
|
|
1516
|
+
return asWireRows(await (await at()).unsafe(sql, bound));
|
|
1320
1517
|
},
|
|
1321
1518
|
async insert(table, data) {
|
|
1322
1519
|
const cols = Object.keys(data);
|
|
@@ -1332,6 +1529,74 @@ function createOps(tx) {
|
|
|
1332
1529
|
}
|
|
1333
1530
|
return asTableRow(table, inserted);
|
|
1334
1531
|
},
|
|
1532
|
+
/**
|
|
1533
|
+
* INSERT the row, or UPDATE it when it collides on `onConflict`.
|
|
1534
|
+
*
|
|
1535
|
+
* WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
|
|
1536
|
+
* unique violation, update instead" does not work here: a request runs in ONE
|
|
1537
|
+
* Postgres transaction, so the failed insert aborts it and every later
|
|
1538
|
+
* statement answers `current transaction is aborted`. A tenant measured that
|
|
1539
|
+
* as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
|
|
1540
|
+
* trigger create the row instead — a workaround that needs a new trigger for
|
|
1541
|
+
* every table with a unique row.
|
|
1542
|
+
*
|
|
1543
|
+
* The conflict columns are excluded from the SET list: they are what MATCHED,
|
|
1544
|
+
* so writing them back is at best a no-op and at worst a surprise.
|
|
1545
|
+
*/
|
|
1546
|
+
async upsert(table, data, opts) {
|
|
1547
|
+
const conflict = opts.onConflict;
|
|
1548
|
+
if (conflict.length === 0) {
|
|
1549
|
+
throw new Error(`upsert into ${table}: onConflict en az bir kolon ad\u0131 ister`);
|
|
1550
|
+
}
|
|
1551
|
+
const cols = Object.keys(data);
|
|
1552
|
+
if (cols.length === 0) throw new Error(`upsert into ${table}: no columns given`);
|
|
1553
|
+
if (searchConfigFor(table)?.validity) {
|
|
1554
|
+
const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1555
|
+
const insertSql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING *`;
|
|
1556
|
+
const live = await at();
|
|
1557
|
+
const bindData = asBindParams(table, cols, data);
|
|
1558
|
+
const first = await live.unsafe(insertSql, bindData);
|
|
1559
|
+
if (first[0]) return asTableRow(table, first[0]);
|
|
1560
|
+
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
1561
|
+
const condBind = conflict.map((c) => data[c]);
|
|
1562
|
+
const cur = await live.unsafe(
|
|
1563
|
+
`SELECT * FROM ${quoteIdent(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
|
|
1564
|
+
condBind
|
|
1565
|
+
);
|
|
1566
|
+
const old = cur[0];
|
|
1567
|
+
if (old === void 0) {
|
|
1568
|
+
throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 ge\xE7erli sat\u0131r bulunamad\u0131, iste\u011Fi yineleyin (D-020)`);
|
|
1569
|
+
}
|
|
1570
|
+
const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
|
|
1571
|
+
await live.unsafe(
|
|
1572
|
+
`UPDATE ${quoteIdent(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`,
|
|
1573
|
+
[old[pk]]
|
|
1574
|
+
);
|
|
1575
|
+
const second = await live.unsafe(insertSql, bindData);
|
|
1576
|
+
if (!second[0]) {
|
|
1577
|
+
throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 yeni sat\u0131r a\xE7\u0131lamad\u0131, iste\u011Fi yineleyin (D-020)`);
|
|
1578
|
+
}
|
|
1579
|
+
const fresh = second[0];
|
|
1580
|
+
await live.unsafe(
|
|
1581
|
+
`UPDATE ${quoteIdent(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`,
|
|
1582
|
+
[old[pk], fresh[pk]]
|
|
1583
|
+
);
|
|
1584
|
+
return asTableRow(table, second[0]);
|
|
1585
|
+
}
|
|
1586
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1587
|
+
const conflictSet = new Set(conflict);
|
|
1588
|
+
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
1589
|
+
const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
1590
|
+
const sql = `INSERT INTO ${quoteIdent(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
1591
|
+
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data));
|
|
1592
|
+
const row = rows[0];
|
|
1593
|
+
if (!row) {
|
|
1594
|
+
throw new Error(
|
|
1595
|
+
`upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
return asTableRow(table, row);
|
|
1599
|
+
},
|
|
1335
1600
|
async update(table, id, data) {
|
|
1336
1601
|
const cols = Object.keys(data);
|
|
1337
1602
|
if (cols.length === 0) return ops.findById(table, id);
|
|
@@ -1343,6 +1608,86 @@ function createOps(tx) {
|
|
|
1343
1608
|
async delete(table, id) {
|
|
1344
1609
|
await (await at()).unsafe(`DELETE FROM ${quoteIdent(table)} WHERE id = $1`, [id]);
|
|
1345
1610
|
},
|
|
1611
|
+
/**
|
|
1612
|
+
* Update every row the filter matches, in ONE statement.
|
|
1613
|
+
*
|
|
1614
|
+
* The capability was already here and only reachable from inside a
|
|
1615
|
+
* transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
|
|
1616
|
+
* only door was `update(id, …)`, so "mark every unapproved row in this
|
|
1617
|
+
* household" was an N+1 loop or hand-written SQL — and hand-written SQL is
|
|
1618
|
+
* where the typed surface and RLS both stop helping.
|
|
1619
|
+
*
|
|
1620
|
+
* The filter language is `findMany`'s, compiled by the same `compileWhere`:
|
|
1621
|
+
* one filter language, or the two spellings drift.
|
|
1622
|
+
*
|
|
1623
|
+
* AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
|
|
1624
|
+
* and the shape that produces it by accident — a filter object built from
|
|
1625
|
+
* request input that happened to come back empty — is exactly the shape that
|
|
1626
|
+
* should not silently succeed. Callers who mean every row say so with a
|
|
1627
|
+
* predicate that is true for every row.
|
|
1628
|
+
*/
|
|
1629
|
+
async updateMany(table, where, set) {
|
|
1630
|
+
const cols = Object.keys(set);
|
|
1631
|
+
if (cols.length === 0) return [];
|
|
1632
|
+
if (Object.keys(where).length === 0) {
|
|
1633
|
+
throw new Error(
|
|
1634
|
+
`updateMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 g\xFCncellerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const params = [];
|
|
1638
|
+
const add = (v) => {
|
|
1639
|
+
params.push(v);
|
|
1640
|
+
return `$${params.length}`;
|
|
1641
|
+
};
|
|
1642
|
+
const assignments = cols.map((c) => `${quoteIdent(c)} = ${add(asBindParams(table, [c], set)[0])}`).join(", ");
|
|
1643
|
+
const known = schemaColumns(table);
|
|
1644
|
+
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
1645
|
+
const sql = `UPDATE ${quoteIdent(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *`;
|
|
1646
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1647
|
+
},
|
|
1648
|
+
/**
|
|
1649
|
+
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
1650
|
+
* many went. Same filter language, same empty-filter refusal as
|
|
1651
|
+
* {@link updateMany} — and here the accident is worse.
|
|
1652
|
+
*/
|
|
1653
|
+
async deleteMany(table, where) {
|
|
1654
|
+
if (Object.keys(where).length === 0) {
|
|
1655
|
+
throw new Error(
|
|
1656
|
+
`deleteMany(${table}): bo\u015F filtre T\xDCM sat\u0131rlar\u0131 silerdi \u2014 bir filtre verin, ya da her sat\u0131ra uyan bir ko\u015Ful yaz\u0131n`
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const params = [];
|
|
1660
|
+
const add = (v) => {
|
|
1661
|
+
params.push(v);
|
|
1662
|
+
return `$${params.length}`;
|
|
1663
|
+
};
|
|
1664
|
+
const known = schemaColumns(table);
|
|
1665
|
+
const whereSql = compileWhere(table, known, where, add, "deleteMany");
|
|
1666
|
+
const sql = `DELETE FROM ${quoteIdent(table)} AS t WHERE true${whereSql} RETURNING 1`;
|
|
1667
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1668
|
+
return rows.length;
|
|
1669
|
+
},
|
|
1670
|
+
/**
|
|
1671
|
+
* How many rows match — the half of pagination `limit`/`offset` cannot
|
|
1672
|
+
* supply. Without it a page count is either a guess or "fetch everything and
|
|
1673
|
+
* read .length", and the second one is the scale risk this surface exists to
|
|
1674
|
+
* remove.
|
|
1675
|
+
*
|
|
1676
|
+
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
1677
|
+
* reads do not destroy anything.
|
|
1678
|
+
*/
|
|
1679
|
+
async count(table, where = {}) {
|
|
1680
|
+
const params = [];
|
|
1681
|
+
const add = (v) => {
|
|
1682
|
+
params.push(v);
|
|
1683
|
+
return `$${params.length}`;
|
|
1684
|
+
};
|
|
1685
|
+
const known = schemaColumns(table);
|
|
1686
|
+
const whereSql = compileWhere(table, known, where, add, "count");
|
|
1687
|
+
const sql = `SELECT count(*) AS n FROM ${quoteIdent(table)} t WHERE true${whereSql}`;
|
|
1688
|
+
const rows = await (await at()).unsafe(sql, params);
|
|
1689
|
+
return Number(rows[0]?.n ?? 0);
|
|
1690
|
+
},
|
|
1346
1691
|
async findById(table, id) {
|
|
1347
1692
|
const rows = await (await at()).unsafe(
|
|
1348
1693
|
`SELECT * FROM ${quoteIdent(table)} WHERE id = $1`,
|
|
@@ -1350,13 +1695,19 @@ function createOps(tx) {
|
|
|
1350
1695
|
);
|
|
1351
1696
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
1352
1697
|
},
|
|
1353
|
-
async findMany(table, query = {}) {
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
)
|
|
1698
|
+
async findMany(table, query = {}, opts = {}) {
|
|
1699
|
+
const params = [];
|
|
1700
|
+
const add = (v) => {
|
|
1701
|
+
params.push(v);
|
|
1702
|
+
return `$${params.length}`;
|
|
1703
|
+
};
|
|
1704
|
+
const known = schemaColumns(table);
|
|
1705
|
+
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
1706
|
+
const order = orderClause(table, known, opts.orderBy);
|
|
1707
|
+
const limit = limitClause(opts.limit);
|
|
1708
|
+
const offset = offsetClause(opts.offset, opts.limit);
|
|
1709
|
+
const sql = `SELECT * FROM ${quoteIdent(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
|
|
1710
|
+
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1360
1711
|
},
|
|
1361
1712
|
/**
|
|
1362
1713
|
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
@@ -1438,7 +1789,9 @@ function createOps(tx) {
|
|
|
1438
1789
|
bindC.push(v);
|
|
1439
1790
|
return `$${bindC.length}`;
|
|
1440
1791
|
};
|
|
1441
|
-
const
|
|
1792
|
+
const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
|
|
1793
|
+
const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1794
|
+
const parentJoinC = ` JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
|
|
1442
1795
|
const liveC = await at();
|
|
1443
1796
|
const K2 = 50;
|
|
1444
1797
|
const cpool = Math.max(limit * 9, 90);
|
|
@@ -1447,8 +1800,19 @@ function createOps(tx) {
|
|
|
1447
1800
|
if (qvC !== null) {
|
|
1448
1801
|
const sch = await vectorSchemaWithGuc(liveC);
|
|
1449
1802
|
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1803
|
+
let exactOrderC = "";
|
|
1804
|
+
if (userWhereC !== "") {
|
|
1805
|
+
const probeRows = await liveC.unsafe(
|
|
1806
|
+
`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
|
|
1807
|
+
bindC.slice()
|
|
1808
|
+
);
|
|
1809
|
+
const n = probeRows?.[0]?.n;
|
|
1810
|
+
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1811
|
+
exactOrderC = " + 0.0";
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1450
1814
|
const vp = addC(toVectorLiteral(qvC));
|
|
1451
|
-
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)) AS r FROM ${quoteIdent(ck.table)} c
|
|
1815
|
+
semC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY (c.embedding OPERATOR(${quoteIdent(sch)}.${op}) ${vp}::${quoteIdent(sch)}.vector)${exactOrderC}) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.embedding IS NOT NULL ORDER BY r LIMIT ${cpool}`;
|
|
1452
1816
|
}
|
|
1453
1817
|
let qpC = "";
|
|
1454
1818
|
let qpCIdx = -1;
|
|
@@ -1456,7 +1820,7 @@ function createOps(tx) {
|
|
|
1456
1820
|
const qTextC = cfg.synonyms !== void 0 ? expandSynonyms(params.query, cfg.synonyms) : params.query;
|
|
1457
1821
|
qpC = addC(qTextC);
|
|
1458
1822
|
qpCIdx = bindC.length - 1;
|
|
1459
|
-
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c
|
|
1823
|
+
kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
|
|
1460
1824
|
}
|
|
1461
1825
|
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
1462
1826
|
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
@@ -1471,13 +1835,13 @@ function createOps(tx) {
|
|
|
1471
1835
|
} else {
|
|
1472
1836
|
fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
|
|
1473
1837
|
}
|
|
1474
|
-
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid
|
|
1838
|
+
const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteIdent(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteIdent(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid`;
|
|
1475
1839
|
return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
1476
1840
|
};
|
|
1477
1841
|
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
1478
1842
|
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
1479
1843
|
const tsch = await trgmSchemaOf(liveC);
|
|
1480
|
-
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c
|
|
1844
|
+
const trgmKwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) DESC) AS r FROM ${quoteIdent(ck.table)} c${parentJoinC}${quoteIdent(tsch)}.word_similarity(${qpC}, coalesce(c.content,'')) > 0.3 ORDER BY r LIMIT ${cpool}`;
|
|
1481
1845
|
const retryBindC = bindC.slice();
|
|
1482
1846
|
if (qpCIdx >= 0) retryBindC[qpCIdx] = params.query;
|
|
1483
1847
|
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
@@ -1573,7 +1937,7 @@ function createOps(tx) {
|
|
|
1573
1937
|
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
1574
1938
|
const tsch = await trgmSchemaOf(live);
|
|
1575
1939
|
const expr = ftsExprOf(cfg.ftsCols);
|
|
1576
|
-
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${
|
|
1940
|
+
const trgmKw = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) DESC) AS r FROM ${quoteIdent(table)} t WHERE ${quoteIdent(tsch)}.word_similarity(${qpRef}, ${expr}) > 0.3${whereSql} ORDER BY r LIMIT ${pool}`;
|
|
1577
1941
|
const retryBind = bind.slice();
|
|
1578
1942
|
if (qpIdx >= 0) retryBind[qpIdx] = params.query;
|
|
1579
1943
|
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
@@ -1596,6 +1960,25 @@ function createOps(tx) {
|
|
|
1596
1960
|
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
1597
1961
|
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
1598
1962
|
*/
|
|
1963
|
+
/** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
|
|
1964
|
+
* `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
|
|
1965
|
+
* yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
|
|
1966
|
+
* search'teki alan geriye-uyum için DURUR. where + validity default'u
|
|
1967
|
+
* sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
|
|
1968
|
+
async facets(table, params) {
|
|
1969
|
+
const cfg = searchConfigFor(table);
|
|
1970
|
+
if (!cfg) {
|
|
1971
|
+
throw new Error(`facets(${table}): tablo aranabilir de\u011Fil \u2014 search beyan\u0131 yok (FR-027)`);
|
|
1972
|
+
}
|
|
1973
|
+
for (const col of params.facets) {
|
|
1974
|
+
if (!cfg.colSet.has(col)) {
|
|
1975
|
+
throw new Error(`facets(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const live = await at();
|
|
1979
|
+
const extra = cfg.validity ? (add) => validitySql(params.validity, add) : () => "";
|
|
1980
|
+
return fetchFacets(live, table, cfg.colSet, params.facets, params.where ?? {}, extra);
|
|
1981
|
+
},
|
|
1599
1982
|
async similar(table, id, opts = {}) {
|
|
1600
1983
|
const cfg = searchConfigFor(table);
|
|
1601
1984
|
if (!cfg) {
|
|
@@ -1747,6 +2130,27 @@ function createOps(tx) {
|
|
|
1747
2130
|
const live = await at();
|
|
1748
2131
|
return live.savepoint(async (sp) => cb(withTables(createOps(sp), currentSchema)));
|
|
1749
2132
|
},
|
|
2133
|
+
/**
|
|
2134
|
+
* Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
|
|
2135
|
+
* rolls back only what that handle wrote and the request can keep writing.
|
|
2136
|
+
*
|
|
2137
|
+
* WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
|
|
2138
|
+
* ... Database.insert(...) ... })`, with no parameter — would have to point
|
|
2139
|
+
* the ambient `Database` at the savepoint for the duration, and a request is
|
|
2140
|
+
* concurrent with itself: `Promise.all([Database.insert(a),
|
|
2141
|
+
* Database.attempt(...)])` would put `a` inside the savepoint and roll it
|
|
2142
|
+
* back with it. Silent data loss, and the same interleaving this file already
|
|
2143
|
+
* refuses for `asService()`. Passing the handle makes the boundary something
|
|
2144
|
+
* you can see in the code that crosses it.
|
|
2145
|
+
*
|
|
2146
|
+
* Postgres, not us: the savepoint is released on success and rolled back on
|
|
2147
|
+
* failure by the driver, so an aborted statement inside `fn` does not poison
|
|
2148
|
+
* the surrounding transaction.
|
|
2149
|
+
*/
|
|
2150
|
+
async attempt(fn) {
|
|
2151
|
+
const live = await at();
|
|
2152
|
+
return live.savepoint(async (sp) => fn(withTables(createOps(sp), currentSchema)));
|
|
2153
|
+
},
|
|
1750
2154
|
/**
|
|
1751
2155
|
* Execute a whole transaction plan — what `Database.transaction(fn)` builds.
|
|
1752
2156
|
*
|
|
@@ -1797,22 +2201,68 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1797
2201
|
update: (id, data) => ops.update(name, id, data),
|
|
1798
2202
|
delete: (id) => ops.delete(name, id),
|
|
1799
2203
|
findById: (id) => ops.findById(name, id),
|
|
1800
|
-
findMany: (query) => ops.findMany(name, query ?? {})
|
|
2204
|
+
findMany: (query, opts) => ops.findMany(name, query ?? {}, opts),
|
|
2205
|
+
upsert: (data, opts) => ops.upsert(name, data, opts)
|
|
1801
2206
|
};
|
|
1802
2207
|
}
|
|
1803
2208
|
const base = /* @__PURE__ */ Object.create(null);
|
|
1804
2209
|
return Object.assign(base, ops, { tables });
|
|
1805
2210
|
}
|
|
1806
2211
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1807
|
-
function
|
|
1808
|
-
const code = e?.code;
|
|
2212
|
+
function isAbortedTransaction(e) {
|
|
1809
2213
|
const message = String(e?.message ?? "");
|
|
1810
|
-
return
|
|
2214
|
+
return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
|
|
2215
|
+
}
|
|
2216
|
+
function sqlstateOf(e) {
|
|
2217
|
+
const err = e;
|
|
2218
|
+
const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
|
|
2219
|
+
if (isState(err?.code)) return err.code;
|
|
2220
|
+
if (isState(err?.errno)) return err.errno;
|
|
2221
|
+
if (typeof err?.errno === "number") {
|
|
2222
|
+
const asText = String(err.errno);
|
|
2223
|
+
if (isState(asText)) return asText;
|
|
2224
|
+
}
|
|
2225
|
+
return void 0;
|
|
1811
2226
|
}
|
|
1812
|
-
function
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2227
|
+
function isForeignKeyViolation(e) {
|
|
2228
|
+
return sqlstateOf(e) === "23503";
|
|
2229
|
+
}
|
|
2230
|
+
function isUniqueViolation(e) {
|
|
2231
|
+
return sqlstateOf(e) === "23505";
|
|
2232
|
+
}
|
|
2233
|
+
function constraintOf(e) {
|
|
2234
|
+
const named = e?.constraint;
|
|
2235
|
+
if (typeof named === "string" && named.length > 0) return named;
|
|
2236
|
+
const message = String(e?.message ?? "");
|
|
2237
|
+
return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
|
|
2238
|
+
}
|
|
2239
|
+
function isLockTimeout(e) {
|
|
2240
|
+
const message = String(e?.message ?? "");
|
|
2241
|
+
return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
|
|
2242
|
+
}
|
|
2243
|
+
function diagnosingDriver(sql, surface = "service") {
|
|
2244
|
+
const original = (e) => String(e?.message ?? e);
|
|
2245
|
+
const explain = (e) => {
|
|
2246
|
+
if (isAbortedTransaction(e)) {
|
|
2247
|
+
return new Error(
|
|
2248
|
+
`This request cannot write any more: an earlier write in it failed, and a request runs in ONE Postgres transaction, so every statement after the failure is refused. Nothing here is retryable in place. Wrap a write you expect to fail in \`Database.attempt(async (tx) => \u2026)\` \u2014 it takes a SAVEPOINT, so only that write rolls back \u2014 or use \`Database.tables.<t>.upsert(data, { onConflict: [...] })\` when the failure you were bracing for is a duplicate row. (${original(e)})`
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
if (isUniqueViolation(e)) {
|
|
2252
|
+
return new UniqueViolation(constraintOf(e));
|
|
2253
|
+
}
|
|
2254
|
+
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
2255
|
+
return new Error(
|
|
2256
|
+
`Database.asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
if (isLockTimeout(e)) {
|
|
2260
|
+
return new Error(
|
|
2261
|
+
`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)})`
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
return e;
|
|
2265
|
+
};
|
|
1816
2266
|
const wrapTx = (tx) => ({
|
|
1817
2267
|
async unsafe(text, params) {
|
|
1818
2268
|
try {
|
|
@@ -1831,13 +2281,13 @@ function diagnosingDriver(sql) {
|
|
|
1831
2281
|
};
|
|
1832
2282
|
}
|
|
1833
2283
|
function createRequestDatabase(sql, identity) {
|
|
1834
|
-
const tx = createLazyTransaction(sql, identity.role, identity.claimsJson);
|
|
2284
|
+
const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson);
|
|
1835
2285
|
let serviceTx = null;
|
|
1836
2286
|
let serviceClient = null;
|
|
1837
2287
|
const asService = () => {
|
|
1838
2288
|
if (serviceClient === null) {
|
|
1839
2289
|
serviceTx = createLazyTransaction(
|
|
1840
|
-
diagnosingDriver(sql),
|
|
2290
|
+
diagnosingDriver(sql, "service"),
|
|
1841
2291
|
identity.serviceRole,
|
|
1842
2292
|
identity.claimsJson,
|
|
1843
2293
|
{ lockTimeout: SERVICE_LOCK_TIMEOUT }
|
|
@@ -1852,9 +2302,9 @@ function createRequestDatabase(sql, identity) {
|
|
|
1852
2302
|
await tx.commit();
|
|
1853
2303
|
await serviceTx?.commit();
|
|
1854
2304
|
},
|
|
1855
|
-
async rollback(
|
|
1856
|
-
await tx.rollback(
|
|
1857
|
-
await serviceTx?.rollback(
|
|
2305
|
+
async rollback(reason2) {
|
|
2306
|
+
await tx.rollback(reason2);
|
|
2307
|
+
await serviceTx?.rollback(reason2);
|
|
1858
2308
|
}
|
|
1859
2309
|
};
|
|
1860
2310
|
}
|
|
@@ -1918,6 +2368,20 @@ async function runPlanOp(sp, op, results) {
|
|
|
1918
2368
|
sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
|
|
1919
2369
|
break;
|
|
1920
2370
|
}
|
|
2371
|
+
case "upsert": {
|
|
2372
|
+
const cols = Object.keys(op.values ?? {});
|
|
2373
|
+
const conflict = op.onConflict ?? [];
|
|
2374
|
+
if (cols.length === 0 || conflict.length === 0) {
|
|
2375
|
+
throw Object.assign(new Error(`upsert on ${op.table} needs columns and onConflict`), {
|
|
2376
|
+
error_code: "tx_bad_upsert"
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
const rendered = cols.map(
|
|
2380
|
+
(c) => renderValue(op.values[c], c, args, results, opVectorCols)
|
|
2381
|
+
);
|
|
2382
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
|
|
2383
|
+
break;
|
|
2384
|
+
}
|
|
1921
2385
|
case "insertMany": {
|
|
1922
2386
|
const rows = op.rows ?? [];
|
|
1923
2387
|
if (rows.length === 0 || !rows[0]) return [];
|
|
@@ -1925,7 +2389,9 @@ async function runPlanOp(sp, op, results) {
|
|
|
1925
2389
|
const tuples = rows.map(
|
|
1926
2390
|
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols)).join(", ")})`
|
|
1927
2391
|
);
|
|
1928
|
-
|
|
2392
|
+
const conflictCols = op.onConflict ?? [];
|
|
2393
|
+
const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
|
|
2394
|
+
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
1929
2395
|
break;
|
|
1930
2396
|
}
|
|
1931
2397
|
case "update": {
|
|
@@ -2000,6 +2466,26 @@ function getRoutes(ctor) {
|
|
|
2000
2466
|
}));
|
|
2001
2467
|
}
|
|
2002
2468
|
|
|
2469
|
+
// src/decorators/controller.ts
|
|
2470
|
+
var APP_DEFAULT_AUTH = /* @__PURE__ */ Symbol.for("palbase.backend.appDefaultAuth");
|
|
2471
|
+
function appAuthSlot() {
|
|
2472
|
+
return globalThis;
|
|
2473
|
+
}
|
|
2474
|
+
function getDefaultAuth() {
|
|
2475
|
+
return appAuthSlot()[APP_DEFAULT_AUTH];
|
|
2476
|
+
}
|
|
2477
|
+
function resolveEffectiveAuth(routeAuth, controllerAuth) {
|
|
2478
|
+
return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;
|
|
2479
|
+
}
|
|
2480
|
+
function assertZeroArgConstructor(Ctrl, kind) {
|
|
2481
|
+
const arity = Ctrl.length ?? 0;
|
|
2482
|
+
if (arity === 0) return;
|
|
2483
|
+
const name = Ctrl.name ?? "<anonymous>";
|
|
2484
|
+
throw new Error(
|
|
2485
|
+
`${kind} ${name} declares a constructor with ${arity} parameter(s). A ${kind} is constructed by the runtime with a zero-argument constructor \u2014 there is no injector to supply them, so every parameter would arrive as undefined. Hold the dependency as a module-level singleton the ${kind} imports (\`const repo = makeRepo()\` beside the class), and construct the service directly in tests (e.g. \`new TodoService(fakeDatabase().db)\`).`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2003
2489
|
// src/engine/router.ts
|
|
2004
2490
|
var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
|
|
2005
2491
|
function toSegments(path) {
|
|
@@ -2018,6 +2504,7 @@ function buildRouteTable(controllers) {
|
|
|
2018
2504
|
`controller ${name} collected zero routes. Either it declares no @Get/@Post/\u2026 , or its decorator metadata was erased at build time \u2014 check that the bundle was compiled with experimentalDecorators enabled.`
|
|
2019
2505
|
);
|
|
2020
2506
|
}
|
|
2507
|
+
assertZeroArgConstructor(Ctrl, "controller");
|
|
2021
2508
|
const instance = new ctor();
|
|
2022
2509
|
for (const r of routes) {
|
|
2023
2510
|
const full = `${basePath}${r.subpath ?? ""}` || "/";
|
|
@@ -2027,7 +2514,10 @@ function buildRouteTable(controllers) {
|
|
|
2027
2514
|
meta: r,
|
|
2028
2515
|
instance,
|
|
2029
2516
|
id: `${r.method} ${full}`,
|
|
2030
|
-
|
|
2517
|
+
// The route's OWN spec is deliberately not folded in here: this field
|
|
2518
|
+
// is what applies when the route is silent, and `effectiveAuth` puts
|
|
2519
|
+
// the route level back on top per request.
|
|
2520
|
+
controllerAuth: resolveEffectiveAuth(void 0, meta?.defaultAuth)
|
|
2031
2521
|
});
|
|
2032
2522
|
}
|
|
2033
2523
|
}
|
|
@@ -2114,6 +2604,41 @@ function verifySignature(presented, expected) {
|
|
|
2114
2604
|
return diff === 0;
|
|
2115
2605
|
}
|
|
2116
2606
|
|
|
2607
|
+
// src/engine/sse.ts
|
|
2608
|
+
var SSE_CONTENT_TYPE = "text/event-stream";
|
|
2609
|
+
function encodeFrame(value) {
|
|
2610
|
+
return `data: ${JSON.stringify(value)}
|
|
2611
|
+
|
|
2612
|
+
`;
|
|
2613
|
+
}
|
|
2614
|
+
function encodeErrorFrame(requestId) {
|
|
2615
|
+
return `event: error
|
|
2616
|
+
data: ${JSON.stringify({ requestId })}
|
|
2617
|
+
|
|
2618
|
+
`;
|
|
2619
|
+
}
|
|
2620
|
+
function makeSseWriter(opts) {
|
|
2621
|
+
let begun = false;
|
|
2622
|
+
let chain = Promise.resolve();
|
|
2623
|
+
return {
|
|
2624
|
+
write(value) {
|
|
2625
|
+
const frame = encodeFrame(value);
|
|
2626
|
+
if (!begun) {
|
|
2627
|
+
begun = true;
|
|
2628
|
+
chain = chain.then(() => opts.onFirstWrite()).then(() => opts.enqueue(frame));
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
chain = chain.then(() => opts.enqueue(frame));
|
|
2632
|
+
},
|
|
2633
|
+
started() {
|
|
2634
|
+
return begun;
|
|
2635
|
+
},
|
|
2636
|
+
drained() {
|
|
2637
|
+
return chain;
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2117
2642
|
// src/engine/fence.ts
|
|
2118
2643
|
var SECRET_ENV = [
|
|
2119
2644
|
"DATABASE_URL",
|
|
@@ -2349,6 +2874,13 @@ async function createApp(opts) {
|
|
|
2349
2874
|
const args = [];
|
|
2350
2875
|
let parsedBody;
|
|
2351
2876
|
let bodyRead = false;
|
|
2877
|
+
let sseEnqueue = null;
|
|
2878
|
+
let sseSettle = async () => {
|
|
2879
|
+
};
|
|
2880
|
+
const sseWriter = makeSseWriter({
|
|
2881
|
+
enqueue: (chunk) => sseEnqueue?.(chunk),
|
|
2882
|
+
onFirstWrite: () => sseSettle()
|
|
2883
|
+
});
|
|
2352
2884
|
for (const p of meta.params ?? []) {
|
|
2353
2885
|
switch (p.kind) {
|
|
2354
2886
|
case "body": {
|
|
@@ -2452,6 +2984,21 @@ async function createApp(opts) {
|
|
|
2452
2984
|
case "req":
|
|
2453
2985
|
args[p.index] = req;
|
|
2454
2986
|
break;
|
|
2987
|
+
// `@Signal()` — the raw request's AbortSignal, which enters the aborted
|
|
2988
|
+
// state when the client disconnects. Measured on Bun: an infinite
|
|
2989
|
+
// producer guarded by it stops within a few frames of the client dying.
|
|
2990
|
+
// It is deliberately NOT reachable through `@Req()`: PBRequest carries
|
|
2991
|
+
// request-scoped data and no signal.
|
|
2992
|
+
case "signal":
|
|
2993
|
+
args[p.index] = req.signal;
|
|
2994
|
+
break;
|
|
2995
|
+
// `@SseOut()` — the frame writer. Meaningful only on an `@Sse` route;
|
|
2996
|
+
// on any other route it is an inert writer whose frames go nowhere,
|
|
2997
|
+
// which is the same shape `@UploadedObject()` has off an `@Upload`
|
|
2998
|
+
// route.
|
|
2999
|
+
case "sseOut":
|
|
3000
|
+
args[p.index] = sseWriter;
|
|
3001
|
+
break;
|
|
2455
3002
|
default:
|
|
2456
3003
|
args[p.index] = void 0;
|
|
2457
3004
|
}
|
|
@@ -2463,6 +3010,84 @@ async function createApp(opts) {
|
|
|
2463
3010
|
});
|
|
2464
3011
|
try {
|
|
2465
3012
|
const services = buildServices(db);
|
|
3013
|
+
if (meta.options?.sseConfig !== void 0) {
|
|
3014
|
+
let sseController;
|
|
3015
|
+
const sseEncoder = new TextEncoder();
|
|
3016
|
+
const stream = new ReadableStream({
|
|
3017
|
+
start(c) {
|
|
3018
|
+
sseController = c;
|
|
3019
|
+
}
|
|
3020
|
+
});
|
|
3021
|
+
sseEnqueue = (chunk) => {
|
|
3022
|
+
try {
|
|
3023
|
+
sseController.enqueue(sseEncoder.encode(chunk));
|
|
3024
|
+
} catch {
|
|
3025
|
+
}
|
|
3026
|
+
};
|
|
3027
|
+
let sawFirstFrame;
|
|
3028
|
+
const firstFrame = new Promise((r) => sawFirstFrame = r);
|
|
3029
|
+
sseSettle = async () => {
|
|
3030
|
+
await db.commit();
|
|
3031
|
+
sawFirstFrame();
|
|
3032
|
+
};
|
|
3033
|
+
const guarded = {
|
|
3034
|
+
...services,
|
|
3035
|
+
Database: new Proxy(services.Database, {
|
|
3036
|
+
get(target, prop, recv) {
|
|
3037
|
+
if (sseWriter.started()) {
|
|
3038
|
+
throw new Error(
|
|
3039
|
+
`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.`
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
return Reflect.get(target, prop, recv);
|
|
3043
|
+
}
|
|
3044
|
+
})
|
|
3045
|
+
};
|
|
3046
|
+
let thrown = null;
|
|
3047
|
+
const ran = Promise.resolve(runWithRuntime(guarded, () => {
|
|
3048
|
+
const box = requestALS.getStore();
|
|
3049
|
+
if (box) {
|
|
3050
|
+
box.userId = userId ?? null;
|
|
3051
|
+
box.requestId = requestId;
|
|
3052
|
+
box.idempotencyKey = req.headers.get("idempotency-key");
|
|
3053
|
+
}
|
|
3054
|
+
const method = hit.entry.instance[meta.fnName];
|
|
3055
|
+
if (typeof method !== "function") {
|
|
3056
|
+
throw new Error(
|
|
3057
|
+
`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
return method.apply(hit.entry.instance, args);
|
|
3061
|
+
})).then(
|
|
3062
|
+
() => void 0,
|
|
3063
|
+
(err) => {
|
|
3064
|
+
thrown = err;
|
|
3065
|
+
}
|
|
3066
|
+
);
|
|
3067
|
+
await Promise.race([firstFrame, ran]);
|
|
3068
|
+
if (!sseWriter.started()) {
|
|
3069
|
+
await ran;
|
|
3070
|
+
await db.commit();
|
|
3071
|
+
if (thrown !== null) throw thrown;
|
|
3072
|
+
return new Response(null, { status: 204 });
|
|
3073
|
+
}
|
|
3074
|
+
void (async () => {
|
|
3075
|
+
await ran;
|
|
3076
|
+
await sseWriter.drained();
|
|
3077
|
+
try {
|
|
3078
|
+
if (thrown !== null) {
|
|
3079
|
+
log.error(`[engine] ${hit.entry.id} threw mid-stream`, thrown);
|
|
3080
|
+
sseController.enqueue(sseEncoder.encode(encodeErrorFrame(requestId)));
|
|
3081
|
+
}
|
|
3082
|
+
sseController.close();
|
|
3083
|
+
} catch {
|
|
3084
|
+
}
|
|
3085
|
+
})();
|
|
3086
|
+
return new Response(stream, {
|
|
3087
|
+
status: 200,
|
|
3088
|
+
headers: { "content-type": SSE_CONTENT_TYPE, "cache-control": "no-cache" }
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
2466
3091
|
const result = await runWithRuntime(services, () => {
|
|
2467
3092
|
const box = requestALS.getStore();
|
|
2468
3093
|
if (box) {
|
|
@@ -2521,18 +3146,29 @@ async function createApp(opts) {
|
|
|
2521
3146
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
2522
3147
|
}
|
|
2523
3148
|
}
|
|
3149
|
+
let runShutdownHooks;
|
|
3150
|
+
try {
|
|
3151
|
+
runShutdownHooks = await __runStartHooks();
|
|
3152
|
+
} catch (err) {
|
|
3153
|
+
await closeDriver(sql);
|
|
3154
|
+
throw err;
|
|
3155
|
+
}
|
|
2524
3156
|
return {
|
|
2525
3157
|
handle,
|
|
2526
3158
|
routes,
|
|
2527
3159
|
config,
|
|
2528
3160
|
runInServiceScope,
|
|
2529
3161
|
async shutdown() {
|
|
2530
|
-
|
|
2531
|
-
await
|
|
2532
|
-
await closable.end?.();
|
|
3162
|
+
await runShutdownHooks();
|
|
3163
|
+
await closeDriver(sql);
|
|
2533
3164
|
}
|
|
2534
3165
|
};
|
|
2535
3166
|
}
|
|
3167
|
+
async function closeDriver(sql) {
|
|
3168
|
+
const closable = sql;
|
|
3169
|
+
await closable.close?.();
|
|
3170
|
+
await closable.end?.();
|
|
3171
|
+
}
|
|
2536
3172
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2537
3173
|
0 && (module.exports = {
|
|
2538
3174
|
AuthVerifier,
|