@palbase/backend 25.0.4 → 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 +2622 -1012
- 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-AILPKEK5.js → chunk-OO7R25AI.js} +1433 -563
- 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 +2562 -981
- 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 +34 -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 +402 -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 -70
- 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-AILPKEK5.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-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 -74
- /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);
|
|
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(", ")}).`;
|
|
1640
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,222 @@ 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");
|
|
2342
|
+
|
|
2343
|
+
// src/engine/attest.ts
|
|
2344
|
+
var HEADER_DEVICE_ID = "x-palbase-device-id";
|
|
2345
|
+
var HEADER_PAYLOAD = "x-palbase-attest-payload";
|
|
2346
|
+
var HEADER_SIGNATURE = "x-palbase-attest-signature";
|
|
2347
|
+
var HEADER_CHALLENGE = "x-palbase-attest-challenge";
|
|
2348
|
+
var HEADER_CAPABLE = "x-palbase-attest-capable";
|
|
2349
|
+
var HEADER_INSTALLATION = "x-palbase-installation";
|
|
2350
|
+
var HEADER_ANCHOR = "x-palbase-installation-anchor";
|
|
2351
|
+
var DEFAULT_POLICY_TTL_MS = 3e4;
|
|
2352
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 2e3;
|
|
2353
|
+
async function bodyDigest(body) {
|
|
2354
|
+
if (!body || body.byteLength === 0) return "";
|
|
2355
|
+
const bytes = new Uint8Array(new ArrayBuffer(body.byteLength));
|
|
2356
|
+
bytes.set(body);
|
|
2357
|
+
const buf = await crypto.subtle.digest("SHA-256", bytes);
|
|
2358
|
+
return btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
2359
|
+
}
|
|
2360
|
+
__name(bodyDigest, "bodyDigest");
|
|
2361
|
+
function readCapable(req) {
|
|
2362
|
+
const v = req.headers.get(HEADER_CAPABLE);
|
|
2363
|
+
return v === "yes" || v === "no" ? v : "unknown";
|
|
2364
|
+
}
|
|
2365
|
+
__name(readCapable, "readCapable");
|
|
2366
|
+
function createAttestGate(deps) {
|
|
2367
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
2368
|
+
const ttl = deps.policyTtlMs ?? DEFAULT_POLICY_TTL_MS;
|
|
2369
|
+
const policyTimeoutMs = deps.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
2370
|
+
let cachedMode = "off";
|
|
2371
|
+
let cachedAt = -Infinity;
|
|
2372
|
+
async function mode() {
|
|
2373
|
+
if (Date.now() - cachedAt < ttl) return cachedMode;
|
|
2374
|
+
try {
|
|
2375
|
+
const res = await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/attest-policy`, {
|
|
2376
|
+
headers: {
|
|
2377
|
+
apikey: deps.serviceRoleKey
|
|
2378
|
+
},
|
|
2379
|
+
signal: AbortSignal.timeout(policyTimeoutMs)
|
|
2380
|
+
});
|
|
2381
|
+
if (!res.ok) throw new Error(`attest-policy ${res.status}`);
|
|
2382
|
+
const body = await res.json();
|
|
2383
|
+
cachedMode = body.mode === "monitor" || body.mode === "enforce" ? body.mode : "off";
|
|
2384
|
+
cachedAt = Date.now();
|
|
2385
|
+
} catch {
|
|
2386
|
+
}
|
|
2387
|
+
return cachedMode;
|
|
2388
|
+
}
|
|
2389
|
+
__name(mode, "mode");
|
|
2390
|
+
return /* @__PURE__ */ __name(async function gate(req, body, _ctx) {
|
|
2391
|
+
const m = await mode();
|
|
2392
|
+
if (m === "off") {
|
|
2393
|
+
return {
|
|
2394
|
+
outcome: "pass",
|
|
2395
|
+
reason: "",
|
|
2396
|
+
device: null,
|
|
2397
|
+
observation: null,
|
|
2398
|
+
nextChallenge: null
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
const deviceId = req.headers.get(HEADER_DEVICE_ID) ?? "";
|
|
2402
|
+
const payload = req.headers.get(HEADER_PAYLOAD) ?? "";
|
|
2403
|
+
const signature = req.headers.get(HEADER_SIGNATURE) ?? "";
|
|
2404
|
+
const hasEvidence = deviceId !== "" && payload !== "" && signature !== "";
|
|
2405
|
+
const observation = {
|
|
2406
|
+
capable: readCapable(req),
|
|
2407
|
+
attested: false,
|
|
2408
|
+
installationId: req.headers.get(HEADER_INSTALLATION) || null
|
|
2409
|
+
};
|
|
2410
|
+
if (m === "monitor") {
|
|
2411
|
+
observation.attested = hasEvidence;
|
|
2412
|
+
return {
|
|
2413
|
+
outcome: "pass",
|
|
2414
|
+
reason: "",
|
|
2415
|
+
device: null,
|
|
2416
|
+
observation,
|
|
2417
|
+
nextChallenge: null
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
const url = new URL(req.url);
|
|
2421
|
+
let verdict;
|
|
2422
|
+
try {
|
|
2423
|
+
const res = await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/assert`, {
|
|
2424
|
+
method: "POST",
|
|
2425
|
+
headers: {
|
|
2426
|
+
apikey: deps.serviceRoleKey,
|
|
2427
|
+
"content-type": "application/json"
|
|
2428
|
+
},
|
|
2429
|
+
// A HUNG palauth must not become a hung tenant. Without this, a
|
|
2430
|
+
// process that accepts the connection and never answers holds every
|
|
2431
|
+
// request open until the client gives up — the gate would take the
|
|
2432
|
+
// whole stack down while claiming to protect it.
|
|
2433
|
+
signal: AbortSignal.timeout(policyTimeoutMs),
|
|
2434
|
+
body: JSON.stringify({
|
|
2435
|
+
installation_id: req.headers.get(HEADER_ANCHOR) ?? "",
|
|
2436
|
+
device_id: deviceId,
|
|
2437
|
+
challenge: req.headers.get(HEADER_CHALLENGE) ?? "",
|
|
2438
|
+
payload,
|
|
2439
|
+
signature,
|
|
2440
|
+
method: req.method,
|
|
2441
|
+
// pathname + search, NOT pathname alone.
|
|
2442
|
+
//
|
|
2443
|
+
// The client signs the path string it was given, and that string can
|
|
2444
|
+
// carry a query — the transport splits and reattaches one on purpose.
|
|
2445
|
+
// Recomputing from pathname alone would drop the query from the
|
|
2446
|
+
// signed definition, which breaks it BOTH ways: an attested call
|
|
2447
|
+
// carrying a query would fail to verify, and a captured assertion
|
|
2448
|
+
// could be replayed against the same route with different query
|
|
2449
|
+
// parameters, which is exactly the substitution the binding exists to
|
|
2450
|
+
// prevent.
|
|
2451
|
+
path: url.pathname + url.search,
|
|
2452
|
+
body_sha256: await bodyDigest(body)
|
|
2453
|
+
})
|
|
2454
|
+
});
|
|
2455
|
+
if (!res.ok) throw new Error(`assert ${res.status}`);
|
|
2456
|
+
verdict = await res.json();
|
|
2457
|
+
} catch {
|
|
2458
|
+
return {
|
|
2459
|
+
outcome: "reject",
|
|
2460
|
+
reason: "app_attest_required",
|
|
2461
|
+
device: null,
|
|
2462
|
+
observation,
|
|
2463
|
+
nextChallenge: null
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
if (!verdict.ok) {
|
|
2467
|
+
const reason = verdict.reason === "assertion_invalid" || verdict.reason === "assertion_stale" ? verdict.reason : "app_attest_required";
|
|
2468
|
+
return {
|
|
2469
|
+
outcome: "reject",
|
|
2470
|
+
reason,
|
|
2471
|
+
device: null,
|
|
2472
|
+
observation,
|
|
2473
|
+
nextChallenge: verdict.next_challenge || null
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
observation.attested = true;
|
|
2477
|
+
return {
|
|
2478
|
+
outcome: "pass",
|
|
2479
|
+
reason: "",
|
|
2480
|
+
device: {
|
|
2481
|
+
installationId: "",
|
|
2482
|
+
authDeviceId: deviceId,
|
|
2483
|
+
platform: "ios",
|
|
2484
|
+
assurance: "attested"
|
|
2485
|
+
},
|
|
2486
|
+
observation,
|
|
2487
|
+
nextChallenge: verdict.next_challenge || null
|
|
2488
|
+
};
|
|
2489
|
+
}, "gate");
|
|
2490
|
+
}
|
|
2491
|
+
__name(createAttestGate, "createAttestGate");
|
|
2492
|
+
function createObservationSink(deps) {
|
|
2493
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
2494
|
+
const maxBatch = deps.maxBatch ?? 1e3;
|
|
2495
|
+
const intervalMs = deps.intervalMs ?? 1e4;
|
|
2496
|
+
let buffer = [];
|
|
2497
|
+
let inFlight = Promise.resolve();
|
|
2498
|
+
const timer = setInterval(() => {
|
|
2499
|
+
void flush();
|
|
2500
|
+
}, intervalMs);
|
|
2501
|
+
timer.unref?.();
|
|
2502
|
+
function bucketsOf(batch) {
|
|
2503
|
+
const counts = {
|
|
2504
|
+
capable: 0,
|
|
2505
|
+
incapable: 0,
|
|
2506
|
+
unknown: 0,
|
|
2507
|
+
no_sdk: 0
|
|
2508
|
+
};
|
|
2509
|
+
for (const o of batch) {
|
|
2510
|
+
if (o.attested || o.capable === "yes") counts.capable += 1;
|
|
2511
|
+
else if (o.capable === "no") counts.incapable += 1;
|
|
2512
|
+
else if (o.installationId) counts.unknown += 1;
|
|
2513
|
+
else counts.no_sdk += 1;
|
|
2514
|
+
}
|
|
2515
|
+
return counts;
|
|
2516
|
+
}
|
|
2517
|
+
__name(bucketsOf, "bucketsOf");
|
|
2518
|
+
async function flush() {
|
|
2519
|
+
if (buffer.length === 0) return;
|
|
2520
|
+
const batch = buffer;
|
|
2521
|
+
buffer = [];
|
|
2522
|
+
inFlight = (async () => {
|
|
2523
|
+
try {
|
|
2524
|
+
await doFetch(`${deps.moduleBaseUrl}/admin/attest/devices/observations`, {
|
|
2525
|
+
method: "POST",
|
|
2526
|
+
headers: {
|
|
2527
|
+
apikey: deps.serviceRoleKey,
|
|
2528
|
+
"content-type": "application/json"
|
|
2529
|
+
},
|
|
2530
|
+
body: JSON.stringify(bucketsOf(batch))
|
|
2531
|
+
});
|
|
2532
|
+
} catch {
|
|
2533
|
+
}
|
|
2534
|
+
})();
|
|
2535
|
+
await inFlight;
|
|
2536
|
+
}
|
|
2537
|
+
__name(flush, "flush");
|
|
2538
|
+
return {
|
|
2539
|
+
record(o) {
|
|
2540
|
+
buffer.push(o);
|
|
2541
|
+
if (buffer.length >= maxBatch) void flush();
|
|
2542
|
+
},
|
|
2543
|
+
flush,
|
|
2544
|
+
stop() {
|
|
2545
|
+
clearInterval(timer);
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
__name(createObservationSink, "createObservationSink");
|
|
1821
2550
|
|
|
1822
2551
|
// src/engine/upload.ts
|
|
1823
2552
|
var AUTHORIZE_PATH = "/__palbase/upload/authorize";
|
|
@@ -1825,10 +2554,12 @@ var SIGNATURE_HEADER = "x-palbase-upload-signature";
|
|
|
1825
2554
|
function renderPath(template, tokens) {
|
|
1826
2555
|
return template.replaceAll("{userId}", sanitizeSegment(tokens.userId ?? "anonymous")).replaceAll("{uploadId}", sanitizeSegment(tokens.uploadId)).replaceAll("{filename}", sanitizeSegment(tokens.filename ?? "file"));
|
|
1827
2556
|
}
|
|
2557
|
+
__name(renderPath, "renderPath");
|
|
1828
2558
|
function sanitizeSegment(raw) {
|
|
1829
2559
|
const cleaned = raw.replace(/[\x00-\x1F\x7F]/g, "").replace(/[/\\]/g, "-").replace(/\.{2,}/g, ".").replace(/^\.+/, "").trim();
|
|
1830
2560
|
return cleaned === "" ? "file" : cleaned.slice(0, 200);
|
|
1831
2561
|
}
|
|
2562
|
+
__name(sanitizeSegment, "sanitizeSegment");
|
|
1832
2563
|
function grantFor(entry, ctx, bucketLimits) {
|
|
1833
2564
|
const cfg = entry?.meta?.options?.uploadConfig;
|
|
1834
2565
|
if (!cfg) return null;
|
|
@@ -1844,12 +2575,16 @@ function grantFor(entry, ctx, bucketLimits) {
|
|
|
1844
2575
|
ownerUid: ctx.userId ?? null
|
|
1845
2576
|
};
|
|
1846
2577
|
}
|
|
2578
|
+
__name(grantFor, "grantFor");
|
|
1847
2579
|
var CompletionLedger = class {
|
|
1848
|
-
|
|
1849
|
-
this
|
|
2580
|
+
static {
|
|
2581
|
+
__name(this, "CompletionLedger");
|
|
1850
2582
|
}
|
|
1851
2583
|
capacity;
|
|
1852
2584
|
seen = /* @__PURE__ */ new Map();
|
|
2585
|
+
constructor(capacity = 1024) {
|
|
2586
|
+
this.capacity = capacity;
|
|
2587
|
+
}
|
|
1853
2588
|
recall(uploadId) {
|
|
1854
2589
|
return this.seen.get(uploadId);
|
|
1855
2590
|
}
|
|
@@ -1874,6 +2609,7 @@ function verifySignature(presented, expected) {
|
|
|
1874
2609
|
}
|
|
1875
2610
|
return diff === 0;
|
|
1876
2611
|
}
|
|
2612
|
+
__name(verifySignature, "verifySignature");
|
|
1877
2613
|
|
|
1878
2614
|
// src/engine/sse.ts
|
|
1879
2615
|
var SSE_CONTENT_TYPE = "text/event-stream";
|
|
@@ -1882,12 +2618,16 @@ function encodeFrame(value) {
|
|
|
1882
2618
|
|
|
1883
2619
|
`;
|
|
1884
2620
|
}
|
|
2621
|
+
__name(encodeFrame, "encodeFrame");
|
|
1885
2622
|
function encodeErrorFrame(requestId) {
|
|
1886
2623
|
return `event: error
|
|
1887
|
-
data: ${JSON.stringify({
|
|
2624
|
+
data: ${JSON.stringify({
|
|
2625
|
+
requestId
|
|
2626
|
+
})}
|
|
1888
2627
|
|
|
1889
2628
|
`;
|
|
1890
2629
|
}
|
|
2630
|
+
__name(encodeErrorFrame, "encodeErrorFrame");
|
|
1891
2631
|
function makeSseWriter(opts) {
|
|
1892
2632
|
let begun = false;
|
|
1893
2633
|
let chain = Promise.resolve();
|
|
@@ -1909,6 +2649,7 @@ function makeSseWriter(opts) {
|
|
|
1909
2649
|
}
|
|
1910
2650
|
};
|
|
1911
2651
|
}
|
|
2652
|
+
__name(makeSseWriter, "makeSseWriter");
|
|
1912
2653
|
|
|
1913
2654
|
// src/engine/fence.ts
|
|
1914
2655
|
var SECRET_ENV = [
|
|
@@ -1929,8 +2670,12 @@ function scrubSecrets(env) {
|
|
|
1929
2670
|
removed.push(name);
|
|
1930
2671
|
}
|
|
1931
2672
|
for (const name of Object.keys(env)) if (name.startsWith("PALBASE_VAR_")) kept.push(name);
|
|
1932
|
-
return {
|
|
2673
|
+
return {
|
|
2674
|
+
removed,
|
|
2675
|
+
kept
|
|
2676
|
+
};
|
|
1933
2677
|
}
|
|
2678
|
+
__name(scrubSecrets, "scrubSecrets");
|
|
1934
2679
|
function hostAllowed(host, allow) {
|
|
1935
2680
|
const h = host.toLowerCase();
|
|
1936
2681
|
for (const raw of allow) {
|
|
@@ -1943,39 +2688,49 @@ function hostAllowed(host, allow) {
|
|
|
1943
2688
|
}
|
|
1944
2689
|
return false;
|
|
1945
2690
|
}
|
|
2691
|
+
__name(hostAllowed, "hostAllowed");
|
|
1946
2692
|
function installEgressFence(policy) {
|
|
1947
2693
|
const original = globalThis.fetch.bind(globalThis);
|
|
1948
2694
|
const declared = policy.allow.length > 0;
|
|
1949
2695
|
const platform = (policy.alwaysAllow ?? []).map((h) => h.toLowerCase()).filter(Boolean);
|
|
1950
2696
|
if (!declared && policy.whenUndeclared === "allow") return original;
|
|
1951
|
-
const fenced = async (input, init) => {
|
|
2697
|
+
const fenced = /* @__PURE__ */ __name(async (input, init) => {
|
|
1952
2698
|
const url = typeof input === "string" ? new URL(input) : input instanceof URL ? input : new URL(input.url);
|
|
1953
2699
|
if (platform.includes(url.hostname.toLowerCase())) return original(input, init);
|
|
1954
2700
|
if (!declared || !hostAllowed(url.hostname, policy.allow)) {
|
|
1955
|
-
throw new Error(
|
|
1956
|
-
`egress denied: ${url.hostname} is not in this backend's declared allowlist. Add it to config/egress.ts and deploy.`
|
|
1957
|
-
);
|
|
2701
|
+
throw new Error(`egress denied: ${url.hostname} is not in this backend's declared allowlist. Add it to config/egress.ts and deploy.`);
|
|
1958
2702
|
}
|
|
1959
2703
|
if (policy.timeoutMs > 0) {
|
|
1960
2704
|
const controller = new AbortController();
|
|
1961
2705
|
const timer = setTimeout(() => controller.abort(), policy.timeoutMs);
|
|
1962
2706
|
try {
|
|
1963
|
-
return await original(input, {
|
|
2707
|
+
return await original(input, {
|
|
2708
|
+
...init,
|
|
2709
|
+
signal: init?.signal ?? controller.signal
|
|
2710
|
+
});
|
|
1964
2711
|
} finally {
|
|
1965
2712
|
clearTimeout(timer);
|
|
1966
2713
|
}
|
|
1967
2714
|
}
|
|
1968
2715
|
return original(input, init);
|
|
1969
|
-
};
|
|
2716
|
+
}, "fenced");
|
|
1970
2717
|
globalThis.fetch = fenced;
|
|
1971
2718
|
return original;
|
|
1972
2719
|
}
|
|
2720
|
+
__name(installEgressFence, "installEgressFence");
|
|
1973
2721
|
|
|
1974
2722
|
// src/engine/index.ts
|
|
1975
|
-
var
|
|
2723
|
+
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
2724
|
+
var JSON_HEADERS = {
|
|
2725
|
+
"content-type": "application/json"
|
|
2726
|
+
};
|
|
1976
2727
|
function fieldErrors(err) {
|
|
1977
|
-
return err.issues.map((i) => ({
|
|
2728
|
+
return err.issues.map((i) => ({
|
|
2729
|
+
field: i.path.join("."),
|
|
2730
|
+
message: i.message
|
|
2731
|
+
}));
|
|
1978
2732
|
}
|
|
2733
|
+
__name(fieldErrors, "fieldErrors");
|
|
1979
2734
|
function headersFor(raw, schema) {
|
|
1980
2735
|
const shape = schema.shape;
|
|
1981
2736
|
if (typeof shape !== "object" || shape === null) return raw;
|
|
@@ -1985,53 +2740,92 @@ function headersFor(raw, schema) {
|
|
|
1985
2740
|
if (lower === declared) continue;
|
|
1986
2741
|
const value = raw[lower];
|
|
1987
2742
|
if (value === void 0) continue;
|
|
1988
|
-
aliased ??= {
|
|
2743
|
+
aliased ??= {
|
|
2744
|
+
...raw
|
|
2745
|
+
};
|
|
1989
2746
|
aliased[declared] = value;
|
|
1990
2747
|
}
|
|
1991
2748
|
return aliased ?? raw;
|
|
1992
2749
|
}
|
|
2750
|
+
__name(headersFor, "headersFor");
|
|
1993
2751
|
function envelope(error, description, status, requestId, extra) {
|
|
1994
|
-
return new Response(
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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
|
+
});
|
|
1998
2762
|
}
|
|
2763
|
+
__name(envelope, "envelope");
|
|
1999
2764
|
function unavailable(name) {
|
|
2000
|
-
throw new Error(
|
|
2001
|
-
`${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.`
|
|
2002
|
-
);
|
|
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.`);
|
|
2003
2766
|
}
|
|
2767
|
+
__name(unavailable, "unavailable");
|
|
2004
2768
|
function stubModule(name) {
|
|
2005
|
-
return new Proxy(
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
apply: () => unavailable(name)
|
|
2010
|
-
}
|
|
2011
|
-
);
|
|
2769
|
+
return new Proxy({}, {
|
|
2770
|
+
get: /* @__PURE__ */ __name(() => unavailable(name), "get"),
|
|
2771
|
+
apply: /* @__PURE__ */ __name(() => unavailable(name), "apply")
|
|
2772
|
+
});
|
|
2012
2773
|
}
|
|
2774
|
+
__name(stubModule, "stubModule");
|
|
2013
2775
|
async function defaultSqlDriver(config) {
|
|
2014
2776
|
const g = globalThis;
|
|
2015
2777
|
if (!g.Bun?.SQL) {
|
|
2016
|
-
throw new BootRefused(
|
|
2017
|
-
[],
|
|
2018
|
-
"boot refused: no SQL driver. Running outside Bun means the driver must be supplied \u2014 pass `sql` to createApp()."
|
|
2019
|
-
);
|
|
2778
|
+
throw new BootRefused([], "boot refused: no SQL driver. Running outside Bun means the driver must be supplied \u2014 pass `sql` to createApp().");
|
|
2020
2779
|
}
|
|
2021
|
-
return new g.Bun.SQL({
|
|
2780
|
+
return new g.Bun.SQL({
|
|
2781
|
+
url: config.databaseUrl,
|
|
2782
|
+
max: config.poolMax
|
|
2783
|
+
});
|
|
2022
2784
|
}
|
|
2023
|
-
|
|
2785
|
+
__name(defaultSqlDriver, "defaultSqlDriver");
|
|
2786
|
+
var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
|
|
2787
|
+
400,
|
|
2788
|
+
401,
|
|
2789
|
+
403,
|
|
2790
|
+
404,
|
|
2791
|
+
409,
|
|
2792
|
+
429
|
|
2793
|
+
]);
|
|
2024
2794
|
async function createApp(opts) {
|
|
2025
2795
|
const { config, controllers } = opts;
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
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);
|
|
2029
2810
|
if (routes.length === 0) {
|
|
2030
2811
|
throw new BootRefused([], "boot refused: zero endpoints collected \u2014 nothing would answer.");
|
|
2031
2812
|
}
|
|
2032
2813
|
const sql = opts.sql ?? await defaultSqlDriver(config);
|
|
2033
2814
|
await sql.unsafe("select 1");
|
|
2034
|
-
|
|
2815
|
+
setSchema(opts.schemas ?? []);
|
|
2816
|
+
setSecretReader(opts.secretReader ?? null);
|
|
2817
|
+
const auth = new AuthVerifier({
|
|
2818
|
+
jwksUrl: config.authJwksUrl,
|
|
2819
|
+
issuer: config.authIssuer
|
|
2820
|
+
});
|
|
2821
|
+
const attestGate = createAttestGate({
|
|
2822
|
+
moduleBaseUrl: config.moduleBaseUrl,
|
|
2823
|
+
serviceRoleKey: config.serviceRoleKey
|
|
2824
|
+
});
|
|
2825
|
+
const observations = createObservationSink({
|
|
2826
|
+
moduleBaseUrl: config.moduleBaseUrl,
|
|
2827
|
+
serviceRoleKey: config.serviceRoleKey
|
|
2828
|
+
});
|
|
2035
2829
|
const limiter = new RateLimiter();
|
|
2036
2830
|
const cache = opts.cache ?? makeMemoryCache();
|
|
2037
2831
|
const log = opts.logger ?? console;
|
|
@@ -2057,6 +2851,7 @@ async function createApp(opts) {
|
|
|
2057
2851
|
Secrets: modules.Secrets ?? stubModule("Secrets")
|
|
2058
2852
|
};
|
|
2059
2853
|
}
|
|
2854
|
+
__name(buildServices, "buildServices");
|
|
2060
2855
|
async function runInServiceScope(fn) {
|
|
2061
2856
|
const db = createRequestDatabase(sql, {
|
|
2062
2857
|
role: config.dbRole,
|
|
@@ -2072,6 +2867,7 @@ async function createApp(opts) {
|
|
|
2072
2867
|
throw err;
|
|
2073
2868
|
}
|
|
2074
2869
|
}
|
|
2870
|
+
__name(runInServiceScope, "runInServiceScope");
|
|
2075
2871
|
async function handleAuthorize(req, requestId) {
|
|
2076
2872
|
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
2077
2873
|
return envelope("unauthorized", "This endpoint is not callable directly", 401, requestId);
|
|
@@ -2095,15 +2891,14 @@ async function createApp(opts) {
|
|
|
2095
2891
|
filename: body.filename
|
|
2096
2892
|
});
|
|
2097
2893
|
if (!grant) {
|
|
2098
|
-
return envelope(
|
|
2099
|
-
"not_an_upload_route",
|
|
2100
|
-
`${body.method ?? "POST"} ${body.path} does not declare @Upload`,
|
|
2101
|
-
400,
|
|
2102
|
-
requestId
|
|
2103
|
-
);
|
|
2894
|
+
return envelope("not_an_upload_route", `${body.method ?? "POST"} ${body.path} does not declare @Upload`, 400, requestId);
|
|
2104
2895
|
}
|
|
2105
|
-
return new Response(JSON.stringify(grant), {
|
|
2896
|
+
return new Response(JSON.stringify(grant), {
|
|
2897
|
+
status: 200,
|
|
2898
|
+
headers: JSON_HEADERS
|
|
2899
|
+
});
|
|
2106
2900
|
}
|
|
2901
|
+
__name(handleAuthorize, "handleAuthorize");
|
|
2107
2902
|
async function handle(req) {
|
|
2108
2903
|
const requestId = `req_${crypto.randomUUID()}`;
|
|
2109
2904
|
const url = new URL(req.url);
|
|
@@ -2113,6 +2908,8 @@ async function createApp(opts) {
|
|
|
2113
2908
|
const hit = matchRoute(routes, req.method, url.pathname);
|
|
2114
2909
|
if (!hit) return envelope("not_found", "No route matches this method and path", 404, requestId);
|
|
2115
2910
|
const { meta } = hit.entry;
|
|
2911
|
+
let attestedDevice = null;
|
|
2912
|
+
let attestChallengeOut = null;
|
|
2116
2913
|
const spec = effectiveAuth(meta.options?.auth, hit.entry.controllerAuth);
|
|
2117
2914
|
const claims = await auth.verify(req.headers.get("authorization"));
|
|
2118
2915
|
if (spec.required && !claims) {
|
|
@@ -2125,45 +2922,108 @@ async function createApp(opts) {
|
|
|
2125
2922
|
if (claims && spec.verifiedEmail && claims.email_verified !== true) {
|
|
2126
2923
|
return envelope("email_not_verified", "A verified email address is required", 403, requestId);
|
|
2127
2924
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2925
|
+
let parsedBody;
|
|
2926
|
+
let bodyRead = false;
|
|
2927
|
+
let rawBody = null;
|
|
2928
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2929
|
+
const declared = Number(req.headers.get("content-length") ?? "");
|
|
2930
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
|
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
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
const buf = await req.arrayBuffer().catch(() => null);
|
|
2942
|
+
if (buf && buf.byteLength > MAX_BODY_BYTES) {
|
|
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
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
if (buf && buf.byteLength > 0) rawBody = new Uint8Array(buf);
|
|
2954
|
+
}
|
|
2955
|
+
const readParsedBody = /* @__PURE__ */ __name(() => {
|
|
2956
|
+
if (bodyRead) return parsedBody;
|
|
2957
|
+
bodyRead = true;
|
|
2958
|
+
if (!rawBody) {
|
|
2959
|
+
parsedBody = {};
|
|
2960
|
+
return parsedBody;
|
|
2961
|
+
}
|
|
2962
|
+
try {
|
|
2963
|
+
parsedBody = JSON.parse(new TextDecoder().decode(rawBody));
|
|
2964
|
+
} catch {
|
|
2965
|
+
parsedBody = {};
|
|
2966
|
+
}
|
|
2967
|
+
return parsedBody;
|
|
2968
|
+
}, "readParsedBody");
|
|
2969
|
+
const attest = await attestGate(req, rawBody, {
|
|
2970
|
+
userId
|
|
2971
|
+
});
|
|
2972
|
+
if (attest.observation) observations.record(attest.observation);
|
|
2973
|
+
if (attest.outcome === "reject") {
|
|
2974
|
+
const headers = {
|
|
2975
|
+
...JSON_HEADERS
|
|
2976
|
+
};
|
|
2977
|
+
if (attest.nextChallenge) headers["x-palbase-attest-challenge"] = attest.nextChallenge;
|
|
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
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
if (attest.device && claims) attestedDevice = attest.device;
|
|
2989
|
+
attestChallengeOut = attest.nextChallenge;
|
|
2990
|
+
const retryAfter = limiter.check(meta.options?.rateLimit, RateLimiter.key(hit.entry.id, userId, req.headers), Date.now());
|
|
2133
2991
|
if (retryAfter !== null) {
|
|
2134
|
-
return new Response(
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
|
|
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
|
+
});
|
|
2144
3007
|
}
|
|
2145
3008
|
let completionUploadId = null;
|
|
2146
3009
|
const args = [];
|
|
2147
|
-
let parsedBody;
|
|
2148
|
-
let bodyRead = false;
|
|
2149
3010
|
let sseEnqueue = null;
|
|
2150
|
-
let sseSettle = async () => {
|
|
2151
|
-
};
|
|
3011
|
+
let sseSettle = /* @__PURE__ */ __name(async () => {
|
|
3012
|
+
}, "sseSettle");
|
|
2152
3013
|
const sseWriter = makeSseWriter({
|
|
2153
|
-
enqueue: (chunk) => sseEnqueue?.(chunk),
|
|
2154
|
-
onFirstWrite: () => sseSettle()
|
|
3014
|
+
enqueue: /* @__PURE__ */ __name((chunk) => sseEnqueue?.(chunk), "enqueue"),
|
|
3015
|
+
onFirstWrite: /* @__PURE__ */ __name(() => sseSettle(), "onFirstWrite")
|
|
2155
3016
|
});
|
|
2156
3017
|
for (const p of meta.params ?? []) {
|
|
2157
3018
|
switch (p.kind) {
|
|
2158
3019
|
case "body": {
|
|
2159
|
-
|
|
2160
|
-
parsedBody = await req.json().catch(() => ({}));
|
|
2161
|
-
bodyRead = true;
|
|
2162
|
-
}
|
|
3020
|
+
readParsedBody();
|
|
2163
3021
|
const r = p.schema.safeParse(parsedBody);
|
|
2164
3022
|
if (!r.success) {
|
|
2165
3023
|
return envelope("bad_request", "Request body failed validation", 400, requestId, {
|
|
2166
|
-
data: {
|
|
3024
|
+
data: {
|
|
3025
|
+
fields: fieldErrors(r.error)
|
|
3026
|
+
}
|
|
2167
3027
|
});
|
|
2168
3028
|
}
|
|
2169
3029
|
args[p.index] = r.data;
|
|
@@ -2173,7 +3033,9 @@ async function createApp(opts) {
|
|
|
2173
3033
|
const r = p.schema.safeParse(Object.fromEntries(url.searchParams));
|
|
2174
3034
|
if (!r.success) {
|
|
2175
3035
|
return envelope("bad_request", "Query parameters failed validation", 400, requestId, {
|
|
2176
|
-
data: {
|
|
3036
|
+
data: {
|
|
3037
|
+
fields: fieldErrors(r.error)
|
|
3038
|
+
}
|
|
2177
3039
|
});
|
|
2178
3040
|
}
|
|
2179
3041
|
args[p.index] = r.data;
|
|
@@ -2191,7 +3053,9 @@ async function createApp(opts) {
|
|
|
2191
3053
|
const r = p.schema.safeParse(headersFor(raw, p.schema));
|
|
2192
3054
|
if (!r.success) {
|
|
2193
3055
|
return envelope("bad_request", "Request headers failed validation", 400, requestId, {
|
|
2194
|
-
data: {
|
|
3056
|
+
data: {
|
|
3057
|
+
fields: fieldErrors(r.error)
|
|
3058
|
+
}
|
|
2195
3059
|
});
|
|
2196
3060
|
}
|
|
2197
3061
|
args[p.index] = r.data;
|
|
@@ -2204,22 +3068,19 @@ async function createApp(opts) {
|
|
|
2204
3068
|
email: claims.email,
|
|
2205
3069
|
role: claims.role,
|
|
2206
3070
|
emailVerified: claims.email_verified === true,
|
|
2207
|
-
metadata: claims.metadata ?? {}
|
|
3071
|
+
metadata: claims.metadata ?? {},
|
|
3072
|
+
// Server-owned and only ever filled from a verdict palauth
|
|
3073
|
+
// returned. Never from a raw header, never from JWT metadata —
|
|
3074
|
+
// the type has said so since it was written and this is the
|
|
3075
|
+
// first writer.
|
|
3076
|
+
device: attestedDevice
|
|
2208
3077
|
} : null;
|
|
2209
3078
|
break;
|
|
2210
3079
|
case "uploadedObject": {
|
|
2211
3080
|
if (uploadSecret === "" || !verifySignature(req.headers.get(SIGNATURE_HEADER) ?? "", uploadSecret)) {
|
|
2212
|
-
return envelope(
|
|
2213
|
-
"unauthorized",
|
|
2214
|
-
"This endpoint accepts uploads through storage, not directly",
|
|
2215
|
-
401,
|
|
2216
|
-
requestId
|
|
2217
|
-
);
|
|
2218
|
-
}
|
|
2219
|
-
if (!bodyRead) {
|
|
2220
|
-
parsedBody = await req.json().catch(() => ({}));
|
|
2221
|
-
bodyRead = true;
|
|
3081
|
+
return envelope("unauthorized", "This endpoint accepts uploads through storage, not directly", 401, requestId);
|
|
2222
3082
|
}
|
|
3083
|
+
readParsedBody();
|
|
2223
3084
|
const envelopeIn = parsedBody;
|
|
2224
3085
|
if (!envelopeIn?.uploadedObject) {
|
|
2225
3086
|
return envelope("bad_request", "the completion call carried no uploaded object", 400, requestId);
|
|
@@ -2230,7 +3091,9 @@ async function createApp(opts) {
|
|
|
2230
3091
|
if (already) {
|
|
2231
3092
|
return new Response(already.body, {
|
|
2232
3093
|
status: already.status,
|
|
2233
|
-
headers: already.contentType ? {
|
|
3094
|
+
headers: already.contentType ? {
|
|
3095
|
+
"content-type": already.contentType
|
|
3096
|
+
} : void 0
|
|
2234
3097
|
});
|
|
2235
3098
|
}
|
|
2236
3099
|
completionUploadId = uploadId;
|
|
@@ -2290,26 +3153,24 @@ async function createApp(opts) {
|
|
|
2290
3153
|
sseController = c;
|
|
2291
3154
|
}
|
|
2292
3155
|
});
|
|
2293
|
-
sseEnqueue = (chunk) => {
|
|
3156
|
+
sseEnqueue = /* @__PURE__ */ __name((chunk) => {
|
|
2294
3157
|
try {
|
|
2295
3158
|
sseController.enqueue(sseEncoder.encode(chunk));
|
|
2296
3159
|
} catch {
|
|
2297
3160
|
}
|
|
2298
|
-
};
|
|
3161
|
+
}, "sseEnqueue");
|
|
2299
3162
|
let sawFirstFrame;
|
|
2300
3163
|
const firstFrame = new Promise((r) => sawFirstFrame = r);
|
|
2301
|
-
sseSettle = async () => {
|
|
3164
|
+
sseSettle = /* @__PURE__ */ __name(async () => {
|
|
2302
3165
|
await db.commit();
|
|
2303
3166
|
sawFirstFrame();
|
|
2304
|
-
};
|
|
3167
|
+
}, "sseSettle");
|
|
2305
3168
|
const guarded = {
|
|
2306
3169
|
...services,
|
|
2307
3170
|
Database: new Proxy(services.Database, {
|
|
2308
3171
|
get(target, prop, recv) {
|
|
2309
3172
|
if (sseWriter.started()) {
|
|
2310
|
-
throw new Error(
|
|
2311
|
-
`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.`
|
|
2312
|
-
);
|
|
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.`);
|
|
2313
3174
|
}
|
|
2314
3175
|
return Reflect.get(target, prop, recv);
|
|
2315
3176
|
}
|
|
@@ -2325,23 +3186,23 @@ async function createApp(opts) {
|
|
|
2325
3186
|
}
|
|
2326
3187
|
const method = hit.entry.instance[meta.fnName];
|
|
2327
3188
|
if (typeof method !== "function") {
|
|
2328
|
-
throw new Error(
|
|
2329
|
-
`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
|
|
2330
|
-
);
|
|
3189
|
+
throw new Error(`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`);
|
|
2331
3190
|
}
|
|
2332
3191
|
return method.apply(hit.entry.instance, args);
|
|
2333
|
-
})).then(
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
3192
|
+
})).then(() => void 0, (err) => {
|
|
3193
|
+
thrown = err;
|
|
3194
|
+
});
|
|
3195
|
+
await Promise.race([
|
|
3196
|
+
firstFrame,
|
|
3197
|
+
ran
|
|
3198
|
+
]);
|
|
2340
3199
|
if (!sseWriter.started()) {
|
|
2341
3200
|
await ran;
|
|
2342
3201
|
await db.commit();
|
|
2343
3202
|
if (thrown !== null) throw thrown;
|
|
2344
|
-
return new Response(null, {
|
|
3203
|
+
return new Response(null, {
|
|
3204
|
+
status: 204
|
|
3205
|
+
});
|
|
2345
3206
|
}
|
|
2346
3207
|
void (async () => {
|
|
2347
3208
|
await ran;
|
|
@@ -2357,7 +3218,10 @@ async function createApp(opts) {
|
|
|
2357
3218
|
})();
|
|
2358
3219
|
return new Response(stream, {
|
|
2359
3220
|
status: 200,
|
|
2360
|
-
headers: {
|
|
3221
|
+
headers: {
|
|
3222
|
+
"content-type": SSE_CONTENT_TYPE,
|
|
3223
|
+
"cache-control": "no-cache"
|
|
3224
|
+
}
|
|
2361
3225
|
});
|
|
2362
3226
|
}
|
|
2363
3227
|
const result = await runWithRuntime(services, () => {
|
|
@@ -2369,9 +3233,7 @@ async function createApp(opts) {
|
|
|
2369
3233
|
}
|
|
2370
3234
|
const method = hit.entry.instance[meta.fnName];
|
|
2371
3235
|
if (typeof method !== "function") {
|
|
2372
|
-
throw new Error(
|
|
2373
|
-
`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`
|
|
2374
|
-
);
|
|
3236
|
+
throw new Error(`route ${hit.entry.id} names method ${meta.fnName}, which the controller does not define`);
|
|
2375
3237
|
}
|
|
2376
3238
|
return method.apply(hit.entry.instance, args);
|
|
2377
3239
|
});
|
|
@@ -2380,19 +3242,20 @@ async function createApp(opts) {
|
|
|
2380
3242
|
const v = meta.returnSchema.safeParse(result);
|
|
2381
3243
|
if (!v.success) {
|
|
2382
3244
|
log.error(`[engine] ${hit.entry.id} returned a value its declared type rejects`, v.error.issues);
|
|
2383
|
-
return envelope(
|
|
2384
|
-
"output_invalid",
|
|
2385
|
-
"The handler returned a value its declared return type rejects",
|
|
2386
|
-
500,
|
|
2387
|
-
requestId
|
|
2388
|
-
);
|
|
3245
|
+
return envelope("output_invalid", "The handler returned a value its declared return type rejects", 500, requestId);
|
|
2389
3246
|
}
|
|
2390
3247
|
}
|
|
2391
3248
|
if (result === void 0 || result === null) {
|
|
2392
3249
|
if (completionUploadId) {
|
|
2393
|
-
completions.remember(completionUploadId, {
|
|
3250
|
+
completions.remember(completionUploadId, {
|
|
3251
|
+
status: 204,
|
|
3252
|
+
body: null,
|
|
3253
|
+
contentType: null
|
|
3254
|
+
});
|
|
2394
3255
|
}
|
|
2395
|
-
return new Response(null, {
|
|
3256
|
+
return new Response(null, {
|
|
3257
|
+
status: 204
|
|
3258
|
+
});
|
|
2396
3259
|
}
|
|
2397
3260
|
const payload = JSON.stringify(result);
|
|
2398
3261
|
if (completionUploadId) {
|
|
@@ -2402,25 +3265,28 @@ async function createApp(opts) {
|
|
|
2402
3265
|
contentType: JSON_HEADERS["content-type"] ?? "application/json"
|
|
2403
3266
|
});
|
|
2404
3267
|
}
|
|
2405
|
-
return new Response(payload, {
|
|
3268
|
+
return new Response(payload, {
|
|
3269
|
+
status: 200,
|
|
3270
|
+
headers: attestChallengeOut ? {
|
|
3271
|
+
...JSON_HEADERS,
|
|
3272
|
+
"x-palbase-attest-challenge": attestChallengeOut
|
|
3273
|
+
} : JSON_HEADERS
|
|
3274
|
+
});
|
|
2406
3275
|
} catch (err) {
|
|
2407
3276
|
await db.rollback(err);
|
|
2408
3277
|
if (isHttpError(err)) {
|
|
2409
3278
|
if (isEngineRaised(err) || !AUTHOR_ANSWERED.has(err.status)) {
|
|
2410
3279
|
log.error(`[engine] unhandled ${err.error} in ${hit.entry.id}`, err);
|
|
2411
3280
|
}
|
|
2412
|
-
return envelope(
|
|
2413
|
-
err.
|
|
2414
|
-
|
|
2415
|
-
err.status,
|
|
2416
|
-
requestId,
|
|
2417
|
-
err.data !== void 0 ? { data: err.data } : void 0
|
|
2418
|
-
);
|
|
3281
|
+
return envelope(err.error, err.errorDescription, err.status, requestId, err.data !== void 0 ? {
|
|
3282
|
+
data: err.data
|
|
3283
|
+
} : void 0);
|
|
2419
3284
|
}
|
|
2420
3285
|
log.error(`[engine] unhandled error in ${hit.entry.id}`, err);
|
|
2421
3286
|
return envelope("internal_error", "The request could not be completed", 500, requestId);
|
|
2422
3287
|
}
|
|
2423
3288
|
}
|
|
3289
|
+
__name(handle, "handle");
|
|
2424
3290
|
const bootServices = buildServices(null);
|
|
2425
3291
|
let runShutdownHooks;
|
|
2426
3292
|
try {
|
|
@@ -2433,18 +3299,22 @@ async function createApp(opts) {
|
|
|
2433
3299
|
handle,
|
|
2434
3300
|
routes,
|
|
2435
3301
|
config,
|
|
3302
|
+
container: liveContainer,
|
|
2436
3303
|
runInServiceScope,
|
|
2437
3304
|
async shutdown() {
|
|
2438
3305
|
await runShutdownHooks();
|
|
2439
3306
|
await closeDriver(sql);
|
|
3307
|
+
released = true;
|
|
2440
3308
|
}
|
|
2441
3309
|
};
|
|
2442
3310
|
}
|
|
3311
|
+
__name(createApp, "createApp");
|
|
2443
3312
|
async function closeDriver(sql) {
|
|
2444
3313
|
const closable = sql;
|
|
2445
3314
|
await closable.close?.();
|
|
2446
3315
|
await closable.end?.();
|
|
2447
3316
|
}
|
|
3317
|
+
__name(closeDriver, "closeDriver");
|
|
2448
3318
|
|
|
2449
3319
|
export {
|
|
2450
3320
|
BootRefused,
|
|
@@ -2465,4 +3335,4 @@ export {
|
|
|
2465
3335
|
installEgressFence,
|
|
2466
3336
|
createApp
|
|
2467
3337
|
};
|
|
2468
|
-
//# sourceMappingURL=chunk-
|
|
3338
|
+
//# sourceMappingURL=chunk-OO7R25AI.js.map
|