weifuwu 0.27.4 → 0.27.6
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 +217 -4
- package/dist/cli.js +241 -23
- package/dist/index.d.ts +9 -0
- package/dist/index.js +327 -507
- package/dist/postgres/index.d.ts +0 -1
- package/dist/postgres/module.d.ts +1 -5
- package/dist/postgres/types.d.ts +0 -7
- package/dist/ssr/assets.d.ts +20 -0
- package/dist/ssr/compile.d.ts +21 -0
- package/dist/ssr/css.d.ts +16 -0
- package/dist/ssr/html.d.ts +53 -0
- package/dist/ssr/layout.d.ts +2 -0
- package/dist/ssr/view.d.ts +36 -0
- package/dist/types.d.ts +6 -0
- package/package.json +6 -1
- package/dist/mailer.d.ts +0 -51
- package/dist/postgres/schema/columns.d.ts +0 -99
- package/dist/postgres/schema/index.d.ts +0 -6
- package/dist/postgres/schema/sql.d.ts +0 -22
- package/dist/postgres/schema/table.d.ts +0 -141
- package/dist/postgres/schema/where.d.ts +0 -29
package/dist/index.js
CHANGED
|
@@ -293,14 +293,14 @@ function serve(handler, options) {
|
|
|
293
293
|
if (!server.listening) return;
|
|
294
294
|
server.close();
|
|
295
295
|
server.closeIdleConnections();
|
|
296
|
-
return new Promise((
|
|
296
|
+
return new Promise((resolve8) => {
|
|
297
297
|
const timer = setTimeout(() => {
|
|
298
298
|
server.closeAllConnections();
|
|
299
|
-
|
|
299
|
+
resolve8();
|
|
300
300
|
}, timeoutMs);
|
|
301
301
|
server.on("close", () => {
|
|
302
302
|
clearTimeout(timer);
|
|
303
|
-
|
|
303
|
+
resolve8();
|
|
304
304
|
});
|
|
305
305
|
});
|
|
306
306
|
}
|
|
@@ -345,7 +345,7 @@ function createHub(opts) {
|
|
|
345
345
|
}
|
|
346
346
|
});
|
|
347
347
|
}
|
|
348
|
-
function
|
|
348
|
+
function join4(key, ws) {
|
|
349
349
|
if (!channels.has(key)) {
|
|
350
350
|
channels.set(key, /* @__PURE__ */ new Set());
|
|
351
351
|
redisSub?.subscribe(`${prefix}${key}`);
|
|
@@ -407,7 +407,7 @@ function createHub(opts) {
|
|
|
407
407
|
redisPub = void 0;
|
|
408
408
|
redisSub = null;
|
|
409
409
|
}
|
|
410
|
-
return { join:
|
|
410
|
+
return { join: join4, leave, broadcast, close };
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
// core/router.ts
|
|
@@ -1176,27 +1176,27 @@ var MIME_TYPES = {
|
|
|
1176
1176
|
};
|
|
1177
1177
|
|
|
1178
1178
|
// middleware/validate.ts
|
|
1179
|
-
function parseFormBody(
|
|
1180
|
-
const params = new URLSearchParams(
|
|
1179
|
+
function parseFormBody(text) {
|
|
1180
|
+
const params = new URLSearchParams(text);
|
|
1181
1181
|
const result = {};
|
|
1182
1182
|
for (const [key, value] of params) {
|
|
1183
1183
|
result[key] = value;
|
|
1184
1184
|
}
|
|
1185
1185
|
return result;
|
|
1186
1186
|
}
|
|
1187
|
-
function parseBody(
|
|
1187
|
+
function parseBody(text, ct) {
|
|
1188
1188
|
if (ct.includes("application/x-www-form-urlencoded")) {
|
|
1189
|
-
return parseFormBody(
|
|
1189
|
+
return parseFormBody(text);
|
|
1190
1190
|
}
|
|
1191
1191
|
const isExplicitJson = ct.includes("application/json") || ct.includes("+json") || ct.includes("text/") || ct.includes("*/json");
|
|
1192
1192
|
const isNotSpecialMultipart = !ct.includes("multipart/form-data") && !ct.includes("application/x-www-form-urlencoded");
|
|
1193
1193
|
if (isExplicitJson || isNotSpecialMultipart) {
|
|
1194
1194
|
try {
|
|
1195
|
-
return JSON.parse(
|
|
1195
|
+
return JSON.parse(text);
|
|
1196
1196
|
} catch {
|
|
1197
1197
|
}
|
|
1198
1198
|
}
|
|
1199
|
-
return
|
|
1199
|
+
return text;
|
|
1200
1200
|
}
|
|
1201
1201
|
function validate(schemas) {
|
|
1202
1202
|
const mw = async (req, ctx, next) => {
|
|
@@ -1693,8 +1693,8 @@ function createSSEStream(iterable, opts) {
|
|
|
1693
1693
|
async start(controller) {
|
|
1694
1694
|
try {
|
|
1695
1695
|
for await (const event of iterable) {
|
|
1696
|
-
const
|
|
1697
|
-
controller.enqueue(encoder.encode(
|
|
1696
|
+
const text = event.type ? formatSSE(event.type, event) : formatSSEData(event);
|
|
1697
|
+
controller.enqueue(encoder.encode(text));
|
|
1698
1698
|
}
|
|
1699
1699
|
} catch (e) {
|
|
1700
1700
|
if (e instanceof Error && e.name !== "AbortError") {
|
|
@@ -1960,7 +1960,7 @@ var TestWSRequest = class {
|
|
|
1960
1960
|
const baseUrl = await this.app._ensureServer();
|
|
1961
1961
|
const wsUrl = baseUrl.replace(/^http/, "ws") + this.path;
|
|
1962
1962
|
const ws = new WSWebSocket(wsUrl, { handshakeTimeout: this._timeout });
|
|
1963
|
-
return new Promise((
|
|
1963
|
+
return new Promise((resolve8, reject) => {
|
|
1964
1964
|
const timer = setTimeout(() => {
|
|
1965
1965
|
reject(new Error(`WebSocket connection timed out after ${this._timeout}ms`));
|
|
1966
1966
|
ws.close();
|
|
@@ -1969,7 +1969,7 @@ var TestWSRequest = class {
|
|
|
1969
1969
|
clearTimeout(timer);
|
|
1970
1970
|
const conn = new TestWSConnection(ws, this._timeout);
|
|
1971
1971
|
this.app._trackConnection(conn);
|
|
1972
|
-
|
|
1972
|
+
resolve8(conn);
|
|
1973
1973
|
});
|
|
1974
1974
|
ws.on("error", (err) => {
|
|
1975
1975
|
clearTimeout(timer);
|
|
@@ -2000,8 +2000,8 @@ var TestWSConnection = class {
|
|
|
2000
2000
|
ws.on("message", (data) => {
|
|
2001
2001
|
const str = data.toString();
|
|
2002
2002
|
if (this.resolveQueue.length > 0) {
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2003
|
+
const resolve8 = this.resolveQueue.shift();
|
|
2004
|
+
resolve8(str);
|
|
2005
2005
|
} else {
|
|
2006
2006
|
this.messageQueue.push(str);
|
|
2007
2007
|
}
|
|
@@ -2031,15 +2031,15 @@ var TestWSConnection = class {
|
|
|
2031
2031
|
if (this._closed) {
|
|
2032
2032
|
throw new Error("WebSocket connection closed");
|
|
2033
2033
|
}
|
|
2034
|
-
return new Promise((
|
|
2034
|
+
return new Promise((resolve8, reject) => {
|
|
2035
2035
|
const timer = setTimeout(() => {
|
|
2036
|
-
const idx = this.resolveQueue.indexOf(
|
|
2036
|
+
const idx = this.resolveQueue.indexOf(resolve8);
|
|
2037
2037
|
if (idx !== -1) this.resolveQueue.splice(idx, 1);
|
|
2038
2038
|
reject(new Error(`WebSocket receive timed out after ${timeout ?? this._timeout}ms`));
|
|
2039
2039
|
}, timeout ?? this._timeout);
|
|
2040
2040
|
this.resolveQueue.push((msg) => {
|
|
2041
2041
|
clearTimeout(timer);
|
|
2042
|
-
|
|
2042
|
+
resolve8(msg);
|
|
2043
2043
|
});
|
|
2044
2044
|
});
|
|
2045
2045
|
}
|
|
@@ -2053,12 +2053,12 @@ var TestWSConnection = class {
|
|
|
2053
2053
|
* Useful for verifying that something did NOT happen.
|
|
2054
2054
|
*/
|
|
2055
2055
|
async expectSilent(ms) {
|
|
2056
|
-
return new Promise((
|
|
2056
|
+
return new Promise((resolve8, reject) => {
|
|
2057
2057
|
if (this.messageQueue.length > 0) {
|
|
2058
2058
|
reject(new Error(`Expected silence but got message: ${this.messageQueue[0].slice(0, 100)}`));
|
|
2059
2059
|
return;
|
|
2060
2060
|
}
|
|
2061
|
-
const timer = setTimeout(() =>
|
|
2061
|
+
const timer = setTimeout(() => resolve8(), ms);
|
|
2062
2062
|
const origPush = this.resolveQueue.push.bind(this.resolveQueue);
|
|
2063
2063
|
this.resolveQueue.push = (_fn) => {
|
|
2064
2064
|
clearTimeout(timer);
|
|
@@ -2092,17 +2092,17 @@ async function createTestDb(options) {
|
|
|
2092
2092
|
await adminSql.unsafe('CREATE SCHEMA IF NOT EXISTS "' + schema.replace(/"/g, '""') + '"');
|
|
2093
2093
|
const schemaUrl = new URL(dbUrl);
|
|
2094
2094
|
schemaUrl.searchParams.set("search_path", schema);
|
|
2095
|
-
const
|
|
2095
|
+
const sql = postgres2(schemaUrl.toString());
|
|
2096
2096
|
await adminSql.end();
|
|
2097
2097
|
return {
|
|
2098
|
-
sql
|
|
2098
|
+
sql,
|
|
2099
2099
|
url: schemaUrl.toString(),
|
|
2100
2100
|
schema,
|
|
2101
2101
|
destroy: async () => {
|
|
2102
2102
|
const destroySql = postgres2(dbUrl);
|
|
2103
2103
|
await destroySql.unsafe('DROP SCHEMA IF EXISTS "' + schema.replace(/"/g, '""') + '" CASCADE');
|
|
2104
2104
|
await destroySql.end();
|
|
2105
|
-
await
|
|
2105
|
+
await sql.end();
|
|
2106
2106
|
}
|
|
2107
2107
|
};
|
|
2108
2108
|
}
|
|
@@ -2121,15 +2121,15 @@ async function withTestDb(optionsOrFn, fn) {
|
|
|
2121
2121
|
const resolvedUrl = dbUrl || process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
|
|
2122
2122
|
if (!resolvedUrl) throw new Error("withTestDb: DATABASE_URL or TEST_DATABASE_URL required");
|
|
2123
2123
|
const { default: postgres2 } = await import("postgres");
|
|
2124
|
-
const
|
|
2124
|
+
const sql = postgres2(resolvedUrl);
|
|
2125
2125
|
try {
|
|
2126
|
-
await
|
|
2126
|
+
await sql.begin(async (txSql) => {
|
|
2127
2127
|
await callback(txSql);
|
|
2128
2128
|
throw void 0;
|
|
2129
2129
|
});
|
|
2130
2130
|
} catch {
|
|
2131
2131
|
} finally {
|
|
2132
|
-
await
|
|
2132
|
+
await sql.end();
|
|
2133
2133
|
}
|
|
2134
2134
|
}
|
|
2135
2135
|
|
|
@@ -2405,8 +2405,8 @@ function aiProvider(options) {
|
|
|
2405
2405
|
if (!_embedModel) _embedModel = client.embedding(m);
|
|
2406
2406
|
return _embedModel;
|
|
2407
2407
|
},
|
|
2408
|
-
async embed(
|
|
2409
|
-
const result = await aiEmbed({ model: this.embeddingModel(), value:
|
|
2408
|
+
async embed(text) {
|
|
2409
|
+
const result = await aiEmbed({ model: this.embeddingModel(), value: text });
|
|
2410
2410
|
return result.embedding;
|
|
2411
2411
|
},
|
|
2412
2412
|
async embedMany(texts) {
|
|
@@ -2444,440 +2444,6 @@ import { openai, createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
|
2444
2444
|
|
|
2445
2445
|
// postgres/client.ts
|
|
2446
2446
|
import postgresFactory from "postgres";
|
|
2447
|
-
|
|
2448
|
-
// postgres/schema/sql.ts
|
|
2449
|
-
var SQL = class {
|
|
2450
|
-
/** Template string parts (interleaved with values). */
|
|
2451
|
-
strings;
|
|
2452
|
-
/** Bound parameter values. */
|
|
2453
|
-
values;
|
|
2454
|
-
constructor(strings, values) {
|
|
2455
|
-
this.strings = strings;
|
|
2456
|
-
this.values = values;
|
|
2457
|
-
}
|
|
2458
|
-
/** Serialize to a raw SQL string (interpolating values inline for DDL use). */
|
|
2459
|
-
toSQL() {
|
|
2460
|
-
let result = "";
|
|
2461
|
-
for (let i = 0; i < this.strings.length; i++) {
|
|
2462
|
-
result += this.strings[i];
|
|
2463
|
-
if (i < this.values.length) {
|
|
2464
|
-
result += String(this.values[i]);
|
|
2465
|
-
}
|
|
2466
|
-
}
|
|
2467
|
-
return result;
|
|
2468
|
-
}
|
|
2469
|
-
};
|
|
2470
|
-
|
|
2471
|
-
// postgres/schema/columns.ts
|
|
2472
|
-
function toDDL(col) {
|
|
2473
|
-
const parts = [`"${col.name}"`, col.sqlType];
|
|
2474
|
-
if (col.isPrimaryKey) parts.push("PRIMARY KEY");
|
|
2475
|
-
if (!col.isPrimaryKey && !col.isNullable) parts.push("NOT NULL");
|
|
2476
|
-
if (col.isUnique) parts.push("UNIQUE");
|
|
2477
|
-
if (col.defaultExpr) parts.push(`DEFAULT ${col.defaultExpr}`);
|
|
2478
|
-
if (col.ref) {
|
|
2479
|
-
parts.push(`REFERENCES "${col.ref.table}"("${col.ref.column}")`);
|
|
2480
|
-
if (col.ref.onDelete) parts.push(`ON DELETE ${col.ref.onDelete.toUpperCase()}`);
|
|
2481
|
-
}
|
|
2482
|
-
return parts.join(" ");
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
// postgres/schema/where.ts
|
|
2486
|
-
function combine(conditions, joiner) {
|
|
2487
|
-
if (conditions.length === 0) return new SQL([""], []);
|
|
2488
|
-
const strings = ["("];
|
|
2489
|
-
const values = [];
|
|
2490
|
-
for (let i = 0; i < conditions.length; i++) {
|
|
2491
|
-
if (i > 0) strings[strings.length - 1] += ` ${joiner} `;
|
|
2492
|
-
const s = conditions[i];
|
|
2493
|
-
for (let j = 0; j < s.strings.length; j++) {
|
|
2494
|
-
strings[strings.length - 1] += s.strings[j];
|
|
2495
|
-
if (j < s.values.length) {
|
|
2496
|
-
strings.push("");
|
|
2497
|
-
values.push(s.values[j]);
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
strings[strings.length - 1] += ")";
|
|
2502
|
-
return new SQL(strings, values);
|
|
2503
|
-
}
|
|
2504
|
-
function and(...conditions) {
|
|
2505
|
-
return combine(conditions, "AND");
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
// postgres/schema/table.ts
|
|
2509
|
-
var Table = class {
|
|
2510
|
-
/** Database table name. */
|
|
2511
|
-
tableName;
|
|
2512
|
-
/** All column builders (order-preserving). */
|
|
2513
|
-
columns;
|
|
2514
|
-
/** Column builders keyed by property name. */
|
|
2515
|
-
builders;
|
|
2516
|
-
colEntries;
|
|
2517
|
-
constructor(tableName, builders) {
|
|
2518
|
-
this.tableName = tableName;
|
|
2519
|
-
this.builders = builders;
|
|
2520
|
-
this.columns = Object.values(builders);
|
|
2521
|
-
this.colEntries = Object.entries(builders).map(([prop, col]) => ({
|
|
2522
|
-
prop,
|
|
2523
|
-
db: col.name,
|
|
2524
|
-
auto: col.isAutoGenerate,
|
|
2525
|
-
column: col
|
|
2526
|
-
}));
|
|
2527
|
-
}
|
|
2528
|
-
/** Check if the table has a column with the given DB name. */
|
|
2529
|
-
hasColumn(dbName) {
|
|
2530
|
-
return this.colEntries.some((e) => e.db === dbName);
|
|
2531
|
-
}
|
|
2532
|
-
/**
|
|
2533
|
-
* Bind this table schema to a SQL connection, returning a `BoundTable`
|
|
2534
|
-
* that can run queries without passing `sql` to every call.
|
|
2535
|
-
*/
|
|
2536
|
-
bind(sql2) {
|
|
2537
|
-
return new BoundTable(sql2, this.tableName, this.builders);
|
|
2538
|
-
}
|
|
2539
|
-
/** Returns the primary key column name (DB name), or 'id' as fallback. */
|
|
2540
|
-
get pkColumn() {
|
|
2541
|
-
const entry = this.colEntries.find((e) => e.column.isPrimaryKey);
|
|
2542
|
-
return entry ? entry.db : "id";
|
|
2543
|
-
}
|
|
2544
|
-
/** Adds `deleted_at IS NULL` condition if the table has soft delete and not explicitly excluded. */
|
|
2545
|
-
_softDeleteFilter(where, opts) {
|
|
2546
|
-
if (!this.hasColumn("deleted_at")) return null;
|
|
2547
|
-
if (opts?.withDeleted) return null;
|
|
2548
|
-
if (where && typeof where === "object" && !Array.isArray(where) && !(where instanceof SQL)) {
|
|
2549
|
-
if ("deleted_at" in where) return null;
|
|
2550
|
-
}
|
|
2551
|
-
return '"deleted_at" IS NULL';
|
|
2552
|
-
}
|
|
2553
|
-
async create(sql2, opts) {
|
|
2554
|
-
const colDDL = this.columns.map(toDDL);
|
|
2555
|
-
let ddl = `CREATE TABLE IF NOT EXISTS "${this.tableName}" (
|
|
2556
|
-
${colDDL.join(",\n ")}
|
|
2557
|
-
)`;
|
|
2558
|
-
if (opts?.partitionBy) {
|
|
2559
|
-
ddl += ` PARTITION BY ${opts.partitionBy.type} ("${opts.partitionBy.column}")`;
|
|
2560
|
-
}
|
|
2561
|
-
await sql2.unsafe(ddl);
|
|
2562
|
-
}
|
|
2563
|
-
async drop(sql2, opts) {
|
|
2564
|
-
const cascade = opts?.cascade ? " CASCADE" : "";
|
|
2565
|
-
await sql2.unsafe(`DROP TABLE IF EXISTS "${this.tableName}"${cascade}`);
|
|
2566
|
-
}
|
|
2567
|
-
async createIndex(sql2, columns, opts) {
|
|
2568
|
-
const cols = Array.isArray(columns) ? columns : [columns];
|
|
2569
|
-
const name = `"${this.tableName}_${cols.join("_")}${opts?.unique ? "_uidx" : "_idx"}"`;
|
|
2570
|
-
const unique = opts?.unique ? "UNIQUE" : "";
|
|
2571
|
-
const using = opts?.type ? `USING ${opts.type.toUpperCase()}` : "";
|
|
2572
|
-
const colList = cols.map((c) => opts?.desc ? `"${c}" DESC` : `"${c}"`).join(", ");
|
|
2573
|
-
const operator = opts?.operator ? ` ${opts.operator}` : "";
|
|
2574
|
-
const ddl = `CREATE ${unique} INDEX IF NOT EXISTS ${name} ON "${this.tableName}" ${using} (${colList}${operator})`.replace(
|
|
2575
|
-
/\s+/g,
|
|
2576
|
-
" "
|
|
2577
|
-
);
|
|
2578
|
-
await sql2.unsafe(ddl);
|
|
2579
|
-
}
|
|
2580
|
-
async createUniqueIndex(sql2, columns) {
|
|
2581
|
-
await this.createIndex(sql2, columns, { unique: true });
|
|
2582
|
-
}
|
|
2583
|
-
// --- Private helpers ---
|
|
2584
|
-
_buildConditions(where, startIndex) {
|
|
2585
|
-
const conditions = [];
|
|
2586
|
-
const values = [];
|
|
2587
|
-
let w = where;
|
|
2588
|
-
if (Array.isArray(w)) {
|
|
2589
|
-
w = w.length > 0 ? and(...w) : void 0;
|
|
2590
|
-
}
|
|
2591
|
-
if (w instanceof SQL) {
|
|
2592
|
-
let fragment = "";
|
|
2593
|
-
for (let i = 0; i < w.strings.length; i++) {
|
|
2594
|
-
fragment += w.strings[i];
|
|
2595
|
-
if (i < w.values.length) {
|
|
2596
|
-
fragment += `$${startIndex + values.length + 1}`;
|
|
2597
|
-
values.push(w.values[i]);
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
conditions.push(fragment);
|
|
2601
|
-
} else {
|
|
2602
|
-
for (const [prop, value] of Object.entries(w || {})) {
|
|
2603
|
-
if (value === void 0) continue;
|
|
2604
|
-
const entry = this.colEntries.find((e) => e.prop === prop);
|
|
2605
|
-
const db = entry ? entry.db : prop;
|
|
2606
|
-
conditions.push(`"${db}" = $${startIndex + values.length + 1}`);
|
|
2607
|
-
values.push(value);
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
return { conditions, values };
|
|
2611
|
-
}
|
|
2612
|
-
_buildSET(data) {
|
|
2613
|
-
const sets = [];
|
|
2614
|
-
const values = [];
|
|
2615
|
-
const d = data;
|
|
2616
|
-
for (const { prop, db } of this.colEntries) {
|
|
2617
|
-
if (prop in d && d[prop] !== void 0) {
|
|
2618
|
-
const val = d[prop];
|
|
2619
|
-
if (val instanceof SQL) {
|
|
2620
|
-
sets.push(`"${db}" = ${val.toSQL()}`);
|
|
2621
|
-
} else {
|
|
2622
|
-
sets.push(`"${db}" = $${sets.length + 1}`);
|
|
2623
|
-
values.push(val);
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
if (this.hasColumn("updated_at") && !d.updated_at) {
|
|
2628
|
-
sets.push('"updated_at" = NOW()');
|
|
2629
|
-
}
|
|
2630
|
-
return { sets, values };
|
|
2631
|
-
}
|
|
2632
|
-
// --- CRUD ---
|
|
2633
|
-
async insert(sql2, data) {
|
|
2634
|
-
const filtered = {};
|
|
2635
|
-
for (const { prop, db, auto } of this.colEntries) {
|
|
2636
|
-
if (auto) continue;
|
|
2637
|
-
const val = data[prop];
|
|
2638
|
-
if (val !== void 0) {
|
|
2639
|
-
filtered[db] = val;
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
const [row] = await sql2`
|
|
2643
|
-
INSERT INTO ${sql2(this.tableName)} ${sql2(filtered)} RETURNING *
|
|
2644
|
-
`;
|
|
2645
|
-
return row;
|
|
2646
|
-
}
|
|
2647
|
-
async insertMany(sql2, data) {
|
|
2648
|
-
const filtered = [];
|
|
2649
|
-
for (const item of data) {
|
|
2650
|
-
const row = {};
|
|
2651
|
-
for (const { prop, db, auto } of this.colEntries) {
|
|
2652
|
-
if (auto) continue;
|
|
2653
|
-
const val = item[prop];
|
|
2654
|
-
if (val !== void 0) {
|
|
2655
|
-
row[db] = val;
|
|
2656
|
-
}
|
|
2657
|
-
}
|
|
2658
|
-
filtered.push(row);
|
|
2659
|
-
}
|
|
2660
|
-
const rows = await sql2`
|
|
2661
|
-
INSERT INTO ${sql2(this.tableName)} ${sql2(filtered)} RETURNING *
|
|
2662
|
-
`;
|
|
2663
|
-
return rows;
|
|
2664
|
-
}
|
|
2665
|
-
async read(sql2, id, opts) {
|
|
2666
|
-
const pk = this.pkColumn;
|
|
2667
|
-
const columns = opts?.select?.length ? opts.select.map((c) => `"${c}"`).join(", ") : "*";
|
|
2668
|
-
const softDel = this._softDeleteFilter(null, opts);
|
|
2669
|
-
const extraAnd = softDel ? ` AND ${softDel}` : "";
|
|
2670
|
-
const [row] = await sql2.unsafe(
|
|
2671
|
-
`SELECT ${columns} FROM "${this.tableName}" WHERE "${pk}" = $1${extraAnd} LIMIT 1`,
|
|
2672
|
-
[id]
|
|
2673
|
-
);
|
|
2674
|
-
return row ?? void 0;
|
|
2675
|
-
}
|
|
2676
|
-
async readMany(sql2, where, opts) {
|
|
2677
|
-
const { conditions, values } = this._buildConditions(where, 0);
|
|
2678
|
-
const softDel = this._softDeleteFilter(where, opts);
|
|
2679
|
-
if (softDel) conditions.push(softDel);
|
|
2680
|
-
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
2681
|
-
const [countRow] = await sql2.unsafe(
|
|
2682
|
-
`SELECT COUNT(*) AS _total FROM "${this.tableName}"${whereClause}`,
|
|
2683
|
-
values
|
|
2684
|
-
);
|
|
2685
|
-
const count = Number(countRow._total);
|
|
2686
|
-
if (conditions.length === 0 && !opts?.orderBy && !opts?.limit && !opts?.offset && !opts?.select) {
|
|
2687
|
-
const rows2 = await sql2`SELECT * FROM ${sql2(this.tableName)}`;
|
|
2688
|
-
return { count, data: rows2 };
|
|
2689
|
-
}
|
|
2690
|
-
const columns = opts?.select?.length ? opts.select.map((c) => `"${c}"`).join(", ") : "*";
|
|
2691
|
-
let query = `SELECT ${columns} FROM "${this.tableName}"${whereClause}`;
|
|
2692
|
-
if (opts?.orderBy) {
|
|
2693
|
-
const orders = Object.entries(opts.orderBy).map(([prop, dir]) => {
|
|
2694
|
-
const entry = this.colEntries.find((e) => e.prop === prop);
|
|
2695
|
-
return `"${entry?.db || prop}" ${dir.toUpperCase()}`;
|
|
2696
|
-
}).join(", ");
|
|
2697
|
-
query += ` ORDER BY ${orders}`;
|
|
2698
|
-
}
|
|
2699
|
-
if (opts?.limit) query += ` LIMIT ${opts.limit}`;
|
|
2700
|
-
if (opts?.offset) query += ` OFFSET ${opts.offset}`;
|
|
2701
|
-
const rows = await sql2.unsafe(query, values);
|
|
2702
|
-
return { count, data: rows };
|
|
2703
|
-
}
|
|
2704
|
-
async update(sql2, id, data) {
|
|
2705
|
-
const { sets, values: setValues } = this._buildSET(data);
|
|
2706
|
-
if (sets.length === 0) return void 0;
|
|
2707
|
-
const pk = this.pkColumn;
|
|
2708
|
-
const [row] = await sql2.unsafe(
|
|
2709
|
-
`UPDATE "${this.tableName}" SET ${sets.join(", ")} WHERE "${pk}" = $${setValues.length + 1} RETURNING *`,
|
|
2710
|
-
[...setValues, id]
|
|
2711
|
-
);
|
|
2712
|
-
return row ?? void 0;
|
|
2713
|
-
}
|
|
2714
|
-
async updateMany(sql2, where, data) {
|
|
2715
|
-
const { sets, values: setValues } = this._buildSET(data);
|
|
2716
|
-
if (sets.length === 0) return 0;
|
|
2717
|
-
const { conditions: wConditions, values: wValues } = this._buildConditions(
|
|
2718
|
-
where,
|
|
2719
|
-
setValues.length
|
|
2720
|
-
);
|
|
2721
|
-
if (wConditions.length === 0) return 0;
|
|
2722
|
-
const rows = await sql2.unsafe(
|
|
2723
|
-
`UPDATE "${this.tableName}" SET ${sets.join(", ")} WHERE ${wConditions.join(" AND ")} RETURNING 1`,
|
|
2724
|
-
[...setValues, ...wValues]
|
|
2725
|
-
);
|
|
2726
|
-
return rows.length;
|
|
2727
|
-
}
|
|
2728
|
-
async delete(sql2, id) {
|
|
2729
|
-
const pk = this.pkColumn;
|
|
2730
|
-
if (this.hasColumn("deleted_at")) {
|
|
2731
|
-
const [row2] = await sql2.unsafe(
|
|
2732
|
-
`UPDATE "${this.tableName}" SET "deleted_at" = NOW() WHERE "${pk}" = $1 RETURNING *`,
|
|
2733
|
-
[id]
|
|
2734
|
-
);
|
|
2735
|
-
return row2 ?? void 0;
|
|
2736
|
-
}
|
|
2737
|
-
const [row] = await sql2.unsafe(
|
|
2738
|
-
`DELETE FROM "${this.tableName}" WHERE "${pk}" = $1 RETURNING *`,
|
|
2739
|
-
[id]
|
|
2740
|
-
);
|
|
2741
|
-
return row ?? void 0;
|
|
2742
|
-
}
|
|
2743
|
-
async hardDelete(sql2, id) {
|
|
2744
|
-
const pk = this.pkColumn;
|
|
2745
|
-
const [row] = await sql2.unsafe(
|
|
2746
|
-
`DELETE FROM "${this.tableName}" WHERE "${pk}" = $1 RETURNING *`,
|
|
2747
|
-
[id]
|
|
2748
|
-
);
|
|
2749
|
-
return row ?? void 0;
|
|
2750
|
-
}
|
|
2751
|
-
async deleteMany(sql2, where) {
|
|
2752
|
-
const { conditions, values } = this._buildConditions(where, 0);
|
|
2753
|
-
if (conditions.length === 0) return 0;
|
|
2754
|
-
if (this.hasColumn("deleted_at")) {
|
|
2755
|
-
const rows2 = await sql2.unsafe(
|
|
2756
|
-
`UPDATE "${this.tableName}" SET "deleted_at" = NOW() WHERE ${conditions.join(" AND ")} RETURNING 1`,
|
|
2757
|
-
values
|
|
2758
|
-
);
|
|
2759
|
-
return rows2.length;
|
|
2760
|
-
}
|
|
2761
|
-
const rows = await sql2.unsafe(
|
|
2762
|
-
`DELETE FROM "${this.tableName}" WHERE ${conditions.join(" AND ")} RETURNING 1`,
|
|
2763
|
-
values
|
|
2764
|
-
);
|
|
2765
|
-
return rows.length;
|
|
2766
|
-
}
|
|
2767
|
-
async hardDeleteMany(sql2, where) {
|
|
2768
|
-
const { conditions, values } = this._buildConditions(where, 0);
|
|
2769
|
-
if (conditions.length === 0) return 0;
|
|
2770
|
-
const rows = await sql2.unsafe(
|
|
2771
|
-
`DELETE FROM "${this.tableName}" WHERE ${conditions.join(" AND ")} RETURNING 1`,
|
|
2772
|
-
values
|
|
2773
|
-
);
|
|
2774
|
-
return rows.length;
|
|
2775
|
-
}
|
|
2776
|
-
async upsert(sql2, data, conflict) {
|
|
2777
|
-
const filtered = {};
|
|
2778
|
-
for (const { prop, db, auto } of this.colEntries) {
|
|
2779
|
-
if (auto) continue;
|
|
2780
|
-
const val = data[prop];
|
|
2781
|
-
if (val !== void 0) {
|
|
2782
|
-
filtered[db] = val;
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
const keys = Object.keys(filtered);
|
|
2786
|
-
if (keys.length === 0) throw new Error("upsert: no data to insert");
|
|
2787
|
-
const conflictCols = Array.isArray(conflict) ? conflict : [conflict];
|
|
2788
|
-
const dbCols = keys.map((c) => `"${c}"`);
|
|
2789
|
-
const placeholders = keys.map((_, i) => `$${i + 1}`);
|
|
2790
|
-
const updateSet = keys.filter((k) => !conflictCols.includes(k)).map((k) => `"${k}" = EXCLUDED."${k}"`).join(", ");
|
|
2791
|
-
if (!updateSet) {
|
|
2792
|
-
const [row2] = await sql2.unsafe(
|
|
2793
|
-
`INSERT INTO "${this.tableName}" (${dbCols.join(", ")}) VALUES (${placeholders.join(", ")}) ON CONFLICT (${conflictCols.map((c) => `"${c}"`).join(", ")}) DO NOTHING RETURNING *`,
|
|
2794
|
-
Object.values(filtered)
|
|
2795
|
-
);
|
|
2796
|
-
return row2 ?? void 0;
|
|
2797
|
-
}
|
|
2798
|
-
const [row] = await sql2.unsafe(
|
|
2799
|
-
`INSERT INTO "${this.tableName}" (${dbCols.join(", ")}) VALUES (${placeholders.join(", ")}) ON CONFLICT (${conflictCols.map((c) => `"${c}"`).join(", ")}) DO UPDATE SET ${updateSet} RETURNING *`,
|
|
2800
|
-
Object.values(filtered)
|
|
2801
|
-
);
|
|
2802
|
-
return row;
|
|
2803
|
-
}
|
|
2804
|
-
async count(sql2, where) {
|
|
2805
|
-
const { conditions, values } = this._buildConditions(where, 0);
|
|
2806
|
-
const softDel = this._softDeleteFilter(where);
|
|
2807
|
-
if (softDel) conditions.push(softDel);
|
|
2808
|
-
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
2809
|
-
const [row] = await sql2.unsafe(
|
|
2810
|
-
`SELECT COUNT(*) AS _total FROM "${this.tableName}"${whereClause}`,
|
|
2811
|
-
values
|
|
2812
|
-
);
|
|
2813
|
-
return Number(row._total);
|
|
2814
|
-
}
|
|
2815
|
-
};
|
|
2816
|
-
var BoundTable = class _BoundTable {
|
|
2817
|
-
inner;
|
|
2818
|
-
sql;
|
|
2819
|
-
/** The underlying table name. */
|
|
2820
|
-
get tableName() {
|
|
2821
|
-
return this.inner.tableName;
|
|
2822
|
-
}
|
|
2823
|
-
constructor(sql2, tableName, builders) {
|
|
2824
|
-
this.inner = new Table(tableName, builders);
|
|
2825
|
-
this.sql = sql2;
|
|
2826
|
-
}
|
|
2827
|
-
async create(opts) {
|
|
2828
|
-
await this.inner.create(this.sql, opts);
|
|
2829
|
-
}
|
|
2830
|
-
async drop(opts) {
|
|
2831
|
-
await this.inner.drop(this.sql, opts);
|
|
2832
|
-
}
|
|
2833
|
-
async createIndex(columns, opts) {
|
|
2834
|
-
await this.inner.createIndex(this.sql, columns, opts);
|
|
2835
|
-
}
|
|
2836
|
-
async createUniqueIndex(columns) {
|
|
2837
|
-
await this.inner.createUniqueIndex(this.sql, columns);
|
|
2838
|
-
}
|
|
2839
|
-
async insert(data) {
|
|
2840
|
-
return await this.inner.insert(this.sql, data);
|
|
2841
|
-
}
|
|
2842
|
-
async insertMany(data) {
|
|
2843
|
-
return await this.inner.insertMany(this.sql, data);
|
|
2844
|
-
}
|
|
2845
|
-
async read(id, opts) {
|
|
2846
|
-
return await this.inner.read(this.sql, id, opts);
|
|
2847
|
-
}
|
|
2848
|
-
async readMany(where, opts) {
|
|
2849
|
-
return await this.inner.readMany(this.sql, where, opts);
|
|
2850
|
-
}
|
|
2851
|
-
async update(id, data) {
|
|
2852
|
-
return await this.inner.update(this.sql, id, data);
|
|
2853
|
-
}
|
|
2854
|
-
async updateMany(where, data) {
|
|
2855
|
-
return await this.inner.updateMany(this.sql, where, data);
|
|
2856
|
-
}
|
|
2857
|
-
async delete(id) {
|
|
2858
|
-
return await this.inner.delete(this.sql, id);
|
|
2859
|
-
}
|
|
2860
|
-
async hardDelete(id) {
|
|
2861
|
-
return await this.inner.hardDelete(this.sql, id);
|
|
2862
|
-
}
|
|
2863
|
-
async deleteMany(where) {
|
|
2864
|
-
return await this.inner.deleteMany(this.sql, where);
|
|
2865
|
-
}
|
|
2866
|
-
async hardDeleteMany(where) {
|
|
2867
|
-
return await this.inner.hardDeleteMany(this.sql, where);
|
|
2868
|
-
}
|
|
2869
|
-
async upsert(data, conflict) {
|
|
2870
|
-
return await this.inner.upsert(this.sql, data, conflict);
|
|
2871
|
-
}
|
|
2872
|
-
async count(where) {
|
|
2873
|
-
return await this.inner.count(this.sql, where);
|
|
2874
|
-
}
|
|
2875
|
-
withSql(sql2) {
|
|
2876
|
-
return new _BoundTable(sql2, this.inner.tableName, this.inner.builders);
|
|
2877
|
-
}
|
|
2878
|
-
};
|
|
2879
|
-
|
|
2880
|
-
// postgres/client.ts
|
|
2881
2447
|
var MIGRATIONS_TABLE = "_weifuwu_migrations";
|
|
2882
2448
|
var RETRYABLE_CODES = /* @__PURE__ */ new Set(["40P01", "40001"]);
|
|
2883
2449
|
function isRetryable(err) {
|
|
@@ -2897,7 +2463,7 @@ function postgres(opts) {
|
|
|
2897
2463
|
const sep2 = connStr.includes("?") ? "&" : "?";
|
|
2898
2464
|
connStr = `${connStr}${sep2}options=-c%20statement_timeout%3D${stmtTimeout}`;
|
|
2899
2465
|
}
|
|
2900
|
-
const
|
|
2466
|
+
const sql = postgresFactory(connStr, {
|
|
2901
2467
|
max: options.max,
|
|
2902
2468
|
ssl: options.ssl,
|
|
2903
2469
|
idle_timeout: options.idle_timeout,
|
|
@@ -2907,7 +2473,7 @@ function postgres(opts) {
|
|
|
2907
2473
|
options.signal.addEventListener(
|
|
2908
2474
|
"abort",
|
|
2909
2475
|
() => {
|
|
2910
|
-
|
|
2476
|
+
sql.end();
|
|
2911
2477
|
},
|
|
2912
2478
|
{ once: true }
|
|
2913
2479
|
);
|
|
@@ -2917,19 +2483,13 @@ function postgres(opts) {
|
|
|
2917
2483
|
const _waiting = 0;
|
|
2918
2484
|
const poolMax = options.max ?? 10;
|
|
2919
2485
|
const mw = ((req, ctx, next) => {
|
|
2920
|
-
ctx.sql =
|
|
2486
|
+
ctx.sql = sql;
|
|
2921
2487
|
return next(req, ctx);
|
|
2922
2488
|
});
|
|
2923
2489
|
mw.__meta = { injects: ["sql"], depends: [] };
|
|
2924
|
-
mw.sql =
|
|
2925
|
-
mw.table = ((tableOrSchema, builders) => {
|
|
2926
|
-
if (typeof tableOrSchema === "string") {
|
|
2927
|
-
return new BoundTable(sql2, tableOrSchema, builders);
|
|
2928
|
-
}
|
|
2929
|
-
return new BoundTable(sql2, tableOrSchema.tableName, tableOrSchema.builders);
|
|
2930
|
-
});
|
|
2490
|
+
mw.sql = sql;
|
|
2931
2491
|
mw.migrate = async () => {
|
|
2932
|
-
await
|
|
2492
|
+
await sql.unsafe(`
|
|
2933
2493
|
CREATE TABLE IF NOT EXISTS "${MIGRATIONS_TABLE}" (
|
|
2934
2494
|
name TEXT PRIMARY KEY,
|
|
2935
2495
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
@@ -2937,13 +2497,13 @@ function postgres(opts) {
|
|
|
2937
2497
|
`);
|
|
2938
2498
|
};
|
|
2939
2499
|
mw.markMigrated = async (moduleName) => {
|
|
2940
|
-
await
|
|
2500
|
+
await sql.unsafe(
|
|
2941
2501
|
`INSERT INTO "${MIGRATIONS_TABLE}" (name) VALUES ($1) ON CONFLICT DO NOTHING`,
|
|
2942
2502
|
[moduleName]
|
|
2943
2503
|
);
|
|
2944
2504
|
};
|
|
2945
2505
|
mw.isMigrated = async (moduleName) => {
|
|
2946
|
-
const [row] = await
|
|
2506
|
+
const [row] = await sql.unsafe(`SELECT 1 FROM "${MIGRATIONS_TABLE}" WHERE name = $1`, [
|
|
2947
2507
|
moduleName
|
|
2948
2508
|
]);
|
|
2949
2509
|
return !!row;
|
|
@@ -2952,7 +2512,7 @@ function postgres(opts) {
|
|
|
2952
2512
|
const maxRetries = retryOpts?.maxRetries ?? 3;
|
|
2953
2513
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2954
2514
|
try {
|
|
2955
|
-
const result = await
|
|
2515
|
+
const result = await sql.begin(fn);
|
|
2956
2516
|
return result;
|
|
2957
2517
|
} catch (err) {
|
|
2958
2518
|
if (attempt < maxRetries && isRetryable(err)) {
|
|
@@ -2971,7 +2531,7 @@ function postgres(opts) {
|
|
|
2971
2531
|
waiting: _waiting,
|
|
2972
2532
|
max: poolMax
|
|
2973
2533
|
});
|
|
2974
|
-
mw.close = () =>
|
|
2534
|
+
mw.close = () => sql.end({ timeout: closeTimeout });
|
|
2975
2535
|
return mw;
|
|
2976
2536
|
}
|
|
2977
2537
|
|
|
@@ -3214,7 +2774,7 @@ function createMemoryQueue(opts) {
|
|
|
3214
2774
|
return q;
|
|
3215
2775
|
}
|
|
3216
2776
|
function createPgQueue(opts) {
|
|
3217
|
-
const
|
|
2777
|
+
const sql = opts.pg.sql;
|
|
3218
2778
|
const pollInterval = opts?.pollInterval ?? 200;
|
|
3219
2779
|
const table = (opts?.prefix ?? "queue") + "_jobs";
|
|
3220
2780
|
const handlers = /* @__PURE__ */ new Map();
|
|
@@ -3224,10 +2784,10 @@ function createPgQueue(opts) {
|
|
|
3224
2784
|
const MAX_FAILED = 1e3;
|
|
3225
2785
|
async function ensureTable() {
|
|
3226
2786
|
if (ready) return;
|
|
3227
|
-
await
|
|
2787
|
+
await sql.unsafe(
|
|
3228
2788
|
`CREATE TABLE IF NOT EXISTS ${escapeIdent(table)} (id UUID PRIMARY KEY, type TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}', run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), schedule TEXT, status TEXT NOT NULL DEFAULT 'pending', error TEXT, failed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`
|
|
3229
2789
|
);
|
|
3230
|
-
await
|
|
2790
|
+
await sql.unsafe(
|
|
3231
2791
|
`CREATE INDEX IF NOT EXISTS ${escapeIdent(table + "_run_at_idx")} ON ${escapeIdent(table)} (run_at, status)`
|
|
3232
2792
|
);
|
|
3233
2793
|
ready = true;
|
|
@@ -3237,12 +2797,12 @@ function createPgQueue(opts) {
|
|
|
3237
2797
|
try {
|
|
3238
2798
|
await handler(job);
|
|
3239
2799
|
_processed++;
|
|
3240
|
-
await
|
|
2800
|
+
await sql.unsafe(`DELETE FROM ${escapeIdent(table)} WHERE id = $1`, [job.id]);
|
|
3241
2801
|
} catch (e) {
|
|
3242
2802
|
_failed++;
|
|
3243
2803
|
const msg = e.message;
|
|
3244
2804
|
console.error("[queue] handler error:", msg);
|
|
3245
|
-
await
|
|
2805
|
+
await sql.unsafe(
|
|
3246
2806
|
`UPDATE ${escapeIdent(table)} SET status = 'failed', error = $2, failed_at = NOW() WHERE id = $1`,
|
|
3247
2807
|
[job.id, msg]
|
|
3248
2808
|
);
|
|
@@ -3252,7 +2812,7 @@ function createPgQueue(opts) {
|
|
|
3252
2812
|
if (job.schedule) {
|
|
3253
2813
|
try {
|
|
3254
2814
|
const nextRun = cronNext(job.schedule);
|
|
3255
|
-
await
|
|
2815
|
+
await sql.unsafe(
|
|
3256
2816
|
`INSERT INTO ${escapeIdent(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`,
|
|
3257
2817
|
[
|
|
3258
2818
|
crypto4.randomUUID(),
|
|
@@ -3271,7 +2831,7 @@ function createPgQueue(opts) {
|
|
|
3271
2831
|
if (!running) return;
|
|
3272
2832
|
try {
|
|
3273
2833
|
while (running && inflight < MAX_CONCURRENT) {
|
|
3274
|
-
const rows = await
|
|
2834
|
+
const rows = await sql.unsafe(
|
|
3275
2835
|
`UPDATE ${escapeIdent(table)} SET status = 'running' WHERE id = (SELECT id FROM ${escapeIdent(table)} WHERE run_at <= NOW() AND status = 'pending' ORDER BY run_at LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING *`
|
|
3276
2836
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3277
2837
|
);
|
|
@@ -3319,7 +2879,7 @@ function createPgQueue(opts) {
|
|
|
3319
2879
|
} else {
|
|
3320
2880
|
runAt = /* @__PURE__ */ new Date();
|
|
3321
2881
|
}
|
|
3322
|
-
await
|
|
2882
|
+
await sql.unsafe(
|
|
3323
2883
|
`INSERT INTO ${escapeIdent(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`,
|
|
3324
2884
|
[id, type, JSON.stringify(payload), runAt.toISOString(), opts2?.schedule || null]
|
|
3325
2885
|
);
|
|
@@ -3345,7 +2905,7 @@ function createPgQueue(opts) {
|
|
|
3345
2905
|
while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
|
|
3346
2906
|
};
|
|
3347
2907
|
mw.jobs = async function jobs(limit) {
|
|
3348
|
-
const rows = await
|
|
2908
|
+
const rows = await sql.unsafe(
|
|
3349
2909
|
`SELECT * FROM ${escapeIdent(table)} WHERE status = 'pending' ORDER BY run_at LIMIT $1`,
|
|
3350
2910
|
[limit ?? 50]
|
|
3351
2911
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -3360,7 +2920,7 @@ function createPgQueue(opts) {
|
|
|
3360
2920
|
}));
|
|
3361
2921
|
};
|
|
3362
2922
|
mw.failedJobs = async function failedJobs(limit) {
|
|
3363
|
-
const rows = await
|
|
2923
|
+
const rows = await sql.unsafe(
|
|
3364
2924
|
`SELECT * FROM ${escapeIdent(table)} WHERE status = 'failed' ORDER BY failed_at DESC LIMIT $1`,
|
|
3365
2925
|
[limit ?? 50]
|
|
3366
2926
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -3377,7 +2937,7 @@ function createPgQueue(opts) {
|
|
|
3377
2937
|
}));
|
|
3378
2938
|
};
|
|
3379
2939
|
mw.retryFailed = async function retryFailed(jobId) {
|
|
3380
|
-
const result = await
|
|
2940
|
+
const result = await sql.unsafe(
|
|
3381
2941
|
`UPDATE ${escapeIdent(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE id = $1 AND status = 'failed' RETURNING id`,
|
|
3382
2942
|
[jobId]
|
|
3383
2943
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -3385,7 +2945,7 @@ function createPgQueue(opts) {
|
|
|
3385
2945
|
return result.length > 0;
|
|
3386
2946
|
};
|
|
3387
2947
|
mw.retryAllFailed = async function retryAllFailed(type) {
|
|
3388
|
-
const result = await
|
|
2948
|
+
const result = await sql.unsafe(
|
|
3389
2949
|
type ? `UPDATE ${escapeIdent(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE status = 'failed' AND type = $1 RETURNING id` : `UPDATE ${escapeIdent(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE status = 'failed' RETURNING id`,
|
|
3390
2950
|
type ? [type] : []
|
|
3391
2951
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -3454,14 +3014,14 @@ function createRedisQueue(opts) {
|
|
|
3454
3014
|
while (running && inflight < MAX_CONCURRENT) {
|
|
3455
3015
|
const result = await redis2.zpopmin(jobKey);
|
|
3456
3016
|
if (result.length < 2) break;
|
|
3457
|
-
const
|
|
3017
|
+
const raw2 = result[0], score = parseInt(result[1], 10);
|
|
3458
3018
|
if (score > now) {
|
|
3459
|
-
await redis2.zadd(jobKey, score,
|
|
3019
|
+
await redis2.zadd(jobKey, score, raw2);
|
|
3460
3020
|
break;
|
|
3461
3021
|
}
|
|
3462
3022
|
let job;
|
|
3463
3023
|
try {
|
|
3464
|
-
job = JSON.parse(
|
|
3024
|
+
job = JSON.parse(raw2);
|
|
3465
3025
|
} catch {
|
|
3466
3026
|
continue;
|
|
3467
3027
|
}
|
|
@@ -3511,8 +3071,8 @@ function createRedisQueue(opts) {
|
|
|
3511
3071
|
redis2.disconnect();
|
|
3512
3072
|
};
|
|
3513
3073
|
mw.jobs = async function jobs(limit) {
|
|
3514
|
-
const
|
|
3515
|
-
return
|
|
3074
|
+
const raw2 = await redis2.zrevrange(jobKey, 0, (limit ?? 50) - 1);
|
|
3075
|
+
return raw2.map((r) => {
|
|
3516
3076
|
try {
|
|
3517
3077
|
return JSON.parse(r);
|
|
3518
3078
|
} catch {
|
|
@@ -3521,8 +3081,8 @@ function createRedisQueue(opts) {
|
|
|
3521
3081
|
}).filter(Boolean);
|
|
3522
3082
|
};
|
|
3523
3083
|
mw.failedJobs = async function failedJobs(limit) {
|
|
3524
|
-
const
|
|
3525
|
-
return
|
|
3084
|
+
const raw2 = await redis2.lrange(failedKey, 0, (limit ?? 50) - 1);
|
|
3085
|
+
return raw2.map((r) => {
|
|
3526
3086
|
try {
|
|
3527
3087
|
return JSON.parse(r);
|
|
3528
3088
|
} catch {
|
|
@@ -3531,8 +3091,8 @@ function createRedisQueue(opts) {
|
|
|
3531
3091
|
}).filter(Boolean);
|
|
3532
3092
|
};
|
|
3533
3093
|
mw.retryFailed = async function retryFailed(jobId) {
|
|
3534
|
-
const
|
|
3535
|
-
for (const entry of
|
|
3094
|
+
const raw2 = await redis2.lrange(failedKey, 0, -1);
|
|
3095
|
+
for (const entry of raw2) {
|
|
3536
3096
|
try {
|
|
3537
3097
|
const job = JSON.parse(entry);
|
|
3538
3098
|
if (job.id === jobId) {
|
|
@@ -3551,8 +3111,8 @@ function createRedisQueue(opts) {
|
|
|
3551
3111
|
};
|
|
3552
3112
|
mw.retryAllFailed = async function retryAllFailed(type) {
|
|
3553
3113
|
let count = 0;
|
|
3554
|
-
const
|
|
3555
|
-
for (const entry of
|
|
3114
|
+
const raw2 = await redis2.lrange(failedKey, 0, -1);
|
|
3115
|
+
for (const entry of raw2) {
|
|
3556
3116
|
try {
|
|
3557
3117
|
const job = JSON.parse(entry);
|
|
3558
3118
|
if (type && job.type !== type) continue;
|
|
@@ -3802,14 +3362,14 @@ function makeSetFlash(name, location) {
|
|
|
3802
3362
|
function flash(options) {
|
|
3803
3363
|
const name = options?.name ?? "flash";
|
|
3804
3364
|
const mw = async (req, ctx, next) => {
|
|
3805
|
-
const
|
|
3365
|
+
const raw2 = getCookies(req)[name] ?? null;
|
|
3806
3366
|
const referer = req.headers.get("referer") || "/";
|
|
3807
3367
|
let value = void 0;
|
|
3808
|
-
if (
|
|
3368
|
+
if (raw2) {
|
|
3809
3369
|
try {
|
|
3810
|
-
value = JSON.parse(decodeURIComponent(
|
|
3370
|
+
value = JSON.parse(decodeURIComponent(raw2));
|
|
3811
3371
|
} catch {
|
|
3812
|
-
value =
|
|
3372
|
+
value = raw2;
|
|
3813
3373
|
}
|
|
3814
3374
|
}
|
|
3815
3375
|
ctx.flash = {
|
|
@@ -3817,7 +3377,7 @@ function flash(options) {
|
|
|
3817
3377
|
set: makeSetFlash(name, referer)
|
|
3818
3378
|
};
|
|
3819
3379
|
const res = await next(req, ctx);
|
|
3820
|
-
if (
|
|
3380
|
+
if (raw2) {
|
|
3821
3381
|
const headers = new Headers(res.headers);
|
|
3822
3382
|
headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
|
|
3823
3383
|
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
@@ -3874,6 +3434,255 @@ function csrf(options) {
|
|
|
3874
3434
|
mw.__meta = { injects: ["csrf"], depends: [] };
|
|
3875
3435
|
return mw;
|
|
3876
3436
|
}
|
|
3437
|
+
|
|
3438
|
+
// ssr/html.ts
|
|
3439
|
+
var ESCAPE_MAP = {
|
|
3440
|
+
"&": "&",
|
|
3441
|
+
"<": "<",
|
|
3442
|
+
">": ">",
|
|
3443
|
+
'"': """,
|
|
3444
|
+
"'": "'"
|
|
3445
|
+
};
|
|
3446
|
+
function escapeHtml(s) {
|
|
3447
|
+
const str = String(s);
|
|
3448
|
+
if (!/[&<>"']/.test(str)) return str;
|
|
3449
|
+
return str.replace(/[&<>"']/g, (c) => ESCAPE_MAP[c] || c);
|
|
3450
|
+
}
|
|
3451
|
+
function raw(s) {
|
|
3452
|
+
return {
|
|
3453
|
+
__brand: "RawString",
|
|
3454
|
+
value: s,
|
|
3455
|
+
toString() {
|
|
3456
|
+
return this.value;
|
|
3457
|
+
}
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
function isRaw(v) {
|
|
3461
|
+
return typeof v === "object" && v !== null && "__brand" in v && v.__brand === "RawString";
|
|
3462
|
+
}
|
|
3463
|
+
function html(strings, ...values) {
|
|
3464
|
+
let result = "";
|
|
3465
|
+
for (let i = 0; i < strings.length; i++) {
|
|
3466
|
+
result += strings[i];
|
|
3467
|
+
if (i < values.length) {
|
|
3468
|
+
result += stringify(values[i]);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
return raw(result);
|
|
3472
|
+
}
|
|
3473
|
+
function stringify(v) {
|
|
3474
|
+
if (v === null || v === void 0 || v === false) return "";
|
|
3475
|
+
if (isRaw(v)) return v.value;
|
|
3476
|
+
if (Array.isArray(v)) {
|
|
3477
|
+
let out = "";
|
|
3478
|
+
for (let i = 0; i < v.length; i++) {
|
|
3479
|
+
out += stringify(v[i]);
|
|
3480
|
+
}
|
|
3481
|
+
return out;
|
|
3482
|
+
}
|
|
3483
|
+
if (typeof v === "number") return String(v);
|
|
3484
|
+
return escapeHtml(v);
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
// ssr/layout.ts
|
|
3488
|
+
import { resolve as resolve4, isAbsolute } from "node:path";
|
|
3489
|
+
function layout(path) {
|
|
3490
|
+
const absPath = isAbsolute(path) ? path : resolve4(process.cwd(), path);
|
|
3491
|
+
let modPromise = null;
|
|
3492
|
+
async function getRenderFn() {
|
|
3493
|
+
if (!modPromise) {
|
|
3494
|
+
modPromise = import(absPath).catch((err) => {
|
|
3495
|
+
modPromise = null;
|
|
3496
|
+
throw new Error(
|
|
3497
|
+
`[layout] Failed to load layout module "${path}": ${err instanceof Error ? err.message : String(err)}`
|
|
3498
|
+
);
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
const mod = await modPromise;
|
|
3502
|
+
const renderFn = mod.default;
|
|
3503
|
+
if (typeof renderFn !== "function") {
|
|
3504
|
+
throw new Error(
|
|
3505
|
+
`[layout] Layout module "${path}" must export a default function, got ${typeof renderFn}`
|
|
3506
|
+
);
|
|
3507
|
+
}
|
|
3508
|
+
return renderFn;
|
|
3509
|
+
}
|
|
3510
|
+
const mw = async (req, ctx, next) => {
|
|
3511
|
+
const renderFn = await getRenderFn();
|
|
3512
|
+
const response = await next(req, ctx);
|
|
3513
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
3514
|
+
if (!ct.includes("text/html")) return response;
|
|
3515
|
+
const body = await response.text();
|
|
3516
|
+
const wrapped = await renderFn(body, ctx);
|
|
3517
|
+
return new Response(wrapped, {
|
|
3518
|
+
status: response.status,
|
|
3519
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
3520
|
+
});
|
|
3521
|
+
};
|
|
3522
|
+
mw.__meta = { injects: [], depends: [] };
|
|
3523
|
+
return mw;
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
// ssr/compile.ts
|
|
3527
|
+
import { resolve as resolve5, isAbsolute as isAbsolute2 } from "node:path";
|
|
3528
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
3529
|
+
var loading = /* @__PURE__ */ new Map();
|
|
3530
|
+
async function loadModule(path) {
|
|
3531
|
+
const absPath = isAbsolute2(path) ? path : resolve5(process.cwd(), path);
|
|
3532
|
+
const cached = moduleCache.get(absPath);
|
|
3533
|
+
if (cached) return cached;
|
|
3534
|
+
const inFlight = loading.get(absPath);
|
|
3535
|
+
if (inFlight) return inFlight;
|
|
3536
|
+
const promise = import(absPath).then((mod) => {
|
|
3537
|
+
loading.delete(absPath);
|
|
3538
|
+
moduleCache.set(absPath, Promise.resolve(mod));
|
|
3539
|
+
return mod;
|
|
3540
|
+
}).catch((err) => {
|
|
3541
|
+
loading.delete(absPath);
|
|
3542
|
+
moduleCache.delete(absPath);
|
|
3543
|
+
throw new Error(
|
|
3544
|
+
`[compile] Failed to load module "${path}": ${err instanceof Error ? err.message : String(err)}`
|
|
3545
|
+
);
|
|
3546
|
+
});
|
|
3547
|
+
loading.set(absPath, promise);
|
|
3548
|
+
return promise;
|
|
3549
|
+
}
|
|
3550
|
+
function clearModuleCache(path) {
|
|
3551
|
+
if (path) {
|
|
3552
|
+
const absPath = isAbsolute2(path) ? path : resolve5(process.cwd(), path);
|
|
3553
|
+
moduleCache.delete(absPath);
|
|
3554
|
+
loading.delete(absPath);
|
|
3555
|
+
} else {
|
|
3556
|
+
moduleCache.clear();
|
|
3557
|
+
loading.clear();
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
// ssr/view.ts
|
|
3562
|
+
function isRawString(v) {
|
|
3563
|
+
return typeof v === "object" && v !== null && "__brand" in v && v.__brand === "RawString";
|
|
3564
|
+
}
|
|
3565
|
+
function isResponse(v) {
|
|
3566
|
+
return v instanceof Response;
|
|
3567
|
+
}
|
|
3568
|
+
function view(path, options) {
|
|
3569
|
+
return async (req, ctx) => {
|
|
3570
|
+
let mod;
|
|
3571
|
+
if (options?.module) {
|
|
3572
|
+
mod = options.module;
|
|
3573
|
+
} else {
|
|
3574
|
+
mod = await loadModule(path);
|
|
3575
|
+
}
|
|
3576
|
+
const renderFn = mod.default;
|
|
3577
|
+
if (typeof renderFn !== "function") {
|
|
3578
|
+
throw new Error(
|
|
3579
|
+
`[view] Module "${path}" must export a default function, got ${typeof renderFn}`
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
const result = renderFn.length >= 1 ? await renderFn(ctx) : await renderFn();
|
|
3583
|
+
if (isResponse(result)) return result;
|
|
3584
|
+
const body = isRawString(result) ? result.value : String(result);
|
|
3585
|
+
return new Response(body, {
|
|
3586
|
+
status: 200,
|
|
3587
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
3588
|
+
});
|
|
3589
|
+
};
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// ssr/css.ts
|
|
3593
|
+
import { createHash } from "node:crypto";
|
|
3594
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
3595
|
+
import { join as join3, resolve as resolve6 } from "node:path";
|
|
3596
|
+
import tailwindPlugin from "@tailwindcss/postcss";
|
|
3597
|
+
import postcss from "postcss";
|
|
3598
|
+
var cssCache = /* @__PURE__ */ new Map();
|
|
3599
|
+
async function compileCSS(cssPath, sourceDir) {
|
|
3600
|
+
if (!existsSync(cssPath)) {
|
|
3601
|
+
return { css: "", hash: "empty", url: "" };
|
|
3602
|
+
}
|
|
3603
|
+
const raw2 = readFileSync2(cssPath, "utf-8");
|
|
3604
|
+
const src = `@source "${sourceDir}";
|
|
3605
|
+
${raw2}`;
|
|
3606
|
+
const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
|
|
3607
|
+
const hash = createHash("md5").update(result.css).digest("hex").slice(0, 8);
|
|
3608
|
+
const asset = { css: result.css, hash, url: `/__wfw/style/${hash}.css` };
|
|
3609
|
+
cssCache.set(cssPath, asset);
|
|
3610
|
+
return asset;
|
|
3611
|
+
}
|
|
3612
|
+
function cssContext(dir) {
|
|
3613
|
+
const appDir = resolve6(dir, "app");
|
|
3614
|
+
const cssPath = join3(appDir, "globals.css");
|
|
3615
|
+
let cached = null;
|
|
3616
|
+
return async (req, ctx, next) => {
|
|
3617
|
+
if (!cached) cached = compileCSS(cssPath, appDir);
|
|
3618
|
+
const asset = await cached;
|
|
3619
|
+
if (asset.css) ctx.css = asset;
|
|
3620
|
+
return next(req, ctx);
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
function cssRouter(dir) {
|
|
3624
|
+
const router = new Router();
|
|
3625
|
+
router.get("/__wfw/style/:hash.css", async () => {
|
|
3626
|
+
const cssPath = join3(resolve6(dir, "app"), "globals.css");
|
|
3627
|
+
const asset = cssCache.get(cssPath);
|
|
3628
|
+
if (!asset) return new Response("", { status: 404 });
|
|
3629
|
+
return new Response(asset.css, {
|
|
3630
|
+
headers: { "content-type": "text/css; charset=utf-8" }
|
|
3631
|
+
});
|
|
3632
|
+
});
|
|
3633
|
+
return router;
|
|
3634
|
+
}
|
|
3635
|
+
function clearCSSCache() {
|
|
3636
|
+
cssCache.clear();
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
// ssr/assets.ts
|
|
3640
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3641
|
+
import { resolve as resolve7 } from "node:path";
|
|
3642
|
+
function resolvePackage(name, file) {
|
|
3643
|
+
return resolve7(process.cwd(), "node_modules", name, file);
|
|
3644
|
+
}
|
|
3645
|
+
var HTMX_PATH = resolvePackage("htmx.org", "dist/htmx.min.js");
|
|
3646
|
+
var ALPINE_PATH = resolvePackage("alpinejs", "dist/cdn.min.js");
|
|
3647
|
+
var htmxContent = null;
|
|
3648
|
+
var alpineContent = null;
|
|
3649
|
+
function loadAsset(path) {
|
|
3650
|
+
try {
|
|
3651
|
+
return readFileSync3(path, "utf-8");
|
|
3652
|
+
} catch {
|
|
3653
|
+
return null;
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
function assetRouter() {
|
|
3657
|
+
const router = new Router();
|
|
3658
|
+
router.get("/__wfw/js/htmx.min.js", () => {
|
|
3659
|
+
if (!htmxContent) htmxContent = loadAsset(HTMX_PATH);
|
|
3660
|
+
if (!htmxContent) return new Response("HTMX not found", { status: 404 });
|
|
3661
|
+
return new Response(htmxContent, {
|
|
3662
|
+
headers: {
|
|
3663
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
3664
|
+
"cache-control": "public, max-age=31536000, immutable"
|
|
3665
|
+
}
|
|
3666
|
+
});
|
|
3667
|
+
});
|
|
3668
|
+
router.get("/__wfw/js/alpine.min.js", () => {
|
|
3669
|
+
if (!alpineContent) alpineContent = loadAsset(ALPINE_PATH);
|
|
3670
|
+
if (!alpineContent) return new Response("Alpine not found", { status: 404 });
|
|
3671
|
+
return new Response(alpineContent, {
|
|
3672
|
+
headers: {
|
|
3673
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
3674
|
+
"cache-control": "public, max-age=31536000, immutable"
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
});
|
|
3678
|
+
return router;
|
|
3679
|
+
}
|
|
3680
|
+
function assetScripts() {
|
|
3681
|
+
return raw(`
|
|
3682
|
+
<script src="/__wfw/js/htmx.min.js"></script>
|
|
3683
|
+
<script defer src="/__wfw/js/alpine.min.js"></script>
|
|
3684
|
+
`);
|
|
3685
|
+
}
|
|
3877
3686
|
export {
|
|
3878
3687
|
DEFAULT_MAX_BODY,
|
|
3879
3688
|
HttpError,
|
|
@@ -3883,6 +3692,10 @@ export {
|
|
|
3883
3692
|
TestRequest,
|
|
3884
3693
|
aiProvider,
|
|
3885
3694
|
aiStream,
|
|
3695
|
+
assetRouter,
|
|
3696
|
+
assetScripts,
|
|
3697
|
+
clearCSSCache,
|
|
3698
|
+
clearModuleCache,
|
|
3886
3699
|
compress,
|
|
3887
3700
|
cors,
|
|
3888
3701
|
createHub,
|
|
@@ -3891,6 +3704,8 @@ export {
|
|
|
3891
3704
|
createTestDb,
|
|
3892
3705
|
createTestServer,
|
|
3893
3706
|
csrf,
|
|
3707
|
+
cssContext,
|
|
3708
|
+
cssRouter,
|
|
3894
3709
|
currentTrace,
|
|
3895
3710
|
currentTraceId,
|
|
3896
3711
|
deleteCookie,
|
|
@@ -3907,16 +3722,20 @@ export {
|
|
|
3907
3722
|
graphql,
|
|
3908
3723
|
health,
|
|
3909
3724
|
helmet,
|
|
3725
|
+
html,
|
|
3910
3726
|
i18n,
|
|
3911
3727
|
isBundled,
|
|
3912
3728
|
isDev,
|
|
3913
3729
|
isProd,
|
|
3730
|
+
layout,
|
|
3914
3731
|
loadEnv,
|
|
3732
|
+
loadModule,
|
|
3915
3733
|
logger,
|
|
3916
3734
|
openai,
|
|
3917
3735
|
postgres,
|
|
3918
3736
|
queue,
|
|
3919
3737
|
rateLimit,
|
|
3738
|
+
raw,
|
|
3920
3739
|
redis,
|
|
3921
3740
|
requestId,
|
|
3922
3741
|
runWithTrace,
|
|
@@ -3933,5 +3752,6 @@ export {
|
|
|
3933
3752
|
traceElapsed,
|
|
3934
3753
|
upload,
|
|
3935
3754
|
validate,
|
|
3755
|
+
view,
|
|
3936
3756
|
withTestDb
|
|
3937
3757
|
};
|