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