weifuwu 0.21.0 → 0.22.1
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/README.md +247 -4
- package/cli.ts +28 -14
- package/dist/cache.d.ts +74 -0
- package/dist/cli.js +27 -13
- package/dist/fts.d.ts +36 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1071 -92
- package/dist/queue/types.d.ts +14 -0
- package/dist/rate-limit.d.ts +7 -0
- package/dist/session.d.ts +83 -0
- package/dist/test-utils.d.ts +52 -0
- package/dist/validate.d.ts +1 -1
- package/dist/webhook.d.ts +54 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
1
7
|
// trace.ts
|
|
2
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
9
|
import { randomUUID } from "node:crypto";
|
|
@@ -1159,11 +1165,33 @@ var MIME_TYPES = {
|
|
|
1159
1165
|
};
|
|
1160
1166
|
|
|
1161
1167
|
// validate.ts
|
|
1168
|
+
function parseFormBody(text2) {
|
|
1169
|
+
const params = new URLSearchParams(text2);
|
|
1170
|
+
const result = {};
|
|
1171
|
+
for (const [key, value] of params) {
|
|
1172
|
+
result[key] = value;
|
|
1173
|
+
}
|
|
1174
|
+
return result;
|
|
1175
|
+
}
|
|
1176
|
+
function parseBody(text2, ct) {
|
|
1177
|
+
if (ct.includes("application/x-www-form-urlencoded")) {
|
|
1178
|
+
return parseFormBody(text2);
|
|
1179
|
+
}
|
|
1180
|
+
const isExplicitJson = ct.includes("application/json") || ct.includes("+json") || ct.includes("text/") || ct.includes("*/json");
|
|
1181
|
+
const isNotSpecialMultipart = !ct.includes("multipart/form-data") && !ct.includes("application/x-www-form-urlencoded");
|
|
1182
|
+
if (isExplicitJson || isNotSpecialMultipart) {
|
|
1183
|
+
try {
|
|
1184
|
+
return JSON.parse(text2);
|
|
1185
|
+
} catch {
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return text2;
|
|
1189
|
+
}
|
|
1162
1190
|
function validate(schemas) {
|
|
1163
1191
|
return async (req, ctx, next) => {
|
|
1164
1192
|
const parsed = {};
|
|
1165
1193
|
const issues = [];
|
|
1166
|
-
if (schemas
|
|
1194
|
+
if (schemas?.params) {
|
|
1167
1195
|
const result = schemas.params.safeParse(ctx.params);
|
|
1168
1196
|
if (result.success) {
|
|
1169
1197
|
parsed.params = result.data;
|
|
@@ -1174,7 +1202,7 @@ function validate(schemas) {
|
|
|
1174
1202
|
})));
|
|
1175
1203
|
}
|
|
1176
1204
|
}
|
|
1177
|
-
if (schemas
|
|
1205
|
+
if (schemas?.query) {
|
|
1178
1206
|
const result = schemas.query.safeParse(ctx.query);
|
|
1179
1207
|
if (result.success) {
|
|
1180
1208
|
parsed.query = result.data;
|
|
@@ -1185,7 +1213,7 @@ function validate(schemas) {
|
|
|
1185
1213
|
})));
|
|
1186
1214
|
}
|
|
1187
1215
|
}
|
|
1188
|
-
if (schemas
|
|
1216
|
+
if (schemas?.headers) {
|
|
1189
1217
|
const rawHeaders = {};
|
|
1190
1218
|
req.headers.forEach((v, k) => {
|
|
1191
1219
|
rawHeaders[k] = v;
|
|
@@ -1200,32 +1228,35 @@ function validate(schemas) {
|
|
|
1200
1228
|
})));
|
|
1201
1229
|
}
|
|
1202
1230
|
}
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1231
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1232
|
+
const ct = req.headers.get("content-type") ?? "";
|
|
1233
|
+
const isForm = ct.includes("application/x-www-form-urlencoded");
|
|
1234
|
+
if (schemas?.body || isForm) {
|
|
1235
|
+
if (req.body === null) {
|
|
1236
|
+
if (schemas?.body) {
|
|
1237
|
+
issues.push({ path: ["body"], message: "Request body is required" });
|
|
1238
|
+
}
|
|
1211
1239
|
} else {
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
try {
|
|
1217
|
-
bodyValue = JSON.parse(bodyText);
|
|
1218
|
-
} catch {
|
|
1240
|
+
const bodyText = await req.text();
|
|
1241
|
+
if (!bodyText) {
|
|
1242
|
+
if (schemas?.body) {
|
|
1243
|
+
issues.push({ path: ["body"], message: "Request body is required" });
|
|
1219
1244
|
}
|
|
1220
|
-
}
|
|
1221
|
-
const result = schemas.body.safeParse(bodyValue);
|
|
1222
|
-
if (result.success) {
|
|
1223
|
-
parsed.body = result.data;
|
|
1224
1245
|
} else {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1246
|
+
const bodyValue = parseBody(bodyText, ct);
|
|
1247
|
+
if (schemas?.body) {
|
|
1248
|
+
const result = schemas.body.safeParse(bodyValue);
|
|
1249
|
+
if (result.success) {
|
|
1250
|
+
parsed.body = result.data;
|
|
1251
|
+
} else {
|
|
1252
|
+
issues.push(...result.error.issues.map((i) => ({
|
|
1253
|
+
path: ["body", ...i.path.map(String)],
|
|
1254
|
+
message: i.message
|
|
1255
|
+
})));
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
parsed.body = bodyValue;
|
|
1259
|
+
}
|
|
1229
1260
|
}
|
|
1230
1261
|
}
|
|
1231
1262
|
}
|
|
@@ -1395,28 +1426,35 @@ function upload(options) {
|
|
|
1395
1426
|
}
|
|
1396
1427
|
|
|
1397
1428
|
// rate-limit.ts
|
|
1429
|
+
function defaultKey(_req, _ctx) {
|
|
1430
|
+
const forwarded = _req.headers.get("x-forwarded-for");
|
|
1431
|
+
if (forwarded) return forwarded.split(",")[0].trim();
|
|
1432
|
+
const realIp = _req.headers.get("x-real-ip");
|
|
1433
|
+
if (realIp) return realIp;
|
|
1434
|
+
const cfIp = _req.headers.get("cf-connecting-ip");
|
|
1435
|
+
if (cfIp) return cfIp;
|
|
1436
|
+
return "global";
|
|
1437
|
+
}
|
|
1398
1438
|
function rateLimit(options) {
|
|
1399
1439
|
const max = options?.max ?? 100;
|
|
1400
1440
|
const window2 = options?.window ?? 6e4;
|
|
1401
|
-
const getKey = options?.key ??
|
|
1402
|
-
const forwarded = _req.headers.get("x-forwarded-for");
|
|
1403
|
-
if (forwarded) return forwarded.split(",")[0].trim();
|
|
1404
|
-
const realIp = _req.headers.get("x-real-ip");
|
|
1405
|
-
if (realIp) return realIp;
|
|
1406
|
-
const cfIp = _req.headers.get("cf-connecting-ip");
|
|
1407
|
-
if (cfIp) return cfIp;
|
|
1408
|
-
return "global";
|
|
1409
|
-
});
|
|
1441
|
+
const getKey = options?.key ?? defaultKey;
|
|
1410
1442
|
const message = options?.message ?? "Too Many Requests";
|
|
1411
|
-
const
|
|
1443
|
+
const storeType = options?.store ?? "memory";
|
|
1444
|
+
if (storeType === "redis" && !options?.redis) {
|
|
1445
|
+
throw new Error('rateLimit: redis client required when store: "redis"');
|
|
1446
|
+
}
|
|
1447
|
+
const redis2 = options?.redis ?? null;
|
|
1448
|
+
const keyPrefix = options?.prefix ?? "ratelimit:";
|
|
1449
|
+
const MAX_ENTRIES2 = 1e4;
|
|
1412
1450
|
const hits = /* @__PURE__ */ new Map();
|
|
1413
|
-
const interval = setInterval(() => {
|
|
1451
|
+
const interval = storeType === "memory" ? setInterval(() => {
|
|
1414
1452
|
const now = Date.now();
|
|
1415
1453
|
for (const [key, entry] of hits) {
|
|
1416
1454
|
if (entry.reset < now) hits.delete(key);
|
|
1417
1455
|
}
|
|
1418
|
-
if (hits.size >
|
|
1419
|
-
const toDelete = hits.size -
|
|
1456
|
+
if (hits.size > MAX_ENTRIES2) {
|
|
1457
|
+
const toDelete = hits.size - MAX_ENTRIES2;
|
|
1420
1458
|
let deleted = 0;
|
|
1421
1459
|
for (const key of hits.keys()) {
|
|
1422
1460
|
if (deleted >= toDelete) break;
|
|
@@ -1424,39 +1462,56 @@ function rateLimit(options) {
|
|
|
1424
1462
|
deleted++;
|
|
1425
1463
|
}
|
|
1426
1464
|
}
|
|
1427
|
-
}, Math.min(window2, 3e4));
|
|
1428
|
-
if (interval
|
|
1429
|
-
|
|
1430
|
-
const key = getKey(req, ctx);
|
|
1465
|
+
}, Math.min(window2, 3e4)) : null;
|
|
1466
|
+
if (interval?.unref) interval.unref();
|
|
1467
|
+
async function checkAndIncrement(key) {
|
|
1431
1468
|
const now = Date.now();
|
|
1469
|
+
if (storeType === "redis" && redis2) {
|
|
1470
|
+
const redisKey = `${keyPrefix}${key}`;
|
|
1471
|
+
const count = await redis2.incr(redisKey);
|
|
1472
|
+
if (count === 1) {
|
|
1473
|
+
await redis2.pexpire(redisKey, window2);
|
|
1474
|
+
}
|
|
1475
|
+
const pttl = await redis2.pttl(redisKey);
|
|
1476
|
+
const reset = pttl > 0 ? now + pttl : now + window2;
|
|
1477
|
+
return { count, reset };
|
|
1478
|
+
}
|
|
1432
1479
|
let entry = hits.get(key);
|
|
1433
1480
|
if (!entry || entry.reset < now) {
|
|
1434
1481
|
hits.set(key, { count: 1, reset: now + window2 });
|
|
1435
|
-
|
|
1436
|
-
const res2 = await next(req, ctx);
|
|
1437
|
-
return addRateLimitHeaders(res2, max, max - 1, now + window2);
|
|
1482
|
+
return { count: 1, reset: now + window2 };
|
|
1438
1483
|
}
|
|
1439
1484
|
entry.count++;
|
|
1440
|
-
|
|
1441
|
-
|
|
1485
|
+
return { count: entry.count, reset: entry.reset };
|
|
1486
|
+
}
|
|
1487
|
+
const mw = async (req, ctx, next) => {
|
|
1488
|
+
const key = getKey(req, ctx);
|
|
1489
|
+
const now = Date.now();
|
|
1490
|
+
const { count, reset } = await checkAndIncrement(key);
|
|
1491
|
+
if (count > max) {
|
|
1442
1492
|
return new Response(message, {
|
|
1443
1493
|
status: 429,
|
|
1444
1494
|
headers: {
|
|
1445
|
-
"Retry-After": String(Math.ceil((
|
|
1495
|
+
"Retry-After": String(Math.ceil((reset - now) / 1e3)),
|
|
1446
1496
|
"X-RateLimit-Limit": String(max),
|
|
1447
1497
|
"X-RateLimit-Remaining": "0",
|
|
1448
|
-
"X-RateLimit-Reset": String(Math.ceil(
|
|
1498
|
+
"X-RateLimit-Reset": String(Math.ceil(reset / 1e3))
|
|
1449
1499
|
}
|
|
1450
1500
|
});
|
|
1451
1501
|
}
|
|
1502
|
+
const remaining = max - count;
|
|
1452
1503
|
const res = await next(req, ctx);
|
|
1453
|
-
return addRateLimitHeaders(res, max, remaining,
|
|
1504
|
+
return addRateLimitHeaders(res, max, remaining, reset);
|
|
1454
1505
|
};
|
|
1455
1506
|
mw.stop = () => {
|
|
1456
|
-
clearInterval(interval);
|
|
1507
|
+
if (interval) clearInterval(interval);
|
|
1457
1508
|
hits.clear();
|
|
1458
1509
|
};
|
|
1459
|
-
mw.stats = () => ({
|
|
1510
|
+
mw.stats = () => ({
|
|
1511
|
+
store: storeType,
|
|
1512
|
+
entries: storeType === "memory" ? hits.size : void 0,
|
|
1513
|
+
maxEntries: MAX_ENTRIES2
|
|
1514
|
+
});
|
|
1460
1515
|
return mw;
|
|
1461
1516
|
}
|
|
1462
1517
|
function addRateLimitHeaders(res, limit, remaining, reset) {
|
|
@@ -1781,6 +1836,55 @@ var TestApp = class {
|
|
|
1781
1836
|
function testApp() {
|
|
1782
1837
|
return new TestApp();
|
|
1783
1838
|
}
|
|
1839
|
+
async function createTestDb(options) {
|
|
1840
|
+
const dbUrl = options?.url || process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
|
|
1841
|
+
if (!dbUrl) throw new Error("createTestDb: DATABASE_URL or TEST_DATABASE_URL required");
|
|
1842
|
+
const schema = options?.schema || `test_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1843
|
+
const { default: postgres2 } = await import("postgres");
|
|
1844
|
+
const adminSql = postgres2(dbUrl);
|
|
1845
|
+
await adminSql.unsafe('CREATE SCHEMA IF NOT EXISTS "' + schema.replace(/"/g, '""') + '"');
|
|
1846
|
+
const schemaUrl = new URL(dbUrl);
|
|
1847
|
+
schemaUrl.searchParams.set("search_path", schema);
|
|
1848
|
+
const sql2 = postgres2(schemaUrl.toString());
|
|
1849
|
+
await adminSql.end();
|
|
1850
|
+
return {
|
|
1851
|
+
sql: sql2,
|
|
1852
|
+
url: schemaUrl.toString(),
|
|
1853
|
+
schema,
|
|
1854
|
+
destroy: async () => {
|
|
1855
|
+
const destroySql = postgres2(dbUrl);
|
|
1856
|
+
await destroySql.unsafe('DROP SCHEMA IF EXISTS "' + schema.replace(/"/g, '""') + '" CASCADE');
|
|
1857
|
+
await destroySql.end();
|
|
1858
|
+
await sql2.end();
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
async function withTestDb(optionsOrFn, fn) {
|
|
1863
|
+
let dbUrl;
|
|
1864
|
+
let callback;
|
|
1865
|
+
if (typeof optionsOrFn === "function") {
|
|
1866
|
+
callback = optionsOrFn;
|
|
1867
|
+
} else if (typeof optionsOrFn === "string") {
|
|
1868
|
+
dbUrl = optionsOrFn;
|
|
1869
|
+
callback = fn;
|
|
1870
|
+
} else {
|
|
1871
|
+
dbUrl = optionsOrFn?.url;
|
|
1872
|
+
callback = fn;
|
|
1873
|
+
}
|
|
1874
|
+
const resolvedUrl = dbUrl || process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
|
|
1875
|
+
if (!resolvedUrl) throw new Error("withTestDb: DATABASE_URL or TEST_DATABASE_URL required");
|
|
1876
|
+
const { default: postgres2 } = await import("postgres");
|
|
1877
|
+
const sql2 = postgres2(resolvedUrl);
|
|
1878
|
+
try {
|
|
1879
|
+
await sql2.begin(async (txSql) => {
|
|
1880
|
+
await callback(txSql);
|
|
1881
|
+
throw void 0;
|
|
1882
|
+
});
|
|
1883
|
+
} catch {
|
|
1884
|
+
} finally {
|
|
1885
|
+
await sql2.end();
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1784
1888
|
|
|
1785
1889
|
// graphql.ts
|
|
1786
1890
|
import { buildSchema, graphql as executeGraphQL, validate as validateQuery, parse } from "graphql";
|
|
@@ -3324,17 +3428,30 @@ function user(options) {
|
|
|
3324
3428
|
}
|
|
3325
3429
|
function middleware() {
|
|
3326
3430
|
return async (req, ctx, next) => {
|
|
3327
|
-
const
|
|
3328
|
-
if (
|
|
3329
|
-
|
|
3431
|
+
const sessionUserId = ctx.session?.userId;
|
|
3432
|
+
if (sessionUserId) {
|
|
3433
|
+
const row = await findById(sessionUserId);
|
|
3434
|
+
if (row) {
|
|
3435
|
+
ctx.user = stripPassword(row);
|
|
3436
|
+
return next(req, ctx);
|
|
3437
|
+
}
|
|
3438
|
+
if (typeof ctx.session?.destroy === "function") {
|
|
3439
|
+
;
|
|
3440
|
+
ctx.session.destroy();
|
|
3441
|
+
} else {
|
|
3442
|
+
delete ctx.session?.userId;
|
|
3443
|
+
}
|
|
3330
3444
|
}
|
|
3331
|
-
const
|
|
3332
|
-
const
|
|
3333
|
-
if (
|
|
3334
|
-
|
|
3445
|
+
const header = req.headers.get("Authorization");
|
|
3446
|
+
const token = header?.startsWith("Bearer ") ? header.slice(7) : null;
|
|
3447
|
+
if (token) {
|
|
3448
|
+
const userData = await verify(token);
|
|
3449
|
+
if (userData) {
|
|
3450
|
+
ctx.user = userData;
|
|
3451
|
+
return next(req, ctx);
|
|
3452
|
+
}
|
|
3335
3453
|
}
|
|
3336
|
-
|
|
3337
|
-
return next(req, ctx);
|
|
3454
|
+
return new Response("Unauthorized", { status: 401, headers: { "WWW-Authenticate": "Bearer" } });
|
|
3338
3455
|
};
|
|
3339
3456
|
}
|
|
3340
3457
|
function router() {
|
|
@@ -3352,12 +3469,19 @@ function user(options) {
|
|
|
3352
3469
|
return Response.json({ error: err.message }, { status });
|
|
3353
3470
|
}
|
|
3354
3471
|
});
|
|
3355
|
-
r2.post("/login", async (req) => {
|
|
3472
|
+
r2.post("/login", async (req, ctx) => {
|
|
3356
3473
|
try {
|
|
3357
3474
|
const body = await req.json();
|
|
3358
3475
|
const result = await login(body);
|
|
3476
|
+
if (ctx.session) {
|
|
3477
|
+
;
|
|
3478
|
+
ctx.session.userId = result.user.id;
|
|
3479
|
+
ctx.session.role = result.user.role;
|
|
3480
|
+
}
|
|
3359
3481
|
const res = Response.json(result);
|
|
3360
|
-
|
|
3482
|
+
if (!ctx.session) {
|
|
3483
|
+
res.headers.set("Set-Cookie", `session=${result.token}; HttpOnly; SameSite=Lax; Path=/`);
|
|
3484
|
+
}
|
|
3361
3485
|
return res;
|
|
3362
3486
|
} catch (err) {
|
|
3363
3487
|
if (err instanceof z2.ZodError) {
|
|
@@ -3476,6 +3600,8 @@ function queue(opts) {
|
|
|
3476
3600
|
let _processed = 0;
|
|
3477
3601
|
let _failed = 0;
|
|
3478
3602
|
const jobKey = `${prefix}:jobs`;
|
|
3603
|
+
const failedKey = `${prefix}:failed`;
|
|
3604
|
+
const MAX_FAILED = 1e3;
|
|
3479
3605
|
const mw = ((req, ctx, next) => {
|
|
3480
3606
|
ctx.queue = q;
|
|
3481
3607
|
return next(req, ctx);
|
|
@@ -3490,7 +3616,15 @@ function queue(opts) {
|
|
|
3490
3616
|
_processed++;
|
|
3491
3617
|
} catch (e) {
|
|
3492
3618
|
_failed++;
|
|
3493
|
-
|
|
3619
|
+
const errMsg = e.message;
|
|
3620
|
+
console.error("[queue] handler error:", errMsg);
|
|
3621
|
+
const failedEntry = JSON.stringify({
|
|
3622
|
+
...job,
|
|
3623
|
+
error: errMsg,
|
|
3624
|
+
failedAt: Date.now()
|
|
3625
|
+
});
|
|
3626
|
+
await redis2.lpush(failedKey, failedEntry);
|
|
3627
|
+
await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
|
|
3494
3628
|
} finally {
|
|
3495
3629
|
inflight--;
|
|
3496
3630
|
}
|
|
@@ -3571,6 +3705,101 @@ function queue(opts) {
|
|
|
3571
3705
|
while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
|
|
3572
3706
|
redis2.disconnect();
|
|
3573
3707
|
};
|
|
3708
|
+
mw.jobs = async function jobs(limit) {
|
|
3709
|
+
const raw = await redis2.zrevrange(jobKey, 0, (limit ?? 50) - 1);
|
|
3710
|
+
return raw.map((r) => {
|
|
3711
|
+
try {
|
|
3712
|
+
return JSON.parse(r);
|
|
3713
|
+
} catch {
|
|
3714
|
+
return null;
|
|
3715
|
+
}
|
|
3716
|
+
}).filter(Boolean);
|
|
3717
|
+
};
|
|
3718
|
+
mw.failedJobs = async function failedJobs(limit) {
|
|
3719
|
+
const raw = await redis2.lrange(failedKey, 0, (limit ?? 50) - 1);
|
|
3720
|
+
return raw.map((r) => {
|
|
3721
|
+
try {
|
|
3722
|
+
return JSON.parse(r);
|
|
3723
|
+
} catch {
|
|
3724
|
+
return null;
|
|
3725
|
+
}
|
|
3726
|
+
}).filter(Boolean);
|
|
3727
|
+
};
|
|
3728
|
+
mw.retryFailed = async function retryFailed(jobId) {
|
|
3729
|
+
const raw = await redis2.lrange(failedKey, 0, -1);
|
|
3730
|
+
for (const entry of raw) {
|
|
3731
|
+
try {
|
|
3732
|
+
const job = JSON.parse(entry);
|
|
3733
|
+
if (job.id === jobId) {
|
|
3734
|
+
await redis2.lrem(failedKey, 1, entry);
|
|
3735
|
+
const reJob = { ...job, runAt: Date.now() };
|
|
3736
|
+
delete reJob.error;
|
|
3737
|
+
delete reJob.failedAt;
|
|
3738
|
+
await redis2.zadd(jobKey, reJob.runAt, JSON.stringify(reJob));
|
|
3739
|
+
_failed--;
|
|
3740
|
+
return true;
|
|
3741
|
+
}
|
|
3742
|
+
} catch {
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
return false;
|
|
3746
|
+
};
|
|
3747
|
+
mw.retryAllFailed = async function retryAllFailed(type) {
|
|
3748
|
+
const raw = await redis2.lrange(failedKey, 0, -1);
|
|
3749
|
+
let count = 0;
|
|
3750
|
+
for (const entry of raw) {
|
|
3751
|
+
try {
|
|
3752
|
+
const job = JSON.parse(entry);
|
|
3753
|
+
if (type && job.type !== type) continue;
|
|
3754
|
+
await redis2.lrem(failedKey, 1, entry);
|
|
3755
|
+
const reJob = { ...job, runAt: Date.now() };
|
|
3756
|
+
delete reJob.error;
|
|
3757
|
+
delete reJob.failedAt;
|
|
3758
|
+
await redis2.zadd(jobKey, reJob.runAt, JSON.stringify(reJob));
|
|
3759
|
+
_failed--;
|
|
3760
|
+
count++;
|
|
3761
|
+
} catch {
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
return count;
|
|
3765
|
+
};
|
|
3766
|
+
mw.dashboard = function dashboard() {
|
|
3767
|
+
const r = new Router();
|
|
3768
|
+
r.get("/", async (req, ctx) => {
|
|
3769
|
+
const s = q.stats();
|
|
3770
|
+
const pending = await q.jobs(100);
|
|
3771
|
+
const byType = {};
|
|
3772
|
+
for (const job of pending) {
|
|
3773
|
+
if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
|
|
3774
|
+
byType[job.type].pending++;
|
|
3775
|
+
}
|
|
3776
|
+
const failed = await q.failedJobs(1e3);
|
|
3777
|
+
for (const job of failed) {
|
|
3778
|
+
if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
|
|
3779
|
+
byType[job.type].failed++;
|
|
3780
|
+
}
|
|
3781
|
+
return Response.json({
|
|
3782
|
+
stats: s,
|
|
3783
|
+
types: byType,
|
|
3784
|
+
failedCount: failed.length
|
|
3785
|
+
});
|
|
3786
|
+
});
|
|
3787
|
+
r.get("/:type/failed", async (req, ctx) => {
|
|
3788
|
+
const failed = await q.failedJobs(100);
|
|
3789
|
+
const filtered = failed.filter((j) => j.type === ctx.params.type);
|
|
3790
|
+
return Response.json({ jobs: filtered, count: filtered.length });
|
|
3791
|
+
});
|
|
3792
|
+
r.post("/:type/retry", async (req, ctx) => {
|
|
3793
|
+
const count = await q.retryAllFailed(ctx.params.type);
|
|
3794
|
+
return Response.json({ retried: count });
|
|
3795
|
+
});
|
|
3796
|
+
r.post("/retry/:id", async (req, ctx) => {
|
|
3797
|
+
const ok = await q.retryFailed(ctx.params.id);
|
|
3798
|
+
if (!ok) return new Response("Job not found", { status: 404 });
|
|
3799
|
+
return Response.json({ retried: true });
|
|
3800
|
+
});
|
|
3801
|
+
return r;
|
|
3802
|
+
};
|
|
3574
3803
|
mw.stats = () => ({
|
|
3575
3804
|
running,
|
|
3576
3805
|
inflight,
|
|
@@ -7254,12 +7483,12 @@ async function buildRouter4(deps) {
|
|
|
7254
7483
|
const router = new Router();
|
|
7255
7484
|
router.post("/sessions", async (req, ctx) => {
|
|
7256
7485
|
const body = await req.json().catch(() => ({}));
|
|
7257
|
-
const
|
|
7486
|
+
const session2 = await createSession(sql2, {
|
|
7258
7487
|
title: body.title,
|
|
7259
7488
|
model: body.model,
|
|
7260
7489
|
systemPrompt: body.systemPrompt || systemPrompt
|
|
7261
7490
|
}, workspace, ctx.mountPath || "");
|
|
7262
|
-
return Response.json(
|
|
7491
|
+
return Response.json(session2, { status: 201 });
|
|
7263
7492
|
});
|
|
7264
7493
|
router.get("/sessions", async () => {
|
|
7265
7494
|
const sessions2 = await listSessions(sql2);
|
|
@@ -7267,10 +7496,10 @@ async function buildRouter4(deps) {
|
|
|
7267
7496
|
});
|
|
7268
7497
|
router.get("/sessions/:id", async (_req, ctx) => {
|
|
7269
7498
|
const id2 = ctx.params.id;
|
|
7270
|
-
const
|
|
7271
|
-
if (!
|
|
7499
|
+
const session2 = await getSession(sql2, id2);
|
|
7500
|
+
if (!session2) return new Response("Not Found", { status: 404 });
|
|
7272
7501
|
const messages2 = await getHistory(sql2, id2);
|
|
7273
|
-
return Response.json({ session, messages: messages2 });
|
|
7502
|
+
return Response.json({ session: session2, messages: messages2 });
|
|
7274
7503
|
});
|
|
7275
7504
|
router.delete("/sessions/:id", async (_req, ctx) => {
|
|
7276
7505
|
const id2 = ctx.params.id;
|
|
@@ -7279,12 +7508,12 @@ async function buildRouter4(deps) {
|
|
|
7279
7508
|
});
|
|
7280
7509
|
router.post("/sessions/:id/message", async (req, ctx) => {
|
|
7281
7510
|
const sessionId = ctx.params.id;
|
|
7282
|
-
const
|
|
7283
|
-
if (!
|
|
7511
|
+
const session2 = await getSession(sql2, sessionId);
|
|
7512
|
+
if (!session2) return new Response("Session Not Found", { status: 404 });
|
|
7284
7513
|
const { content } = await req.json();
|
|
7285
7514
|
if (!content) return new Response("Missing content", { status: 400 });
|
|
7286
7515
|
const toolCtx = {
|
|
7287
|
-
workspace:
|
|
7516
|
+
workspace: session2.workspace || workspace,
|
|
7288
7517
|
permissions,
|
|
7289
7518
|
pendingQuestions,
|
|
7290
7519
|
skillsRegistry: deps.skillsRegistry
|
|
@@ -7292,10 +7521,10 @@ async function buildRouter4(deps) {
|
|
|
7292
7521
|
const tools = createTools(toolCtx);
|
|
7293
7522
|
const allSkills = [...skills];
|
|
7294
7523
|
const sysPrompt = buildSystemPrompt({
|
|
7295
|
-
workspace:
|
|
7296
|
-
model:
|
|
7524
|
+
workspace: session2.workspace || workspace,
|
|
7525
|
+
model: session2.model,
|
|
7297
7526
|
skills: allSkills,
|
|
7298
|
-
systemPrompt:
|
|
7527
|
+
systemPrompt: session2.system_prompt || systemPrompt
|
|
7299
7528
|
});
|
|
7300
7529
|
const history = await getHistory(sql2, sessionId);
|
|
7301
7530
|
await addTextMessage(sql2, sessionId, "user", content);
|
|
@@ -7312,8 +7541,8 @@ async function buildRouter4(deps) {
|
|
|
7312
7541
|
});
|
|
7313
7542
|
router.get("/sessions/:id/usage", async (_req, ctx) => {
|
|
7314
7543
|
const sessionId = ctx.params.id;
|
|
7315
|
-
const
|
|
7316
|
-
if (!
|
|
7544
|
+
const session2 = await getSession(sql2, sessionId);
|
|
7545
|
+
if (!session2) return new Response("Not Found", { status: 404 });
|
|
7317
7546
|
const rows = await sql2`
|
|
7318
7547
|
SELECT
|
|
7319
7548
|
COUNT(*)::int AS message_count,
|
|
@@ -7363,13 +7592,13 @@ function createWSHandler2(deps) {
|
|
|
7363
7592
|
switch (msg.type) {
|
|
7364
7593
|
case "create": {
|
|
7365
7594
|
try {
|
|
7366
|
-
const
|
|
7595
|
+
const session2 = await createSession(sql2, {
|
|
7367
7596
|
userId: client.userId,
|
|
7368
7597
|
title: msg.title,
|
|
7369
7598
|
model: msg.model,
|
|
7370
7599
|
systemPrompt: msg.systemPrompt || systemPrompt
|
|
7371
7600
|
}, workspace, client.mountPath);
|
|
7372
|
-
ws.send(JSON.stringify({ type: "session_created", session }));
|
|
7601
|
+
ws.send(JSON.stringify({ type: "session_created", session: session2 }));
|
|
7373
7602
|
} catch (e) {
|
|
7374
7603
|
ws.send(JSON.stringify({ type: "error", error: e.message }));
|
|
7375
7604
|
}
|
|
@@ -7386,13 +7615,13 @@ function createWSHandler2(deps) {
|
|
|
7386
7615
|
client.abortController = controller;
|
|
7387
7616
|
client.currentSessionId = session_id;
|
|
7388
7617
|
try {
|
|
7389
|
-
const
|
|
7390
|
-
if (!
|
|
7618
|
+
const session2 = await getSession(sql2, session_id);
|
|
7619
|
+
if (!session2) {
|
|
7391
7620
|
ws.send(JSON.stringify({ type: "error", error: "Session not found" }));
|
|
7392
7621
|
return;
|
|
7393
7622
|
}
|
|
7394
7623
|
const toolCtx = {
|
|
7395
|
-
workspace:
|
|
7624
|
+
workspace: session2.workspace || workspace,
|
|
7396
7625
|
permissions,
|
|
7397
7626
|
pendingQuestions,
|
|
7398
7627
|
skillsRegistry
|
|
@@ -7400,10 +7629,10 @@ function createWSHandler2(deps) {
|
|
|
7400
7629
|
const tools = createTools(toolCtx);
|
|
7401
7630
|
const allSkills = [...skills];
|
|
7402
7631
|
const sysPrompt = buildSystemPrompt({
|
|
7403
|
-
workspace:
|
|
7404
|
-
model:
|
|
7632
|
+
workspace: session2.workspace || workspace,
|
|
7633
|
+
model: session2.model,
|
|
7405
7634
|
skills: allSkills,
|
|
7406
|
-
systemPrompt:
|
|
7635
|
+
systemPrompt: session2.system_prompt || systemPrompt
|
|
7407
7636
|
});
|
|
7408
7637
|
const history = await getHistory(sql2, session_id);
|
|
7409
7638
|
await addTextMessage(sql2, session_id, "user", content);
|
|
@@ -7911,14 +8140,14 @@ function preferences(options) {
|
|
|
7911
8140
|
const dir = options.dir ? resolve13(options.dir) : void 0;
|
|
7912
8141
|
const localeOpts = { ...defaults.locale, ...options.locale };
|
|
7913
8142
|
const themeOpts = { ...defaults.theme, ...options.theme };
|
|
7914
|
-
const
|
|
8143
|
+
const cache3 = /* @__PURE__ */ new Map();
|
|
7915
8144
|
function validLocale(locale) {
|
|
7916
8145
|
return /^[\w-]+$/.test(locale) && !locale.includes("..");
|
|
7917
8146
|
}
|
|
7918
8147
|
async function load(locale) {
|
|
7919
8148
|
if (!dir) return {};
|
|
7920
8149
|
if (!validLocale(locale)) return {};
|
|
7921
|
-
const cached =
|
|
8150
|
+
const cached = cache3.get(locale);
|
|
7922
8151
|
if (cached) return cached;
|
|
7923
8152
|
const filePath = join7(dir, `${locale}.json`);
|
|
7924
8153
|
let data = null;
|
|
@@ -7926,16 +8155,16 @@ function preferences(options) {
|
|
|
7926
8155
|
await stat2(filePath);
|
|
7927
8156
|
const content = await readFile2(filePath, "utf-8");
|
|
7928
8157
|
data = JSON.parse(content);
|
|
7929
|
-
|
|
8158
|
+
cache3.set(locale, data);
|
|
7930
8159
|
return data;
|
|
7931
8160
|
} catch {
|
|
7932
8161
|
}
|
|
7933
8162
|
if (!data) {
|
|
7934
8163
|
const short = locale.split("-")[0];
|
|
7935
8164
|
if (short !== locale) {
|
|
7936
|
-
const fallback =
|
|
8165
|
+
const fallback = cache3.get(short) || await load(short);
|
|
7937
8166
|
if (fallback && Object.keys(fallback).length > 0) {
|
|
7938
|
-
|
|
8167
|
+
cache3.set(locale, fallback);
|
|
7939
8168
|
return fallback;
|
|
7940
8169
|
}
|
|
7941
8170
|
}
|
|
@@ -9352,9 +9581,753 @@ function registerWorker(url) {
|
|
|
9352
9581
|
}
|
|
9353
9582
|
};
|
|
9354
9583
|
}
|
|
9584
|
+
|
|
9585
|
+
// session.ts
|
|
9586
|
+
import crypto7 from "node:crypto";
|
|
9587
|
+
var kSaved = /* @__PURE__ */ Symbol("session.saved");
|
|
9588
|
+
var kDestroyed = /* @__PURE__ */ Symbol("session.destroyed");
|
|
9589
|
+
var kId = /* @__PURE__ */ Symbol("session.id");
|
|
9590
|
+
var kStore = /* @__PURE__ */ Symbol("session.store");
|
|
9591
|
+
var kTtl = /* @__PURE__ */ Symbol("session.ttl");
|
|
9592
|
+
var MAX_SESSIONS = 1e5;
|
|
9593
|
+
var MemoryStore = class {
|
|
9594
|
+
store = /* @__PURE__ */ new Map();
|
|
9595
|
+
interval;
|
|
9596
|
+
constructor(cleanupMs = 6e4) {
|
|
9597
|
+
this.interval = setInterval(() => this.cleanup(), cleanupMs);
|
|
9598
|
+
if (this.interval.unref) this.interval.unref();
|
|
9599
|
+
}
|
|
9600
|
+
async get(sid) {
|
|
9601
|
+
const entry = this.store.get(sid);
|
|
9602
|
+
if (!entry) return null;
|
|
9603
|
+
if (Date.now() > entry.expires) {
|
|
9604
|
+
this.store.delete(sid);
|
|
9605
|
+
return null;
|
|
9606
|
+
}
|
|
9607
|
+
return entry.data;
|
|
9608
|
+
}
|
|
9609
|
+
async set(sid, data, ttl) {
|
|
9610
|
+
if (this.store.size >= MAX_SESSIONS) {
|
|
9611
|
+
const oldest = this.store.keys().next();
|
|
9612
|
+
if (!oldest.done) this.store.delete(oldest.value);
|
|
9613
|
+
}
|
|
9614
|
+
this.store.set(sid, { data, expires: Date.now() + ttl });
|
|
9615
|
+
}
|
|
9616
|
+
async destroy(sid) {
|
|
9617
|
+
this.store.delete(sid);
|
|
9618
|
+
}
|
|
9619
|
+
cleanup() {
|
|
9620
|
+
const now = Date.now();
|
|
9621
|
+
for (const [key, entry] of this.store) {
|
|
9622
|
+
if (entry.expires < now) this.store.delete(key);
|
|
9623
|
+
}
|
|
9624
|
+
}
|
|
9625
|
+
close() {
|
|
9626
|
+
clearInterval(this.interval);
|
|
9627
|
+
this.store.clear();
|
|
9628
|
+
}
|
|
9629
|
+
/** Testing only: return approximate count. */
|
|
9630
|
+
get size() {
|
|
9631
|
+
return this.store.size;
|
|
9632
|
+
}
|
|
9633
|
+
};
|
|
9634
|
+
var RedisStore = class {
|
|
9635
|
+
redis;
|
|
9636
|
+
prefix;
|
|
9637
|
+
constructor(redis2, prefix = "session:") {
|
|
9638
|
+
this.redis = redis2;
|
|
9639
|
+
this.prefix = prefix;
|
|
9640
|
+
}
|
|
9641
|
+
key(sid) {
|
|
9642
|
+
return `${this.prefix}${sid}`;
|
|
9643
|
+
}
|
|
9644
|
+
async get(sid) {
|
|
9645
|
+
const raw = await this.redis.get(this.key(sid));
|
|
9646
|
+
if (!raw) return null;
|
|
9647
|
+
try {
|
|
9648
|
+
return JSON.parse(raw);
|
|
9649
|
+
} catch {
|
|
9650
|
+
await this.redis.del(this.key(sid));
|
|
9651
|
+
return null;
|
|
9652
|
+
}
|
|
9653
|
+
}
|
|
9654
|
+
async set(sid, data, ttl) {
|
|
9655
|
+
const ttlSec = Math.ceil(ttl / 1e3);
|
|
9656
|
+
await this.redis.setex(this.key(sid), ttlSec, JSON.stringify(data));
|
|
9657
|
+
}
|
|
9658
|
+
async destroy(sid) {
|
|
9659
|
+
await this.redis.del(this.key(sid));
|
|
9660
|
+
}
|
|
9661
|
+
};
|
|
9662
|
+
function createSessionObject(data, sid, store2, ttl) {
|
|
9663
|
+
const obj = data ?? {};
|
|
9664
|
+
obj[kSaved] = false;
|
|
9665
|
+
obj[kDestroyed] = false;
|
|
9666
|
+
obj[kId] = sid;
|
|
9667
|
+
obj[kStore] = store2;
|
|
9668
|
+
obj[kTtl] = ttl;
|
|
9669
|
+
obj.save = () => {
|
|
9670
|
+
obj[kSaved] = true;
|
|
9671
|
+
};
|
|
9672
|
+
obj.destroy = () => {
|
|
9673
|
+
obj[kDestroyed] = true;
|
|
9674
|
+
obj[kSaved] = false;
|
|
9675
|
+
for (const key of Object.keys(obj)) {
|
|
9676
|
+
if (typeof key === "symbol") continue;
|
|
9677
|
+
delete obj[key];
|
|
9678
|
+
}
|
|
9679
|
+
};
|
|
9680
|
+
Object.defineProperty(obj, "id", {
|
|
9681
|
+
get: () => obj[kId],
|
|
9682
|
+
enumerable: false,
|
|
9683
|
+
configurable: false
|
|
9684
|
+
});
|
|
9685
|
+
Object.defineProperty(obj, "save", { enumerable: false, configurable: true, writable: true, value: obj.save });
|
|
9686
|
+
Object.defineProperty(obj, "destroy", { enumerable: false, configurable: true, writable: true, value: obj.destroy });
|
|
9687
|
+
return obj;
|
|
9688
|
+
}
|
|
9689
|
+
function isSessionActive(session2) {
|
|
9690
|
+
for (const key of Object.keys(session2)) {
|
|
9691
|
+
if (key !== "save" && key !== "destroy") return true;
|
|
9692
|
+
}
|
|
9693
|
+
return false;
|
|
9694
|
+
}
|
|
9695
|
+
function session(options) {
|
|
9696
|
+
const ttl = options?.ttl ?? 24 * 60 * 60 * 1e3;
|
|
9697
|
+
const cookieName = options?.cookieName ?? "__session";
|
|
9698
|
+
const cookieOpts = {
|
|
9699
|
+
path: options?.cookie?.path ?? "/",
|
|
9700
|
+
domain: options?.cookie?.domain,
|
|
9701
|
+
httpOnly: options?.cookie?.httpOnly ?? true,
|
|
9702
|
+
secure: options?.cookie?.secure ?? process.env.NODE_ENV === "production",
|
|
9703
|
+
sameSite: options?.cookie?.sameSite ?? "lax"
|
|
9704
|
+
};
|
|
9705
|
+
let store2;
|
|
9706
|
+
let closeStore = null;
|
|
9707
|
+
if (options?.store && typeof options.store.get === "function") {
|
|
9708
|
+
store2 = options.store;
|
|
9709
|
+
} else if (options?.store === "redis") {
|
|
9710
|
+
if (!options.redis) throw new Error('session: redis client required when store: "redis"');
|
|
9711
|
+
store2 = new RedisStore(options.redis);
|
|
9712
|
+
} else {
|
|
9713
|
+
const mem = new MemoryStore();
|
|
9714
|
+
store2 = mem;
|
|
9715
|
+
closeStore = () => mem.close();
|
|
9716
|
+
}
|
|
9717
|
+
const mw = (async (req, ctx, next) => {
|
|
9718
|
+
const cookies = getCookies(req);
|
|
9719
|
+
const sid = cookies[cookieName];
|
|
9720
|
+
let session2;
|
|
9721
|
+
let loadedSid = sid ?? null;
|
|
9722
|
+
if (sid) {
|
|
9723
|
+
const data = await store2.get(sid);
|
|
9724
|
+
if (data) {
|
|
9725
|
+
session2 = createSessionObject(data, sid, store2, ttl);
|
|
9726
|
+
} else {
|
|
9727
|
+
loadedSid = null;
|
|
9728
|
+
session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
|
|
9729
|
+
}
|
|
9730
|
+
} else {
|
|
9731
|
+
session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
|
|
9732
|
+
}
|
|
9733
|
+
const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
|
|
9734
|
+
ctx.session = session2;
|
|
9735
|
+
ctx.sessionId = session2.id;
|
|
9736
|
+
const res = await next(req, ctx);
|
|
9737
|
+
const currentSession = ctx.session;
|
|
9738
|
+
if (!currentSession || currentSession[kDestroyed]) {
|
|
9739
|
+
if (loadedSid) {
|
|
9740
|
+
await store2.destroy(loadedSid);
|
|
9741
|
+
}
|
|
9742
|
+
return deleteCookie(res, cookieName, cookieOpts);
|
|
9743
|
+
}
|
|
9744
|
+
const currentData = isSessionActive(currentSession) ? JSON.stringify(currentSession) : null;
|
|
9745
|
+
const wasSaved = currentSession[kSaved];
|
|
9746
|
+
const changed = wasSaved || currentData !== snapshot;
|
|
9747
|
+
if (!changed) {
|
|
9748
|
+
if (loadedSid) {
|
|
9749
|
+
if (store2 instanceof RedisStore) {
|
|
9750
|
+
await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
|
|
9751
|
+
}
|
|
9752
|
+
}
|
|
9753
|
+
return res;
|
|
9754
|
+
}
|
|
9755
|
+
if (currentData && currentData !== "{}") {
|
|
9756
|
+
const targetSid = loadedSid ?? currentSession.id;
|
|
9757
|
+
const data = JSON.parse(currentData);
|
|
9758
|
+
await store2.set(targetSid, data, ttl);
|
|
9759
|
+
if (!loadedSid) {
|
|
9760
|
+
const cookieRes = setCookie(res, cookieName, targetSid, cookieOpts);
|
|
9761
|
+
return cookieRes;
|
|
9762
|
+
}
|
|
9763
|
+
} else if (loadedSid) {
|
|
9764
|
+
await store2.destroy(loadedSid);
|
|
9765
|
+
return deleteCookie(res, cookieName, cookieOpts);
|
|
9766
|
+
}
|
|
9767
|
+
return res;
|
|
9768
|
+
});
|
|
9769
|
+
mw.close = () => {
|
|
9770
|
+
closeStore?.();
|
|
9771
|
+
};
|
|
9772
|
+
mw.store = store2;
|
|
9773
|
+
return mw;
|
|
9774
|
+
}
|
|
9775
|
+
|
|
9776
|
+
// cache.ts
|
|
9777
|
+
import crypto8 from "node:crypto";
|
|
9778
|
+
var BINARY_PREFIXES = [
|
|
9779
|
+
"image/",
|
|
9780
|
+
"audio/",
|
|
9781
|
+
"video/",
|
|
9782
|
+
"application/octet-stream",
|
|
9783
|
+
"application/pdf",
|
|
9784
|
+
"application/zip",
|
|
9785
|
+
"application/gzip",
|
|
9786
|
+
"application/x-tar",
|
|
9787
|
+
"application/vnd.ms-",
|
|
9788
|
+
"application/vnd.openxmlformats-"
|
|
9789
|
+
];
|
|
9790
|
+
function isCacheableContentType(ct) {
|
|
9791
|
+
return !BINARY_PREFIXES.some((p) => ct.startsWith(p));
|
|
9792
|
+
}
|
|
9793
|
+
function isCacheableStatus(status, allowed) {
|
|
9794
|
+
return allowed.includes(status);
|
|
9795
|
+
}
|
|
9796
|
+
function defaultCacheKey(req) {
|
|
9797
|
+
const hash = crypto8.createHash("sha256");
|
|
9798
|
+
hash.update(req.method);
|
|
9799
|
+
hash.update(req.url);
|
|
9800
|
+
return hash.digest("hex");
|
|
9801
|
+
}
|
|
9802
|
+
var MAX_ENTRIES = 1e5;
|
|
9803
|
+
var MemoryCache = class {
|
|
9804
|
+
store = /* @__PURE__ */ new Map();
|
|
9805
|
+
tagIndex = /* @__PURE__ */ new Map();
|
|
9806
|
+
interval;
|
|
9807
|
+
constructor(cleanupMs = 6e4) {
|
|
9808
|
+
this.interval = setInterval(() => this.cleanup(), cleanupMs);
|
|
9809
|
+
if (this.interval.unref) this.interval.unref();
|
|
9810
|
+
}
|
|
9811
|
+
async get(key) {
|
|
9812
|
+
const entry = this.store.get(key);
|
|
9813
|
+
if (!entry) return null;
|
|
9814
|
+
if (Date.now() > entry.expires) {
|
|
9815
|
+
this.store.delete(key);
|
|
9816
|
+
return null;
|
|
9817
|
+
}
|
|
9818
|
+
return entry.data;
|
|
9819
|
+
}
|
|
9820
|
+
async set(key, data, ttl) {
|
|
9821
|
+
if (this.store.size >= MAX_ENTRIES) {
|
|
9822
|
+
const oldest = this.store.keys().next();
|
|
9823
|
+
if (!oldest.done) this.store.delete(oldest.value);
|
|
9824
|
+
}
|
|
9825
|
+
this.store.set(key, { data, expires: Date.now() + ttl });
|
|
9826
|
+
for (const tag of data.tags) {
|
|
9827
|
+
let set = this.tagIndex.get(tag);
|
|
9828
|
+
if (!set) {
|
|
9829
|
+
set = /* @__PURE__ */ new Set();
|
|
9830
|
+
this.tagIndex.set(tag, set);
|
|
9831
|
+
}
|
|
9832
|
+
set.add(key);
|
|
9833
|
+
}
|
|
9834
|
+
}
|
|
9835
|
+
async delete(key) {
|
|
9836
|
+
this.store.delete(key);
|
|
9837
|
+
for (const [, set] of this.tagIndex) {
|
|
9838
|
+
set.delete(key);
|
|
9839
|
+
}
|
|
9840
|
+
}
|
|
9841
|
+
async invalidate(tag) {
|
|
9842
|
+
const keys = this.tagIndex.get(tag);
|
|
9843
|
+
if (!keys) return;
|
|
9844
|
+
for (const key of keys) {
|
|
9845
|
+
this.store.delete(key);
|
|
9846
|
+
}
|
|
9847
|
+
this.tagIndex.delete(tag);
|
|
9848
|
+
}
|
|
9849
|
+
async flush() {
|
|
9850
|
+
this.store.clear();
|
|
9851
|
+
this.tagIndex.clear();
|
|
9852
|
+
}
|
|
9853
|
+
cleanup() {
|
|
9854
|
+
const now = Date.now();
|
|
9855
|
+
for (const [key, entry] of this.store) {
|
|
9856
|
+
if (entry.expires < now) {
|
|
9857
|
+
this.store.delete(key);
|
|
9858
|
+
for (const [, set] of this.tagIndex) {
|
|
9859
|
+
set.delete(key);
|
|
9860
|
+
}
|
|
9861
|
+
}
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
close() {
|
|
9865
|
+
clearInterval(this.interval);
|
|
9866
|
+
this.store.clear();
|
|
9867
|
+
this.tagIndex.clear();
|
|
9868
|
+
}
|
|
9869
|
+
/** Testing only. */
|
|
9870
|
+
get size() {
|
|
9871
|
+
return this.store.size;
|
|
9872
|
+
}
|
|
9873
|
+
};
|
|
9874
|
+
var RedisCache = class {
|
|
9875
|
+
redis;
|
|
9876
|
+
prefix;
|
|
9877
|
+
tagPrefix;
|
|
9878
|
+
constructor(redis2, prefix = "cache:") {
|
|
9879
|
+
this.redis = redis2;
|
|
9880
|
+
this.prefix = prefix;
|
|
9881
|
+
this.tagPrefix = `${prefix}tag:`;
|
|
9882
|
+
}
|
|
9883
|
+
key(sid) {
|
|
9884
|
+
return `${this.prefix}${sid}`;
|
|
9885
|
+
}
|
|
9886
|
+
tagKey(tag) {
|
|
9887
|
+
return `${this.tagPrefix}${tag}`;
|
|
9888
|
+
}
|
|
9889
|
+
async get(key) {
|
|
9890
|
+
const raw = await this.redis.get(this.key(key));
|
|
9891
|
+
if (!raw) return null;
|
|
9892
|
+
try {
|
|
9893
|
+
return JSON.parse(raw);
|
|
9894
|
+
} catch {
|
|
9895
|
+
await this.redis.del(this.key(key));
|
|
9896
|
+
return null;
|
|
9897
|
+
}
|
|
9898
|
+
}
|
|
9899
|
+
async set(key, entry, ttl) {
|
|
9900
|
+
const multi = this.redis.multi();
|
|
9901
|
+
multi.psetex(this.key(key), ttl, JSON.stringify(entry));
|
|
9902
|
+
const ttlSec = Math.ceil(ttl / 1e3);
|
|
9903
|
+
for (const tag of entry.tags) {
|
|
9904
|
+
multi.sadd(this.tagKey(tag), key);
|
|
9905
|
+
multi.expire(this.tagKey(tag), ttlSec);
|
|
9906
|
+
}
|
|
9907
|
+
await multi.exec();
|
|
9908
|
+
}
|
|
9909
|
+
async delete(key) {
|
|
9910
|
+
await this.redis.del(this.key(key));
|
|
9911
|
+
}
|
|
9912
|
+
async invalidate(tag) {
|
|
9913
|
+
const key = this.tagKey(tag);
|
|
9914
|
+
const members = await this.redis.smembers(key);
|
|
9915
|
+
if (members.length > 0) {
|
|
9916
|
+
await this.redis.del(key, ...members.map((m) => this.key(m)));
|
|
9917
|
+
}
|
|
9918
|
+
}
|
|
9919
|
+
async flush() {
|
|
9920
|
+
const keys = await this.redis.keys(`${this.prefix}*`);
|
|
9921
|
+
if (keys.length > 0) await this.redis.del(...keys);
|
|
9922
|
+
}
|
|
9923
|
+
};
|
|
9924
|
+
var DEFAULT_TTL = 3e5;
|
|
9925
|
+
var DEFAULT_MAX_BODY2 = 1024 * 1024;
|
|
9926
|
+
function shouldSkipCache(req) {
|
|
9927
|
+
if (req.method !== "GET" && req.method !== "HEAD") return true;
|
|
9928
|
+
if (req.headers.get("authorization")) return true;
|
|
9929
|
+
if (req.headers.get("cookie")) return true;
|
|
9930
|
+
return false;
|
|
9931
|
+
}
|
|
9932
|
+
function cache2(options) {
|
|
9933
|
+
const ttl = options?.ttl ?? DEFAULT_TTL;
|
|
9934
|
+
const cacheStatus = options?.cacheStatus ?? [200];
|
|
9935
|
+
const maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY2;
|
|
9936
|
+
const getKey = options?.key ?? defaultCacheKey;
|
|
9937
|
+
const getTag = options?.tag;
|
|
9938
|
+
const cacheCookies = options?.cacheCookies ?? false;
|
|
9939
|
+
let store2;
|
|
9940
|
+
let closeStore = null;
|
|
9941
|
+
if (options?.store && typeof options.store.get === "function") {
|
|
9942
|
+
store2 = options.store;
|
|
9943
|
+
} else if (options?.store === "redis") {
|
|
9944
|
+
if (!options.redis) throw new Error('cache: redis client required when store: "redis"');
|
|
9945
|
+
store2 = new RedisCache(options.redis);
|
|
9946
|
+
} else {
|
|
9947
|
+
const mem = new MemoryCache();
|
|
9948
|
+
store2 = mem;
|
|
9949
|
+
closeStore = () => mem.close();
|
|
9950
|
+
}
|
|
9951
|
+
const mw = (async (req, ctx, next) => {
|
|
9952
|
+
if (shouldSkipCache(req)) {
|
|
9953
|
+
return next(req, ctx);
|
|
9954
|
+
}
|
|
9955
|
+
const cacheKey = getKey(req);
|
|
9956
|
+
const cached = await store2.get(cacheKey);
|
|
9957
|
+
if (cached) {
|
|
9958
|
+
const age = Math.floor((Date.now() - cached.createdAt) / 1e3);
|
|
9959
|
+
const headers2 = new Headers(cached.headers);
|
|
9960
|
+
headers2.set("Age", String(age));
|
|
9961
|
+
headers2.set("X-Cache", "HIT");
|
|
9962
|
+
return new Response(cached.body, {
|
|
9963
|
+
status: cached.status,
|
|
9964
|
+
statusText: cached.statusText,
|
|
9965
|
+
headers: headers2
|
|
9966
|
+
});
|
|
9967
|
+
}
|
|
9968
|
+
const res = await next(req, ctx);
|
|
9969
|
+
if (!isCacheableStatus(res.status, cacheStatus)) return res;
|
|
9970
|
+
if (res.headers.get("set-cookie") && !cacheCookies) return res;
|
|
9971
|
+
if (res.headers.get("cache-control")?.includes("no-store")) return res;
|
|
9972
|
+
if (res.body && res.headers.get("content-type")?.includes("text/event-stream")) return res;
|
|
9973
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
9974
|
+
if (!isCacheableContentType(ct)) return res;
|
|
9975
|
+
const clone = res.clone();
|
|
9976
|
+
const bodyText = await clone.text();
|
|
9977
|
+
if (bodyText.length > maxBodySize) return res;
|
|
9978
|
+
const tags = [];
|
|
9979
|
+
if (getTag) {
|
|
9980
|
+
const result = getTag(req, ctx);
|
|
9981
|
+
if (result) {
|
|
9982
|
+
if (Array.isArray(result)) tags.push(...result);
|
|
9983
|
+
else tags.push(result);
|
|
9984
|
+
}
|
|
9985
|
+
}
|
|
9986
|
+
const headers = {};
|
|
9987
|
+
res.headers.forEach((value, key) => {
|
|
9988
|
+
if (key.toLowerCase() === "set-cookie" && !cacheCookies) return;
|
|
9989
|
+
headers[key] = value;
|
|
9990
|
+
});
|
|
9991
|
+
const entry = {
|
|
9992
|
+
status: res.status,
|
|
9993
|
+
statusText: res.statusText,
|
|
9994
|
+
headers,
|
|
9995
|
+
body: bodyText,
|
|
9996
|
+
createdAt: Date.now(),
|
|
9997
|
+
tags
|
|
9998
|
+
};
|
|
9999
|
+
await store2.set(cacheKey, entry, ttl);
|
|
10000
|
+
return res;
|
|
10001
|
+
});
|
|
10002
|
+
mw.store = store2;
|
|
10003
|
+
mw.invalidate = async (tag) => store2.invalidate(tag);
|
|
10004
|
+
mw.flush = async () => store2.flush();
|
|
10005
|
+
mw.close = () => {
|
|
10006
|
+
closeStore?.();
|
|
10007
|
+
};
|
|
10008
|
+
return mw;
|
|
10009
|
+
}
|
|
10010
|
+
|
|
10011
|
+
// webhook.ts
|
|
10012
|
+
import crypto9 from "node:crypto";
|
|
10013
|
+
function timingSafeEqual2(a, b) {
|
|
10014
|
+
try {
|
|
10015
|
+
return crypto9.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
10016
|
+
} catch {
|
|
10017
|
+
return false;
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
function createStripeVerifier(config) {
|
|
10021
|
+
return (body, headers) => {
|
|
10022
|
+
const sigHeader = headers["stripe-signature"];
|
|
10023
|
+
if (!sigHeader) return { valid: false, provider: "stripe", event: "", id: void 0 };
|
|
10024
|
+
const parts = sigHeader.split(",").reduce((acc, p) => {
|
|
10025
|
+
const [k, ...v] = p.split("=");
|
|
10026
|
+
if (k) acc[k.trim()] = v.join("=").trim();
|
|
10027
|
+
return acc;
|
|
10028
|
+
}, {});
|
|
10029
|
+
const timestamp = parts["t"];
|
|
10030
|
+
const signature = parts["v1"];
|
|
10031
|
+
if (!timestamp || !signature) return { valid: false, provider: "stripe", event: "", id: void 0 };
|
|
10032
|
+
const signed = `${timestamp}.${body}`;
|
|
10033
|
+
const expected = crypto9.createHmac("sha256", config.secret).update(signed).digest("hex");
|
|
10034
|
+
const valid = timingSafeEqual2(signature, expected);
|
|
10035
|
+
let event = "";
|
|
10036
|
+
let id2;
|
|
10037
|
+
try {
|
|
10038
|
+
const parsed = JSON.parse(body);
|
|
10039
|
+
event = parsed.type ?? "";
|
|
10040
|
+
id2 = parsed.id;
|
|
10041
|
+
} catch {
|
|
10042
|
+
}
|
|
10043
|
+
return { valid, provider: "stripe", event, id: id2 };
|
|
10044
|
+
};
|
|
10045
|
+
}
|
|
10046
|
+
function createGitHubVerifier(config) {
|
|
10047
|
+
return (body, headers) => {
|
|
10048
|
+
const sig = headers["x-hub-signature-256"];
|
|
10049
|
+
if (!sig) return { valid: false, provider: "github", event: "", id: void 0 };
|
|
10050
|
+
const expected = `sha256=${crypto9.createHmac("sha256", config.secret).update(body).digest("hex")}`;
|
|
10051
|
+
const valid = timingSafeEqual2(sig, expected);
|
|
10052
|
+
let event = headers["x-github-event"] ?? "";
|
|
10053
|
+
let id2;
|
|
10054
|
+
try {
|
|
10055
|
+
const parsed = JSON.parse(body);
|
|
10056
|
+
id2 = headers["x-github-delivery"] || parsed.id;
|
|
10057
|
+
} catch {
|
|
10058
|
+
}
|
|
10059
|
+
return { valid, provider: "github", event, id: id2 };
|
|
10060
|
+
};
|
|
10061
|
+
}
|
|
10062
|
+
function createSlackVerifier(config) {
|
|
10063
|
+
return (body, headers) => {
|
|
10064
|
+
const signature = headers["x-slack-signature"];
|
|
10065
|
+
const timestamp = headers["x-slack-request-timestamp"];
|
|
10066
|
+
if (!signature || !timestamp) return { valid: false, provider: "slack", event: "", id: void 0 };
|
|
10067
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
10068
|
+
const ts = parseInt(timestamp, 10);
|
|
10069
|
+
if (isNaN(ts) || Math.abs(now - ts) > 300) {
|
|
10070
|
+
return { valid: false, provider: "slack", event: "", id: void 0 };
|
|
10071
|
+
}
|
|
10072
|
+
const sigBase = `v0:${timestamp}:${body}`;
|
|
10073
|
+
const expected = `v0=${crypto9.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
|
|
10074
|
+
const valid = timingSafeEqual2(signature, expected);
|
|
10075
|
+
let event = "";
|
|
10076
|
+
let id2;
|
|
10077
|
+
try {
|
|
10078
|
+
const parsed = JSON.parse(body);
|
|
10079
|
+
event = parsed.event?.type ?? parsed.type ?? (parsed.challenge ? "url_verification" : "");
|
|
10080
|
+
id2 = parsed.event_id || parsed.ssl?.event_id;
|
|
10081
|
+
} catch {
|
|
10082
|
+
}
|
|
10083
|
+
return { valid, provider: "slack", event, id: id2 };
|
|
10084
|
+
};
|
|
10085
|
+
}
|
|
10086
|
+
var IdempotencyStore = class {
|
|
10087
|
+
store = /* @__PURE__ */ new Map();
|
|
10088
|
+
ttl;
|
|
10089
|
+
constructor(ttl) {
|
|
10090
|
+
this.ttl = ttl;
|
|
10091
|
+
}
|
|
10092
|
+
/** Returns true if this event ID has already been processed. */
|
|
10093
|
+
isDuplicate(id2) {
|
|
10094
|
+
if (this.store.has(id2)) return true;
|
|
10095
|
+
this.store.set(id2, Date.now());
|
|
10096
|
+
return false;
|
|
10097
|
+
}
|
|
10098
|
+
/** Periodic cleanup */
|
|
10099
|
+
cleanup() {
|
|
10100
|
+
const now = Date.now();
|
|
10101
|
+
for (const [key, ts] of this.store) {
|
|
10102
|
+
if (now - ts > this.ttl) this.store.delete(key);
|
|
10103
|
+
}
|
|
10104
|
+
}
|
|
10105
|
+
};
|
|
10106
|
+
var EventBus = class {
|
|
10107
|
+
handlers = /* @__PURE__ */ new Map();
|
|
10108
|
+
on(event, handler) {
|
|
10109
|
+
let set = this.handlers.get(event);
|
|
10110
|
+
if (!set) {
|
|
10111
|
+
set = /* @__PURE__ */ new Set();
|
|
10112
|
+
this.handlers.set(event, set);
|
|
10113
|
+
}
|
|
10114
|
+
set.add(handler);
|
|
10115
|
+
}
|
|
10116
|
+
off(event, handler) {
|
|
10117
|
+
const set = this.handlers.get(event);
|
|
10118
|
+
if (!set) return;
|
|
10119
|
+
set.delete(handler);
|
|
10120
|
+
if (set.size === 0) this.handlers.delete(event);
|
|
10121
|
+
}
|
|
10122
|
+
async emit(event, payload, provider, id2, ctx) {
|
|
10123
|
+
const we = { event, payload, provider, id: id2 };
|
|
10124
|
+
const specific = this.handlers.get(event);
|
|
10125
|
+
if (specific) {
|
|
10126
|
+
for (const handler of specific) {
|
|
10127
|
+
await handler(we, ctx);
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10130
|
+
const wildcard = this.handlers.get("*");
|
|
10131
|
+
if (wildcard) {
|
|
10132
|
+
for (const handler of wildcard) {
|
|
10133
|
+
await handler(we, ctx);
|
|
10134
|
+
}
|
|
10135
|
+
}
|
|
10136
|
+
}
|
|
10137
|
+
};
|
|
10138
|
+
function webhook(options) {
|
|
10139
|
+
const replayProtection = options?.replayProtection ?? true;
|
|
10140
|
+
const idempotencyTTL = options?.idempotencyTTL ?? 36e5;
|
|
10141
|
+
const mountPath = options?.path ?? "/";
|
|
10142
|
+
const verifiers = [];
|
|
10143
|
+
if (options?.stripe) verifiers.push(createStripeVerifier(options.stripe));
|
|
10144
|
+
if (options?.github) verifiers.push(createGitHubVerifier(options.github));
|
|
10145
|
+
if (options?.slack) verifiers.push(createSlackVerifier(options.slack));
|
|
10146
|
+
if (options?.custom) {
|
|
10147
|
+
for (const c of options.custom) {
|
|
10148
|
+
verifiers.push(async (body, headers) => {
|
|
10149
|
+
const valid = await c.verify(body, headers);
|
|
10150
|
+
let event = "";
|
|
10151
|
+
try {
|
|
10152
|
+
event = c.event(JSON.parse(body), headers);
|
|
10153
|
+
} catch {
|
|
10154
|
+
}
|
|
10155
|
+
return { valid, provider: c.name, event, id: void 0 };
|
|
10156
|
+
});
|
|
10157
|
+
}
|
|
10158
|
+
}
|
|
10159
|
+
const bus = new EventBus();
|
|
10160
|
+
const idempotency = new IdempotencyStore(idempotencyTTL);
|
|
10161
|
+
const cleanupInterval = setInterval(() => idempotency.cleanup(), 6e4);
|
|
10162
|
+
if (cleanupInterval.unref) cleanupInterval.unref();
|
|
10163
|
+
const router = new Router();
|
|
10164
|
+
const handler = async (req, ctx) => {
|
|
10165
|
+
const body = await req.text();
|
|
10166
|
+
if (!body) {
|
|
10167
|
+
return new Response("Empty body", { status: 400 });
|
|
10168
|
+
}
|
|
10169
|
+
const headers = {};
|
|
10170
|
+
req.headers.forEach((v, k) => {
|
|
10171
|
+
headers[k] = v;
|
|
10172
|
+
});
|
|
10173
|
+
for (const verify of verifiers) {
|
|
10174
|
+
const result = await verify(body, headers);
|
|
10175
|
+
if (!result.valid) continue;
|
|
10176
|
+
if (replayProtection && result.id) {
|
|
10177
|
+
if (idempotency.isDuplicate(result.id)) {
|
|
10178
|
+
return new Response("OK", { status: 200 });
|
|
10179
|
+
}
|
|
10180
|
+
}
|
|
10181
|
+
let payload;
|
|
10182
|
+
try {
|
|
10183
|
+
payload = JSON.parse(body);
|
|
10184
|
+
} catch {
|
|
10185
|
+
return new Response("Invalid JSON body", { status: 400 });
|
|
10186
|
+
}
|
|
10187
|
+
if (result.provider === "slack" && payload?.challenge) {
|
|
10188
|
+
return new Response(JSON.stringify({ challenge: payload.challenge }), {
|
|
10189
|
+
headers: { "content-type": "application/json" }
|
|
10190
|
+
});
|
|
10191
|
+
}
|
|
10192
|
+
try {
|
|
10193
|
+
await bus.emit(result.event, payload, result.provider, result.id, ctx);
|
|
10194
|
+
} catch (err) {
|
|
10195
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10196
|
+
console.error(`[webhook] handler error for ${result.provider}.${result.event}: ${msg}`);
|
|
10197
|
+
return new Response("Handler error", { status: 500 });
|
|
10198
|
+
}
|
|
10199
|
+
return new Response("OK", { status: 200 });
|
|
10200
|
+
}
|
|
10201
|
+
return new Response("Unauthorized", { status: 401 });
|
|
10202
|
+
};
|
|
10203
|
+
router.post(mountPath, handler);
|
|
10204
|
+
const mod = router;
|
|
10205
|
+
mod.on = (event, handler2) => {
|
|
10206
|
+
bus.on(event, handler2);
|
|
10207
|
+
return mod;
|
|
10208
|
+
};
|
|
10209
|
+
mod.off = (event, handler2) => {
|
|
10210
|
+
bus.off(event, handler2);
|
|
10211
|
+
return mod;
|
|
10212
|
+
};
|
|
10213
|
+
mod._cleanup = () => {
|
|
10214
|
+
clearInterval(cleanupInterval);
|
|
10215
|
+
};
|
|
10216
|
+
return mod;
|
|
10217
|
+
}
|
|
10218
|
+
|
|
10219
|
+
// fts.ts
|
|
10220
|
+
var fts_exports = {};
|
|
10221
|
+
__export(fts_exports, {
|
|
10222
|
+
createIndex: () => createIndex,
|
|
10223
|
+
dropIndex: () => dropIndex,
|
|
10224
|
+
search: () => search,
|
|
10225
|
+
suggest: () => suggest
|
|
10226
|
+
});
|
|
10227
|
+
function resolveTableName(table) {
|
|
10228
|
+
const name = table.inner?.tableName ?? table.tableName;
|
|
10229
|
+
if (!name || typeof name !== "string") {
|
|
10230
|
+
throw new Error("fts: could not determine table name. Ensure you pass a pg.table() result.");
|
|
10231
|
+
}
|
|
10232
|
+
return name;
|
|
10233
|
+
}
|
|
10234
|
+
function escapeIdent(s) {
|
|
10235
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
10236
|
+
}
|
|
10237
|
+
function sqlLit(s) {
|
|
10238
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
10239
|
+
}
|
|
10240
|
+
async function createIndex(sql2, table, fields, options) {
|
|
10241
|
+
const language = options?.language ?? "english";
|
|
10242
|
+
const tableName = resolveTableName(table);
|
|
10243
|
+
const indexName = options?.indexName ?? `${tableName}_fts_idx`;
|
|
10244
|
+
const vectorExpr = fields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
|
|
10245
|
+
const indexType = options?.indexType ?? "gin";
|
|
10246
|
+
await sql2.unsafe(`
|
|
10247
|
+
CREATE INDEX IF NOT EXISTS ${escapeIdent(indexName)}
|
|
10248
|
+
ON ${escapeIdent(tableName)}
|
|
10249
|
+
USING ${indexType}
|
|
10250
|
+
(to_tsvector(${sqlLit(language)}, ${vectorExpr}))
|
|
10251
|
+
`);
|
|
10252
|
+
}
|
|
10253
|
+
async function dropIndex(sql2, table, options) {
|
|
10254
|
+
const tableName = resolveTableName(table);
|
|
10255
|
+
const indexName = options?.indexName ?? `${tableName}_fts_idx`;
|
|
10256
|
+
await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent(indexName)}`);
|
|
10257
|
+
}
|
|
10258
|
+
async function search(sql2, table, query, options) {
|
|
10259
|
+
const tableName = resolveTableName(table);
|
|
10260
|
+
const language = options?.language ?? "english";
|
|
10261
|
+
const searchFields = options?.fields;
|
|
10262
|
+
const limit = options?.limit ?? 20;
|
|
10263
|
+
const offset = options?.offset ?? 0;
|
|
10264
|
+
const rankCol = options?.rankColumn ?? "_rank";
|
|
10265
|
+
if (!searchFields?.length) {
|
|
10266
|
+
throw new Error("fts.search: `fields` option is required. Specify which columns to search.");
|
|
10267
|
+
}
|
|
10268
|
+
const sanitized = query.trim();
|
|
10269
|
+
if (!sanitized) return [];
|
|
10270
|
+
const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
|
|
10271
|
+
const langLit = sqlLit(language);
|
|
10272
|
+
const queryLit = sqlLit(sanitized);
|
|
10273
|
+
const rankColId = escapeIdent(rankCol);
|
|
10274
|
+
const tableId = escapeIdent(tableName);
|
|
10275
|
+
const headlineExpr = options?.headline ? searchFields.map(
|
|
10276
|
+
(f) => `ts_headline(${langLit}, ${escapeIdent(f)}, websearch_to_tsquery(${langLit}, ${queryLit}), 'MaxWords=30,MinWords=15') as ${escapeIdent(f + "_headline")}`
|
|
10277
|
+
).join(",\n ") : "";
|
|
10278
|
+
const sql_query = `
|
|
10279
|
+
SELECT
|
|
10280
|
+
*,
|
|
10281
|
+
ts_rank(
|
|
10282
|
+
to_tsvector(${langLit}, ${vectorExpr}),
|
|
10283
|
+
websearch_to_tsquery(${langLit}, ${queryLit})
|
|
10284
|
+
) as ${rankColId}
|
|
10285
|
+
${headlineExpr ? "," + headlineExpr : ""}
|
|
10286
|
+
FROM ${tableId}
|
|
10287
|
+
WHERE to_tsvector(${langLit}, ${vectorExpr}) @@ websearch_to_tsquery(${langLit}, ${queryLit})
|
|
10288
|
+
ORDER BY ${rankColId} DESC
|
|
10289
|
+
LIMIT ${limit} OFFSET ${offset}
|
|
10290
|
+
`;
|
|
10291
|
+
const rows = await sql2.unsafe(sql_query);
|
|
10292
|
+
return rows.map((row) => {
|
|
10293
|
+
const result = {
|
|
10294
|
+
id: row.id,
|
|
10295
|
+
rank: Number(row[rankCol]) || 0,
|
|
10296
|
+
row
|
|
10297
|
+
};
|
|
10298
|
+
if (options?.headline && searchFields) {
|
|
10299
|
+
const snippets = searchFields.map((f) => row[`${f}_headline`]).filter(Boolean);
|
|
10300
|
+
result.headline = snippets.join(" ... ");
|
|
10301
|
+
}
|
|
10302
|
+
return result;
|
|
10303
|
+
});
|
|
10304
|
+
}
|
|
10305
|
+
async function suggest(sql2, table, prefix, options) {
|
|
10306
|
+
const tableName = resolveTableName(table);
|
|
10307
|
+
const field = options?.field;
|
|
10308
|
+
const language = options?.language ?? "english";
|
|
10309
|
+
const limit = options?.limit ?? 10;
|
|
10310
|
+
if (!field) throw new Error("fts.suggest: `field` option is required");
|
|
10311
|
+
const sanitized = prefix.replace(/[^\w\s-]/g, " ").trim();
|
|
10312
|
+
if (!sanitized) return [];
|
|
10313
|
+
const rows = await sql2.unsafe(`
|
|
10314
|
+
SELECT DISTINCT ts_lexize(${sqlLit(language)}, word) as tokens
|
|
10315
|
+
FROM (
|
|
10316
|
+
SELECT regexp_split_to_table(lower(${escapeIdent(field)}), E'\\W+') as word
|
|
10317
|
+
FROM ${escapeIdent(tableName)}
|
|
10318
|
+
) words
|
|
10319
|
+
WHERE word LIKE ${sqlLit(sanitized + "%")}
|
|
10320
|
+
LIMIT ${limit}
|
|
10321
|
+
`);
|
|
10322
|
+
return rows.map((r) => r.tokens?.[0] ?? "").filter(Boolean);
|
|
10323
|
+
}
|
|
9355
10324
|
export {
|
|
9356
10325
|
DEFAULT_MAX_BODY,
|
|
9357
10326
|
MIGRATIONS_TABLE,
|
|
10327
|
+
MemoryCache,
|
|
10328
|
+
MemoryStore,
|
|
10329
|
+
RedisCache,
|
|
10330
|
+
RedisStore,
|
|
9358
10331
|
Router,
|
|
9359
10332
|
TestApp,
|
|
9360
10333
|
TestRequest,
|
|
@@ -9363,11 +10336,13 @@ export {
|
|
|
9363
10336
|
aiStream,
|
|
9364
10337
|
analytics,
|
|
9365
10338
|
auth,
|
|
10339
|
+
cache2 as cache,
|
|
9366
10340
|
compress,
|
|
9367
10341
|
cors,
|
|
9368
10342
|
createHub,
|
|
9369
10343
|
createOpenAI,
|
|
9370
10344
|
createSSEStream,
|
|
10345
|
+
createTestDb,
|
|
9371
10346
|
createTestServer,
|
|
9372
10347
|
createWorker,
|
|
9373
10348
|
csrf,
|
|
@@ -9380,6 +10355,7 @@ export {
|
|
|
9380
10355
|
embedMany,
|
|
9381
10356
|
formatSSE,
|
|
9382
10357
|
formatSSEData,
|
|
10358
|
+
fts_exports as fts,
|
|
9383
10359
|
generateObject,
|
|
9384
10360
|
generateText2 as generateText,
|
|
9385
10361
|
getCookies,
|
|
@@ -9410,6 +10386,7 @@ export {
|
|
|
9410
10386
|
seoTags,
|
|
9411
10387
|
serve,
|
|
9412
10388
|
serveStatic,
|
|
10389
|
+
session,
|
|
9413
10390
|
setCookie,
|
|
9414
10391
|
smoothStream,
|
|
9415
10392
|
ssr,
|
|
@@ -9421,5 +10398,7 @@ export {
|
|
|
9421
10398
|
traceElapsed,
|
|
9422
10399
|
upload,
|
|
9423
10400
|
user,
|
|
9424
|
-
validate
|
|
10401
|
+
validate,
|
|
10402
|
+
webhook,
|
|
10403
|
+
withTestDb
|
|
9425
10404
|
};
|