@palbase/backend 25.1.0 → 27.1.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 +2432 -1039
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +87 -51
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/chunk-BQN723PL.js +930 -0
- package/dist/chunk-BQN723PL.js.map +1 -0
- package/dist/chunk-CGNN2PUH.js +213 -0
- package/dist/chunk-CGNN2PUH.js.map +1 -0
- package/dist/{chunk-VDF2T4AS.js → chunk-EB3TUX5J.js} +1228 -591
- package/dist/chunk-EB3TUX5J.js.map +1 -0
- package/dist/chunk-JVZQCC77.js +728 -0
- package/dist/chunk-JVZQCC77.js.map +1 -0
- package/dist/chunk-OZKSM3JW.js +370 -0
- package/dist/chunk-OZKSM3JW.js.map +1 -0
- package/dist/{chunk-YOY5DFQS.js → chunk-TWX6JTGJ.js} +76 -34
- package/dist/{chunk-YOY5DFQS.js.map → chunk-TWX6JTGJ.js.map} +1 -1
- package/dist/{chunk-35PNTIRN.js → chunk-VVMJEVQP.js} +63 -162
- package/dist/chunk-VVMJEVQP.js.map +1 -0
- package/dist/{chunk-7D4SUZUM.js → chunk-VXPNPVAG.js} +3 -1
- package/dist/chunk-XABHGMUT.js +885 -0
- package/dist/chunk-XABHGMUT.js.map +1 -0
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +2 -2
- package/dist/db/env.d.ts +2 -2
- package/dist/db/index.cjs +780 -344
- 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 +7 -4
- package/dist/engine/index.cjs +2366 -1002
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +6 -6
- package/dist/engine/index.d.ts +6 -6
- package/dist/engine/index.js +7 -6
- package/dist/{index-CUomTA3e.d.ts → index-CAKOgAlP.d.ts} +171 -296
- package/dist/index-CgE4sVhg.d.cts +4864 -0
- package/dist/{index-ClpDeSos.d.cts → index-H-0qv5d4.d.cts} +171 -296
- package/dist/index-V7QRh1wg.d.ts +4864 -0
- package/dist/index.cjs +2720 -1169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -19
- package/dist/index.d.ts +165 -19
- package/dist/index.js +738 -477
- package/dist/index.js.map +1 -1
- package/dist/module-Dl1KFVtc.d.cts +54 -0
- package/dist/module-Dl1KFVtc.d.ts +54 -0
- package/dist/openapi/index.cjs +1330 -484
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +4 -2
- package/dist/openapi/index.d.ts +4 -2
- package/dist/openapi/index.js +1264 -474
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-dZZ5JKYg.d.ts → registry-4EI8aaFs.d.ts} +1 -1
- package/dist/{registry-CC0WBQq6.d.cts → registry-DHsPDY0_.d.cts} +1 -1
- package/dist/stack.cjs.map +1 -1
- package/dist/test/index.cjs +732 -141
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +30 -4
- package/dist/test/index.d.ts +30 -4
- package/dist/test/index.js +490 -124
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +33 -18
- package/docs/auth.md +1 -1
- package/docs/background.md +2 -2
- package/docs/database.md +255 -50
- package/docs/endpoints.md +3 -4
- package/docs/events.md +3 -3
- package/docs/getting-started.md +1 -1
- package/docs/llms-full.txt +435 -117
- package/docs/migrations.md +2 -2
- package/docs/schema.md +19 -10
- package/docs/services.md +116 -26
- package/package.json +8 -4
- package/stager/generics.js +205 -0
- package/stager/stage.js +39 -3
- package/template/AGENTS.md +110 -72
- package/template/db/public.ts +1 -1
- package/template/{controllers → modules/health}/health.controller.ts +1 -1
- package/template/modules/health/health.module.ts +24 -0
- package/template/modules/notes/note.service.test.ts +49 -0
- package/template/modules/notes/note.service.ts +108 -0
- package/template/{controllers → modules/notes}/notes.controller.ts +17 -11
- package/template/modules/notes/notes.module.ts +37 -0
- package/template/package.json +5 -3
- package/template/scripts/test.sh +33 -0
- package/template/tsconfig.json +29 -30
- package/dist/chunk-35PNTIRN.js.map +0 -1
- package/dist/chunk-CJSKYY76.js +0 -627
- package/dist/chunk-CJSKYY76.js.map +0 -1
- package/dist/chunk-CRQKCRGF.js +0 -276
- package/dist/chunk-CRQKCRGF.js.map +0 -1
- package/dist/chunk-G4R6BTLV.js +0 -662
- package/dist/chunk-G4R6BTLV.js.map +0 -1
- package/dist/chunk-VDF2T4AS.js.map +0 -1
- package/dist/chunk-XABBC7JP.js +0 -55
- package/dist/chunk-XABBC7JP.js.map +0 -1
- package/dist/endpoint-CTEHhb7A.d.ts +0 -2386
- package/dist/endpoint-DYHMo6cC.d.cts +0 -2386
- package/dist/index-CW21M9Z3.d.ts +0 -1222
- package/dist/index-CmBK76nx.d.cts +0 -1222
- package/template/services/note.service.test.ts +0 -45
- package/template/services/note.service.ts +0 -76
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-VXPNPVAG.js.map} +0 -0
- /package/template/{models/notes → modules/notes/dto}/create.ts +0 -0
|
@@ -1,27 +1,48 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__requestALS,
|
|
3
3
|
__runStartHooks,
|
|
4
|
-
__runWithRuntime
|
|
5
|
-
|
|
4
|
+
__runWithRuntime,
|
|
5
|
+
assertNoOrphanEntryPoints,
|
|
6
|
+
buildContainer,
|
|
7
|
+
buildRelations
|
|
8
|
+
} from "./chunk-BQN723PL.js";
|
|
6
9
|
import {
|
|
10
|
+
qualifiedTableKey
|
|
11
|
+
} from "./chunk-OZKSM3JW.js";
|
|
12
|
+
import {
|
|
13
|
+
assertNoExpressionHandles,
|
|
7
14
|
assertUsableFilter,
|
|
8
|
-
assertUsableWriteValues
|
|
9
|
-
|
|
15
|
+
assertUsableWriteValues,
|
|
16
|
+
columnExprOf,
|
|
17
|
+
isColRef,
|
|
18
|
+
isColumnExpr,
|
|
19
|
+
isNowExpr,
|
|
20
|
+
isPlanRef,
|
|
21
|
+
isSqlFragment,
|
|
22
|
+
looksLikeUnbrandedPlanRef,
|
|
23
|
+
looksLikeUnbrandedRef
|
|
24
|
+
} from "./chunk-JVZQCC77.js";
|
|
10
25
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
getRoutes,
|
|
27
|
+
resolveEffectiveAuth
|
|
28
|
+
} from "./chunk-VVMJEVQP.js";
|
|
13
29
|
import {
|
|
30
|
+
DeadlockDetected,
|
|
31
|
+
SerializationFailure,
|
|
14
32
|
UniqueViolation,
|
|
15
|
-
assertZeroArgConstructor,
|
|
16
|
-
getRoutes,
|
|
17
33
|
isEngineRaised,
|
|
18
34
|
isHttpError,
|
|
19
|
-
markEngineRaised
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
markEngineRaised
|
|
36
|
+
} from "./chunk-CGNN2PUH.js";
|
|
37
|
+
import {
|
|
38
|
+
__name
|
|
39
|
+
} from "./chunk-VXPNPVAG.js";
|
|
22
40
|
|
|
23
41
|
// src/engine/config.ts
|
|
24
42
|
var BootRefused = class extends Error {
|
|
43
|
+
static {
|
|
44
|
+
__name(this, "BootRefused");
|
|
45
|
+
}
|
|
25
46
|
missing;
|
|
26
47
|
constructor(missing, message) {
|
|
27
48
|
super(message);
|
|
@@ -30,18 +51,21 @@ var BootRefused = class extends Error {
|
|
|
30
51
|
}
|
|
31
52
|
};
|
|
32
53
|
var MANDATORY = [
|
|
33
|
-
{
|
|
34
|
-
|
|
54
|
+
{
|
|
55
|
+
key: "DATABASE_URL",
|
|
56
|
+
what: "the stack's Postgres (Database module)"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "AUTH_JWKS_URL",
|
|
60
|
+
what: "where this stack publishes its token signing keys (Auth module)"
|
|
61
|
+
}
|
|
35
62
|
];
|
|
36
63
|
function loadConfig(env) {
|
|
37
64
|
const missing = MANDATORY.filter((m) => !env[m.key]?.trim()).map((m) => m.key);
|
|
38
65
|
if (missing.length > 0) {
|
|
39
66
|
const detail = MANDATORY.filter((m) => missing.includes(m.key)).map((m) => ` ${m.key.padEnd(16)}${m.what}`).join("\n");
|
|
40
|
-
throw new BootRefused(
|
|
41
|
-
|
|
42
|
-
`boot refused: mandatory module not configured \u2014 missing ${missing.join(", ")}.
|
|
43
|
-
${detail}`
|
|
44
|
-
);
|
|
67
|
+
throw new BootRefused(missing, `boot refused: mandatory module not configured \u2014 missing ${missing.join(", ")}.
|
|
68
|
+
${detail}`);
|
|
45
69
|
}
|
|
46
70
|
const port = Number(env.PORT ?? 3e3);
|
|
47
71
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
@@ -75,6 +99,7 @@ ${detail}`
|
|
|
75
99
|
poolMax
|
|
76
100
|
};
|
|
77
101
|
}
|
|
102
|
+
__name(loadConfig, "loadConfig");
|
|
78
103
|
|
|
79
104
|
// src/engine/auth.ts
|
|
80
105
|
function b64urlToBytes(s) {
|
|
@@ -85,7 +110,11 @@ function b64urlToBytes(s) {
|
|
|
85
110
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
86
111
|
return out;
|
|
87
112
|
}
|
|
113
|
+
__name(b64urlToBytes, "b64urlToBytes");
|
|
88
114
|
var AuthVerifier = class {
|
|
115
|
+
static {
|
|
116
|
+
__name(this, "AuthVerifier");
|
|
117
|
+
}
|
|
89
118
|
keys = /* @__PURE__ */ new Map();
|
|
90
119
|
fetchedAt = 0;
|
|
91
120
|
inflight = null;
|
|
@@ -111,16 +140,18 @@ var AuthVerifier = class {
|
|
|
111
140
|
for (const jwk of body.keys ?? []) {
|
|
112
141
|
if (jwk.kty !== "EC" || jwk.crv !== "P-256") continue;
|
|
113
142
|
try {
|
|
114
|
-
next.set(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
next.set(jwk.kid, await crypto.subtle.importKey("jwk", {
|
|
144
|
+
kty: "EC",
|
|
145
|
+
crv: jwk.crv,
|
|
146
|
+
x: jwk.x,
|
|
147
|
+
y: jwk.y,
|
|
148
|
+
ext: true
|
|
149
|
+
}, {
|
|
150
|
+
name: "ECDSA",
|
|
151
|
+
namedCurve: "P-256"
|
|
152
|
+
}, true, [
|
|
153
|
+
"verify"
|
|
154
|
+
]));
|
|
124
155
|
} catch {
|
|
125
156
|
}
|
|
126
157
|
}
|
|
@@ -140,12 +171,12 @@ var AuthVerifier = class {
|
|
|
140
171
|
return this.keys.get(kid) ?? null;
|
|
141
172
|
}
|
|
142
173
|
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
174
|
+
* Verify an `Authorization` header value.
|
|
175
|
+
*
|
|
176
|
+
* @returns the verified claims, or `null` for absent / malformed / expired /
|
|
177
|
+
* wrong-issuer / bad-signature. One `null` for every failure on purpose:
|
|
178
|
+
* the caller answers 401 either way, and a detailed reason is an oracle.
|
|
179
|
+
*/
|
|
149
180
|
async verify(authorization) {
|
|
150
181
|
if (!authorization || !authorization.startsWith("Bearer ")) return null;
|
|
151
182
|
const parts = authorization.slice(7).trim().split(".");
|
|
@@ -167,12 +198,10 @@ var AuthVerifier = class {
|
|
|
167
198
|
if (!key) return null;
|
|
168
199
|
let ok = false;
|
|
169
200
|
try {
|
|
170
|
-
ok = await crypto.subtle.verify(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
new TextEncoder().encode(`${h}.${p}`)
|
|
175
|
-
);
|
|
201
|
+
ok = await crypto.subtle.verify({
|
|
202
|
+
name: "ECDSA",
|
|
203
|
+
hash: "SHA-256"
|
|
204
|
+
}, key, b64urlToBytes(sig), new TextEncoder().encode(`${h}.${p}`));
|
|
176
205
|
} catch {
|
|
177
206
|
return null;
|
|
178
207
|
}
|
|
@@ -184,9 +213,18 @@ var AuthVerifier = class {
|
|
|
184
213
|
};
|
|
185
214
|
function effectiveAuth(routeAuth, controllerAuth) {
|
|
186
215
|
const spec = routeAuth !== void 0 ? routeAuth : controllerAuth;
|
|
187
|
-
if (spec === false) return {
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
if (spec === false) return {
|
|
217
|
+
required: false,
|
|
218
|
+
verifiedEmail: false
|
|
219
|
+
};
|
|
220
|
+
if (spec === true || spec === void 0 || spec === null) return {
|
|
221
|
+
required: true,
|
|
222
|
+
verifiedEmail: false
|
|
223
|
+
};
|
|
224
|
+
if (typeof spec !== "object") return {
|
|
225
|
+
required: true,
|
|
226
|
+
verifiedEmail: false
|
|
227
|
+
};
|
|
190
228
|
const o = spec;
|
|
191
229
|
const role = typeof o.role === "string" && o.role.trim() !== "" ? o.role.trim() : void 0;
|
|
192
230
|
return {
|
|
@@ -195,23 +233,27 @@ function effectiveAuth(routeAuth, controllerAuth) {
|
|
|
195
233
|
verifiedEmail: o.verifiedEmail === true
|
|
196
234
|
};
|
|
197
235
|
}
|
|
236
|
+
__name(effectiveAuth, "effectiveAuth");
|
|
198
237
|
|
|
199
238
|
// src/engine/ratelimit.ts
|
|
200
239
|
var RateLimiter = class {
|
|
240
|
+
static {
|
|
241
|
+
__name(this, "RateLimiter");
|
|
242
|
+
}
|
|
243
|
+
maxKeys;
|
|
244
|
+
buckets = /* @__PURE__ */ new Map();
|
|
201
245
|
/** Bound on distinct keys held, so an attacker cycling identities cannot
|
|
202
|
-
|
|
203
|
-
|
|
246
|
+
* grow this map without limit. On overflow the oldest windows are dropped —
|
|
247
|
+
* forgiving, consistent with the restart behaviour above. */
|
|
204
248
|
constructor(maxKeys = 1e5) {
|
|
205
249
|
this.maxKeys = maxKeys;
|
|
206
250
|
}
|
|
207
|
-
maxKeys;
|
|
208
|
-
buckets = /* @__PURE__ */ new Map();
|
|
209
251
|
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
252
|
+
* Identify the caller: the signed-in user when the route resolved one,
|
|
253
|
+
* otherwise the address the edge forwarded. Callers the edge did not
|
|
254
|
+
* identify share one bucket — deliberately conservative, since the
|
|
255
|
+
* alternative is a limit anyone resets by omitting a header.
|
|
256
|
+
*/
|
|
215
257
|
static key(routeId, userId, headers) {
|
|
216
258
|
if (userId) return `${routeId}\0u:${userId}`;
|
|
217
259
|
const fwd = headers.get("x-forwarded-for");
|
|
@@ -219,16 +261,19 @@ var RateLimiter = class {
|
|
|
219
261
|
return `${routeId}\0a:${addr || "anonymous"}`;
|
|
220
262
|
}
|
|
221
263
|
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
264
|
+
* @returns `null` when the request may proceed, or the number of seconds to
|
|
265
|
+
* wait (never 0 — a caller told to wait 0 comes straight back to the same
|
|
266
|
+
* refusal).
|
|
267
|
+
*/
|
|
226
268
|
check(rule, key, now) {
|
|
227
269
|
if (!rule || !(rule.max > 0) || !(rule.window > 0)) return null;
|
|
228
270
|
const bucket = this.buckets.get(key);
|
|
229
271
|
if (!bucket || now >= bucket.resetAt) {
|
|
230
272
|
if (this.buckets.size >= this.maxKeys) this.evict(now);
|
|
231
|
-
this.buckets.set(key, {
|
|
273
|
+
this.buckets.set(key, {
|
|
274
|
+
count: 1,
|
|
275
|
+
resetAt: now + rule.window * 1e3
|
|
276
|
+
});
|
|
232
277
|
return null;
|
|
233
278
|
}
|
|
234
279
|
if (bucket.count < rule.max) {
|
|
@@ -238,7 +283,7 @@ var RateLimiter = class {
|
|
|
238
283
|
return Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3));
|
|
239
284
|
}
|
|
240
285
|
/** Drop expired windows; if none are expired, drop the earliest-resetting
|
|
241
|
-
|
|
286
|
+
* quarter so the map cannot wedge at the ceiling. */
|
|
242
287
|
evict(now) {
|
|
243
288
|
let dropped = 0;
|
|
244
289
|
for (const [k, b] of this.buckets) {
|
|
@@ -248,7 +293,9 @@ var RateLimiter = class {
|
|
|
248
293
|
}
|
|
249
294
|
}
|
|
250
295
|
if (dropped > 0) return;
|
|
251
|
-
const byReset = [
|
|
296
|
+
const byReset = [
|
|
297
|
+
...this.buckets.entries()
|
|
298
|
+
].sort((a, b) => a[1].resetAt - b[1].resetAt);
|
|
252
299
|
for (let i = 0; i < Math.ceil(byReset.length / 4); i++) {
|
|
253
300
|
const victim = byReset[i];
|
|
254
301
|
if (victim) this.buckets.delete(victim[0]);
|
|
@@ -266,7 +313,7 @@ function makeMemoryCache(opts = {}) {
|
|
|
266
313
|
const now = opts.now ?? (() => Date.now());
|
|
267
314
|
const store = /* @__PURE__ */ new Map();
|
|
268
315
|
const inflight = /* @__PURE__ */ new Map();
|
|
269
|
-
const live = (key) => {
|
|
316
|
+
const live = /* @__PURE__ */ __name((key) => {
|
|
270
317
|
const e = store.get(key);
|
|
271
318
|
if (!e) return void 0;
|
|
272
319
|
if (e.expiresAt !== 0 && e.expiresAt <= now()) {
|
|
@@ -274,8 +321,8 @@ function makeMemoryCache(opts = {}) {
|
|
|
274
321
|
return void 0;
|
|
275
322
|
}
|
|
276
323
|
return e;
|
|
277
|
-
};
|
|
278
|
-
const evict = () => {
|
|
324
|
+
}, "live");
|
|
325
|
+
const evict = /* @__PURE__ */ __name(() => {
|
|
279
326
|
const t = now();
|
|
280
327
|
let dropped = 0;
|
|
281
328
|
for (const [k, e] of store) {
|
|
@@ -285,18 +332,21 @@ function makeMemoryCache(opts = {}) {
|
|
|
285
332
|
}
|
|
286
333
|
}
|
|
287
334
|
if (dropped > 0) return;
|
|
288
|
-
const order = [
|
|
289
|
-
|
|
290
|
-
);
|
|
335
|
+
const order = [
|
|
336
|
+
...store.entries()
|
|
337
|
+
].sort((a, b) => (a[1].expiresAt || Infinity) - (b[1].expiresAt || Infinity));
|
|
291
338
|
for (let i = 0; i < Math.ceil(order.length / 4); i++) {
|
|
292
339
|
const victim = order[i];
|
|
293
340
|
if (victim) store.delete(victim[0]);
|
|
294
341
|
}
|
|
295
|
-
};
|
|
296
|
-
const set = async (key, value, ttl) => {
|
|
342
|
+
}, "evict");
|
|
343
|
+
const set = /* @__PURE__ */ __name(async (key, value, ttl) => {
|
|
297
344
|
if (store.size >= maxEntries && !store.has(key)) evict();
|
|
298
|
-
store.set(key, {
|
|
299
|
-
|
|
345
|
+
store.set(key, {
|
|
346
|
+
value,
|
|
347
|
+
expiresAt: ttl && ttl > 0 ? now() + ttl * 1e3 : 0
|
|
348
|
+
});
|
|
349
|
+
}, "set");
|
|
300
350
|
return {
|
|
301
351
|
async get(key) {
|
|
302
352
|
const e = live(key);
|
|
@@ -309,7 +359,10 @@ function makeMemoryCache(opts = {}) {
|
|
|
309
359
|
async incr(key) {
|
|
310
360
|
const e = live(key);
|
|
311
361
|
const next = (typeof e?.value === "number" ? e.value : 0) + 1;
|
|
312
|
-
store.set(key, {
|
|
362
|
+
store.set(key, {
|
|
363
|
+
value: next,
|
|
364
|
+
expiresAt: e?.expiresAt ?? 0
|
|
365
|
+
});
|
|
313
366
|
return next;
|
|
314
367
|
},
|
|
315
368
|
async getOrSet(key, ttl, fn) {
|
|
@@ -331,34 +384,45 @@ function makeMemoryCache(opts = {}) {
|
|
|
331
384
|
}
|
|
332
385
|
};
|
|
333
386
|
}
|
|
387
|
+
__name(makeMemoryCache, "makeMemoryCache");
|
|
334
388
|
|
|
335
389
|
// src/engine/db.ts
|
|
336
390
|
function quoteIdent(name) {
|
|
337
391
|
return `"${name.replace(/"/g, '""')}"`;
|
|
338
392
|
}
|
|
393
|
+
__name(quoteIdent, "quoteIdent");
|
|
339
394
|
function quoteTable(key) {
|
|
340
395
|
const dot = key.indexOf(".");
|
|
341
396
|
if (dot === -1) return quoteIdent(key);
|
|
342
397
|
return `${quoteIdent(key.slice(0, dot))}.${quoteIdent(key.slice(dot + 1))}`;
|
|
343
398
|
}
|
|
399
|
+
__name(quoteTable, "quoteTable");
|
|
344
400
|
function encodePgArray(value) {
|
|
345
|
-
const element = (x) => {
|
|
401
|
+
const element = /* @__PURE__ */ __name((x) => {
|
|
346
402
|
if (x === null || x === void 0) return "NULL";
|
|
347
403
|
if (Array.isArray(x)) return encodePgArray(x);
|
|
348
404
|
return `"${String(x).replace(/(["\\])/g, "\\$1")}"`;
|
|
349
|
-
};
|
|
405
|
+
}, "element");
|
|
350
406
|
return `{${value.map(element).join(",")}}`;
|
|
351
407
|
}
|
|
408
|
+
__name(encodePgArray, "encodePgArray");
|
|
352
409
|
var BIND_SQL = "select set_config('role',$1,true), set_config('search_path','public',true), set_config('request.jwt.claims',$2,true)";
|
|
353
410
|
function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
354
411
|
const { lockTimeout } = options;
|
|
355
412
|
const bindSql = lockTimeout ? `${BIND_SQL}, set_config('lock_timeout',$3,true)` : BIND_SQL;
|
|
356
|
-
const bindParams = lockTimeout ? [
|
|
413
|
+
const bindParams = lockTimeout ? [
|
|
414
|
+
role,
|
|
415
|
+
claimsJson,
|
|
416
|
+
lockTimeout
|
|
417
|
+
] : [
|
|
418
|
+
role,
|
|
419
|
+
claimsJson
|
|
420
|
+
];
|
|
357
421
|
let opening = null;
|
|
358
422
|
let release = null;
|
|
359
423
|
let fail = null;
|
|
360
424
|
let settled = null;
|
|
361
|
-
const ensure = () => {
|
|
425
|
+
const ensure = /* @__PURE__ */ __name(() => {
|
|
362
426
|
if (opening) return opening;
|
|
363
427
|
opening = new Promise((resolveTx2, rejectTx) => {
|
|
364
428
|
const parked = new Promise((res, rej) => {
|
|
@@ -375,7 +439,7 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
375
439
|
});
|
|
376
440
|
});
|
|
377
441
|
return opening;
|
|
378
|
-
};
|
|
442
|
+
}, "ensure");
|
|
379
443
|
return {
|
|
380
444
|
ensure,
|
|
381
445
|
get opened() {
|
|
@@ -393,24 +457,29 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
|
|
|
393
457
|
}
|
|
394
458
|
};
|
|
395
459
|
}
|
|
396
|
-
|
|
460
|
+
__name(createLazyTransaction, "createLazyTransaction");
|
|
461
|
+
var resolveTx = /* @__PURE__ */ __name(async (tx) => typeof tx.ensure === "function" ? await tx.ensure() : tx, "resolveTx");
|
|
397
462
|
function asWireValue(value) {
|
|
398
463
|
if (value instanceof Date) return value.toISOString();
|
|
399
464
|
if (Array.isArray(value)) return value.map(asWireValue);
|
|
400
465
|
return value;
|
|
401
466
|
}
|
|
467
|
+
__name(asWireValue, "asWireValue");
|
|
402
468
|
function asWireRow(row) {
|
|
403
469
|
if (row === null || typeof row !== "object") return row;
|
|
404
470
|
const out = {};
|
|
405
471
|
for (const [key, value] of Object.entries(row)) out[key] = asWireValue(value);
|
|
406
472
|
return out;
|
|
407
473
|
}
|
|
474
|
+
__name(asWireRow, "asWireRow");
|
|
408
475
|
function asWireRows(rows) {
|
|
409
476
|
return rows.map((row) => asWireRow(row));
|
|
410
477
|
}
|
|
478
|
+
__name(asWireRows, "asWireRows");
|
|
411
479
|
function toVectorLiteral(v) {
|
|
412
480
|
return `[${v.join(",")}]`;
|
|
413
481
|
}
|
|
482
|
+
__name(toVectorLiteral, "toVectorLiteral");
|
|
414
483
|
function vectorColumnsOf(schema, table) {
|
|
415
484
|
const out = /* @__PURE__ */ new Set();
|
|
416
485
|
const def = tableDefOf(table, schema);
|
|
@@ -422,9 +491,11 @@ function vectorColumnsOf(schema, table) {
|
|
|
422
491
|
}
|
|
423
492
|
return out;
|
|
424
493
|
}
|
|
494
|
+
__name(vectorColumnsOf, "vectorColumnsOf");
|
|
425
495
|
function tableDefOf(key, schema = currentSchema) {
|
|
426
496
|
return schema.tables?.[key] ?? null;
|
|
427
497
|
}
|
|
498
|
+
__name(tableDefOf, "tableDefOf");
|
|
428
499
|
function reviveVectors(row, vectorCols) {
|
|
429
500
|
if (row === null || typeof row !== "object" || vectorCols.size === 0) return row;
|
|
430
501
|
const out = row;
|
|
@@ -434,6 +505,7 @@ function reviveVectors(row, vectorCols) {
|
|
|
434
505
|
}
|
|
435
506
|
return row;
|
|
436
507
|
}
|
|
508
|
+
__name(reviveVectors, "reviveVectors");
|
|
437
509
|
function transformsOf(schema, table) {
|
|
438
510
|
const out = /* @__PURE__ */ new Map();
|
|
439
511
|
const def = tableDefOf(table, schema);
|
|
@@ -448,6 +520,7 @@ function transformsOf(schema, table) {
|
|
|
448
520
|
}
|
|
449
521
|
return out;
|
|
450
522
|
}
|
|
523
|
+
__name(transformsOf, "transformsOf");
|
|
451
524
|
function applyFromDb(row, transforms) {
|
|
452
525
|
if (transforms.size === 0) return row;
|
|
453
526
|
for (const [col, t] of transforms) {
|
|
@@ -458,17 +531,21 @@ function applyFromDb(row, transforms) {
|
|
|
458
531
|
}
|
|
459
532
|
return row;
|
|
460
533
|
}
|
|
534
|
+
__name(applyFromDb, "applyFromDb");
|
|
461
535
|
function asTableRow(table, row) {
|
|
462
536
|
const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
|
|
463
537
|
return applyFromDb(revived, transformsOf(currentSchema, table));
|
|
464
538
|
}
|
|
539
|
+
__name(asTableRow, "asTableRow");
|
|
465
540
|
function asTableRows(table, rows) {
|
|
466
541
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
467
542
|
const transforms = transformsOf(currentSchema, table);
|
|
468
543
|
return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
|
|
469
544
|
}
|
|
545
|
+
__name(asTableRows, "asTableRows");
|
|
470
546
|
function asBindParams(table, cols, data, caller) {
|
|
471
547
|
assertUsableWriteValues(caller, table, cols, data);
|
|
548
|
+
assertNoExpressionHandles(caller, table, cols, data);
|
|
472
549
|
const vectorCols = vectorColumnsOf(currentSchema, table);
|
|
473
550
|
const transforms = transformsOf(currentSchema, table);
|
|
474
551
|
return cols.map((c) => {
|
|
@@ -479,6 +556,7 @@ function asBindParams(table, cols, data, caller) {
|
|
|
479
556
|
return v;
|
|
480
557
|
});
|
|
481
558
|
}
|
|
559
|
+
__name(asBindParams, "asBindParams");
|
|
482
560
|
var SELECTIVITY_EXACT_THRESHOLD = 1e4;
|
|
483
561
|
var METRIC_OPERATOR = {
|
|
484
562
|
cosine: "<=>",
|
|
@@ -489,27 +567,33 @@ function searchConfigFor(table) {
|
|
|
489
567
|
const t = tableDefOf(table);
|
|
490
568
|
if (!t) return null;
|
|
491
569
|
const columns = t.columns ?? {};
|
|
492
|
-
const defOf = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
|
|
570
|
+
const defOf = /* @__PURE__ */ __name((c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {}, "defOf");
|
|
493
571
|
const cols = Object.keys(columns);
|
|
494
572
|
const vectorCols = cols.filter((c) => defOf(columns[c]).type === "vector");
|
|
495
|
-
const defOfFull = (c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {};
|
|
573
|
+
const defOfFull = /* @__PURE__ */ __name((c) => c !== null && typeof c === "object" && "_def" in c ? c._def : c ?? {}, "defOfFull");
|
|
496
574
|
const pkCols = cols.filter((c) => defOfFull(columns[c]).primaryKey === true);
|
|
497
575
|
const pk = pkCols.length === 1 ? pkCols[0] : cols.includes("id") ? "id" : null;
|
|
498
576
|
if (pk === null) {
|
|
499
|
-
throw new Error(
|
|
500
|
-
`search(${table}): tek-kolon primary key bulunamad\u0131 \u2014 arama s\u0131ralamas\u0131 ve sat\u0131r birle\u015Fimi PK ister (FR-020)`
|
|
501
|
-
);
|
|
577
|
+
throw new Error(`search(${table}): tek-kolon primary key bulunamad\u0131 \u2014 arama s\u0131ralamas\u0131 ve sat\u0131r birle\u015Fimi PK ister (FR-020)`);
|
|
502
578
|
}
|
|
503
579
|
const search = t.search;
|
|
504
|
-
const synonyms = search?.synonyms !== void 0 && Object.keys(search.synonyms).length > 0 ? {
|
|
505
|
-
|
|
580
|
+
const synonyms = search?.synonyms !== void 0 && Object.keys(search.synonyms).length > 0 ? {
|
|
581
|
+
synonyms: search.synonyms
|
|
582
|
+
} : {};
|
|
583
|
+
const validity = search?.validity === true ? {
|
|
584
|
+
validity: true
|
|
585
|
+
} : {};
|
|
506
586
|
if (search?.from !== void 0 && search.model !== void 0) {
|
|
507
587
|
const m = search.model;
|
|
508
588
|
const embed = {
|
|
509
589
|
model: m.model,
|
|
510
590
|
apiKeyName: m.apiKeyName ?? "OPENAI_API_KEY",
|
|
511
|
-
...m.baseURL !== void 0 ? {
|
|
512
|
-
|
|
591
|
+
...m.baseURL !== void 0 ? {
|
|
592
|
+
baseURL: m.baseURL
|
|
593
|
+
} : {},
|
|
594
|
+
...m.dimensions !== void 0 ? {
|
|
595
|
+
dimensions: m.dimensions
|
|
596
|
+
} : {}
|
|
513
597
|
};
|
|
514
598
|
const metric = search.metric ?? "cosine";
|
|
515
599
|
const ftsOn = search.text !== false;
|
|
@@ -520,7 +604,13 @@ function searchConfigFor(table) {
|
|
|
520
604
|
cols,
|
|
521
605
|
colSet: new Set(cols),
|
|
522
606
|
ftsCols: ftsColsNew,
|
|
523
|
-
legs: [
|
|
607
|
+
legs: [
|
|
608
|
+
{
|
|
609
|
+
column: vectorCols[0],
|
|
610
|
+
metric,
|
|
611
|
+
embed
|
|
612
|
+
}
|
|
613
|
+
],
|
|
524
614
|
...synonyms,
|
|
525
615
|
...validity
|
|
526
616
|
};
|
|
@@ -531,13 +621,20 @@ function searchConfigFor(table) {
|
|
|
531
621
|
colSet: new Set(cols),
|
|
532
622
|
ftsCols: ftsColsNew,
|
|
533
623
|
legs: [],
|
|
534
|
-
chunk: {
|
|
624
|
+
chunk: {
|
|
625
|
+
table: `${table}__palbase_chunks`,
|
|
626
|
+
metric,
|
|
627
|
+
embed,
|
|
628
|
+
fts: ftsOn
|
|
629
|
+
},
|
|
535
630
|
...synonyms,
|
|
536
631
|
...validity
|
|
537
632
|
};
|
|
538
633
|
}
|
|
539
634
|
const ftsCols = (Array.isArray(search?.text) ? search.text : void 0) ?? [];
|
|
540
|
-
const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [
|
|
635
|
+
const rawLegs = search?.vector === void 0 ? [] : Array.isArray(search.vector) ? search.vector : [
|
|
636
|
+
search.vector
|
|
637
|
+
];
|
|
541
638
|
let legs;
|
|
542
639
|
if (rawLegs.length > 0) {
|
|
543
640
|
legs = rawLegs.map((leg) => {
|
|
@@ -550,35 +647,57 @@ function searchConfigFor(table) {
|
|
|
550
647
|
return {
|
|
551
648
|
column,
|
|
552
649
|
metric: l.metric ?? "cosine",
|
|
553
|
-
...model !== void 0 ? {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
650
|
+
...model !== void 0 ? {
|
|
651
|
+
embed: {
|
|
652
|
+
model: model.model,
|
|
653
|
+
apiKeyName: model.apiKeyName ?? "OPENAI_API_KEY",
|
|
654
|
+
...model.baseURL !== void 0 ? {
|
|
655
|
+
baseURL: model.baseURL
|
|
656
|
+
} : {},
|
|
657
|
+
...model.dimensions !== void 0 ? {
|
|
658
|
+
dimensions: model.dimensions
|
|
659
|
+
} : {}
|
|
660
|
+
}
|
|
661
|
+
} : {}
|
|
559
662
|
};
|
|
560
663
|
});
|
|
561
664
|
} else {
|
|
562
|
-
legs = vectorCols.map((column) => ({
|
|
665
|
+
legs = vectorCols.map((column) => ({
|
|
666
|
+
column,
|
|
667
|
+
metric: "cosine"
|
|
668
|
+
}));
|
|
563
669
|
}
|
|
564
670
|
if (ftsCols.length === 0 && legs.length === 0) return null;
|
|
565
|
-
return {
|
|
671
|
+
return {
|
|
672
|
+
pk,
|
|
673
|
+
cols,
|
|
674
|
+
colSet: new Set(cols),
|
|
675
|
+
ftsCols,
|
|
676
|
+
legs,
|
|
677
|
+
...synonyms,
|
|
678
|
+
...validity
|
|
679
|
+
};
|
|
566
680
|
}
|
|
681
|
+
__name(searchConfigFor, "searchConfigFor");
|
|
567
682
|
function pickLeg(table, legs, using) {
|
|
568
683
|
if (legs.length === 0) return null;
|
|
569
684
|
if (using !== void 0) {
|
|
570
685
|
const hit = legs.find((l) => l.column === using);
|
|
571
686
|
if (!hit) {
|
|
572
|
-
throw new Error(
|
|
573
|
-
`search(${table}): using "${using}" bir vekt\xF6r kolunu adlam\u0131yor \u2014 mevcut: ${legs.map((l) => l.column).join(", ")}`
|
|
574
|
-
);
|
|
687
|
+
throw new Error(`search(${table}): using "${using}" bir vekt\xF6r kolunu adlam\u0131yor \u2014 mevcut: ${legs.map((l) => l.column).join(", ")}`);
|
|
575
688
|
}
|
|
576
689
|
return hit;
|
|
577
690
|
}
|
|
578
691
|
if (legs.length === 1) return legs[0];
|
|
579
692
|
throw new Error(`search(${table}): birden \xE7ok vekt\xF6r kolu var \u2014 'using' ile se\xE7in (FR-013) \u2014 salt metin ar\u0131yorsan mode:"text" kullan`);
|
|
580
693
|
}
|
|
581
|
-
|
|
694
|
+
__name(pickLeg, "pickLeg");
|
|
695
|
+
var HALF_LIFE_UNIT_SECONDS = {
|
|
696
|
+
s: 1,
|
|
697
|
+
m: 60,
|
|
698
|
+
h: 3600,
|
|
699
|
+
d: 86400
|
|
700
|
+
};
|
|
582
701
|
function parseHalfLife(s) {
|
|
583
702
|
const m = /^(\d+)(s|m|h|d)$/.exec(s);
|
|
584
703
|
const sec = m === null ? 0 : Number(m[1]) * HALF_LIFE_UNIT_SECONDS[m[2]];
|
|
@@ -587,10 +706,20 @@ function parseHalfLife(s) {
|
|
|
587
706
|
}
|
|
588
707
|
return sec;
|
|
589
708
|
}
|
|
590
|
-
|
|
709
|
+
__name(parseHalfLife, "parseHalfLife");
|
|
710
|
+
var WHERE_OPS = {
|
|
711
|
+
gt: ">",
|
|
712
|
+
gte: ">=",
|
|
713
|
+
lt: "<",
|
|
714
|
+
lte: "<=",
|
|
715
|
+
neq: "<>"
|
|
716
|
+
};
|
|
591
717
|
function ftsExprOf(ftsCols) {
|
|
592
|
-
return [
|
|
718
|
+
return [
|
|
719
|
+
...ftsCols
|
|
720
|
+
].sort().map((c) => `coalesce(t.${quoteIdent(c)},'')`).join(" || ' ' || ");
|
|
593
721
|
}
|
|
722
|
+
__name(ftsExprOf, "ftsExprOf");
|
|
594
723
|
function expandSynonyms(query, map) {
|
|
595
724
|
const lower = {};
|
|
596
725
|
for (const [word, alts] of Object.entries(map)) lower[word.toLowerCase()] = alts;
|
|
@@ -603,9 +732,10 @@ function expandSynonyms(query, map) {
|
|
|
603
732
|
}).join("");
|
|
604
733
|
}).join("");
|
|
605
734
|
}
|
|
606
|
-
|
|
735
|
+
__name(expandSynonyms, "expandSynonyms");
|
|
736
|
+
var searchLogger = /* @__PURE__ */ __name((evt, fields) => {
|
|
607
737
|
console.log(evt, JSON.stringify(fields));
|
|
608
|
-
};
|
|
738
|
+
}, "searchLogger");
|
|
609
739
|
function logNoResults(table, query, mode, n) {
|
|
610
740
|
if (n === 0) {
|
|
611
741
|
searchLogger?.("palbase.search.no_results", {
|
|
@@ -615,26 +745,29 @@ function logNoResults(table, query, mode, n) {
|
|
|
615
745
|
});
|
|
616
746
|
}
|
|
617
747
|
}
|
|
748
|
+
__name(logNoResults, "logNoResults");
|
|
618
749
|
async function fetchFacets(live, table, colSet, facets, where, extraWhere = () => "") {
|
|
619
750
|
const bind = [];
|
|
620
|
-
const add = (v) => {
|
|
751
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
621
752
|
bind.push(v);
|
|
622
753
|
return `$${bind.length}`;
|
|
623
|
-
};
|
|
754
|
+
}, "add");
|
|
624
755
|
const whereSql = compileWhere(table, colSet, where, add) + extraWhere(add);
|
|
625
|
-
const parts = facets.map(
|
|
626
|
-
(col) => `(SELECT '${col.replace(/'/g, "''")}' AS f, t.${quoteIdent(col)}::text AS v, count(*) AS n FROM ${quoteTable(table)} t WHERE true${whereSql} GROUP BY 2 ORDER BY 3 DESC LIMIT 20)`
|
|
627
|
-
);
|
|
756
|
+
const parts = facets.map((col) => `(SELECT '${col.replace(/'/g, "''")}' AS f, t.${quoteIdent(col)}::text AS v, count(*) AS n FROM ${quoteTable(table)} t WHERE true${whereSql} GROUP BY 2 ORDER BY 3 DESC LIMIT 20)`);
|
|
628
757
|
const rows = await live.unsafe(parts.join(" UNION ALL "), bind);
|
|
629
758
|
const out = {};
|
|
630
759
|
for (const col of facets) out[col] = [];
|
|
631
760
|
for (const r of rows) {
|
|
632
761
|
if (typeof r.f === "string" && out[r.f] !== void 0) {
|
|
633
|
-
out[r.f].push({
|
|
762
|
+
out[r.f].push({
|
|
763
|
+
value: r.v ?? null,
|
|
764
|
+
count: Number(r.n)
|
|
765
|
+
});
|
|
634
766
|
}
|
|
635
767
|
}
|
|
636
768
|
return out;
|
|
637
769
|
}
|
|
770
|
+
__name(fetchFacets, "fetchFacets");
|
|
638
771
|
function validitySql(v, add) {
|
|
639
772
|
if (v === "all") return "";
|
|
640
773
|
if (v !== void 0) {
|
|
@@ -643,26 +776,96 @@ function validitySql(v, add) {
|
|
|
643
776
|
}
|
|
644
777
|
return ` AND t."valid_to" IS NULL`;
|
|
645
778
|
}
|
|
779
|
+
__name(validitySql, "validitySql");
|
|
646
780
|
function excludeSql(pk, ids, add) {
|
|
647
781
|
if (ids.length === 0) return "";
|
|
648
782
|
if (ids.length === 1) return ` AND t.${quoteIdent(pk)} <> ${add(ids[0])}`;
|
|
649
783
|
return ` AND t.${quoteIdent(pk)}::text <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
|
|
650
784
|
}
|
|
785
|
+
__name(excludeSql, "excludeSql");
|
|
786
|
+
function primaryKeyOf(table) {
|
|
787
|
+
const t = tableDefOf(table);
|
|
788
|
+
const columns = t?.columns ?? {};
|
|
789
|
+
const cols = Object.keys(columns);
|
|
790
|
+
if (cols.length === 0) return null;
|
|
791
|
+
const declared = t?.primaryKey;
|
|
792
|
+
if (Array.isArray(declared)) {
|
|
793
|
+
if (declared.length !== 1) return null;
|
|
794
|
+
const only = declared[0];
|
|
795
|
+
return typeof only === "string" && cols.includes(only) ? only : null;
|
|
796
|
+
}
|
|
797
|
+
const pkOf = /* @__PURE__ */ __name((c) => {
|
|
798
|
+
if (c === null || typeof c !== "object") return false;
|
|
799
|
+
const rec = c;
|
|
800
|
+
const def = "_def" in rec ? rec._def : rec;
|
|
801
|
+
return def !== null && typeof def === "object" && def.primaryKey === true;
|
|
802
|
+
}, "pkOf");
|
|
803
|
+
const pks = cols.filter((c) => pkOf(columns[c]));
|
|
804
|
+
return pks.length === 1 ? pks[0] : null;
|
|
805
|
+
}
|
|
806
|
+
__name(primaryKeyOf, "primaryKeyOf");
|
|
807
|
+
function isSinglePkEquality(where, pk) {
|
|
808
|
+
if (pk === null) return false;
|
|
809
|
+
const keys = Object.keys(where);
|
|
810
|
+
if (keys.length !== 1 || keys[0] !== pk) return false;
|
|
811
|
+
const v = where[pk];
|
|
812
|
+
return v !== null && (typeof v !== "object" || Array.isArray(v));
|
|
813
|
+
}
|
|
814
|
+
__name(isSinglePkEquality, "isSinglePkEquality");
|
|
815
|
+
function assertNotAppendOnly(caller, table) {
|
|
816
|
+
const def = tableDefOf(table);
|
|
817
|
+
if (def?.appendOnly !== true) return;
|
|
818
|
+
throw new Error(`${caller}(${table}): "${table}" append-only \u2014 sat\u0131r g\xFCncellenemez ya da silinemez. D\xFCzeltme i\xE7in TELAF\u0130 KAYDI ekleyin: insert("${table}", { \u2026 }) ile ters i\u015Fareti ta\u015F\u0131yan yeni bir sat\u0131r yaz\u0131n; ge\xE7mi\u015F oldu\u011Fu gibi kals\u0131n diye tablo b\xF6yle bildirilmi\u015F (appendOnly: true).`);
|
|
819
|
+
}
|
|
820
|
+
__name(assertNotAppendOnly, "assertNotAppendOnly");
|
|
651
821
|
function schemaColumns(table) {
|
|
652
822
|
const t = tableDefOf(table);
|
|
653
823
|
const cols = t?.columns ? Object.keys(t.columns) : [];
|
|
654
824
|
return cols.length > 0 ? new Set(cols) : null;
|
|
655
825
|
}
|
|
826
|
+
__name(schemaColumns, "schemaColumns");
|
|
827
|
+
function colRefSql(table, known, ref, caller, alias = "t") {
|
|
828
|
+
const name = ref.$col;
|
|
829
|
+
const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
830
|
+
if (known !== null ? !known.has(name) : !shapeOK) {
|
|
831
|
+
throw new Error(`${caller}(${table}): col("${name}") tabloda yok (FR-011)`);
|
|
832
|
+
}
|
|
833
|
+
return `${alias}.${quoteIdent(name)}`;
|
|
834
|
+
}
|
|
835
|
+
__name(colRefSql, "colRefSql");
|
|
836
|
+
function selectClause(table, known, select) {
|
|
837
|
+
if (select !== void 0 && select.length === 0) {
|
|
838
|
+
throw new Error(`findMany(${table}): select bo\u015F bir liste ald\u0131 \u2014 bu "hepsini ver" DE\u011E\u0130L, "hi\xE7birini saymad\u0131m" demek. Projeksiyon istemiyorsan\u0131z anahtar\u0131 hi\xE7 koymay\u0131n; istiyorsan\u0131z en az bir kolon ad\u0131 verin.`);
|
|
839
|
+
}
|
|
840
|
+
if (select === void 0) return "*";
|
|
841
|
+
return select.map((col) => {
|
|
842
|
+
const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(col);
|
|
843
|
+
if (known !== null ? !known.has(col) : !shapeOK) {
|
|
844
|
+
throw new Error(`findMany(${table}): select kolonu "${col}" tabloda yok`);
|
|
845
|
+
}
|
|
846
|
+
return `t.${quoteIdent(col)}`;
|
|
847
|
+
}).join(", ");
|
|
848
|
+
}
|
|
849
|
+
__name(selectClause, "selectClause");
|
|
656
850
|
function orderClause(table, known, orderBy) {
|
|
657
851
|
if (!orderBy) return "";
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
852
|
+
const items = Array.isArray(orderBy) ? orderBy : [
|
|
853
|
+
orderBy
|
|
854
|
+
];
|
|
855
|
+
if (items.length === 0) return "";
|
|
856
|
+
const rendered = items.map((o) => {
|
|
857
|
+
const col = o.column;
|
|
858
|
+
const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(col);
|
|
859
|
+
if (known !== null ? !known.has(col) : !shapeOK) {
|
|
860
|
+
throw new Error(`findMany(${table}): orderBy kolonu "${col}" tabloda yok`);
|
|
861
|
+
}
|
|
862
|
+
const dir = o.direction === "desc" ? "DESC" : "ASC";
|
|
863
|
+
const nulls = o.nulls === "first" ? " NULLS FIRST" : o.nulls === "last" ? " NULLS LAST" : "";
|
|
864
|
+
return `${quoteIdent(col)} ${dir}${nulls}`;
|
|
865
|
+
});
|
|
866
|
+
return ` ORDER BY ${rendered.join(", ")}`;
|
|
665
867
|
}
|
|
868
|
+
__name(orderClause, "orderClause");
|
|
666
869
|
function limitClause(limit) {
|
|
667
870
|
if (limit === void 0) return "";
|
|
668
871
|
if (!Number.isInteger(limit) || limit < 0) {
|
|
@@ -670,46 +873,131 @@ function limitClause(limit) {
|
|
|
670
873
|
}
|
|
671
874
|
return ` LIMIT ${limit}`;
|
|
672
875
|
}
|
|
876
|
+
__name(limitClause, "limitClause");
|
|
673
877
|
function offsetClause(offset, limit) {
|
|
674
878
|
if (offset === void 0) return "";
|
|
675
879
|
if (!Number.isInteger(offset) || offset < 0) {
|
|
676
|
-
throw new Error(
|
|
677
|
-
`findMany: offset bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(offset)})`
|
|
678
|
-
);
|
|
880
|
+
throw new Error(`findMany: offset bir negatif olmayan tam say\u0131 olmal\u0131 (geldi: ${String(offset)})`);
|
|
679
881
|
}
|
|
680
882
|
if (limit === void 0) {
|
|
681
|
-
throw new Error(
|
|
682
|
-
"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"
|
|
683
|
-
);
|
|
883
|
+
throw new Error("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");
|
|
684
884
|
}
|
|
685
885
|
return ` OFFSET ${offset}`;
|
|
686
886
|
}
|
|
687
|
-
|
|
887
|
+
__name(offsetClause, "offsetClause");
|
|
888
|
+
function compileWhereBare(table, colSet, where, add, caller = "search", alias = "t", depth = 0) {
|
|
688
889
|
const parts = [];
|
|
689
890
|
const transforms = transformsOf(currentSchema, table);
|
|
690
|
-
const bindFor = (col) => {
|
|
891
|
+
const bindFor = /* @__PURE__ */ __name((col) => {
|
|
691
892
|
const t = transforms.get(col);
|
|
692
893
|
return t?.toDb === void 0 ? add : (v) => add(v === null || v === void 0 ? v : t.toDb(v));
|
|
693
|
-
};
|
|
894
|
+
}, "bindFor");
|
|
694
895
|
assertUsableFilter(caller, table, where);
|
|
896
|
+
if (isSqlFragment(where)) {
|
|
897
|
+
const { text, values } = where.$sql;
|
|
898
|
+
if (text.length !== values.length + 1) {
|
|
899
|
+
throw new Error(`${caller}(${table}): sqlFragment par\xE7alar\u0131 bozuk (${text.length} metin, ${values.length} de\u011Fer) \u2014 bunu \xFCreten tek \u015Fey template tag'i, elle kurulmu\u015F bir nesne de\u011Fil`);
|
|
900
|
+
}
|
|
901
|
+
let out = text[0] ?? "";
|
|
902
|
+
for (let i = 0; i < values.length; i++) out += `${add(values[i])}${text[i + 1] ?? ""}`;
|
|
903
|
+
return `(${out})`;
|
|
904
|
+
}
|
|
695
905
|
for (const [col, cond] of Object.entries(where)) {
|
|
906
|
+
if (col === "OR" || col === "AND") {
|
|
907
|
+
const branches = cond;
|
|
908
|
+
if (branches.length === 0) {
|
|
909
|
+
parts.push(col === "OR" ? "false" : "true");
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const compiled = branches.map((b) => `(${compileWhereBare(table, colSet, b, add, caller, alias, depth)})`);
|
|
913
|
+
parts.push(`(${compiled.join(col === "OR" ? " OR " : " AND ")})`);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (col === "has") {
|
|
917
|
+
const rels = currentRelations.get(table) ?? [];
|
|
918
|
+
for (const [relName, inner] of Object.entries(cond)) {
|
|
919
|
+
const rel = rels.find((r) => r.name === relName);
|
|
920
|
+
if (rel === void 0) {
|
|
921
|
+
const known = rels.map((r) => r.name).sort();
|
|
922
|
+
throw new Error(`${caller}(${table}): has.${relName} diye bir ili\u015Fki yok${known.length > 0 ? ` (bilinenler: ${known.join(", ")})` : " \u2014 bu tablonun hi\xE7 ili\u015Fkisi yok"}. \u0130li\u015Fkiler yabanc\u0131 anahtarlardan T\xDCRET\u0130L\u0130R; ad\u0131n\u0131 de\u011Fi\u015Ftirmek i\xE7in references(..., { as, reverseAs }).`);
|
|
923
|
+
}
|
|
924
|
+
const ourCol = rel.kind === "many" ? primaryKeyOf(table) : rel.via;
|
|
925
|
+
const theirCol = rel.kind === "many" ? rel.via : primaryKeyOf(rel.to);
|
|
926
|
+
if (ourCol === null || theirCol === null) {
|
|
927
|
+
const missing = ourCol === null ? table : rel.to;
|
|
928
|
+
throw new Error(`${caller}(${table}): has.${relName} kurulam\u0131yor \u2014 "${missing}" tablosunun tek kolonlu bir primary key'i yok. Korelasyon bir anahtar ister; \xE7ok kolonlu anahtarda ili\u015Fki \xFCzerinden s\xFCzmek yerine sqlFragment ile yaz\u0131n.`);
|
|
929
|
+
}
|
|
930
|
+
const sub = `r${depth + 1}`;
|
|
931
|
+
const innerSql = compileWhereBare(rel.to, schemaColumns(rel.to), inner ?? {}, add, caller, sub, depth + 1);
|
|
932
|
+
parts.push(`${alias}.${quoteIdent(ourCol)} IN (SELECT ${sub}.${quoteIdent(theirCol)} FROM ${quoteTable(rel.to)} ${sub}${innerSql === "" ? "" : ` WHERE ${innerSql}`})`);
|
|
933
|
+
}
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (col === "NOT") {
|
|
937
|
+
parts.push(`NOT (${compileWhereBare(table, colSet, cond, add, caller, alias, depth)})`);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
696
940
|
if (colSet !== null && !colSet.has(col)) {
|
|
697
941
|
throw new Error(`${caller}(${table}): where kolonu "${col}" tabloda yok (FR-016)`);
|
|
698
942
|
}
|
|
699
|
-
const q =
|
|
943
|
+
const q = `${alias}.${quoteIdent(col)}`;
|
|
700
944
|
const bind = bindFor(col);
|
|
945
|
+
if (isColRef(cond)) {
|
|
946
|
+
parts.push(`${q} = ${colRefSql(table, colSet, cond, caller, alias)}`);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (isNowExpr(cond)) {
|
|
950
|
+
parts.push(`${q} = now()`);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
701
953
|
if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
|
|
702
954
|
for (const [op, v] of Object.entries(cond)) {
|
|
955
|
+
if (op === "contains" || op === "icontains" || op === "startsWith" || op === "endsWith") {
|
|
956
|
+
if (typeof v !== "string") {
|
|
957
|
+
throw new Error(`${caller}(${table}): where.${col}.${op} bir string olmal\u0131`);
|
|
958
|
+
}
|
|
959
|
+
const lit = v.replace(/([\\%_])/g, "\\$1");
|
|
960
|
+
const pattern = op === "startsWith" ? `${lit}%` : op === "endsWith" ? `%${lit}` : `%${lit}%`;
|
|
961
|
+
parts.push(`${q} ${op === "icontains" ? "ILIKE" : "LIKE"} ${bind(pattern)}`);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (op === "isNull") {
|
|
965
|
+
if (typeof v !== "boolean") {
|
|
966
|
+
throw new Error(`${caller}(${table}): where.${col}.isNull bir boolean olmal\u0131`);
|
|
967
|
+
}
|
|
968
|
+
parts.push(`${q} IS ${v ? "NULL" : "NOT NULL"}`);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
703
971
|
if (op === "in") {
|
|
704
972
|
if (!Array.isArray(v)) throw new Error(`${caller}(${table}): where.${col}.in bir dizi olmal\u0131`);
|
|
973
|
+
if (v.some(isColRef)) {
|
|
974
|
+
throw new Error(`${caller}(${table}): where.${col}.in col() ile kullan\u0131lamaz \u2014 kolon kar\u015F\u0131la\u015Ft\u0131rmas\u0131 i\xE7in \xE7\u0131plak bi\xE7imi ({ ${col}: col("\u2026") }) ya da gt/gte/lt/lte/neq kullan\u0131n`);
|
|
975
|
+
}
|
|
705
976
|
if (v.length === 0) {
|
|
706
977
|
parts.push("false");
|
|
707
978
|
continue;
|
|
708
979
|
}
|
|
709
980
|
parts.push(`${q} IN (${v.map((x) => bind(x)).join(", ")})`);
|
|
981
|
+
} else if (isNowExpr(v)) {
|
|
982
|
+
if (!(op in WHERE_OPS)) {
|
|
983
|
+
throw new Error(`${caller}(${table}): where.${col}.${op} now() ile kullan\u0131lamaz \u2014 e\u015Fitlik i\xE7in \xE7\u0131plak bi\xE7imi kullan\u0131n: { ${col}: now() }`);
|
|
984
|
+
}
|
|
985
|
+
parts.push(`${q} ${WHERE_OPS[op]} now()`);
|
|
986
|
+
} else if (isColumnExpr(v)) {
|
|
987
|
+
throw new Error(`${caller}(${table}): where.${col}.${op} bir YAZMA ifadesi ald\u0131 (increment/decrement) \u2014 kar\u015F\u0131la\u015Ft\u0131rma de\u011Feri de\u011Fil. Sunucu saati i\xE7in now() kullan\u0131n; say\u0131sal bir e\u015Fik i\xE7in de\u011Feri yaz\u0131n.`);
|
|
988
|
+
} else if (isColRef(v)) {
|
|
989
|
+
if (!(op in WHERE_OPS)) {
|
|
990
|
+
throw new Error(`${caller}(${table}): where.${col}.${op} col() ile kullan\u0131lamaz \u2014 e\u015Fitlik i\xE7in \xE7\u0131plak bi\xE7imi kullan\u0131n: { ${col}: col("\u2026") }`);
|
|
991
|
+
}
|
|
992
|
+
parts.push(`${q} ${WHERE_OPS[op]} ${colRefSql(table, colSet, v, caller, alias)}`);
|
|
993
|
+
} else if (looksLikeUnbrandedRef(v) !== null) {
|
|
994
|
+
const kind = looksLikeUnbrandedRef(v);
|
|
995
|
+
const made = kind === "col" ? "col()" : kind === "sql" ? "sqlFragment" : "now()";
|
|
996
|
+
const how = kind === "col" ? 'col("kolon_ad\u0131")' : kind === "sql" ? "sqlFragment`\u2026`" : "now()";
|
|
997
|
+
throw new Error(`${caller}(${table}): where.${col}.${op} bir ${made} TAKL\u0130D\u0130 ald\u0131 \u2014 bu nesne bu s\xFCre\xE7te ${made} ile \xFCretilmemi\u015F. \u0130stek g\xF6vdesinden gelen bir filtre i\u015Faret\xE7i TA\u015EIYAMAZ; ko\u015Fulu kendi kodunuzda ${how} ile kurun.`);
|
|
710
998
|
} else if (op in WHERE_OPS) {
|
|
711
|
-
if (v === null &&
|
|
712
|
-
parts.push(`${q} IS
|
|
999
|
+
if (v === null && op === "neq") {
|
|
1000
|
+
parts.push(`${q} IS NOT NULL`);
|
|
713
1001
|
continue;
|
|
714
1002
|
}
|
|
715
1003
|
parts.push(`${q} ${WHERE_OPS[op]} ${bind(v)}`);
|
|
@@ -723,8 +1011,14 @@ function compileWhere(table, colSet, where, add, caller = "search") {
|
|
|
723
1011
|
parts.push(`${q} = ${bind(cond)}`);
|
|
724
1012
|
}
|
|
725
1013
|
}
|
|
726
|
-
return parts.
|
|
1014
|
+
return parts.join(" AND ");
|
|
727
1015
|
}
|
|
1016
|
+
__name(compileWhereBare, "compileWhereBare");
|
|
1017
|
+
function compileWhere(table, colSet, where, add, caller = "search") {
|
|
1018
|
+
const bare = compileWhereBare(table, colSet, where, add, caller);
|
|
1019
|
+
return bare === "" ? "" : ` AND ${bare}`;
|
|
1020
|
+
}
|
|
1021
|
+
__name(compileWhere, "compileWhere");
|
|
728
1022
|
function onConflictTail(cols, conflict, action) {
|
|
729
1023
|
const target = `ON CONFLICT (${conflict.map(quoteIdent).join(", ")})`;
|
|
730
1024
|
if (action === "ignore") return `${target} DO NOTHING`;
|
|
@@ -732,26 +1026,22 @@ function onConflictTail(cols, conflict, action) {
|
|
|
732
1026
|
const sets = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
733
1027
|
return sets.length ? `${target} DO UPDATE SET ${sets.join(", ")}` : `${target} DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
734
1028
|
}
|
|
1029
|
+
__name(onConflictTail, "onConflictTail");
|
|
735
1030
|
var cachedVectorSchema = null;
|
|
736
1031
|
var cachedTrgmSchema = null;
|
|
737
1032
|
async function trgmSchemaOf(live) {
|
|
738
1033
|
if (cachedTrgmSchema !== null) return cachedTrgmSchema;
|
|
739
|
-
const rows = await live.unsafe(
|
|
740
|
-
"select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'"
|
|
741
|
-
);
|
|
1034
|
+
const rows = await live.unsafe("select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_trgm'");
|
|
742
1035
|
cachedTrgmSchema = rows?.[0]?.nspname ?? "public";
|
|
743
1036
|
return cachedTrgmSchema;
|
|
744
1037
|
}
|
|
1038
|
+
__name(trgmSchemaOf, "trgmSchemaOf");
|
|
745
1039
|
async function vectorSchemaWithGuc(runner) {
|
|
746
1040
|
if (cachedVectorSchema !== null) {
|
|
747
|
-
await runner.unsafe(
|
|
748
|
-
"select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true)"
|
|
749
|
-
);
|
|
1041
|
+
await runner.unsafe("select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true)");
|
|
750
1042
|
return cachedVectorSchema;
|
|
751
1043
|
}
|
|
752
|
-
const rows = await runner.unsafe(
|
|
753
|
-
"select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true), (select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'vector') as nspname"
|
|
754
|
-
);
|
|
1044
|
+
const rows = await runner.unsafe("select set_config('hnsw.iterative_scan','relaxed_order',true), set_config('hnsw.max_scan_tuples','200000',true), set_config('hnsw.ef_search','200',true), (select n.nspname from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'vector') as nspname");
|
|
755
1045
|
const name = rows?.[0]?.nspname;
|
|
756
1046
|
if (typeof name !== "string" || name === "") {
|
|
757
1047
|
throw new Error("pgvector extension kurulu de\u011Fil \u2014 vector aramas\u0131 \xE7al\u0131\u015Famaz (extensions beyan\u0131 deploy'dan ge\xE7ti mi?)");
|
|
@@ -759,11 +1049,13 @@ async function vectorSchemaWithGuc(runner) {
|
|
|
759
1049
|
cachedVectorSchema = name;
|
|
760
1050
|
return name;
|
|
761
1051
|
}
|
|
1052
|
+
__name(vectorSchemaWithGuc, "vectorSchemaWithGuc");
|
|
762
1053
|
var secretReader = null;
|
|
763
1054
|
function setSecretReader(fn) {
|
|
764
1055
|
secretReader = fn;
|
|
765
1056
|
}
|
|
766
|
-
|
|
1057
|
+
__name(setSecretReader, "setSecretReader");
|
|
1058
|
+
var embedFetch = /* @__PURE__ */ __name((url, init) => fetch(url, init), "embedFetch");
|
|
767
1059
|
async function embedQuery(embed, text) {
|
|
768
1060
|
if (secretReader === null) {
|
|
769
1061
|
throw new Error(`query embed: secret reader ba\u011Flanmam\u0131\u015F \u2014 ${embed.apiKeyName} okunam\u0131yor`);
|
|
@@ -778,11 +1070,18 @@ async function embedQuery(embed, text) {
|
|
|
778
1070
|
try {
|
|
779
1071
|
const res = await embedFetch(url, {
|
|
780
1072
|
method: "POST",
|
|
781
|
-
headers: {
|
|
1073
|
+
headers: {
|
|
1074
|
+
"Content-Type": "application/json",
|
|
1075
|
+
Authorization: `Bearer ${key}`
|
|
1076
|
+
},
|
|
782
1077
|
body: JSON.stringify({
|
|
783
1078
|
model: embed.model,
|
|
784
|
-
input: [
|
|
785
|
-
|
|
1079
|
+
input: [
|
|
1080
|
+
text
|
|
1081
|
+
],
|
|
1082
|
+
...embed.dimensions !== void 0 ? {
|
|
1083
|
+
dimensions: embed.dimensions
|
|
1084
|
+
} : {}
|
|
786
1085
|
}),
|
|
787
1086
|
signal: controller.signal
|
|
788
1087
|
});
|
|
@@ -799,9 +1098,13 @@ async function embedQuery(embed, text) {
|
|
|
799
1098
|
clearTimeout(timer);
|
|
800
1099
|
}
|
|
801
1100
|
}
|
|
1101
|
+
__name(embedQuery, "embedQuery");
|
|
802
1102
|
function createOps(tx) {
|
|
803
|
-
const at = () => resolveTx(tx);
|
|
1103
|
+
const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
|
|
804
1104
|
const ops = {
|
|
1105
|
+
// Generic dönüş, çağıranın İDDİASIDIR (FR-021). Motor her zaman satır objeleri
|
|
1106
|
+
// üretir; `T` onlara bir ad vermekten ibarettir ve verilmezse `unknown` kalır —
|
|
1107
|
+
// yani "bilmiyorum", ki ham SQL için doğru cevap odur.
|
|
805
1108
|
async query(sql, params = []) {
|
|
806
1109
|
const bound = params.map((p) => Array.isArray(p) ? encodePgArray(p) : p);
|
|
807
1110
|
return asWireRows(await (await at()).unsafe(sql, bound));
|
|
@@ -814,27 +1117,141 @@ function createOps(tx) {
|
|
|
814
1117
|
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data, "insert"));
|
|
815
1118
|
const inserted = rows[0];
|
|
816
1119
|
if (!inserted) {
|
|
817
|
-
throw new Error(
|
|
818
|
-
`insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
|
|
819
|
-
);
|
|
1120
|
+
throw new Error(`insert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`);
|
|
820
1121
|
}
|
|
821
1122
|
return asTableRow(table, inserted);
|
|
822
1123
|
},
|
|
823
1124
|
/**
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1125
|
+
* Bir ADI kilitle — satırı değil (FR-054).
|
|
1126
|
+
*
|
|
1127
|
+
* "Aynı anda tek bir fatura kapanışı koşsun" gibi, kilitlenecek satırı
|
|
1128
|
+
* OLMAYAN işler için. Anahtar bir metin; Postgres 64-bit tamsayı istediği
|
|
1129
|
+
* için `hashtextextended` ile hash'leniyor ve metin SQL'e hiç GİRMİYOR.
|
|
1130
|
+
*
|
|
1131
|
+
* YALNIZ transaction kapsamlı aile (`pg_advisory_xact_lock`). Oturum
|
|
1132
|
+
* kapsamlı sürüm bırakıldığı yerde KALIR: bağlantı havuza döner, kilit
|
|
1133
|
+
* üstünde kalır ve bir sonraki isteği — hiç bitmeyecek şekilde — bekletir.
|
|
1134
|
+
* Transaction kapsamlısı commit/rollback'te Postgres tarafından bırakılır,
|
|
1135
|
+
* yani unutulması imkânsız.
|
|
1136
|
+
*/
|
|
1137
|
+
async advisoryXactLock(key) {
|
|
1138
|
+
await (await at()).unsafe("select pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
1139
|
+
key
|
|
1140
|
+
]);
|
|
1141
|
+
},
|
|
1142
|
+
/**
|
|
1143
|
+
* Bu satırları KİLİTLE, ve sırayı KATMAN koysun (FR-035).
|
|
1144
|
+
*
|
|
1145
|
+
* D-017: katman sıralamayı değil, **sıralamayı mümkün kılan primitifi**
|
|
1146
|
+
* verir. Uygulama dokunacağı kümeyi önden bildirir; sırayı burası koyar.
|
|
1147
|
+
* Çağıranın verdiği sıraya güvenmek, iki çağıranın aynı iki satıra ters
|
|
1148
|
+
* sırayla girmesi demektir — ve deadlock'un bedeli ucuz değil:
|
|
1149
|
+
* `deadlock_timeout` varsayılanı **1 saniye**, yani kaybeden işlem bir
|
|
1150
|
+
* saniye bekledikten sonra hata alır.
|
|
1151
|
+
*
|
|
1152
|
+
* `FOR NO KEY UPDATE`, `updateMany`'nin CTE'siyle AYNI kilit modu: iki
|
|
1153
|
+
* yazma yolunun tek bir sırası ve tek bir modu olmak zorunda, yoksa biri
|
|
1154
|
+
* diğerini bekletir.
|
|
1155
|
+
*/
|
|
1156
|
+
async lockRows(table, ids) {
|
|
1157
|
+
const unique = [
|
|
1158
|
+
...new Set(ids)
|
|
1159
|
+
].sort();
|
|
1160
|
+
if (unique.length === 0) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const pk = primaryKeyOf(table);
|
|
1164
|
+
if (pk === null) {
|
|
1165
|
+
throw new Error(`lockRows(${table}): tek-kolon primary key bulunamad\u0131 \u2014 kilit s\u0131ras\u0131 bir anahtar ister (FR-035).`);
|
|
1166
|
+
}
|
|
1167
|
+
const params = [];
|
|
1168
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
1169
|
+
params.push(v);
|
|
1170
|
+
return `$${params.length}`;
|
|
1171
|
+
}, "add");
|
|
1172
|
+
const list = unique.map((id) => add(id)).join(", ");
|
|
1173
|
+
await (await at()).unsafe(`SELECT t.${quoteIdent(pk)} FROM ${quoteTable(table)} t WHERE t.${quoteIdent(pk)} IN (${list}) ORDER BY t.${quoteIdent(pk)} FOR NO KEY UPDATE`, params);
|
|
1174
|
+
},
|
|
1175
|
+
/**
|
|
1176
|
+
* Bir idempotency anahtarını SAHİPLEN (FR-033).
|
|
1177
|
+
*
|
|
1178
|
+
* `{ inserted: true, row }` → bu çağrı yazdı.
|
|
1179
|
+
* `{ inserted: false, row }` → başkası önce yazdı; dönen satır ONUN satırı.
|
|
1180
|
+
*
|
|
1181
|
+
* ÖLÇÜLDÜ (pg16, spec Research Appendix) — üç dalın üçü de farklı davranıyor:
|
|
1182
|
+
*
|
|
1183
|
+
* plain INSERT ikinci oturum 2121 ms BLOKLANIR, sonra 23505 alır;
|
|
1184
|
+
* birincinin satırı korunur ← istenen
|
|
1185
|
+
* ON CONFLICT DO UPDATE ikincinin verisi birincininkini EZER
|
|
1186
|
+
* ON CONFLICT DO NOTHING `INSERT 0 0` — sessiz; çağıran ne olduğunu bilemez
|
|
1187
|
+
*
|
|
1188
|
+
* Bu yüzden `ON CONFLICT` KULLANILMIYOR. Ödeme yolunda "ikinci çağrı
|
|
1189
|
+
* birincinin sonucunu görsün" idempotency'nin tanımıdır; ezmek ya da
|
|
1190
|
+
* susmak ikisi de yanlış cevap.
|
|
1191
|
+
*
|
|
1192
|
+
* INSERT bir SAVEPOINT içinde koşuyor: başarısız bir INSERT transaction'ı
|
|
1193
|
+
* ABORT eder (25P02) ve savepoint olmadan yakalama işe yaramaz — hata
|
|
1194
|
+
* yakalanır ama sonraki SELECT de "current transaction is aborted" alır.
|
|
1195
|
+
*/
|
|
1196
|
+
async claim(table, unique, extra = {}) {
|
|
1197
|
+
const keyCols = Object.keys(unique);
|
|
1198
|
+
if (keyCols.length === 0) {
|
|
1199
|
+
throw new Error(`claim(${table}): benzersiz alan verilmedi. claim, bir anahtar\u0131 sahiplenmektir; anahtar yoksa sahiplenecek bir \u015Fey de yok \u2014 insert(${table}, \u2026) kullan\u0131n.`);
|
|
1200
|
+
}
|
|
1201
|
+
assertUsableWriteValues("claim", table, keyCols, unique);
|
|
1202
|
+
const data = {
|
|
1203
|
+
...unique,
|
|
1204
|
+
...extra
|
|
1205
|
+
};
|
|
1206
|
+
const cols = Object.keys(data);
|
|
1207
|
+
const live = await at();
|
|
1208
|
+
try {
|
|
1209
|
+
return await live.savepoint(async (sp) => {
|
|
1210
|
+
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1211
|
+
const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`, asBindParams(table, cols, data, "claim"));
|
|
1212
|
+
if (!rows[0]) {
|
|
1213
|
+
throw new Error(`claim(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si).`);
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
inserted: true,
|
|
1217
|
+
row: asTableRow(table, rows[0])
|
|
1218
|
+
};
|
|
1219
|
+
});
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
if (!isUniqueViolation(e)) throw e;
|
|
1222
|
+
const params = [];
|
|
1223
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
1224
|
+
params.push(v);
|
|
1225
|
+
return `$${params.length}`;
|
|
1226
|
+
}, "add");
|
|
1227
|
+
const where = keyCols.map((c) => `t.${quoteIdent(c)} = ${add(unique[c])}`).join(" AND ");
|
|
1228
|
+
const found = await (await at()).unsafe(`SELECT * FROM ${quoteTable(table)} t WHERE ${where} LIMIT 1`, params);
|
|
1229
|
+
if (!found[0]) {
|
|
1230
|
+
const named = constraintOf(e);
|
|
1231
|
+
throw new Error(`claim(${table}): benzersizlik ihlali${named ? ` (${named})` : ""} yakaland\u0131 ama ${keyCols.join(", ")} ile sat\u0131r bulunamad\u0131. Ya ihlal BA\u015EKA bir unique k\u0131s\u0131ttan geliyor \u2014 o zaman claim() bu \xE7a\u011Fr\u0131 i\xE7in yanl\u0131\u015F ara\xE7 \u2014 ya da sat\u0131r var ve bir RLS policy'si onu bu \xE7a\u011F\u0131randan gizliyor.`);
|
|
1232
|
+
}
|
|
1233
|
+
return {
|
|
1234
|
+
inserted: false,
|
|
1235
|
+
row: asTableRow(table, found[0])
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
},
|
|
1239
|
+
/**
|
|
1240
|
+
* INSERT the row, or UPDATE it when it collides on `onConflict`.
|
|
1241
|
+
*
|
|
1242
|
+
* WHY IT IS AN OPERATION rather than a recipe. "Try the insert, catch the
|
|
1243
|
+
* unique violation, update instead" does not work here: a request runs in ONE
|
|
1244
|
+
* Postgres transaction, so the failed insert aborts it and every later
|
|
1245
|
+
* statement answers `current transaction is aborted`. A tenant measured that
|
|
1246
|
+
* as 7 of 8 concurrent requests returning 500, gave up on upsert, and had a
|
|
1247
|
+
* trigger create the row instead — a workaround that needs a new trigger for
|
|
1248
|
+
* every table with a unique row.
|
|
1249
|
+
*
|
|
1250
|
+
* The conflict columns are excluded from the SET list: they are what MATCHED,
|
|
1251
|
+
* so writing them back is at best a no-op and at worst a surprise.
|
|
1252
|
+
*/
|
|
1253
|
+
async put(table, data, opts) {
|
|
1254
|
+
assertNotAppendOnly("put", table);
|
|
838
1255
|
const conflict = opts.onConflict;
|
|
839
1256
|
if (conflict.length === 0) {
|
|
840
1257
|
throw new Error(`upsert into ${table}: onConflict en az bir kolon ad\u0131 ister`);
|
|
@@ -845,33 +1262,29 @@ function createOps(tx) {
|
|
|
845
1262
|
const ph = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
846
1263
|
const insertSql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING *`;
|
|
847
1264
|
const live = await at();
|
|
848
|
-
const bindData = asBindParams(table, cols, data, "
|
|
1265
|
+
const bindData = asBindParams(table, cols, data, "put");
|
|
849
1266
|
const first = await live.unsafe(insertSql, bindData);
|
|
850
1267
|
if (first[0]) return asTableRow(table, first[0]);
|
|
851
1268
|
const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
|
|
852
1269
|
const condBind = conflict.map((c) => data[c]);
|
|
853
|
-
const cur = await live.unsafe(
|
|
854
|
-
`SELECT * FROM ${quoteTable(table)} WHERE ${condSql} AND "valid_to" IS NULL`,
|
|
855
|
-
condBind
|
|
856
|
-
);
|
|
1270
|
+
const cur = await live.unsafe(`SELECT * FROM ${quoteTable(table)} WHERE ${condSql} AND "valid_to" IS NULL`, condBind);
|
|
857
1271
|
const old = cur[0];
|
|
858
1272
|
if (old === void 0) {
|
|
859
1273
|
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)`);
|
|
860
1274
|
}
|
|
861
1275
|
const pk = Object.keys(old).includes("id") ? "id" : Object.keys(old)[0];
|
|
862
|
-
await live.unsafe(
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
);
|
|
1276
|
+
await live.unsafe(`UPDATE ${quoteTable(table)} SET "valid_to" = now() WHERE ${quoteIdent(pk)} = $1 AND "valid_to" IS NULL`, [
|
|
1277
|
+
old[pk]
|
|
1278
|
+
]);
|
|
866
1279
|
const second = await live.unsafe(insertSql, bindData);
|
|
867
1280
|
if (!second[0]) {
|
|
868
1281
|
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)`);
|
|
869
1282
|
}
|
|
870
1283
|
const fresh = second[0];
|
|
871
|
-
await live.unsafe(
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
);
|
|
1284
|
+
await live.unsafe(`UPDATE ${quoteTable(table)} SET "superseded_by" = $2 WHERE ${quoteIdent(pk)} = $1`, [
|
|
1285
|
+
old[pk],
|
|
1286
|
+
fresh[pk]
|
|
1287
|
+
]);
|
|
875
1288
|
return asTableRow(table, second[0]);
|
|
876
1289
|
}
|
|
877
1290
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
@@ -879,50 +1292,54 @@ function createOps(tx) {
|
|
|
879
1292
|
const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
|
|
880
1293
|
const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
|
|
881
1294
|
const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
|
|
882
|
-
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data, "
|
|
1295
|
+
const rows = await (await at()).unsafe(sql, asBindParams(table, cols, data, "put"));
|
|
883
1296
|
const row = rows[0];
|
|
884
1297
|
if (!row) {
|
|
885
|
-
throw new Error(
|
|
886
|
-
`upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`
|
|
887
|
-
);
|
|
1298
|
+
throw new Error(`upsert into ${table} returned no row \u2014 the write was rejected (an RLS policy, most likely).`);
|
|
888
1299
|
}
|
|
889
1300
|
return asTableRow(table, row);
|
|
890
1301
|
},
|
|
891
1302
|
async update(table, id, data) {
|
|
1303
|
+
assertNotAppendOnly("update", table);
|
|
892
1304
|
const cols = Object.keys(data);
|
|
893
1305
|
if (cols.length === 0) return ops.findById(table, id);
|
|
894
1306
|
const assignments = cols.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(", ");
|
|
895
1307
|
const sql = `UPDATE ${quoteTable(table)} SET ${assignments} WHERE id = $${cols.length + 1} RETURNING *`;
|
|
896
|
-
const rows = await (await at()).unsafe(sql, [
|
|
1308
|
+
const rows = await (await at()).unsafe(sql, [
|
|
1309
|
+
...asBindParams(table, cols, data, "update"),
|
|
1310
|
+
id
|
|
1311
|
+
]);
|
|
897
1312
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
898
1313
|
},
|
|
899
1314
|
async delete(table, id) {
|
|
900
|
-
|
|
1315
|
+
assertNotAppendOnly("delete", table);
|
|
1316
|
+
await (await at()).unsafe(`DELETE FROM ${quoteTable(table)} WHERE id = $1`, [
|
|
1317
|
+
id
|
|
1318
|
+
]);
|
|
901
1319
|
},
|
|
902
1320
|
/**
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1321
|
+
* Update every row the filter matches, in ONE statement.
|
|
1322
|
+
*
|
|
1323
|
+
* The capability was already here and only reachable from inside a
|
|
1324
|
+
* transaction plan (`tx.tables.x.updateWhere`, since 11.0.0). Outside it the
|
|
1325
|
+
* only door was `update(id, …)`, so "mark every unapproved row in this
|
|
1326
|
+
* household" was an N+1 loop or hand-written SQL — and hand-written SQL is
|
|
1327
|
+
* where the typed surface and RLS both stop helping.
|
|
1328
|
+
*
|
|
1329
|
+
* The filter language is `findMany`'s, compiled by the same `compileWhere`:
|
|
1330
|
+
* one filter language, or the two spellings drift.
|
|
1331
|
+
*
|
|
1332
|
+
* AN EMPTY FILTER IS REFUSED. `UPDATE … WHERE true` is a whole-table write,
|
|
1333
|
+
* and the shape that produces it by accident — a filter object built from
|
|
1334
|
+
* request input that happened to come back empty — is exactly the shape that
|
|
1335
|
+
* should not silently succeed. Callers who mean every row say so with a
|
|
1336
|
+
* predicate that is true for every row.
|
|
1337
|
+
*/
|
|
920
1338
|
async updateMany(table, where, set) {
|
|
1339
|
+
assertNotAppendOnly("updateMany", table);
|
|
921
1340
|
const cols = Object.keys(set);
|
|
922
1341
|
if (cols.length === 0) {
|
|
923
|
-
throw new Error(
|
|
924
|
-
`updateMany(${table}): nothing to set. An update with no columns is not a no-op worth pretending happened \u2014 pass the columns to write.`
|
|
925
|
-
);
|
|
1342
|
+
throw new Error(`updateMany(${table}): nothing to set. An update with no columns is not a no-op worth pretending happened \u2014 pass the columns to write.`);
|
|
926
1343
|
}
|
|
927
1344
|
const known = schemaColumns(table);
|
|
928
1345
|
if (known !== null) {
|
|
@@ -933,27 +1350,43 @@ function createOps(tx) {
|
|
|
933
1350
|
}
|
|
934
1351
|
}
|
|
935
1352
|
const params = [];
|
|
936
|
-
const add = (v) => {
|
|
1353
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
937
1354
|
params.push(v);
|
|
938
1355
|
return `$${params.length}`;
|
|
939
|
-
};
|
|
940
|
-
const assignments = cols.map((c) =>
|
|
1356
|
+
}, "add");
|
|
1357
|
+
const assignments = cols.map((c) => {
|
|
1358
|
+
const expr = columnExprOf(set[c]);
|
|
1359
|
+
if (expr !== null && expr.fn === "now") {
|
|
1360
|
+
return `${quoteIdent(c)} = now()`;
|
|
1361
|
+
}
|
|
1362
|
+
if (expr !== null) {
|
|
1363
|
+
const operator = expr.fn === "inc" ? "+" : "-";
|
|
1364
|
+
const t = transformsOf(currentSchema, table).get(c);
|
|
1365
|
+
const amount = t?.toDb === void 0 || expr.by === null || expr.by === void 0 ? expr.by : t.toDb(expr.by);
|
|
1366
|
+
return `${quoteIdent(c)} = ${quoteIdent(c)} ${operator} ${add(amount)}`;
|
|
1367
|
+
}
|
|
1368
|
+
return `${quoteIdent(c)} = ${add(asBindParams(table, [
|
|
1369
|
+
c
|
|
1370
|
+
], set, "updateMany")[0])}`;
|
|
1371
|
+
}).join(", ");
|
|
941
1372
|
const whereSql = compileWhere(table, known, where, add, "updateMany");
|
|
942
1373
|
assertHasPredicate(whereSql, "updateMany", table);
|
|
943
|
-
const
|
|
1374
|
+
const pk = primaryKeyOf(table);
|
|
1375
|
+
const sql = isSinglePkEquality(where, pk) || pk === null ? `UPDATE ${quoteTable(table)} AS t SET ${assignments} WHERE true${whereSql} RETURNING *` : `WITH locked AS (SELECT t.${quoteIdent(pk)} FROM ${quoteTable(table)} t WHERE true${whereSql} ORDER BY t.${quoteIdent(pk)} FOR NO KEY UPDATE) UPDATE ${quoteTable(table)} AS t SET ${assignments} FROM locked WHERE t.${quoteIdent(pk)} = locked.${quoteIdent(pk)} RETURNING t.*`;
|
|
944
1376
|
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
945
1377
|
},
|
|
946
1378
|
/**
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1379
|
+
* Delete every row the filter matches, in ONE statement; resolves to how
|
|
1380
|
+
* many went. Same filter language, same empty-filter refusal as
|
|
1381
|
+
* {@link updateMany} — and here the accident is worse.
|
|
1382
|
+
*/
|
|
951
1383
|
async deleteMany(table, where) {
|
|
1384
|
+
assertNotAppendOnly("deleteMany", table);
|
|
952
1385
|
const params = [];
|
|
953
|
-
const add = (v) => {
|
|
1386
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
954
1387
|
params.push(v);
|
|
955
1388
|
return `$${params.length}`;
|
|
956
|
-
};
|
|
1389
|
+
}, "add");
|
|
957
1390
|
const known = schemaColumns(table);
|
|
958
1391
|
const whereSql = compileWhere(table, known, where, add, "deleteMany");
|
|
959
1392
|
assertHasPredicate(whereSql, "deleteMany", table);
|
|
@@ -962,20 +1395,20 @@ function createOps(tx) {
|
|
|
962
1395
|
return rows.length;
|
|
963
1396
|
},
|
|
964
1397
|
/**
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1398
|
+
* How many rows match — the half of pagination `limit`/`offset` cannot
|
|
1399
|
+
* supply. Without it a page count is either a guess or "fetch everything and
|
|
1400
|
+
* read .length", and the second one is the scale risk this surface exists to
|
|
1401
|
+
* remove.
|
|
1402
|
+
*
|
|
1403
|
+
* An empty filter is legitimate HERE: counting a whole table is a read, and
|
|
1404
|
+
* reads do not destroy anything.
|
|
1405
|
+
*/
|
|
973
1406
|
async count(table, where = {}) {
|
|
974
1407
|
const params = [];
|
|
975
|
-
const add = (v) => {
|
|
1408
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
976
1409
|
params.push(v);
|
|
977
1410
|
return `$${params.length}`;
|
|
978
|
-
};
|
|
1411
|
+
}, "add");
|
|
979
1412
|
const known = schemaColumns(table);
|
|
980
1413
|
const whereSql = compileWhere(table, known, where, add, "count");
|
|
981
1414
|
const sql = `SELECT count(*) AS n FROM ${quoteTable(table)} t WHERE true${whereSql}`;
|
|
@@ -983,33 +1416,33 @@ function createOps(tx) {
|
|
|
983
1416
|
return Number(rows[0]?.n ?? 0);
|
|
984
1417
|
},
|
|
985
1418
|
async findById(table, id) {
|
|
986
|
-
const rows = await (await at()).unsafe(
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
);
|
|
1419
|
+
const rows = await (await at()).unsafe(`SELECT * FROM ${quoteTable(table)} WHERE id = $1`, [
|
|
1420
|
+
id
|
|
1421
|
+
]);
|
|
990
1422
|
return rows[0] ? asTableRow(table, rows[0]) : null;
|
|
991
1423
|
},
|
|
992
1424
|
async findMany(table, query = {}, opts = {}) {
|
|
993
1425
|
const params = [];
|
|
994
|
-
const add = (v) => {
|
|
1426
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
995
1427
|
params.push(v);
|
|
996
1428
|
return `$${params.length}`;
|
|
997
|
-
};
|
|
1429
|
+
}, "add");
|
|
998
1430
|
const known = schemaColumns(table);
|
|
999
1431
|
const whereSql = compileWhere(table, known, query, add, "findMany");
|
|
1000
1432
|
const order = orderClause(table, known, opts.orderBy);
|
|
1001
1433
|
const limit = limitClause(opts.limit);
|
|
1002
1434
|
const offset = offsetClause(opts.offset, opts.limit);
|
|
1003
|
-
const
|
|
1435
|
+
const cols = selectClause(table, known, opts.select);
|
|
1436
|
+
const sql = `SELECT ${cols} FROM ${quoteTable(table)} t WHERE true${whereSql}${order}${limit}${offset}`;
|
|
1004
1437
|
return asTableRows(table, await (await at()).unsafe(sql, params));
|
|
1005
1438
|
},
|
|
1006
1439
|
/**
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1440
|
+
* Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
|
|
1441
|
+
* (1/(50+rank), CLAIM-N4). Operatör şema-nitelikli (C-10, M-1); GUC
|
|
1442
|
+
* hnsw.iterative_scan=relaxed_order aynı tx'te set_config ile (CLAIM-N3 —
|
|
1443
|
+
* RLS/filtre altında LIMIT-altı dönüş açığını kapatır). Sorgu-anı embed
|
|
1444
|
+
* T024'te gelir; o zamana dek vector kolu yalnız params.vector ile koşar.
|
|
1445
|
+
*/
|
|
1013
1446
|
async search(table, params = {}) {
|
|
1014
1447
|
const cfg = searchConfigFor(table);
|
|
1015
1448
|
if (!cfg) {
|
|
@@ -1017,18 +1450,12 @@ function createOps(tx) {
|
|
|
1017
1450
|
}
|
|
1018
1451
|
const excl = params.__excludeIds ?? [];
|
|
1019
1452
|
if (params.facets !== void 0) {
|
|
1020
|
-
|
|
1021
|
-
if (!cfg.colSet.has(col)) {
|
|
1022
|
-
throw new Error(`search(${table}): facets kolonu "${col}" tabloda yok (FR-027)`);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1453
|
+
throw new Error(`search(${table}): facets art\u0131k search'\xFCn parametresi de\u011Fil \u2014 saya\xE7lar dizi \xFCst\xFCnde ta\u015F\u0131n\u0131nca JSON.stringify onlar\u0131 d\xFC\u015F\xFCr\xFCyordu (FR-058). Ba\u011F\u0131ms\u0131z \xE7a\u011F\u0131r\u0131n: facets({ facets: [...], where })`);
|
|
1025
1454
|
}
|
|
1026
1455
|
if (params.validity !== void 0 && cfg.validity !== true) {
|
|
1027
|
-
throw new Error(
|
|
1028
|
-
`search(${table}): validity parametresi verildi ama tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`
|
|
1029
|
-
);
|
|
1456
|
+
throw new Error(`search(${table}): validity parametresi verildi ama tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`);
|
|
1030
1457
|
}
|
|
1031
|
-
const validityFor = (adder) => cfg.validity === true ? validitySql(params.validity, adder) : "";
|
|
1458
|
+
const validityFor = /* @__PURE__ */ __name((adder) => cfg.validity === true ? validitySql(params.validity, adder) : "", "validityFor");
|
|
1032
1459
|
const rawLimit = params.limit ?? 20;
|
|
1033
1460
|
if (typeof rawLimit !== "number" || !Number.isFinite(rawLimit)) {
|
|
1034
1461
|
throw new Error(`search(${table}): limit sonlu bir say\u0131 olmal\u0131, ${String(rawLimit)} verildi (FR-013)`);
|
|
@@ -1074,15 +1501,13 @@ function createOps(tx) {
|
|
|
1074
1501
|
}
|
|
1075
1502
|
}
|
|
1076
1503
|
if (!wantTextC && qvC === null) {
|
|
1077
|
-
throw new Error(
|
|
1078
|
-
`search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
|
|
1079
|
-
);
|
|
1504
|
+
throw new Error(`search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`);
|
|
1080
1505
|
}
|
|
1081
1506
|
const bindC = [];
|
|
1082
|
-
const addC = (v) => {
|
|
1507
|
+
const addC = /* @__PURE__ */ __name((v) => {
|
|
1083
1508
|
bindC.push(v);
|
|
1084
1509
|
return `$${bindC.length}`;
|
|
1085
|
-
};
|
|
1510
|
+
}, "addC");
|
|
1086
1511
|
const userWhereC = compileWhere(table, cfg.colSet, params.where ?? {}, addC);
|
|
1087
1512
|
const whereC = userWhereC + excludeSql(cfg.pk, excl, addC) + validityFor(addC);
|
|
1088
1513
|
const parentJoinC = ` JOIN ${quoteTable(table)} t ON t.${quoteIdent(cfg.pk)} = c.${pidQ} WHERE true${whereC} AND `;
|
|
@@ -1096,10 +1521,7 @@ function createOps(tx) {
|
|
|
1096
1521
|
const op = METRIC_OPERATOR[ck.metric] ?? METRIC_OPERATOR.cosine;
|
|
1097
1522
|
let exactOrderC = "";
|
|
1098
1523
|
if (userWhereC !== "") {
|
|
1099
|
-
const probeRows = await liveC.unsafe(
|
|
1100
|
-
`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteTable(ck.table)} c${parentJoinC}c.embedding IS NOT NULL LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
|
|
1101
|
-
bindC.slice()
|
|
1102
|
-
);
|
|
1524
|
+
const probeRows = await liveC.unsafe(`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteTable(ck.table)} c${parentJoinC}c.embedding IS NOT NULL LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`, bindC.slice());
|
|
1103
1525
|
const n = probeRows?.[0]?.n;
|
|
1104
1526
|
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1105
1527
|
exactOrderC = " + 0.0";
|
|
@@ -1119,7 +1541,7 @@ function createOps(tx) {
|
|
|
1119
1541
|
const msC = params.minScore === void 0 ? "" : addC(params.minScore);
|
|
1120
1542
|
const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
1121
1543
|
const scoreC = scoreMul === "" ? "g._score" : `(g._score)${scoreMul}`;
|
|
1122
|
-
const assembleC = (kwIn, withKwn) => {
|
|
1544
|
+
const assembleC = /* @__PURE__ */ __name((kwIn, withKwn) => {
|
|
1123
1545
|
const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
|
|
1124
1546
|
let fusedSrc;
|
|
1125
1547
|
if (semC !== "" && kwIn !== "") {
|
|
@@ -1131,7 +1553,7 @@ function createOps(tx) {
|
|
|
1131
1553
|
}
|
|
1132
1554
|
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 ${quoteTable(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 ${quoteTable(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid`;
|
|
1133
1555
|
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}`;
|
|
1134
|
-
};
|
|
1556
|
+
}, "assembleC");
|
|
1135
1557
|
let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
|
|
1136
1558
|
if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
|
|
1137
1559
|
const tsch = await trgmSchemaOf(liveC);
|
|
@@ -1141,12 +1563,10 @@ function createOps(tx) {
|
|
|
1141
1563
|
rowsC = await liveC.unsafe(assembleC(trgmKwC, false), retryBindC);
|
|
1142
1564
|
}
|
|
1143
1565
|
for (const r of rowsC) delete r._kwn;
|
|
1144
|
-
const outC = asTableRows(table, rowsC).map((r) => ({
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1566
|
+
const outC = asTableRows(table, rowsC).map((r) => ({
|
|
1567
|
+
...r,
|
|
1568
|
+
blocks: r.blocks ?? []
|
|
1569
|
+
}));
|
|
1150
1570
|
logNoResults(table, params.query, params.mode, outC.length);
|
|
1151
1571
|
return outC;
|
|
1152
1572
|
}
|
|
@@ -1165,15 +1585,13 @@ function createOps(tx) {
|
|
|
1165
1585
|
}
|
|
1166
1586
|
const wantVector = leg !== null && qv !== null;
|
|
1167
1587
|
if (!wantText && !wantVector) {
|
|
1168
|
-
throw new Error(
|
|
1169
|
-
`search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`
|
|
1170
|
-
);
|
|
1588
|
+
throw new Error(`search(${table}): ko\u015Fulabilir kol yok \u2014 metin i\xE7in 'query' (FTS beyan\u0131 gerekir), semantik i\xE7in 'vector' verin (FR-015)`);
|
|
1171
1589
|
}
|
|
1172
1590
|
const bind = [];
|
|
1173
|
-
const add = (v) => {
|
|
1591
|
+
const add = /* @__PURE__ */ __name((v) => {
|
|
1174
1592
|
bind.push(v);
|
|
1175
1593
|
return `$${bind.length}`;
|
|
1176
|
-
};
|
|
1594
|
+
}, "add");
|
|
1177
1595
|
const userWhere = compileWhere(table, cfg.colSet, params.where ?? {}, add);
|
|
1178
1596
|
const whereSql = userWhere + excludeSql(cfg.pk, excl, add) + validityFor(add);
|
|
1179
1597
|
const live = await at();
|
|
@@ -1185,10 +1603,7 @@ function createOps(tx) {
|
|
|
1185
1603
|
const op = METRIC_OPERATOR[leg.metric] ?? METRIC_OPERATOR.cosine;
|
|
1186
1604
|
let exactOrder = "";
|
|
1187
1605
|
if (userWhere !== "") {
|
|
1188
|
-
const probeRows = await live.unsafe(
|
|
1189
|
-
`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteTable(table)} t WHERE t.${quoteIdent(leg.column)} IS NOT NULL${whereSql} LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`,
|
|
1190
|
-
bind.slice()
|
|
1191
|
-
);
|
|
1606
|
+
const probeRows = await live.unsafe(`SELECT count(*)::int AS n FROM (SELECT 1 FROM ${quoteTable(table)} t WHERE t.${quoteIdent(leg.column)} IS NOT NULL${whereSql} LIMIT ${SELECTIVITY_EXACT_THRESHOLD + 1}) s`, bind.slice());
|
|
1192
1607
|
const n = probeRows?.[0]?.n;
|
|
1193
1608
|
if (typeof n === "number" && n <= SELECTIVITY_EXACT_THRESHOLD) {
|
|
1194
1609
|
exactOrder = " + 0.0";
|
|
@@ -1209,7 +1624,7 @@ function createOps(tx) {
|
|
|
1209
1624
|
const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
|
|
1210
1625
|
const msP = params.minScore === void 0 ? "" : add(params.minScore);
|
|
1211
1626
|
const hlCol = params.highlight === true && wantText ? `, ts_headline('simple', ${ftsExprOf(cfg.ftsCols)}, websearch_to_tsquery('simple', ${qpRef})) AS _highlight` : "";
|
|
1212
|
-
const assemble = (kwIn, withKwn) => {
|
|
1627
|
+
const assemble = /* @__PURE__ */ __name((kwIn, withKwn) => {
|
|
1213
1628
|
const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
|
|
1214
1629
|
let inner;
|
|
1215
1630
|
let orderScore;
|
|
@@ -1226,7 +1641,7 @@ function createOps(tx) {
|
|
|
1226
1641
|
orderScore = "_score";
|
|
1227
1642
|
}
|
|
1228
1643
|
return msP === "" ? `${inner} ORDER BY ${orderScore} DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msP} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
|
|
1229
|
-
};
|
|
1644
|
+
}, "assemble");
|
|
1230
1645
|
let rows = await live.unsafe(assemble(kwSql, kwSql !== ""), bind);
|
|
1231
1646
|
if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
|
|
1232
1647
|
const tsch = await trgmSchemaOf(live);
|
|
@@ -1237,28 +1652,26 @@ function createOps(tx) {
|
|
|
1237
1652
|
rows = await live.unsafe(assemble(trgmKw, false), retryBind);
|
|
1238
1653
|
}
|
|
1239
1654
|
for (const r of rows) delete r._kwn;
|
|
1240
|
-
const out = asTableRows(table, rows).map((r) => ({
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1655
|
+
const out = asTableRows(table, rows).map((r) => ({
|
|
1656
|
+
...r,
|
|
1657
|
+
blocks: []
|
|
1658
|
+
}));
|
|
1246
1659
|
logNoResults(table, params.query, params.mode, out.length);
|
|
1247
1660
|
return out;
|
|
1248
1661
|
},
|
|
1249
1662
|
/**
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1663
|
+
* similar (T018, FR-022): "bu satıra benzeyenler". Hedef vektör DB'den
|
|
1664
|
+
* okunur — satır-modu satırın kendi kolonu, chunk-modu parent chunk'larının
|
|
1665
|
+
* şema-nitelikli avg'ı — ve ana arama search'ün vector yoluyla koşar;
|
|
1666
|
+
* kaynak satır sonuçtan düşer. İKİ tur BİLİNÇLİ (plan T018): target-CTE'li
|
|
1667
|
+
* tek SQL'de "id yok" ile "0 komşu" ayrılamazdı; +1 küçük turla id-yokluğu
|
|
1668
|
+
* adlandırılmış hataya çevrilir. Sonuç şekli search ile aynı (FR-015).
|
|
1669
|
+
*/
|
|
1257
1670
|
/** D-021 (FR-027 DX): sayaçlar BAĞIMSIZ op'la — search'ün dizi-üstü
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1671
|
+
* `_facets` özelliği JSON.stringify'da kaybolur (dizi özelliği), tenant
|
|
1672
|
+
* yanıtına koyunca sessizce yok olurdu. Ayrı dönüş ciddi bir sözleşmedir;
|
|
1673
|
+
* search'teki alan geriye-uyum için DURUR. where + validity default'u
|
|
1674
|
+
* sayaçlara da uygulanır (sayaç, kullanıcının gördüğü kümeyi anlatır). */
|
|
1262
1675
|
async facets(table, params) {
|
|
1263
1676
|
const cfg = searchConfigFor(table);
|
|
1264
1677
|
if (!cfg) {
|
|
@@ -1282,20 +1695,18 @@ function createOps(tx) {
|
|
|
1282
1695
|
let v;
|
|
1283
1696
|
if (cfg.chunk !== void 0) {
|
|
1284
1697
|
const sch = await vectorSchemaWithGuc(live);
|
|
1285
|
-
const rows = await live.unsafe(
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
);
|
|
1698
|
+
const rows = await live.unsafe(`SELECT ${quoteIdent(sch)}.avg(c.embedding) AS v FROM ${quoteTable(cfg.chunk.table)} c WHERE c.${quoteIdent(`parent_${cfg.pk}`)} = $1 AND c.embedding IS NOT NULL`, [
|
|
1699
|
+
id
|
|
1700
|
+
]);
|
|
1289
1701
|
v = rows?.[0]?.v;
|
|
1290
1702
|
} else {
|
|
1291
1703
|
const leg = pickLeg(table, cfg.legs, opts.using);
|
|
1292
1704
|
if (leg === null) {
|
|
1293
1705
|
throw new Error(`similar(${table}): tabloda vekt\xF6r kolonu yok \u2014 benzerlik semantik vekt\xF6r ister (FR-022)`);
|
|
1294
1706
|
}
|
|
1295
|
-
const rows = await live.unsafe(
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
);
|
|
1707
|
+
const rows = await live.unsafe(`SELECT t.${quoteIdent(leg.column)} AS v FROM ${quoteTable(table)} t WHERE t.${quoteIdent(cfg.pk)} = $1`, [
|
|
1708
|
+
id
|
|
1709
|
+
]);
|
|
1299
1710
|
v = rows?.[0]?.v;
|
|
1300
1711
|
}
|
|
1301
1712
|
if (typeof v !== "string") {
|
|
@@ -1312,18 +1723,20 @@ function createOps(tx) {
|
|
|
1312
1723
|
blocksPerRow: opts.blocksPerRow,
|
|
1313
1724
|
validity: opts.validity,
|
|
1314
1725
|
boost: opts.boost,
|
|
1315
|
-
__excludeIds: [
|
|
1726
|
+
__excludeIds: [
|
|
1727
|
+
id
|
|
1728
|
+
]
|
|
1316
1729
|
};
|
|
1317
1730
|
return ops.search(table, p);
|
|
1318
1731
|
},
|
|
1319
1732
|
/**
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1733
|
+
* recommend (T018, FR-023): positive/negative id kümelerinden öneri.
|
|
1734
|
+
* Hedef vektör DB-İÇİ CTE'lerle türer: pos = avg(embedding of positives),
|
|
1735
|
+
* negative varsa hedef = pos.v + (pos.v - neg.v) — pgvector avg agregası
|
|
1736
|
+
* ve +/- operatörleri ŞEMA-NİTELİKLİ (M-1: search_path'e güvenilmez).
|
|
1737
|
+
* Vektör matematiği istemciye inmez; bulunamayan positive/negative
|
|
1738
|
+
* adlandırılmış hatadır ve kaynak id'ler sonuçtan düşer.
|
|
1739
|
+
*/
|
|
1327
1740
|
async recommend(table, opts) {
|
|
1328
1741
|
const positive = opts?.positive;
|
|
1329
1742
|
if (!Array.isArray(positive) || positive.length === 0) {
|
|
@@ -1344,14 +1757,14 @@ function createOps(tx) {
|
|
|
1344
1757
|
const live = await at();
|
|
1345
1758
|
const sch = await vectorSchemaWithGuc(live);
|
|
1346
1759
|
const bind = [];
|
|
1347
|
-
const add = (x) => {
|
|
1760
|
+
const add = /* @__PURE__ */ __name((x) => {
|
|
1348
1761
|
bind.push(x);
|
|
1349
1762
|
return `$${bind.length}`;
|
|
1350
|
-
};
|
|
1351
|
-
const avgSel = (ids) => {
|
|
1763
|
+
}, "add");
|
|
1764
|
+
const avgSel = /* @__PURE__ */ __name((ids) => {
|
|
1352
1765
|
const inList = ids.map((x) => add(x)).join(", ");
|
|
1353
1766
|
return cfg.chunk !== void 0 ? `SELECT ${quoteIdent(sch)}.avg(c.embedding) AS v FROM ${quoteTable(cfg.chunk.table)} c WHERE c.${quoteIdent(`parent_${cfg.pk}`)} IN (${inList}) AND c.embedding IS NOT NULL` : `SELECT ${quoteIdent(sch)}.avg(t.${quoteIdent(leg.column)}) AS v FROM ${quoteTable(table)} t WHERE t.${quoteIdent(cfg.pk)} IN (${inList}) AND t.${quoteIdent(leg.column)} IS NOT NULL`;
|
|
1354
|
-
};
|
|
1767
|
+
}, "avgSel");
|
|
1355
1768
|
const targetSql = negative.length === 0 ? `WITH pos AS (${avgSel(positive)}) SELECT pos.v AS v, (pos.v IS NULL) AS pos_missing FROM pos` : `WITH pos AS (${avgSel(positive)}), neg AS (${avgSel(negative)}) SELECT (pos.v OPERATOR(${quoteIdent(sch)}.+) (pos.v OPERATOR(${quoteIdent(sch)}.-) neg.v)) AS v, (pos.v IS NULL) AS pos_missing, (neg.v IS NULL) AS neg_missing FROM pos, neg`;
|
|
1356
1769
|
const rows = await live.unsafe(targetSql, bind);
|
|
1357
1770
|
const r0 = rows?.[0];
|
|
@@ -1375,21 +1788,25 @@ function createOps(tx) {
|
|
|
1375
1788
|
blocksPerRow: opts.blocksPerRow,
|
|
1376
1789
|
validity: opts.validity,
|
|
1377
1790
|
boost: opts.boost,
|
|
1378
|
-
__excludeIds: [
|
|
1791
|
+
__excludeIds: [
|
|
1792
|
+
...positive,
|
|
1793
|
+
...negative
|
|
1794
|
+
]
|
|
1379
1795
|
};
|
|
1380
1796
|
return ops.search(table, p);
|
|
1381
1797
|
},
|
|
1382
1798
|
/**
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1799
|
+
* supersede (T021, FR-029, C-9): validity'li tabloda satırın YENİ
|
|
1800
|
+
* versiyonunu TEK savepoint'te yazar — eski satır kapatılır
|
|
1801
|
+
* (valid_to = now(), superseded_by = yeni pk), yeni satır eklenir, dönüş
|
|
1802
|
+
* yeni satırdır. Yeni pk CLIENT'ta üretilir (row'da verilmemişse
|
|
1803
|
+
* randomUUID — declared şemaların uuid().defaultRandom() standardı) ki
|
|
1804
|
+
* kapatma UPDATE'i INSERT'ten ÖNCE koşabilsin: 0 satır = zaten superseded
|
|
1805
|
+
* ya da yok → INSERT hiç denenmez; INSERT hatası ise savepoint'le
|
|
1806
|
+
* kapatmayı da geri sarar — yarım supersede diye bir durum yoktur.
|
|
1807
|
+
*/
|
|
1392
1808
|
async supersede(table, id, row) {
|
|
1809
|
+
assertNotAppendOnly("supersede", table);
|
|
1393
1810
|
const cfg = searchConfigFor(table);
|
|
1394
1811
|
if (cfg === null || cfg.validity !== true) {
|
|
1395
1812
|
throw new Error(`supersede(${table}): tablo validity beyan\u0131 ta\u015F\u0131m\u0131yor (FR-029)`);
|
|
@@ -1397,24 +1814,22 @@ function createOps(tx) {
|
|
|
1397
1814
|
const live = await at();
|
|
1398
1815
|
return live.savepoint(async (sp) => {
|
|
1399
1816
|
const newId = row[cfg.pk] ?? crypto.randomUUID();
|
|
1400
|
-
const closed = await sp.unsafe(
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
);
|
|
1817
|
+
const closed = await sp.unsafe(`UPDATE ${quoteTable(table)} SET "valid_to" = now(), "superseded_by" = $1 WHERE ${quoteIdent(cfg.pk)} = $2 AND "valid_to" IS NULL RETURNING ${quoteIdent(cfg.pk)}`, [
|
|
1818
|
+
newId,
|
|
1819
|
+
id
|
|
1820
|
+
]);
|
|
1404
1821
|
if (closed.length === 0) {
|
|
1405
1822
|
throw new Error(`supersede(${table}): id "${String(id)}" zaten superseded ya da yok (FR-029)`);
|
|
1406
1823
|
}
|
|
1407
|
-
const data = {
|
|
1824
|
+
const data = {
|
|
1825
|
+
...row,
|
|
1826
|
+
[cfg.pk]: newId
|
|
1827
|
+
};
|
|
1408
1828
|
const cols = Object.keys(data);
|
|
1409
1829
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
1410
|
-
const rows = await sp.unsafe(
|
|
1411
|
-
`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`,
|
|
1412
|
-
asBindParams(table, cols, data, "supersede")
|
|
1413
|
-
);
|
|
1830
|
+
const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) RETURNING *`, asBindParams(table, cols, data, "supersede"));
|
|
1414
1831
|
if (!rows[0]) {
|
|
1415
|
-
throw new Error(
|
|
1416
|
-
`supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`
|
|
1417
|
-
);
|
|
1832
|
+
throw new Error(`supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`);
|
|
1418
1833
|
}
|
|
1419
1834
|
return asTableRow(table, rows[0]);
|
|
1420
1835
|
});
|
|
@@ -1425,61 +1840,68 @@ function createOps(tx) {
|
|
|
1425
1840
|
return live.savepoint(async (sp) => cb(withTables(createOps(sp), currentSchema)));
|
|
1426
1841
|
},
|
|
1427
1842
|
/**
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1843
|
+
* Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
|
|
1844
|
+
* rolls back only what that handle wrote and the request can keep writing.
|
|
1845
|
+
*
|
|
1846
|
+
* WHY THE HANDLE IS AN ARGUMENT. The obvious shape — `attempt(async () => {
|
|
1847
|
+
* ... Database.$insert(...) ... })`, with no parameter — would have to point
|
|
1848
|
+
* the ambient `Database` at the savepoint for the duration, and a request is
|
|
1849
|
+
* concurrent with itself: `Promise.all([Database.$insert(a),
|
|
1850
|
+
* Database.$attempt(...)])` would put `a` inside the savepoint and roll it
|
|
1851
|
+
* back with it. Silent data loss, and the same interleaving this file already
|
|
1852
|
+
* refuses for `asService()`. Passing the handle makes the boundary something
|
|
1853
|
+
* you can see in the code that crosses it.
|
|
1854
|
+
*
|
|
1855
|
+
* Postgres, not us: the savepoint is released on success and rolled back on
|
|
1856
|
+
* failure by the driver, so an aborted statement inside `fn` does not poison
|
|
1857
|
+
* the surrounding transaction.
|
|
1858
|
+
*/
|
|
1444
1859
|
async attempt(fn) {
|
|
1445
1860
|
const live = await at();
|
|
1446
1861
|
return live.savepoint(async (sp) => fn(withTables(createOps(sp), currentSchema)));
|
|
1447
1862
|
},
|
|
1448
1863
|
/**
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1864
|
+
* Execute a whole transaction plan — what `Database.$transaction(fn)` builds.
|
|
1865
|
+
*
|
|
1866
|
+
* WHY IT RUNS HERE. The platform used to carry a complete implementation
|
|
1867
|
+
* of this at `/internal-api/db/tx`, for tenant code that ran in an isolate
|
|
1868
|
+
* with no connection of its own. Running the plan there means running it on
|
|
1869
|
+
* a DIFFERENT connection: a transaction would not see the uncommitted
|
|
1870
|
+
* writes of the request that started it, and the two would hold separate
|
|
1871
|
+
* RLS bindings of the same identity. In this stack the tenant's code and
|
|
1872
|
+
* the connection share a process, so the plan runs on the request's own
|
|
1873
|
+
* transaction inside one SAVEPOINT — and that surface was removed on
|
|
1874
|
+
* 2026-08-15, once this was the last thing that could have called it.
|
|
1875
|
+
*
|
|
1876
|
+
* Until 2026-08-15 it ran NOWHERE: `runTxPlan` called `transport.txPlan` and
|
|
1877
|
+
* nothing here implemented it, so a live handler answered
|
|
1878
|
+
* "transport.txPlan is not a function" while every test that covered
|
|
1879
|
+
* transactions passed against a mock that did implement it.
|
|
1880
|
+
*/
|
|
1466
1881
|
async txPlan(plan) {
|
|
1467
1882
|
const live = await at();
|
|
1468
1883
|
return live.savepoint(async (sp) => {
|
|
1469
1884
|
const results = [];
|
|
1470
1885
|
for (const op of plan.ops) {
|
|
1471
1886
|
const rows = asTableRows(op.table, await runPlanOp(sp, op, results));
|
|
1472
|
-
const result = {
|
|
1887
|
+
const result = {
|
|
1888
|
+
rows,
|
|
1889
|
+
rows_affected: rows.length
|
|
1890
|
+
};
|
|
1473
1891
|
results.push(result);
|
|
1474
1892
|
assertGuard(op, result);
|
|
1475
1893
|
}
|
|
1476
|
-
return {
|
|
1894
|
+
return {
|
|
1895
|
+
results
|
|
1896
|
+
};
|
|
1477
1897
|
});
|
|
1478
1898
|
}
|
|
1479
1899
|
};
|
|
1480
1900
|
return ops;
|
|
1481
1901
|
}
|
|
1902
|
+
__name(createOps, "createOps");
|
|
1482
1903
|
var currentSchema = {};
|
|
1904
|
+
var currentRelations = /* @__PURE__ */ new Map();
|
|
1483
1905
|
function setSchema(schemas) {
|
|
1484
1906
|
cachedVectorSchema = null;
|
|
1485
1907
|
cachedTrgmSchema = null;
|
|
@@ -1492,22 +1914,52 @@ function setSchema(schemas) {
|
|
|
1492
1914
|
tables[qualifiedTableKey(schemaName, table.name ?? key)] = table;
|
|
1493
1915
|
}
|
|
1494
1916
|
}
|
|
1495
|
-
currentSchema = {
|
|
1917
|
+
currentSchema = {
|
|
1918
|
+
tables
|
|
1919
|
+
};
|
|
1920
|
+
currentRelations = buildRelations(schemas.map((entry) => {
|
|
1921
|
+
const mod = entry;
|
|
1922
|
+
const def = (mod && "default" in mod ? mod.default : mod) ?? {};
|
|
1923
|
+
const tables2 = {};
|
|
1924
|
+
for (const [key, t] of Object.entries(def.tables ?? {})) {
|
|
1925
|
+
const table = t ?? {};
|
|
1926
|
+
const columns = {};
|
|
1927
|
+
for (const [cname, c] of Object.entries(table.columns ?? {})) {
|
|
1928
|
+
const cd = c ?? {};
|
|
1929
|
+
columns[cname] = "_def" in cd ? cd : {
|
|
1930
|
+
...cd,
|
|
1931
|
+
_def: cd
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
tables2[key] = {
|
|
1935
|
+
...table,
|
|
1936
|
+
name: table.name ?? key,
|
|
1937
|
+
columns
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
return {
|
|
1941
|
+
name: def.name ?? "public",
|
|
1942
|
+
tables: tables2
|
|
1943
|
+
};
|
|
1944
|
+
}));
|
|
1496
1945
|
}
|
|
1946
|
+
__name(setSchema, "setSchema");
|
|
1497
1947
|
function withTables(ops, schema = currentSchema) {
|
|
1498
|
-
const bind = (key) => ({
|
|
1499
|
-
insert: (data) => ops.insert(key, data),
|
|
1500
|
-
update: (id, data) => ops.update(key, id, data),
|
|
1501
|
-
delete: (id) => ops.delete(key, id),
|
|
1502
|
-
findById: (id) => ops.findById(key, id),
|
|
1503
|
-
findMany: (query, opts) => ops.findMany(key, query ?? {}, opts),
|
|
1504
|
-
|
|
1505
|
-
|
|
1948
|
+
const bind = /* @__PURE__ */ __name((key) => ({
|
|
1949
|
+
insert: /* @__PURE__ */ __name((data) => ops.insert(key, data), "insert"),
|
|
1950
|
+
update: /* @__PURE__ */ __name((id, data) => ops.update(key, id, data), "update"),
|
|
1951
|
+
delete: /* @__PURE__ */ __name((id) => ops.delete(key, id), "delete"),
|
|
1952
|
+
findById: /* @__PURE__ */ __name((id) => ops.findById(key, id), "findById"),
|
|
1953
|
+
findMany: /* @__PURE__ */ __name((query, opts) => ops.findMany(key, query ?? {}, opts), "findMany"),
|
|
1954
|
+
put: /* @__PURE__ */ __name((q) => ops.put(key, q.data, {
|
|
1955
|
+
onConflict: q.onConflict
|
|
1956
|
+
}), "put")
|
|
1957
|
+
}), "bind");
|
|
1506
1958
|
const tables = {};
|
|
1507
1959
|
for (const key of Object.keys(schema.tables ?? {})) {
|
|
1508
1960
|
if (!key.includes(".")) tables[key] = bind(key);
|
|
1509
1961
|
}
|
|
1510
|
-
const schemaOf = (name) => {
|
|
1962
|
+
const schemaOf = /* @__PURE__ */ __name((name) => {
|
|
1511
1963
|
const out = {};
|
|
1512
1964
|
const prefix = `${name}.`;
|
|
1513
1965
|
for (const key of Object.keys(schema.tables ?? {})) {
|
|
@@ -1517,25 +1969,31 @@ function withTables(ops, schema = currentSchema) {
|
|
|
1517
1969
|
out[key.slice(prefix.length)] = bind(key);
|
|
1518
1970
|
}
|
|
1519
1971
|
}
|
|
1520
|
-
return {
|
|
1521
|
-
|
|
1972
|
+
return {
|
|
1973
|
+
tables: out
|
|
1974
|
+
};
|
|
1975
|
+
}, "schemaOf");
|
|
1522
1976
|
const base = /* @__PURE__ */ Object.create(null);
|
|
1523
|
-
return Object.assign(base, ops, {
|
|
1977
|
+
return Object.assign(base, ops, {
|
|
1978
|
+
tables,
|
|
1979
|
+
schema: schemaOf
|
|
1980
|
+
});
|
|
1524
1981
|
}
|
|
1982
|
+
__name(withTables, "withTables");
|
|
1525
1983
|
var SERVICE_LOCK_TIMEOUT = "5s";
|
|
1526
1984
|
function isAbortedTransaction(e) {
|
|
1527
1985
|
const message = String(e?.message ?? "");
|
|
1528
1986
|
return sqlstateOf(e) === "25P02" || /current transaction is aborted/i.test(message);
|
|
1529
1987
|
}
|
|
1988
|
+
__name(isAbortedTransaction, "isAbortedTransaction");
|
|
1530
1989
|
function assertHasPredicate(whereSql, op, table) {
|
|
1531
1990
|
if (whereSql.trim().length > 0) return;
|
|
1532
|
-
throw new Error(
|
|
1533
|
-
`${op}(${table}): the filter compiled to no condition, so this would have written EVERY row. An empty filter \u2014 or an operator object with no operators in it, like { col: {} } \u2014 is refused. Name a condition.`
|
|
1534
|
-
);
|
|
1991
|
+
throw new Error(`${op}(${table}): the filter compiled to no condition, so this would have written EVERY row. An empty filter \u2014 or an operator object with no operators in it, like { col: {} } \u2014 is refused. Name a condition.`);
|
|
1535
1992
|
}
|
|
1993
|
+
__name(assertHasPredicate, "assertHasPredicate");
|
|
1536
1994
|
function sqlstateOf(e) {
|
|
1537
1995
|
const err = e;
|
|
1538
|
-
const isState = (v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v);
|
|
1996
|
+
const isState = /* @__PURE__ */ __name((v) => typeof v === "string" && /^[0-9A-Z]{5}$/.test(v), "isState");
|
|
1539
1997
|
if (isState(err?.code)) return err.code;
|
|
1540
1998
|
if (isState(err?.errno)) return err.errno;
|
|
1541
1999
|
if (typeof err?.errno === "number") {
|
|
@@ -1544,46 +2002,48 @@ function sqlstateOf(e) {
|
|
|
1544
2002
|
}
|
|
1545
2003
|
return void 0;
|
|
1546
2004
|
}
|
|
2005
|
+
__name(sqlstateOf, "sqlstateOf");
|
|
1547
2006
|
function isForeignKeyViolation(e) {
|
|
1548
2007
|
return sqlstateOf(e) === "23503";
|
|
1549
2008
|
}
|
|
2009
|
+
__name(isForeignKeyViolation, "isForeignKeyViolation");
|
|
1550
2010
|
function isUniqueViolation(e) {
|
|
1551
|
-
return sqlstateOf(e) === "23505";
|
|
2011
|
+
return sqlstateOf(e) === "23505" || UniqueViolation.is(e);
|
|
1552
2012
|
}
|
|
2013
|
+
__name(isUniqueViolation, "isUniqueViolation");
|
|
1553
2014
|
function constraintOf(e) {
|
|
1554
2015
|
const named = e?.constraint;
|
|
1555
2016
|
if (typeof named === "string" && named.length > 0) return named;
|
|
1556
2017
|
const message = String(e?.message ?? "");
|
|
1557
2018
|
return /violates unique constraint "([^"]+)"/.exec(message)?.[1] ?? "";
|
|
1558
2019
|
}
|
|
2020
|
+
__name(constraintOf, "constraintOf");
|
|
1559
2021
|
function isLockTimeout(e) {
|
|
1560
2022
|
const message = String(e?.message ?? "");
|
|
1561
2023
|
return sqlstateOf(e) === "55P03" || /lock timeout/i.test(message);
|
|
1562
2024
|
}
|
|
2025
|
+
__name(isLockTimeout, "isLockTimeout");
|
|
1563
2026
|
function diagnosingDriver(sql, surface = "service") {
|
|
1564
|
-
const original = (e) => String(e?.message ?? e);
|
|
1565
|
-
const explain = (e) => {
|
|
2027
|
+
const original = /* @__PURE__ */ __name((e) => String(e?.message ?? e), "original");
|
|
2028
|
+
const explain = /* @__PURE__ */ __name((e) => {
|
|
1566
2029
|
if (isAbortedTransaction(e)) {
|
|
1567
|
-
return new Error(
|
|
1568
|
-
`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)})`
|
|
1569
|
-
);
|
|
2030
|
+
return new Error(`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.public.<t>.upsert(data, { onConflict: [...] })\` when the failure you were bracing for is a duplicate row. (${original(e)})`);
|
|
1570
2031
|
}
|
|
1571
2032
|
if (isUniqueViolation(e)) {
|
|
1572
2033
|
return markEngineRaised(new UniqueViolation(constraintOf(e)));
|
|
1573
2034
|
}
|
|
2035
|
+
const state = sqlstateOf(e);
|
|
2036
|
+
if (state === "40001") return markEngineRaised(new SerializationFailure(state));
|
|
2037
|
+
if (state === "40P01") return markEngineRaised(new DeadlockDetected(state));
|
|
1574
2038
|
if (surface === "service" && isForeignKeyViolation(e)) {
|
|
1575
|
-
return new Error(
|
|
1576
|
-
`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)})`
|
|
1577
|
-
);
|
|
2039
|
+
return new Error(`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)})`);
|
|
1578
2040
|
}
|
|
1579
2041
|
if (isLockTimeout(e)) {
|
|
1580
|
-
return new Error(
|
|
1581
|
-
`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)})`
|
|
1582
|
-
);
|
|
2042
|
+
return new Error(`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)})`);
|
|
1583
2043
|
}
|
|
1584
2044
|
return e;
|
|
1585
|
-
};
|
|
1586
|
-
const wrapTx = (tx) => ({
|
|
2045
|
+
}, "explain");
|
|
2046
|
+
const wrapTx = /* @__PURE__ */ __name((tx) => ({
|
|
1587
2047
|
async unsafe(text, params) {
|
|
1588
2048
|
try {
|
|
1589
2049
|
return await tx.unsafe(text, params);
|
|
@@ -1594,30 +2054,30 @@ function diagnosingDriver(sql, surface = "service") {
|
|
|
1594
2054
|
savepoint(cb) {
|
|
1595
2055
|
return tx.savepoint((sp) => cb(wrapTx(sp)));
|
|
1596
2056
|
}
|
|
1597
|
-
});
|
|
2057
|
+
}), "wrapTx");
|
|
1598
2058
|
return {
|
|
1599
|
-
unsafe: (text, params) => sql.unsafe(text, params),
|
|
1600
|
-
begin: (cb) => sql.begin((tx) => cb(wrapTx(tx)))
|
|
2059
|
+
unsafe: /* @__PURE__ */ __name((text, params) => sql.unsafe(text, params), "unsafe"),
|
|
2060
|
+
begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))), "begin")
|
|
1601
2061
|
};
|
|
1602
2062
|
}
|
|
2063
|
+
__name(diagnosingDriver, "diagnosingDriver");
|
|
1603
2064
|
function createRequestDatabase(sql, identity) {
|
|
1604
2065
|
const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson);
|
|
1605
2066
|
let serviceTx = null;
|
|
1606
2067
|
let serviceClient = null;
|
|
1607
|
-
const asService = () => {
|
|
2068
|
+
const asService = /* @__PURE__ */ __name(() => {
|
|
1608
2069
|
if (serviceClient === null) {
|
|
1609
|
-
serviceTx = createLazyTransaction(
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
identity.claimsJson,
|
|
1613
|
-
{ lockTimeout: SERVICE_LOCK_TIMEOUT }
|
|
1614
|
-
);
|
|
2070
|
+
serviceTx = createLazyTransaction(diagnosingDriver(sql, "service"), identity.serviceRole, identity.claimsJson, {
|
|
2071
|
+
lockTimeout: SERVICE_LOCK_TIMEOUT
|
|
2072
|
+
});
|
|
1615
2073
|
serviceClient = withTables(createOps(serviceTx));
|
|
1616
2074
|
}
|
|
1617
2075
|
return serviceClient;
|
|
1618
|
-
};
|
|
2076
|
+
}, "asService");
|
|
1619
2077
|
return {
|
|
1620
|
-
client: Object.assign(withTables(createOps(tx)), {
|
|
2078
|
+
client: Object.assign(withTables(createOps(tx)), {
|
|
2079
|
+
asService
|
|
2080
|
+
}),
|
|
1621
2081
|
async commit() {
|
|
1622
2082
|
await tx.commit();
|
|
1623
2083
|
await serviceTx?.commit();
|
|
@@ -1628,7 +2088,11 @@ function createRequestDatabase(sql, identity) {
|
|
|
1628
2088
|
}
|
|
1629
2089
|
};
|
|
1630
2090
|
}
|
|
1631
|
-
|
|
2091
|
+
__name(createRequestDatabase, "createRequestDatabase");
|
|
2092
|
+
var Args = class Args2 {
|
|
2093
|
+
static {
|
|
2094
|
+
__name(this, "Args");
|
|
2095
|
+
}
|
|
1632
2096
|
values = [];
|
|
1633
2097
|
bind(value) {
|
|
1634
2098
|
this.values.push(value);
|
|
@@ -1636,17 +2100,37 @@ var Args = class {
|
|
|
1636
2100
|
}
|
|
1637
2101
|
};
|
|
1638
2102
|
function isRef(v) {
|
|
1639
|
-
return
|
|
2103
|
+
return isPlanRef(v);
|
|
2104
|
+
}
|
|
2105
|
+
__name(isRef, "isRef");
|
|
2106
|
+
function refUnresolvedMessage(ref, results) {
|
|
2107
|
+
if (ref.op < 0 || ref.op >= results.length) {
|
|
2108
|
+
return `transaction plan: referans ${ref.op} numaral\u0131 i\u015Fleme bak\u0131yor ama planda ${results.length} i\u015Flem var (0\u2026${results.length - 1}). Referans yaln\u0131z KEND\u0130NDEN \xD6NCEK\u0130 bir i\u015Flemin sat\u0131r\u0131na bakabilir.`;
|
|
2109
|
+
}
|
|
2110
|
+
const row = results[ref.op]?.rows[0];
|
|
2111
|
+
if (!row) {
|
|
2112
|
+
return `transaction plan: ${ref.op} numaral\u0131 i\u015Flem H\u0130\xC7 SATIR d\xF6nd\xFCrmedi, o y\xFCzden "${ref.field}" okunam\u0131yor. Bir referans okunacak sat\u0131r\u0131n var oldu\u011Funu garanti etmez \u2014 \xF6nce beklentiyi bildirin (\`.expectOne(err)\`).`;
|
|
2113
|
+
}
|
|
2114
|
+
return `transaction plan: ${ref.op} numaral\u0131 i\u015Flemin sat\u0131r\u0131nda "${ref.field}" kolonu yok (d\xF6nen kolonlar: ${Object.keys(row).join(", ")}).`;
|
|
1640
2115
|
}
|
|
2116
|
+
__name(refUnresolvedMessage, "refUnresolvedMessage");
|
|
2117
|
+
function refuseUnbrandedRef(v, where) {
|
|
2118
|
+
if (looksLikeUnbrandedPlanRef(v)) {
|
|
2119
|
+
throw new Error(`${where}: bir plan referans\u0131 TAKL\u0130D\u0130 ald\u0131 ({ $ref: \u2026 }) \u2014 bu nesne bu plan\u0131n builder'\u0131 taraf\u0131ndan \xFCretilmemi\u015F. Referans yaln\u0131z kendi kodunuzda, \xF6nceki bir i\u015Flemin sat\u0131r\u0131ndan okunarak kurulur (\xF6r. insert(...).expectOne(err).id); istek g\xF6vdesinden gelen bir filtre referans TA\u015EIYAMAZ.`);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
__name(refuseUnbrandedRef, "refuseUnbrandedRef");
|
|
1641
2123
|
function isExpr(v) {
|
|
1642
2124
|
return typeof v === "object" && v !== null && "$expr" in v;
|
|
1643
2125
|
}
|
|
2126
|
+
__name(isExpr, "isExpr");
|
|
1644
2127
|
function renderValue(value, column, args, results, vectorCols, transforms) {
|
|
2128
|
+
refuseUnbrandedRef(value, `transaction plan (${column})`);
|
|
1645
2129
|
if (isRef(value)) {
|
|
1646
2130
|
const source = results[value.$ref.op];
|
|
1647
2131
|
const row = source?.rows[0];
|
|
1648
2132
|
if (!row || !(value.$ref.field in row)) {
|
|
1649
|
-
throw Object.assign(new Error(
|
|
2133
|
+
throw Object.assign(new Error(refUnresolvedMessage(value.$ref, results)), {
|
|
1650
2134
|
error_code: "tx_ref_unresolved"
|
|
1651
2135
|
});
|
|
1652
2136
|
}
|
|
@@ -1660,16 +2144,50 @@ function renderValue(value, column, args, results, vectorCols, transforms) {
|
|
|
1660
2144
|
}
|
|
1661
2145
|
return bindMaybeVector(args, column, vectorCols, value, transforms);
|
|
1662
2146
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2147
|
+
__name(renderValue, "renderValue");
|
|
2148
|
+
function resolveRefsDeep(value, results) {
|
|
2149
|
+
refuseUnbrandedRef(value, "transaction plan filtresi");
|
|
2150
|
+
if (isRef(value)) {
|
|
2151
|
+
const source = results[value.$ref.op];
|
|
2152
|
+
const row = source?.rows[0];
|
|
2153
|
+
if (!row || !(value.$ref.field in row)) {
|
|
2154
|
+
throw Object.assign(new Error(refUnresolvedMessage(value.$ref, results)), {
|
|
2155
|
+
error_code: "tx_ref_unresolved"
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
return row[value.$ref.field];
|
|
2159
|
+
}
|
|
2160
|
+
if (Array.isArray(value)) return value.map((v) => resolveRefsDeep(v, results));
|
|
2161
|
+
if (value !== null && typeof value === "object" && !isColRef(value) && !isSqlFragment(value) && !isExpr(value) && !(value instanceof Date)) {
|
|
2162
|
+
const out = {};
|
|
2163
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2164
|
+
out[k] = resolveRefsDeep(v, results);
|
|
2165
|
+
}
|
|
2166
|
+
return out;
|
|
2167
|
+
}
|
|
2168
|
+
return value;
|
|
2169
|
+
}
|
|
2170
|
+
__name(resolveRefsDeep, "resolveRefsDeep");
|
|
2171
|
+
function renderWhere(table, where, args, results) {
|
|
2172
|
+
return planWhere(table, where, args, results).sql;
|
|
2173
|
+
}
|
|
2174
|
+
__name(renderWhere, "renderWhere");
|
|
2175
|
+
function planWhere(table, where, args, results) {
|
|
2176
|
+
if (where === void 0 || Object.keys(where).length === 0) {
|
|
2177
|
+
return {
|
|
2178
|
+
sql: "",
|
|
2179
|
+
resolved: {}
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
const resolved = resolveRefsDeep(where, results);
|
|
2183
|
+
const known = schemaColumns(table);
|
|
2184
|
+
const bare = compileWhereBare(table, known, resolved, (v) => args.bind(v), "transaction plan");
|
|
2185
|
+
return {
|
|
2186
|
+
sql: bare === "" ? "" : ` AND ${bare}`,
|
|
2187
|
+
resolved
|
|
2188
|
+
};
|
|
1672
2189
|
}
|
|
2190
|
+
__name(planWhere, "planWhere");
|
|
1673
2191
|
function bindMaybeVector(args, column, vectorCols, value, transforms) {
|
|
1674
2192
|
if (column !== void 0 && vectorCols?.has(column) && Array.isArray(value)) {
|
|
1675
2193
|
return args.bind(toVectorLiteral(value));
|
|
@@ -1680,11 +2198,12 @@ function bindMaybeVector(args, column, vectorCols, value, transforms) {
|
|
|
1680
2198
|
}
|
|
1681
2199
|
return args.bind(value);
|
|
1682
2200
|
}
|
|
2201
|
+
__name(bindMaybeVector, "bindMaybeVector");
|
|
1683
2202
|
async function runPlanOp(sp, op, results) {
|
|
1684
2203
|
const opVectorCols = vectorColumnsOf(currentSchema, op.table);
|
|
1685
2204
|
const opTransforms = transformsOf(currentSchema, op.table);
|
|
1686
2205
|
const args = new Args();
|
|
1687
|
-
const table =
|
|
2206
|
+
const table = quoteTable(op.table);
|
|
1688
2207
|
let sql;
|
|
1689
2208
|
switch (op.op) {
|
|
1690
2209
|
case "insert": {
|
|
@@ -1694,6 +2213,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
1694
2213
|
break;
|
|
1695
2214
|
}
|
|
1696
2215
|
case "upsert": {
|
|
2216
|
+
assertNotAppendOnly("put", op.table);
|
|
1697
2217
|
const cols = Object.keys(op.values ?? {});
|
|
1698
2218
|
const conflict = op.onConflict ?? [];
|
|
1699
2219
|
if (cols.length === 0 || conflict.length === 0) {
|
|
@@ -1701,9 +2221,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
1701
2221
|
error_code: "tx_bad_upsert"
|
|
1702
2222
|
});
|
|
1703
2223
|
}
|
|
1704
|
-
const rendered = cols.map(
|
|
1705
|
-
(c) => renderValue(op.values[c], c, args, results, opVectorCols, opTransforms)
|
|
1706
|
-
);
|
|
2224
|
+
const rendered = cols.map((c) => renderValue(op.values[c], c, args, results, opVectorCols, opTransforms));
|
|
1707
2225
|
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
|
|
1708
2226
|
break;
|
|
1709
2227
|
}
|
|
@@ -1711,31 +2229,44 @@ async function runPlanOp(sp, op, results) {
|
|
|
1711
2229
|
const rows = op.rows ?? [];
|
|
1712
2230
|
if (rows.length === 0 || !rows[0]) return [];
|
|
1713
2231
|
const cols = Object.keys(rows[0]);
|
|
1714
|
-
const tuples = rows.map(
|
|
1715
|
-
(r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols, opTransforms)).join(", ")})`
|
|
1716
|
-
);
|
|
2232
|
+
const tuples = rows.map((r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols, opTransforms)).join(", ")})`);
|
|
1717
2233
|
const conflictCols = op.onConflict ?? [];
|
|
1718
2234
|
const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
|
|
1719
2235
|
sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
|
|
1720
2236
|
break;
|
|
1721
2237
|
}
|
|
1722
2238
|
case "update": {
|
|
2239
|
+
assertNotAppendOnly("updateWhere", op.table);
|
|
1723
2240
|
const cols = Object.keys(op.set ?? {});
|
|
1724
2241
|
if (cols.length === 0) throw new Error(`update ${op.table}: nothing to set`);
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2242
|
+
const knownSet = schemaColumns(op.table);
|
|
2243
|
+
for (const c of cols) {
|
|
2244
|
+
if (knownSet !== null && !knownSet.has(c)) {
|
|
2245
|
+
throw new Error(`transaction plan (${op.table}): set kolonu "${c}" tabloda yok`);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
const assignments = cols.map((c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results, opVectorCols, opTransforms)}`);
|
|
2249
|
+
const planned = planWhere(op.table, op.where, args, results);
|
|
2250
|
+
const planPk = primaryKeyOf(op.table);
|
|
2251
|
+
sql = isSinglePkEquality(planned.resolved, planPk) || planPk === null ? `UPDATE ${table} AS t SET ${assignments.join(", ")} WHERE true${planned.sql} RETURNING t.*` : `WITH locked AS (SELECT t.${quoteIdent(planPk)} FROM ${table} t WHERE true${planned.sql} ORDER BY t.${quoteIdent(planPk)} FOR NO KEY UPDATE) UPDATE ${table} AS t SET ${assignments.join(", ")} FROM locked WHERE t.${quoteIdent(planPk)} = locked.${quoteIdent(planPk)} RETURNING t.*`;
|
|
1729
2252
|
break;
|
|
1730
2253
|
}
|
|
1731
2254
|
case "delete": {
|
|
1732
|
-
|
|
2255
|
+
assertNotAppendOnly("deleteWhere", op.table);
|
|
2256
|
+
sql = `DELETE FROM ${table} AS t WHERE true${renderWhere(op.table, op.where, args, results)} RETURNING t.*`;
|
|
1733
2257
|
break;
|
|
1734
2258
|
}
|
|
1735
2259
|
case "select": {
|
|
1736
|
-
|
|
2260
|
+
let limit = "";
|
|
2261
|
+
if (op.limit !== void 0) {
|
|
2262
|
+
const n = Number(op.limit);
|
|
2263
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
2264
|
+
throw new Error(`transaction plan (${op.table}): limit bir tam say\u0131 olmal\u0131, "${String(op.limit)}" al\u0131nd\u0131`);
|
|
2265
|
+
}
|
|
2266
|
+
limit = ` LIMIT ${n}`;
|
|
2267
|
+
}
|
|
1737
2268
|
const lock = op.lock === "update" ? " FOR UPDATE" : "";
|
|
1738
|
-
sql = `SELECT
|
|
2269
|
+
sql = `SELECT t.* FROM ${table} t WHERE true${renderWhere(op.table, op.where, args, results)}${limit}${lock}`;
|
|
1739
2270
|
break;
|
|
1740
2271
|
}
|
|
1741
2272
|
default:
|
|
@@ -1743,6 +2274,7 @@ async function runPlanOp(sp, op, results) {
|
|
|
1743
2274
|
}
|
|
1744
2275
|
return await sp.unsafe(sql, args.values);
|
|
1745
2276
|
}
|
|
2277
|
+
__name(runPlanOp, "runPlanOp");
|
|
1746
2278
|
function assertGuard(op, result) {
|
|
1747
2279
|
const guard = op.guard;
|
|
1748
2280
|
if (!guard) return;
|
|
@@ -1754,6 +2286,7 @@ function assertGuard(op, result) {
|
|
|
1754
2286
|
slot: guard.slot
|
|
1755
2287
|
});
|
|
1756
2288
|
}
|
|
2289
|
+
__name(assertGuard, "assertGuard");
|
|
1757
2290
|
|
|
1758
2291
|
// src/engine/router.ts
|
|
1759
2292
|
var ROOM_META = /* @__PURE__ */ Symbol.for("palbase.backend.room");
|
|
@@ -1761,7 +2294,8 @@ var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta
|
|
|
1761
2294
|
function toSegments(path) {
|
|
1762
2295
|
return path.split("/").filter(Boolean).map((s) => s.startsWith("{") && s.endsWith("}") ? `:${s.slice(1, -1)}` : s);
|
|
1763
2296
|
}
|
|
1764
|
-
|
|
2297
|
+
__name(toSegments, "toSegments");
|
|
2298
|
+
function buildRouteTable(controllers, container) {
|
|
1765
2299
|
const table = [];
|
|
1766
2300
|
for (const Ctrl of controllers) {
|
|
1767
2301
|
if (Ctrl[ROOM_META] !== void 0) continue;
|
|
@@ -1771,12 +2305,9 @@ function buildRouteTable(controllers) {
|
|
|
1771
2305
|
const routes = getRoutes(Ctrl);
|
|
1772
2306
|
if (routes.length === 0) {
|
|
1773
2307
|
const name = Ctrl.name ?? "<anonymous>";
|
|
1774
|
-
throw new Error(
|
|
1775
|
-
`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.`
|
|
1776
|
-
);
|
|
2308
|
+
throw new Error(`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.`);
|
|
1777
2309
|
}
|
|
1778
|
-
|
|
1779
|
-
const instance = new ctor();
|
|
2310
|
+
const instance = container.get(Ctrl);
|
|
1780
2311
|
for (const r of routes) {
|
|
1781
2312
|
const full = `${basePath}${r.subpath ?? ""}` || "/";
|
|
1782
2313
|
table.push({
|
|
@@ -1794,6 +2325,7 @@ function buildRouteTable(controllers) {
|
|
|
1794
2325
|
}
|
|
1795
2326
|
return table;
|
|
1796
2327
|
}
|
|
2328
|
+
__name(buildRouteTable, "buildRouteTable");
|
|
1797
2329
|
function matchRoute(table, method, pathname) {
|
|
1798
2330
|
const parts = pathname.split("/").filter(Boolean);
|
|
1799
2331
|
for (const entry of table) {
|
|
@@ -1814,10 +2346,14 @@ function matchRoute(table, method, pathname) {
|
|
|
1814
2346
|
break;
|
|
1815
2347
|
}
|
|
1816
2348
|
}
|
|
1817
|
-
if (ok) return {
|
|
2349
|
+
if (ok) return {
|
|
2350
|
+
entry,
|
|
2351
|
+
params
|
|
2352
|
+
};
|
|
1818
2353
|
}
|
|
1819
2354
|
return null;
|
|
1820
2355
|
}
|
|
2356
|
+
__name(matchRoute, "matchRoute");
|
|
1821
2357
|
|
|
1822
2358
|
// src/engine/attest.ts
|
|
1823
2359
|
var HEADER_DEVICE_ID = "x-palbase-device-id";
|
|
@@ -1836,10 +2372,12 @@ async function bodyDigest(body) {
|
|
|
1836
2372
|
const buf = await crypto.subtle.digest("SHA-256", bytes);
|
|
1837
2373
|
return btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
1838
2374
|
}
|
|
2375
|
+
__name(bodyDigest, "bodyDigest");
|
|
1839
2376
|
function readCapable(req) {
|
|
1840
2377
|
const v = req.headers.get(HEADER_CAPABLE);
|
|
1841
2378
|
return v === "yes" || v === "no" ? v : "unknown";
|
|
1842
2379
|
}
|
|
2380
|
+
__name(readCapable, "readCapable");
|
|
1843
2381
|
function createAttestGate(deps) {
|
|
1844
2382
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
1845
2383
|
const ttl = deps.policyTtlMs ?? DEFAULT_POLICY_TTL_MS;
|
|
@@ -1850,7 +2388,9 @@ function createAttestGate(deps) {
|
|
|
1850
2388
|
if (Date.now() - cachedAt < ttl) return cachedMode;
|
|
1851
2389
|
try {
|
|
1852
2390
|
const res = await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/attest-policy`, {
|
|
1853
|
-
headers: {
|
|
2391
|
+
headers: {
|
|
2392
|
+
apikey: deps.serviceRoleKey
|
|
2393
|
+
},
|
|
1854
2394
|
signal: AbortSignal.timeout(policyTimeoutMs)
|
|
1855
2395
|
});
|
|
1856
2396
|
if (!res.ok) throw new Error(`attest-policy ${res.status}`);
|
|
@@ -1861,10 +2401,17 @@ function createAttestGate(deps) {
|
|
|
1861
2401
|
}
|
|
1862
2402
|
return cachedMode;
|
|
1863
2403
|
}
|
|
1864
|
-
|
|
2404
|
+
__name(mode, "mode");
|
|
2405
|
+
return /* @__PURE__ */ __name(async function gate(req, body, _ctx) {
|
|
1865
2406
|
const m = await mode();
|
|
1866
2407
|
if (m === "off") {
|
|
1867
|
-
return {
|
|
2408
|
+
return {
|
|
2409
|
+
outcome: "pass",
|
|
2410
|
+
reason: "",
|
|
2411
|
+
device: null,
|
|
2412
|
+
observation: null,
|
|
2413
|
+
nextChallenge: null
|
|
2414
|
+
};
|
|
1868
2415
|
}
|
|
1869
2416
|
const deviceId = req.headers.get(HEADER_DEVICE_ID) ?? "";
|
|
1870
2417
|
const payload = req.headers.get(HEADER_PAYLOAD) ?? "";
|
|
@@ -1877,14 +2424,23 @@ function createAttestGate(deps) {
|
|
|
1877
2424
|
};
|
|
1878
2425
|
if (m === "monitor") {
|
|
1879
2426
|
observation.attested = hasEvidence;
|
|
1880
|
-
return {
|
|
2427
|
+
return {
|
|
2428
|
+
outcome: "pass",
|
|
2429
|
+
reason: "",
|
|
2430
|
+
device: null,
|
|
2431
|
+
observation,
|
|
2432
|
+
nextChallenge: null
|
|
2433
|
+
};
|
|
1881
2434
|
}
|
|
1882
2435
|
const url = new URL(req.url);
|
|
1883
2436
|
let verdict;
|
|
1884
2437
|
try {
|
|
1885
2438
|
const res = await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/assert`, {
|
|
1886
2439
|
method: "POST",
|
|
1887
|
-
headers: {
|
|
2440
|
+
headers: {
|
|
2441
|
+
apikey: deps.serviceRoleKey,
|
|
2442
|
+
"content-type": "application/json"
|
|
2443
|
+
},
|
|
1888
2444
|
// A HUNG palauth must not become a hung tenant. Without this, a
|
|
1889
2445
|
// process that accepts the connection and never answers holds every
|
|
1890
2446
|
// request open until the client gives up — the gate would take the
|
|
@@ -1945,8 +2501,9 @@ function createAttestGate(deps) {
|
|
|
1945
2501
|
observation,
|
|
1946
2502
|
nextChallenge: verdict.next_challenge || null
|
|
1947
2503
|
};
|
|
1948
|
-
};
|
|
2504
|
+
}, "gate");
|
|
1949
2505
|
}
|
|
2506
|
+
__name(createAttestGate, "createAttestGate");
|
|
1950
2507
|
function createObservationSink(deps) {
|
|
1951
2508
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
1952
2509
|
const maxBatch = deps.maxBatch ?? 1e3;
|
|
@@ -1958,7 +2515,12 @@ function createObservationSink(deps) {
|
|
|
1958
2515
|
}, intervalMs);
|
|
1959
2516
|
timer.unref?.();
|
|
1960
2517
|
function bucketsOf(batch) {
|
|
1961
|
-
const counts = {
|
|
2518
|
+
const counts = {
|
|
2519
|
+
capable: 0,
|
|
2520
|
+
incapable: 0,
|
|
2521
|
+
unknown: 0,
|
|
2522
|
+
no_sdk: 0
|
|
2523
|
+
};
|
|
1962
2524
|
for (const o of batch) {
|
|
1963
2525
|
if (o.attested || o.capable === "yes") counts.capable += 1;
|
|
1964
2526
|
else if (o.capable === "no") counts.incapable += 1;
|
|
@@ -1967,6 +2529,7 @@ function createObservationSink(deps) {
|
|
|
1967
2529
|
}
|
|
1968
2530
|
return counts;
|
|
1969
2531
|
}
|
|
2532
|
+
__name(bucketsOf, "bucketsOf");
|
|
1970
2533
|
async function flush() {
|
|
1971
2534
|
if (buffer.length === 0) return;
|
|
1972
2535
|
const batch = buffer;
|
|
@@ -1975,7 +2538,10 @@ function createObservationSink(deps) {
|
|
|
1975
2538
|
try {
|
|
1976
2539
|
await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/observations`, {
|
|
1977
2540
|
method: "POST",
|
|
1978
|
-
headers: {
|
|
2541
|
+
headers: {
|
|
2542
|
+
apikey: deps.serviceRoleKey,
|
|
2543
|
+
"content-type": "application/json"
|
|
2544
|
+
},
|
|
1979
2545
|
body: JSON.stringify(bucketsOf(batch))
|
|
1980
2546
|
});
|
|
1981
2547
|
} catch {
|
|
@@ -1983,6 +2549,7 @@ function createObservationSink(deps) {
|
|
|
1983
2549
|
})();
|
|
1984
2550
|
await inFlight;
|
|
1985
2551
|
}
|
|
2552
|
+
__name(flush, "flush");
|
|
1986
2553
|
return {
|
|
1987
2554
|
record(o) {
|
|
1988
2555
|
buffer.push(o);
|
|
@@ -1994,6 +2561,7 @@ function createObservationSink(deps) {
|
|
|
1994
2561
|
}
|
|
1995
2562
|
};
|
|
1996
2563
|
}
|
|
2564
|
+
__name(createObservationSink, "createObservationSink");
|
|
1997
2565
|
|
|
1998
2566
|
// src/engine/upload.ts
|
|
1999
2567
|
var AUTHORIZE_PATH = "/__palbase/upload/authorize";
|
|
@@ -2001,10 +2569,12 @@ var SIGNATURE_HEADER = "x-palbase-upload-signature";
|
|
|
2001
2569
|
function renderPath(template, tokens) {
|
|
2002
2570
|
return template.replaceAll("{userId}", sanitizeSegment(tokens.userId ?? "anonymous")).replaceAll("{uploadId}", sanitizeSegment(tokens.uploadId)).replaceAll("{filename}", sanitizeSegment(tokens.filename ?? "file"));
|
|
2003
2571
|
}
|
|
2572
|
+
__name(renderPath, "renderPath");
|
|
2004
2573
|
function sanitizeSegment(raw) {
|
|
2005
2574
|
const cleaned = raw.replace(/[\x00-\x1F\x7F]/g, "").replace(/[/\\]/g, "-").replace(/\.{2,}/g, ".").replace(/^\.+/, "").trim();
|
|
2006
2575
|
return cleaned === "" ? "file" : cleaned.slice(0, 200);
|
|
2007
2576
|
}
|
|
2577
|
+
__name(sanitizeSegment, "sanitizeSegment");
|
|
2008
2578
|
function grantFor(entry, ctx, bucketLimits) {
|
|
2009
2579
|
const cfg = entry?.meta?.options?.uploadConfig;
|
|
2010
2580
|
if (!cfg) return null;
|
|
@@ -2020,12 +2590,16 @@ function grantFor(entry, ctx, bucketLimits) {
|
|
|
2020
2590
|
ownerUid: ctx.userId ?? null
|
|
2021
2591
|
};
|
|
2022
2592
|
}
|
|
2593
|
+
__name(grantFor, "grantFor");
|
|
2023
2594
|
var CompletionLedger = class {
|
|
2024
|
-
|
|
2025
|
-
this
|
|
2595
|
+
static {
|
|
2596
|
+
__name(this, "CompletionLedger");
|
|
2026
2597
|
}
|
|
2027
2598
|
capacity;
|
|
2028
2599
|
seen = /* @__PURE__ */ new Map();
|
|
2600
|
+
constructor(capacity = 1024) {
|
|
2601
|
+
this.capacity = capacity;
|
|
2602
|
+
}
|
|
2029
2603
|
recall(uploadId) {
|
|
2030
2604
|
return this.seen.get(uploadId);
|
|
2031
2605
|
}
|
|
@@ -2050,6 +2624,7 @@ function verifySignature(presented, expected) {
|
|
|
2050
2624
|
}
|
|
2051
2625
|
return diff === 0;
|
|
2052
2626
|
}
|
|
2627
|
+
__name(verifySignature, "verifySignature");
|
|
2053
2628
|
|
|
2054
2629
|
// src/engine/sse.ts
|
|
2055
2630
|
var SSE_CONTENT_TYPE = "text/event-stream";
|
|
@@ -2058,12 +2633,16 @@ function encodeFrame(value) {
|
|
|
2058
2633
|
|
|
2059
2634
|
`;
|
|
2060
2635
|
}
|
|
2636
|
+
__name(encodeFrame, "encodeFrame");
|
|
2061
2637
|
function encodeErrorFrame(requestId) {
|
|
2062
2638
|
return `event: error
|
|
2063
|
-
data: ${JSON.stringify({
|
|
2639
|
+
data: ${JSON.stringify({
|
|
2640
|
+
requestId
|
|
2641
|
+
})}
|
|
2064
2642
|
|
|
2065
2643
|
`;
|
|
2066
2644
|
}
|
|
2645
|
+
__name(encodeErrorFrame, "encodeErrorFrame");
|
|
2067
2646
|
function makeSseWriter(opts) {
|
|
2068
2647
|
let begun = false;
|
|
2069
2648
|
let chain = Promise.resolve();
|
|
@@ -2085,6 +2664,7 @@ function makeSseWriter(opts) {
|
|
|
2085
2664
|
}
|
|
2086
2665
|
};
|
|
2087
2666
|
}
|
|
2667
|
+
__name(makeSseWriter, "makeSseWriter");
|
|
2088
2668
|
|
|
2089
2669
|
// src/engine/fence.ts
|
|
2090
2670
|
var SECRET_ENV = [
|
|
@@ -2105,8 +2685,12 @@ function scrubSecrets(env) {
|
|
|
2105
2685
|
removed.push(name);
|
|
2106
2686
|
}
|
|
2107
2687
|
for (const name of Object.keys(env)) if (name.startsWith("PALBASE_VAR_")) kept.push(name);
|
|
2108
|
-
return {
|
|
2688
|
+
return {
|
|
2689
|
+
removed,
|
|
2690
|
+
kept
|
|
2691
|
+
};
|
|
2109
2692
|
}
|
|
2693
|
+
__name(scrubSecrets, "scrubSecrets");
|
|
2110
2694
|
function hostAllowed(host, allow) {
|
|
2111
2695
|
const h = host.toLowerCase();
|
|
2112
2696
|
for (const raw of allow) {
|
|
@@ -2119,40 +2703,49 @@ function hostAllowed(host, allow) {
|
|
|
2119
2703
|
}
|
|
2120
2704
|
return false;
|
|
2121
2705
|
}
|
|
2706
|
+
__name(hostAllowed, "hostAllowed");
|
|
2122
2707
|
function installEgressFence(policy) {
|
|
2123
2708
|
const original = globalThis.fetch.bind(globalThis);
|
|
2124
2709
|
const declared = policy.allow.length > 0;
|
|
2125
2710
|
const platform = (policy.alwaysAllow ?? []).map((h) => h.toLowerCase()).filter(Boolean);
|
|
2126
2711
|
if (!declared && policy.whenUndeclared === "allow") return original;
|
|
2127
|
-
const fenced = async (input, init) => {
|
|
2712
|
+
const fenced = /* @__PURE__ */ __name(async (input, init) => {
|
|
2128
2713
|
const url = typeof input === "string" ? new URL(input) : input instanceof URL ? input : new URL(input.url);
|
|
2129
2714
|
if (platform.includes(url.hostname.toLowerCase())) return original(input, init);
|
|
2130
2715
|
if (!declared || !hostAllowed(url.hostname, policy.allow)) {
|
|
2131
|
-
throw new Error(
|
|
2132
|
-
`egress denied: ${url.hostname} is not in this backend's declared allowlist. Add it to config/egress.ts and deploy.`
|
|
2133
|
-
);
|
|
2716
|
+
throw new Error(`egress denied: ${url.hostname} is not in this backend's declared allowlist. Add it to config/egress.ts and deploy.`);
|
|
2134
2717
|
}
|
|
2135
2718
|
if (policy.timeoutMs > 0) {
|
|
2136
2719
|
const controller = new AbortController();
|
|
2137
2720
|
const timer = setTimeout(() => controller.abort(), policy.timeoutMs);
|
|
2138
2721
|
try {
|
|
2139
|
-
return await original(input, {
|
|
2722
|
+
return await original(input, {
|
|
2723
|
+
...init,
|
|
2724
|
+
signal: init?.signal ?? controller.signal
|
|
2725
|
+
});
|
|
2140
2726
|
} finally {
|
|
2141
2727
|
clearTimeout(timer);
|
|
2142
2728
|
}
|
|
2143
2729
|
}
|
|
2144
2730
|
return original(input, init);
|
|
2145
|
-
};
|
|
2731
|
+
}, "fenced");
|
|
2146
2732
|
globalThis.fetch = fenced;
|
|
2147
2733
|
return original;
|
|
2148
2734
|
}
|
|
2735
|
+
__name(installEgressFence, "installEgressFence");
|
|
2149
2736
|
|
|
2150
2737
|
// src/engine/index.ts
|
|
2151
2738
|
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
2152
|
-
var JSON_HEADERS = {
|
|
2739
|
+
var JSON_HEADERS = {
|
|
2740
|
+
"content-type": "application/json"
|
|
2741
|
+
};
|
|
2153
2742
|
function fieldErrors(err) {
|
|
2154
|
-
return err.issues.map((i) => ({
|
|
2743
|
+
return err.issues.map((i) => ({
|
|
2744
|
+
field: i.path.join("."),
|
|
2745
|
+
message: i.message
|
|
2746
|
+
}));
|
|
2155
2747
|
}
|
|
2748
|
+
__name(fieldErrors, "fieldErrors");
|
|
2156
2749
|
function headersFor(raw, schema) {
|
|
2157
2750
|
const shape = schema.shape;
|
|
2158
2751
|
if (typeof shape !== "object" || shape === null) return raw;
|
|
@@ -2162,53 +2755,84 @@ function headersFor(raw, schema) {
|
|
|
2162
2755
|
if (lower === declared) continue;
|
|
2163
2756
|
const value = raw[lower];
|
|
2164
2757
|
if (value === void 0) continue;
|
|
2165
|
-
aliased ??= {
|
|
2758
|
+
aliased ??= {
|
|
2759
|
+
...raw
|
|
2760
|
+
};
|
|
2166
2761
|
aliased[declared] = value;
|
|
2167
2762
|
}
|
|
2168
2763
|
return aliased ?? raw;
|
|
2169
2764
|
}
|
|
2765
|
+
__name(headersFor, "headersFor");
|
|
2170
2766
|
function envelope(error, description, status, requestId, extra) {
|
|
2171
|
-
return new Response(
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2767
|
+
return new Response(JSON.stringify({
|
|
2768
|
+
error,
|
|
2769
|
+
error_description: description,
|
|
2770
|
+
status,
|
|
2771
|
+
request_id: requestId,
|
|
2772
|
+
...extra
|
|
2773
|
+
}), {
|
|
2774
|
+
status,
|
|
2775
|
+
headers: JSON_HEADERS
|
|
2776
|
+
});
|
|
2175
2777
|
}
|
|
2778
|
+
__name(envelope, "envelope");
|
|
2176
2779
|
function unavailable(name) {
|
|
2177
|
-
throw new Error(
|
|
2178
|
-
`${name} is unavailable. Either this backend was started without module clients (set MODULE_BASE_URL and the API keys so the engine can reach the module surface), or ${name} is not one of the modules this backend provides.`
|
|
2179
|
-
);
|
|
2780
|
+
throw new Error(`${name} is unavailable. Either this backend was started without module clients (set MODULE_BASE_URL and the API keys so the engine can reach the module surface), or ${name} is not one of the modules this backend provides.`);
|
|
2180
2781
|
}
|
|
2782
|
+
__name(unavailable, "unavailable");
|
|
2181
2783
|
function stubModule(name) {
|
|
2182
|
-
return new Proxy(
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
apply: () => unavailable(name)
|
|
2187
|
-
}
|
|
2188
|
-
);
|
|
2784
|
+
return new Proxy({}, {
|
|
2785
|
+
get: /* @__PURE__ */ __name(() => unavailable(name), "get"),
|
|
2786
|
+
apply: /* @__PURE__ */ __name(() => unavailable(name), "apply")
|
|
2787
|
+
});
|
|
2189
2788
|
}
|
|
2789
|
+
__name(stubModule, "stubModule");
|
|
2190
2790
|
async function defaultSqlDriver(config) {
|
|
2191
2791
|
const g = globalThis;
|
|
2192
2792
|
if (!g.Bun?.SQL) {
|
|
2193
|
-
throw new BootRefused(
|
|
2194
|
-
[],
|
|
2195
|
-
"boot refused: no SQL driver. Running outside Bun means the driver must be supplied \u2014 pass `sql` to createApp()."
|
|
2196
|
-
);
|
|
2793
|
+
throw new BootRefused([], "boot refused: no SQL driver. Running outside Bun means the driver must be supplied \u2014 pass `sql` to createApp().");
|
|
2197
2794
|
}
|
|
2198
|
-
return new g.Bun.SQL({
|
|
2795
|
+
return new g.Bun.SQL({
|
|
2796
|
+
url: config.databaseUrl,
|
|
2797
|
+
max: config.poolMax
|
|
2798
|
+
});
|
|
2199
2799
|
}
|
|
2200
|
-
|
|
2800
|
+
__name(defaultSqlDriver, "defaultSqlDriver");
|
|
2801
|
+
var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
|
|
2802
|
+
400,
|
|
2803
|
+
401,
|
|
2804
|
+
403,
|
|
2805
|
+
404,
|
|
2806
|
+
409,
|
|
2807
|
+
429
|
|
2808
|
+
]);
|
|
2201
2809
|
async function createApp(opts) {
|
|
2202
2810
|
const { config, controllers } = opts;
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
const
|
|
2811
|
+
const container = buildContainer();
|
|
2812
|
+
let released = false;
|
|
2813
|
+
const liveContainer = {
|
|
2814
|
+
get(t) {
|
|
2815
|
+
if (released) {
|
|
2816
|
+
throw new Error("this app has shut down \u2014 its container no longer resolves. A class resolved here would hold a closed database pool.");
|
|
2817
|
+
}
|
|
2818
|
+
return container.get(t);
|
|
2819
|
+
},
|
|
2820
|
+
owned: container.owned,
|
|
2821
|
+
pressure: container.pressure
|
|
2822
|
+
};
|
|
2823
|
+
assertNoOrphanEntryPoints(controllers, container.owned);
|
|
2824
|
+
const routes = buildRouteTable(controllers, liveContainer);
|
|
2206
2825
|
if (routes.length === 0) {
|
|
2207
2826
|
throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
|
|
2208
2827
|
}
|
|
2209
2828
|
const sql = opts.sql ?? await defaultSqlDriver(config);
|
|
2210
2829
|
await sql.unsafe("select 1");
|
|
2211
|
-
|
|
2830
|
+
setSchema(opts.schemas ?? []);
|
|
2831
|
+
setSecretReader(opts.secretReader ?? null);
|
|
2832
|
+
const auth = new AuthVerifier({
|
|
2833
|
+
jwksUrl: config.authJwksUrl,
|
|
2834
|
+
issuer: config.authIssuer
|
|
2835
|
+
});
|
|
2212
2836
|
const attestGate = createAttestGate({
|
|
2213
2837
|
moduleBaseUrl: config.moduleBaseUrl,
|
|
2214
2838
|
serviceRoleKey: config.serviceRoleKey
|
|
@@ -2242,6 +2866,7 @@ async function createApp(opts) {
|
|
|
2242
2866
|
Secrets: modules.Secrets ?? stubModule("Secrets")
|
|
2243
2867
|
};
|
|
2244
2868
|
}
|
|
2869
|
+
__name(buildServices, "buildServices");
|
|
2245
2870
|
async function runInServiceScope(fn) {
|
|
2246
2871
|
const db = createRequestDatabase(sql, {
|
|
2247
2872
|
role: config.dbRole,
|
|
@@ -2257,6 +2882,7 @@ async function createApp(opts) {
|
|
|
2257
2882
|
throw err;
|
|
2258
2883
|
}
|
|
2259
2884
|
}
|
|
2885
|
+
__name(runInServiceScope, "runInServiceScope");
|
|
2260
2886
|
async function handleAuthorize(req, requestId) {
|
|
2261
2887
|
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
2262
2888
|
return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
|
|
@@ -2280,15 +2906,14 @@ async function createApp(opts) {
|
|
|
2280
2906
|
filename: body.filename
|
|
2281
2907
|
});
|
|
2282
2908
|
if (!grant) {
|
|
2283
|
-
return envelope(
|
|
2284
|
-
"not_an_upload_route",
|
|
2285
|
-
`${body.method ?? "POST"} ${body.path} does not declare @Upload`,
|
|
2286
|
-
400,
|
|
2287
|
-
requestId
|
|
2288
|
-
);
|
|
2909
|
+
return envelope("not_an_upload_route", `${body.method ?? "POST"} ${body.path} does not declare @Upload`, 400, requestId);
|
|
2289
2910
|
}
|
|
2290
|
-
return new Response(JSON.stringify(grant), {
|
|
2911
|
+
return new Response(JSON.stringify(grant), {
|
|
2912
|
+
status: 200,
|
|
2913
|
+
headers: JSON_HEADERS
|
|
2914
|
+
});
|
|
2291
2915
|
}
|
|
2916
|
+
__name(handleAuthorize, "handleAuthorize");
|
|
2292
2917
|
async function handle(req) {
|
|
2293
2918
|
const requestId = `req_${crypto.randomUUID()}`;
|
|
2294
2919
|
const url = new URL(req.url);
|
|
@@ -2318,31 +2943,31 @@ async function createApp(opts) {
|
|
|
2318
2943
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2319
2944
|
const declared = Number(req.headers.get("content-length") ?? "");
|
|
2320
2945
|
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
2321
|
-
return new Response(
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
);
|
|
2946
|
+
return new Response(JSON.stringify({
|
|
2947
|
+
error: "payload_too_large",
|
|
2948
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
2949
|
+
status: 413,
|
|
2950
|
+
request_id: requestId
|
|
2951
|
+
}), {
|
|
2952
|
+
status: 413,
|
|
2953
|
+
headers: JSON_HEADERS
|
|
2954
|
+
});
|
|
2330
2955
|
}
|
|
2331
2956
|
const buf = await req.arrayBuffer().catch(() => null);
|
|
2332
2957
|
if (buf && buf.byteLength > MAX_BODY_BYTES) {
|
|
2333
|
-
return new Response(
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
);
|
|
2958
|
+
return new Response(JSON.stringify({
|
|
2959
|
+
error: "payload_too_large",
|
|
2960
|
+
error_description: `Request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
2961
|
+
status: 413,
|
|
2962
|
+
request_id: requestId
|
|
2963
|
+
}), {
|
|
2964
|
+
status: 413,
|
|
2965
|
+
headers: JSON_HEADERS
|
|
2966
|
+
});
|
|
2342
2967
|
}
|
|
2343
2968
|
if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
|
|
2344
2969
|
}
|
|
2345
|
-
const readParsedBody = () => {
|
|
2970
|
+
const readParsedBody = /* @__PURE__ */ __name(() => {
|
|
2346
2971
|
if (bodyRead) return parsedBody;
|
|
2347
2972
|
bodyRead = true;
|
|
2348
2973
|
if (!rawBody) {
|
|
@@ -2355,49 +2980,54 @@ async function createApp(opts) {
|
|
|
2355
2980
|
parsedBody = {};
|
|
2356
2981
|
}
|
|
2357
2982
|
return parsedBody;
|
|
2358
|
-
};
|
|
2359
|
-
const attest = await attestGate(req, rawBody, {
|
|
2983
|
+
}, "readParsedBody");
|
|
2984
|
+
const attest = await attestGate(req, rawBody, {
|
|
2985
|
+
userId
|
|
2986
|
+
});
|
|
2360
2987
|
if (attest.observation) observations.record(attest.observation);
|
|
2361
2988
|
if (attest.outcome === "reject") {
|
|
2362
|
-
const headers = {
|
|
2989
|
+
const headers = {
|
|
2990
|
+
...JSON_HEADERS
|
|
2991
|
+
};
|
|
2363
2992
|
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
2364
|
-
return new Response(
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
);
|
|
2993
|
+
return new Response(JSON.stringify({
|
|
2994
|
+
error: attest.reason,
|
|
2995
|
+
error_description: "This endpoint requires a verified app instance",
|
|
2996
|
+
status: 401,
|
|
2997
|
+
request_id: requestId
|
|
2998
|
+
}), {
|
|
2999
|
+
status: 401,
|
|
3000
|
+
headers
|
|
3001
|
+
});
|
|
2373
3002
|
}
|
|
2374
3003
|
if (attest.device && claims) attestedDevice = attest.device;
|
|
2375
3004
|
attestChallengeOut = attest.nextChallenge;
|
|
2376
|
-
const retryAfter = limiter.check(
|
|
2377
|
-
meta.options?.rateLimit,
|
|
2378
|
-
RateLimiter.key(hit.entry.id, userId, req.headers),
|
|
2379
|
-
Date.now()
|
|
2380
|
-
);
|
|
3005
|
+
const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
|
|
2381
3006
|
if (retryAfter !== null) {
|
|
2382
|
-
return new Response(
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
|
|
3007
|
+
return new Response(JSON.stringify({
|
|
3008
|
+
error: "too_many_requests",
|
|
3009
|
+
error_description: "Rate limit exceeded for this endpoint",
|
|
3010
|
+
status: 429,
|
|
3011
|
+
request_id: requestId,
|
|
3012
|
+
data: {
|
|
3013
|
+
retryAfter
|
|
3014
|
+
}
|
|
3015
|
+
}), {
|
|
3016
|
+
status: 429,
|
|
3017
|
+
headers: {
|
|
3018
|
+
...JSON_HEADERS,
|
|
3019
|
+
"retry-after": String(retryAfter)
|
|
3020
|
+
}
|
|
3021
|
+
});
|
|
2392
3022
|
}
|
|
2393
3023
|
let completionUploadId = null;
|
|
2394
3024
|
const args = [];
|
|
2395
3025
|
let sseEnqueue = null;
|
|
2396
|
-
let sseSettle = async () => {
|
|
2397
|
-
};
|
|
3026
|
+
let sseSettle = /* @__PURE__ */ __name(async () => {
|
|
3027
|
+
}, "sseSettle");
|
|
2398
3028
|
const sseWriter = makeSseWriter({
|
|
2399
|
-
enqueue: (chunk) => sseEnqueue?.(chunk),
|
|
2400
|
-
onFirstWrite: () => sseSettle()
|
|
3029
|
+
enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
|
|
3030
|
+
onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
|
|
2401
3031
|
});
|
|
2402
3032
|
for (const p of meta.params ?? []) {
|
|
2403
3033
|
switch (p.kind) {
|
|
@@ -2406,7 +3036,9 @@ async function createApp(opts) {
|
|
|
2406
3036
|
const r = p.schema.safeParse(parsedBody);
|
|
2407
3037
|
if (!r.success) {
|
|
2408
3038
|
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
2409
|
-
data: {
|
|
3039
|
+
data: {
|
|
3040
|
+
fields: fieldErrors(r.error)
|
|
3041
|
+
}
|
|
2410
3042
|
});
|
|
2411
3043
|
}
|
|
2412
3044
|
args[p.index] = r.data;
|
|
@@ -2416,7 +3048,9 @@ async function createApp(opts) {
|
|
|
2416
3048
|
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
2417
3049
|
if (!r.success) {
|
|
2418
3050
|
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
2419
|
-
data: {
|
|
3051
|
+
data: {
|
|
3052
|
+
fields: fieldErrors(r.error)
|
|
3053
|
+
}
|
|
2420
3054
|
});
|
|
2421
3055
|
}
|
|
2422
3056
|
args[p.index] = r.data;
|
|
@@ -2434,7 +3068,9 @@ async function createApp(opts) {
|
|
|
2434
3068
|
const r = p.schema.safeParse(headersFor(raw, p.schema));
|
|
2435
3069
|
if (!r.success) {
|
|
2436
3070
|
return envelope("bad_request", "Request headers failed validation", 400, requestId, {
|
|
2437
|
-
data: {
|
|
3071
|
+
data: {
|
|
3072
|
+
fields: fieldErrors(r.error)
|
|
3073
|
+
}
|
|
2438
3074
|
});
|
|
2439
3075
|
}
|
|
2440
3076
|
args[p.index] = r.data;
|
|
@@ -2457,12 +3093,7 @@ async function createApp(opts) {
|
|
|
2457
3093
|
break;
|
|
2458
3094
|
case "uploadedObject": {
|
|
2459
3095
|
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
2460
|
-
return envelope(
|
|
2461
|
-
"unauthorized",
|
|
2462
|
-
"This endpoint accepts uploads through storage, not directly",
|
|
2463
|
-
401,
|
|
2464
|
-
requestId
|
|
2465
|
-
);
|
|
3096
|
+
return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
|
|
2466
3097
|
}
|
|
2467
3098
|
readParsedBody();
|
|
2468
3099
|
const envelopeIn = parsedBody;
|
|
@@ -2475,7 +3106,9 @@ async function createApp(opts) {
|
|
|
2475
3106
|
if (already) {
|
|
2476
3107
|
return new Response(already.body, {
|
|
2477
3108
|
status: already.status,
|
|
2478
|
-
headers: already.contentType ? {
|
|
3109
|
+
headers: already.contentType ? {
|
|
3110
|
+
"content-type": already.contentType
|
|
3111
|
+
} : void 0
|
|
2479
3112
|
});
|
|
2480
3113
|
}
|
|
2481
3114
|
completionUploadId = uploadId;
|
|
@@ -2535,26 +3168,24 @@ async function createApp(opts) {
|
|
|
2535
3168
|
sseController = c;
|
|
2536
3169
|
}
|
|
2537
3170
|
});
|
|
2538
|
-
sseEnqueue = (chunk) => {
|
|
3171
|
+
sseEnqueue = /* @__PURE__ */ __name((chunk) => {
|
|
2539
3172
|
try {
|
|
2540
3173
|
sseController.enqueue(sseEncoder.encode(chunk));
|
|
2541
3174
|
} catch {
|
|
2542
3175
|
}
|
|
2543
|
-
};
|
|
3176
|
+
}, "sseEnqueue");
|
|
2544
3177
|
let sawFirstFrame;
|
|
2545
3178
|
const firstFrame = new Promise((r) => sawFirstFrame = r);
|
|
2546
|
-
sseSettle = async () => {
|
|
3179
|
+
sseSettle = /* @__PURE__ */ __name(async () => {
|
|
2547
3180
|
await db.commit();
|
|
2548
3181
|
sawFirstFrame();
|
|
2549
|
-
};
|
|
3182
|
+
}, "sseSettle");
|
|
2550
3183
|
const guarded = {
|
|
2551
3184
|
...services,
|
|
2552
3185
|
Database: new Proxy(services.Database, {
|
|
2553
3186
|
get(target, prop, recv) {
|
|
2554
3187
|
if (sseWriter.started()) {
|
|
2555
|
-
throw new Error(
|
|
2556
|
-
`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.`
|
|
2557
|
-
);
|
|
3188
|
+
throw new Error(`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.`);
|
|
2558
3189
|
}
|
|
2559
3190
|
return Reflect.get(target, prop, recv);
|
|
2560
3191
|
}
|
|
@@ -2570,23 +3201,23 @@ async function createApp(opts) {
|
|
|
2570
3201
|
}
|
|
2571
3202
|
const method = hit.entry.instance[meta.fnName];
|
|
2572
3203
|
if (typeof method !== "function") {
|
|
2573
|
-
throw new Error(
|
|
2574
|
-
`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
|
|
2575
|
-
);
|
|
3204
|
+
throw new Error(`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`);
|
|
2576
3205
|
}
|
|
2577
3206
|
return method.apply(hit.entry.instance, args);
|
|
2578
|
-
})).then(
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
3207
|
+
})).then(() => void 0, (err) => {
|
|
3208
|
+
thrown = err;
|
|
3209
|
+
});
|
|
3210
|
+
await Promise.race([
|
|
3211
|
+
firstFrame,
|
|
3212
|
+
ran
|
|
3213
|
+
]);
|
|
2585
3214
|
if (!sseWriter.started()) {
|
|
2586
3215
|
await ran;
|
|
2587
3216
|
await db.commit();
|
|
2588
3217
|
if (thrown !== null) throw thrown;
|
|
2589
|
-
return new Response(null, {
|
|
3218
|
+
return new Response(null, {
|
|
3219
|
+
status: 204
|
|
3220
|
+
});
|
|
2590
3221
|
}
|
|
2591
3222
|
void (async () => {
|
|
2592
3223
|
await ran;
|
|
@@ -2602,7 +3233,10 @@ async function createApp(opts) {
|
|
|
2602
3233
|
})();
|
|
2603
3234
|
return new Response(stream, {
|
|
2604
3235
|
status: 200,
|
|
2605
|
-
headers: {
|
|
3236
|
+
headers: {
|
|
3237
|
+
"content-type": SSE_CONTENT_TYPE,
|
|
3238
|
+
"cache-control": "no-cache"
|
|
3239
|
+
}
|
|
2606
3240
|
});
|
|
2607
3241
|
}
|
|
2608
3242
|
const result = await runWithRuntime(services, () => {
|
|
@@ -2614,9 +3248,7 @@ async function createApp(opts) {
|
|
|
2614
3248
|
}
|
|
2615
3249
|
const method = hit.entry.instance[meta.fnName];
|
|
2616
3250
|
if (typeof method !== "function") {
|
|
2617
|
-
throw new Error(
|
|
2618
|
-
`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
|
|
2619
|
-
);
|
|
3251
|
+
throw new Error(`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`);
|
|
2620
3252
|
}
|
|
2621
3253
|
return method.apply(hit.entry.instance, args);
|
|
2622
3254
|
});
|
|
@@ -2625,19 +3257,20 @@ async function createApp(opts) {
|
|
|
2625
3257
|
const v = meta.returnSchema.safeParse(result);
|
|
2626
3258
|
if (!v.success) {
|
|
2627
3259
|
log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
|
|
2628
|
-
return envelope(
|
|
2629
|
-
"output_invalid",
|
|
2630
|
-
"The handler returned a value its declared return type rejects",
|
|
2631
|
-
500,
|
|
2632
|
-
requestId
|
|
2633
|
-
);
|
|
3260
|
+
return envelope("output_invalid", "The handler returned a value its declared return type rejects", 500, requestId);
|
|
2634
3261
|
}
|
|
2635
3262
|
}
|
|
2636
3263
|
if (result === void 0 || result === null) {
|
|
2637
3264
|
if (completionUploadId) {
|
|
2638
|
-
completions.remember(completionUploadId, {
|
|
3265
|
+
completions.remember(completionUploadId, {
|
|
3266
|
+
status: 204,
|
|
3267
|
+
body: null,
|
|
3268
|
+
contentType: null
|
|
3269
|
+
});
|
|
2639
3270
|
}
|
|
2640
|
-
return new Response(null, {
|
|
3271
|
+
return new Response(null, {
|
|
3272
|
+
status: 204
|
|
3273
|
+
});
|
|
2641
3274
|
}
|
|
2642
3275
|
const payload = JSON.stringify(result);
|
|
2643
3276
|
if (completionUploadId) {
|
|
@@ -2649,7 +3282,10 @@ async function createApp(opts) {
|
|
|
2649
3282
|
}
|
|
2650
3283
|
return new Response(payload, {
|
|
2651
3284
|
status: 200,
|
|
2652
|
-
headers: attestChallengeOut ? {
|
|
3285
|
+
headers: attestChallengeOut ? {
|
|
3286
|
+
...JSON_HEADERS,
|
|
3287
|
+
"x-palbase-attest-challenge": attestChallengeOut
|
|
3288
|
+
} : JSON_HEADERS
|
|
2653
3289
|
});
|
|
2654
3290
|
} catch (err) {
|
|
2655
3291
|
await db.rollback(err);
|
|
@@ -2657,18 +3293,15 @@ async function createApp(opts) {
|
|
|
2657
3293
|
if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
|
|
2658
3294
|
log.error(`[engine] unhandled ${err.error} in ${hit.entry.id}`, err);
|
|
2659
3295
|
}
|
|
2660
|
-
return envelope(
|
|
2661
|
-
err.
|
|
2662
|
-
|
|
2663
|
-
err.status,
|
|
2664
|
-
requestId,
|
|
2665
|
-
err.data !== void 0 ? { data: err.data } : void 0
|
|
2666
|
-
);
|
|
3296
|
+
return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
|
|
3297
|
+
data: err.data
|
|
3298
|
+
} : void 0);
|
|
2667
3299
|
}
|
|
2668
3300
|
log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
|
|
2669
3301
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
2670
3302
|
}
|
|
2671
3303
|
}
|
|
3304
|
+
__name(handle, "handle");
|
|
2672
3305
|
const bootServices = buildServices(null);
|
|
2673
3306
|
let runShutdownHooks;
|
|
2674
3307
|
try {
|
|
@@ -2681,18 +3314,22 @@ async function createApp(opts) {
|
|
|
2681
3314
|
handle,
|
|
2682
3315
|
routes,
|
|
2683
3316
|
config,
|
|
3317
|
+
container: liveContainer,
|
|
2684
3318
|
runInServiceScope,
|
|
2685
3319
|
async shutdown() {
|
|
2686
3320
|
await runShutdownHooks();
|
|
2687
3321
|
await closeDriver(sql);
|
|
3322
|
+
released = true;
|
|
2688
3323
|
}
|
|
2689
3324
|
};
|
|
2690
3325
|
}
|
|
3326
|
+
__name(createApp, "createApp");
|
|
2691
3327
|
async function closeDriver(sql) {
|
|
2692
3328
|
const closable = sql;
|
|
2693
3329
|
await closable.close?.();
|
|
2694
3330
|
await closable.end?.();
|
|
2695
3331
|
}
|
|
3332
|
+
__name(closeDriver, "closeDriver");
|
|
2696
3333
|
|
|
2697
3334
|
export {
|
|
2698
3335
|
BootRefused,
|
|
@@ -2713,4 +3350,4 @@ export {
|
|
|
2713
3350
|
installEgressFence,
|
|
2714
3351
|
createApp
|
|
2715
3352
|
};
|
|
2716
|
-
//# sourceMappingURL=chunk-
|
|
3353
|
+
//# sourceMappingURL=chunk-EB3TUX5J.js.map
|