weifuwu 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1040 -81
- 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/README.md
CHANGED
|
@@ -141,6 +141,8 @@ The `ctx` object accumulates properties as it passes through the middleware chai
|
|
|
141
141
|
| `env` | `loadEnv()` | `Record<string, string>` | Public env vars (`WEIFUWU_PUBLIC_*`) |
|
|
142
142
|
| `csrfToken` | `csrf()` | `string` | CSRF token |
|
|
143
143
|
| `requestId` | `requestId()` | `string` | Request ID |
|
|
144
|
+
| `session` | `session()` | `Session` | Session data object |
|
|
145
|
+
| `sessionId` | `session()` | `string` | Session ID |
|
|
144
146
|
| `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
|
|
145
147
|
| `redis` | `redis()` | `Redis` | Redis client |
|
|
146
148
|
| `queue` | `queue()` | `Queue` | Job queue |
|
|
@@ -154,6 +156,8 @@ The `ctx` object accumulates properties as it passes through the middleware chai
|
|
|
154
156
|
| `setPref` | `preferences()` | `(key, val) => Response` | Set preference cookie + redirect |
|
|
155
157
|
| `compiledTailwindCss` | `ssr()` internal | `string` | Compiled CSS content (internal) |
|
|
156
158
|
| `tailwindCssUrl` | `ssr()` internal | `string` | Compiled CSS route URL (internal) |
|
|
159
|
+
| `session` | `session()` | `Session` | Session data object |
|
|
160
|
+
| `sessionId` | `session()` | `string` | Session ID |
|
|
157
161
|
|
|
158
162
|
### Type-Safe Context
|
|
159
163
|
|
|
@@ -303,6 +307,31 @@ assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
|
|
|
303
307
|
| `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
|
|
304
308
|
| `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
|
|
305
309
|
|
|
310
|
+
### Database test isolation
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { createTestDb, withTestDb } from 'weifuwu'
|
|
314
|
+
|
|
315
|
+
// Isolated schema — each test gets its own schema, destroyed after
|
|
316
|
+
const db = await createTestDb()
|
|
317
|
+
await db.sql`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
|
|
318
|
+
await db.sql`INSERT INTO users (name) VALUES ('Alice')`
|
|
319
|
+
await db.destroy() // DROP SCHEMA ... CASCADE
|
|
320
|
+
|
|
321
|
+
// Transaction rollback — all changes are rolled back after callback
|
|
322
|
+
await withTestDb(async (sql) => {
|
|
323
|
+
await sql`INSERT INTO users ...`
|
|
324
|
+
// Automatically rolled back
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| Function | Description |
|
|
329
|
+
|----------|-------------|
|
|
330
|
+
| `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
|
|
331
|
+
| `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback |
|
|
332
|
+
|
|
333
|
+
Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset.
|
|
334
|
+
|
|
306
335
|
---
|
|
307
336
|
|
|
308
337
|
## Module Reference
|
|
@@ -414,6 +443,46 @@ app.use(cors({ credentials: true, maxAge: 3600 }))
|
|
|
414
443
|
| `credentials` | `boolean` | `false` | Allow cookies/credentials |
|
|
415
444
|
| `maxAge` | `number` | — | Preflight cache duration (seconds) |
|
|
416
445
|
|
|
446
|
+
### cache [α]
|
|
447
|
+
|
|
448
|
+
Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
app.use(cache()) // in-memory, 5min TTL
|
|
452
|
+
app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
|
|
453
|
+
app.use(cache({
|
|
454
|
+
ttl: 30_000,
|
|
455
|
+
tag: (req, ctx) => ctx.user ? `user:${ctx.user.id}` : undefined, // per-user invalidation
|
|
456
|
+
}))
|
|
457
|
+
|
|
458
|
+
// Programmatic invalidation
|
|
459
|
+
const c = cache({ store: 'redis', redis: ctx.redis })
|
|
460
|
+
app.use(c)
|
|
461
|
+
await c.invalidate('users') // invalidate all entries tagged with 'users'
|
|
462
|
+
await c.flush() // clear entire cache
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
| Option | Type | Default | Description |
|
|
466
|
+
|--------|------|---------|-------------|
|
|
467
|
+
| `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
|
|
468
|
+
| `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
|
|
469
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
470
|
+
| `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
|
|
471
|
+
| `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
|
|
472
|
+
| `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
|
|
473
|
+
| `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
|
|
474
|
+
| `maxBodySize` | `number` | `1048576` (1MB) | Max body bytes to cache |
|
|
475
|
+
|
|
476
|
+
Cached responses include `X-Cache: HIT` and `Age` headers. Requests with `Authorization` or `Cookie` headers are never cached. Binary content types (image, audio, video) are skipped.
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
import { MemoryCache, RedisCache } from 'weifuwu'
|
|
480
|
+
|
|
481
|
+
const mem = new MemoryCache()
|
|
482
|
+
await mem.set('key', { status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] }, 300_000)
|
|
483
|
+
mem.close()
|
|
484
|
+
```
|
|
485
|
+
|
|
417
486
|
### csrf [α]
|
|
418
487
|
|
|
419
488
|
```ts
|
|
@@ -871,6 +940,42 @@ class MyModule extends PgModule {
|
|
|
871
940
|
|
|
872
941
|
Where helpers + `and`/`or`/`not` can be imported from `'weifuwu'` alongside `postgres`. Full column builders and table helpers are in the same barrel.
|
|
873
942
|
|
|
943
|
+
### fts — Full-Text Search (PostgreSQL)
|
|
944
|
+
|
|
945
|
+
Utilities for PostgreSQL full-text search: create GIN indexes, search with ranking, and generate highlighted snippets.
|
|
946
|
+
|
|
947
|
+
```ts
|
|
948
|
+
import { fts } from 'weifuwu'
|
|
949
|
+
|
|
950
|
+
const articles = pg.table('articles', {
|
|
951
|
+
id: serial('id').primaryKey(),
|
|
952
|
+
title: text('title'),
|
|
953
|
+
body: text('body'),
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
// Create search index
|
|
957
|
+
await fts.createIndex(pg.sql, articles, ['title', 'body'], { language: 'english' })
|
|
958
|
+
|
|
959
|
+
// Search with ranking
|
|
960
|
+
const results = await fts.search(pg.sql, articles, 'node.js framework', {
|
|
961
|
+
fields: ['title', 'body'],
|
|
962
|
+
limit: 20,
|
|
963
|
+
headline: true, // highlighted snippets via ts_headline
|
|
964
|
+
})
|
|
965
|
+
// → [{ id, rank: 0.8, row: { title, body, ... }, headline: '...<b>Node.js</b> framework...' }]
|
|
966
|
+
|
|
967
|
+
// Drop index
|
|
968
|
+
await fts.dropIndex(pg.sql, articles)
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
| Function | Description |
|
|
972
|
+
|----------|-------------|
|
|
973
|
+
| `createIndex(sql, table, fields, opts?)` | Create GIN/GiST tsvector index |
|
|
974
|
+
| `search(sql, table, query, opts?)` | Search with ts_rank ordering |
|
|
975
|
+
| `dropIndex(sql, table, opts?)` | Drop the index |
|
|
976
|
+
|
|
977
|
+
Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
|
|
978
|
+
|
|
874
979
|
### preferences [α]
|
|
875
980
|
|
|
876
981
|
Locale detection + theme + translations. `/__lang/:locale` and `/__theme/:theme` auto-routed.
|
|
@@ -923,16 +1028,34 @@ await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
|
|
|
923
1028
|
| `.process(handler)` | Register job processor |
|
|
924
1029
|
| `.run()` | Start processing |
|
|
925
1030
|
| `.stop()` | Stop processing |
|
|
1031
|
+
| `.jobs(limit?)` | List pending jobs |
|
|
1032
|
+
| `.failedJobs(limit?)` | List failed jobs with error messages |
|
|
1033
|
+
| `.retryFailed(jobId)` | Retry a specific failed job |
|
|
1034
|
+
| `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
|
|
1035
|
+
| `.dashboard()` | Returns a Router with management endpoints |
|
|
926
1036
|
| `.close()` | Cleanup |
|
|
927
1037
|
|
|
1038
|
+
**Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
|
|
1039
|
+
|
|
1040
|
+
| Method | Path | Description |
|
|
1041
|
+
|--------|------|-------------|
|
|
1042
|
+
| GET | `/` | Queue stats + pending/failed counts by type |
|
|
1043
|
+
| GET | `/:type/failed` | List failed jobs for a type |
|
|
1044
|
+
| POST | `/:type/retry` | Retry all failed jobs of a type |
|
|
1045
|
+
| POST | `/retry/:id` | Retry a specific failed job by ID |
|
|
1046
|
+
|
|
928
1047
|
### rateLimit [α]
|
|
929
1048
|
|
|
930
1049
|
```ts
|
|
931
|
-
app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
|
|
1050
|
+
app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
|
|
932
1051
|
app.get('/api', rateLimit({ max: 10 }), handler) // per-route
|
|
933
1052
|
app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
|
|
1053
|
+
|
|
1054
|
+
// Multi-process: Redis-backed rate limiting
|
|
1055
|
+
app.use(rateLimit({ max: 100, store: 'redis', redis: ctx.redis }))
|
|
1056
|
+
|
|
934
1057
|
// Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After headers
|
|
935
|
-
// m.stop() — clear interval
|
|
1058
|
+
// m.stop() — clear interval (memory) or Redis cleanup
|
|
936
1059
|
```
|
|
937
1060
|
|
|
938
1061
|
| Option | Type | Default | Description |
|
|
@@ -941,6 +1064,11 @@ app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' })
|
|
|
941
1064
|
| `window` | `number` | `60_000` | Window duration (ms) |
|
|
942
1065
|
| `key` | `(req) => string` | IP-based | Key function |
|
|
943
1066
|
| `message` | `string` | `'Too Many Requests'` | 429 response body |
|
|
1067
|
+
| `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
|
|
1068
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
1069
|
+
| `prefix` | `string` | `'ratelimit:'` | Redis key prefix |
|
|
1070
|
+
|
|
1071
|
+
Redis mode uses `INCR` + `EXPIRE` for atomic counting, enabling accurate rate limiting across multiple server processes. Memory mode is ideal for single-process deployments.
|
|
944
1072
|
|
|
945
1073
|
### redis [α]
|
|
946
1074
|
|
|
@@ -989,6 +1117,58 @@ Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML st
|
|
|
989
1117
|
| `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
|
|
990
1118
|
| `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
|
|
991
1119
|
|
|
1120
|
+
### session [α]
|
|
1121
|
+
|
|
1122
|
+
Cookie-based server-side session management with memory and Redis stores.
|
|
1123
|
+
|
|
1124
|
+
```ts
|
|
1125
|
+
app.use(session()) // in-memory store (default)
|
|
1126
|
+
app.use(session({ store: 'redis', redis: ctx.redis })) // Redis store
|
|
1127
|
+
app.use(session({ store: 'redis', redis, ttl: 30 * 60_000, cookieName: 'sid' }))
|
|
1128
|
+
|
|
1129
|
+
app.get('/login', async (req, ctx) => {
|
|
1130
|
+
ctx.session.userId = 42
|
|
1131
|
+
ctx.session.role = 'admin'
|
|
1132
|
+
// Auto-saved on response — cookie set automatically
|
|
1133
|
+
return Response.json({ ok: true })
|
|
1134
|
+
})
|
|
1135
|
+
|
|
1136
|
+
app.get('/logout', async (req, ctx) => {
|
|
1137
|
+
ctx.session.destroy() // or ctx.session = null
|
|
1138
|
+
return Response.json({ ok: true })
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
// ctx.session.id — readonly session ID
|
|
1142
|
+
// ctx.session.save() — explicit dirty mark (for deep mutations)
|
|
1143
|
+
// ctx.session.destroy() — clear session + remove cookie
|
|
1144
|
+
// Session mutations are auto-detected on property set/delete
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
| Option | Type | Default | Description |
|
|
1148
|
+
|--------|------|---------|-------------|
|
|
1149
|
+
| `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
|
|
1150
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
1151
|
+
| `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
|
|
1152
|
+
| `cookieName` | `string` | `'__session'` | Cookie name |
|
|
1153
|
+
| `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
|
|
1154
|
+
| `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
|
|
1155
|
+
| `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
|
|
1156
|
+
| `cookie.path` | `string` | `'/'` | Cookie path |
|
|
1157
|
+
| `cookie.domain` | `string` | — | Cookie domain |
|
|
1158
|
+
|
|
1159
|
+
**Stores** are also exported for standalone use:
|
|
1160
|
+
|
|
1161
|
+
```ts
|
|
1162
|
+
import { MemoryStore, RedisStore } from 'weifuwu'
|
|
1163
|
+
|
|
1164
|
+
const mem = new MemoryStore() // auto-cleanup every 60s
|
|
1165
|
+
await mem.set('sid', { userId: 1 }, 86400000)
|
|
1166
|
+
mem.close()
|
|
1167
|
+
|
|
1168
|
+
const redis = new RedisStore(redisClient, 'myapp:session:')
|
|
1169
|
+
await redis.destroy('sid')
|
|
1170
|
+
```
|
|
1171
|
+
|
|
992
1172
|
### ssr({ dir }) [β]
|
|
993
1173
|
|
|
994
1174
|
One-stop Server-Side Rendering. Accepts a directory and returns a Router that handles all SSR routes, tailwind CSS, hydration bundles, and livereload — using Next.js-style file conventions.
|
|
@@ -1119,7 +1299,67 @@ app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string(
|
|
|
1119
1299
|
// ctx.parsed.headers — typed & validated
|
|
1120
1300
|
})
|
|
1121
1301
|
// Validation failure: returns 400 with { error: 'Validation failed', issues: [...] }
|
|
1302
|
+
|
|
1303
|
+
**Form body auto-parsing** — `application/x-www-form-urlencoded` bodies are automatically parsed into `Record<string, string>` via `URLSearchParams`, even without a Zod schema:
|
|
1304
|
+
|
|
1305
|
+
```ts
|
|
1306
|
+
// No schema needed — just parse the form
|
|
1307
|
+
app.post('/contact', validate(), (req, ctx) => {
|
|
1308
|
+
const email = ctx.parsed.body.email // string
|
|
1309
|
+
const msg = ctx.parsed.body.message // string
|
|
1310
|
+
return Response.json({ received: true })
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
// Or validate with Zod
|
|
1314
|
+
app.post('/contact', validate({ body: z.object({ email: z.string().email() }) }), handler)
|
|
1122
1315
|
```
|
|
1316
|
+
|
|
1317
|
+
| Option | Type | Default | Description |
|
|
1318
|
+
|--------|------|---------|-------------|
|
|
1319
|
+
| `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
|
|
1320
|
+
| `query` | `ZodSchema` | — | Query validation schema |
|
|
1321
|
+
| `params` | `ZodSchema` | — | URL params validation schema |
|
|
1322
|
+
| `headers` | `ZodSchema` | — | Header validation schema |
|
|
1323
|
+
|
|
1324
|
+
### webhook [β]
|
|
1325
|
+
|
|
1326
|
+
Webhook receiver with built-in signature verification for Stripe, GitHub, and Slack. Event-based dispatch with replay protection.
|
|
1327
|
+
|
|
1328
|
+
```ts
|
|
1329
|
+
import { webhook } from 'weifuwu'
|
|
1330
|
+
|
|
1331
|
+
const wh = webhook({
|
|
1332
|
+
stripe: { secret: process.env.STRIPE_WEBHOOK_SECRET! },
|
|
1333
|
+
github: { secret: process.env.GITHUB_WEBHOOK_SECRET! },
|
|
1334
|
+
slack: { secret: process.env.SLACK_WEBHOOK_SECRET! },
|
|
1335
|
+
})
|
|
1336
|
+
|
|
1337
|
+
app.use('/webhooks', wh)
|
|
1338
|
+
|
|
1339
|
+
wh.on('checkout.session.completed', async (event, ctx) => {
|
|
1340
|
+
await fulfillOrder(event.payload.data.object)
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
wh.on('push', async (event, ctx) => {
|
|
1344
|
+
await triggerCI(event.payload)
|
|
1345
|
+
})
|
|
1346
|
+
|
|
1347
|
+
wh.on('*', (event) => {
|
|
1348
|
+
console.log(`Received ${event.provider}.${event.event}`)
|
|
1349
|
+
})
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
| Option | Type | Default | Description |
|
|
1353
|
+
|--------|------|---------|-------------|
|
|
1354
|
+
| `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
|
|
1355
|
+
| `github` | `PlatformConfig` | — | GitHub webhook config |
|
|
1356
|
+
| `slack` | `PlatformConfig` | — | Slack webhook config |
|
|
1357
|
+
| `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
|
|
1358
|
+
| `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
|
|
1359
|
+
| `idempotencyTTL` | `number` | `3600000` | Dedup TTL (ms) |
|
|
1360
|
+
|
|
1361
|
+
Built-in verifiers handle HMAC-SHA256, timestamp validation (Slack's 5-min window), and Stripe's `t=` / `v1=` signature format. Slack URL verification challenges are auto-responded.
|
|
1362
|
+
|
|
1123
1363
|
### Client-side navigation
|
|
1124
1364
|
|
|
1125
1365
|
```tsx
|
|
@@ -1309,7 +1549,8 @@ currentTraceId, currentTrace, runWithTrace, traceElapsed, TraceContext,
|
|
|
1309
1549
|
|
|
1310
1550
|
```ts
|
|
1311
1551
|
auth, cors, csrf, compress, helmet, logger, rateLimit, requestId, validate, upload,
|
|
1312
|
-
preferences, serveStatic
|
|
1552
|
+
preferences, serveStatic, session, MemoryStore, RedisStore, SessionStore,
|
|
1553
|
+
cache, MemoryCache, RedisCache, CacheStore
|
|
1313
1554
|
```
|
|
1314
1555
|
|
|
1315
1556
|
### Database
|
|
@@ -1353,13 +1594,15 @@ openai, createOpenAI
|
|
|
1353
1594
|
preferences, health, analytics, seo, seoMiddleware, seoTags,
|
|
1354
1595
|
user, mailer, graphql, aiStream, runWorkflow,
|
|
1355
1596
|
logdb, messager, agent, iii, createWorker, registerWorker,
|
|
1356
|
-
opencode, deploy, defineConfig,
|
|
1597
|
+
opencode, deploy, defineConfig, webhook,
|
|
1357
1598
|
testApp, TestApp, TestRequest, TestResponse,
|
|
1599
|
+
createTestDb, withTestDb,
|
|
1358
1600
|
getCookies, setCookie, deleteCookie,
|
|
1359
1601
|
createSSEStream, formatSSE, formatSSEData,
|
|
1360
1602
|
currentTraceId, currentTrace, runWithTrace, traceElapsed,
|
|
1361
1603
|
createHub, Hub, HubOptions,
|
|
1362
1604
|
DEFAULT_MAX_BODY, MIGRATIONS_TABLE,
|
|
1605
|
+
fts,
|
|
1363
1606
|
```
|
|
1364
1607
|
|
|
1365
1608
|
---
|
package/cli.ts
CHANGED
|
@@ -63,8 +63,6 @@ async function cmdInit(name: string, opts: { minimal?: boolean; skipInstall?: bo
|
|
|
63
63
|
|
|
64
64
|
// Minimal mode: strip SSR/i18n/theme, keep only HTTP core
|
|
65
65
|
if (opts.minimal) {
|
|
66
|
-
// Remove UI directory
|
|
67
|
-
await cp(join(templateDir, '..', '..', '..'), '/dev/null') // noop
|
|
68
66
|
try { await rmrf(join(targetDir, 'ui')) } catch {}
|
|
69
67
|
try { await rmrf(join(targetDir, 'locales')) } catch {}
|
|
70
68
|
|
|
@@ -245,6 +243,8 @@ async function rmrf(dir: string) {
|
|
|
245
243
|
} catch { /* ignore */ }
|
|
246
244
|
}
|
|
247
245
|
|
|
246
|
+
import { parseArgs } from 'node:util'
|
|
247
|
+
|
|
248
248
|
// ── CLI dispatcher ──────────────────────────────────────────────────────
|
|
249
249
|
|
|
250
250
|
const cmd = process.argv[2]
|
|
@@ -253,11 +253,12 @@ const HELP = `
|
|
|
253
253
|
weifuwu — Web-standard HTTP framework for Node.js
|
|
254
254
|
|
|
255
255
|
Usage:
|
|
256
|
-
npx weifuwu init <name>
|
|
257
|
-
npx weifuwu init <name> --minimal
|
|
258
|
-
npx weifuwu
|
|
259
|
-
npx weifuwu
|
|
260
|
-
npx weifuwu
|
|
256
|
+
npx weifuwu init <name> Create a new project (SSR + i18n + theme)
|
|
257
|
+
npx weifuwu init <name> --minimal Create a minimal HTTP project
|
|
258
|
+
npx weifuwu init <name> --skip-install Create project, skip npm install
|
|
259
|
+
npx weifuwu dev Start dev server in current directory
|
|
260
|
+
npx weifuwu generate module <name> Scaffold a new module
|
|
261
|
+
npx weifuwu version Print version
|
|
261
262
|
`
|
|
262
263
|
|
|
263
264
|
if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
@@ -265,19 +266,32 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
265
266
|
} else if (cmd === 'skill') {
|
|
266
267
|
cmdSkill().catch(console.error)
|
|
267
268
|
} else if (cmd === 'init') {
|
|
268
|
-
const
|
|
269
|
+
const { values, positionals } = parseArgs({
|
|
270
|
+
args: process.argv.slice(3),
|
|
271
|
+
options: {
|
|
272
|
+
minimal: { type: 'boolean', short: 'm' },
|
|
273
|
+
'skip-install': { type: 'boolean' },
|
|
274
|
+
},
|
|
275
|
+
strict: false,
|
|
276
|
+
allowPositionals: true,
|
|
277
|
+
})
|
|
278
|
+
const name = positionals[0]
|
|
269
279
|
if (!name) {
|
|
270
|
-
console.error('Usage: npx weifuwu init <name> [--minimal]')
|
|
280
|
+
console.error('Usage: npx weifuwu init <name> [--minimal] [--skip-install]')
|
|
271
281
|
process.exit(1)
|
|
272
282
|
}
|
|
273
|
-
|
|
274
|
-
const skipInstall = process.argv.includes('--skip-install')
|
|
275
|
-
cmdInit(name, { minimal, skipInstall }).catch(console.error)
|
|
283
|
+
cmdInit(name, { minimal: !!values.minimal, skipInstall: !!values['skip-install'] }).catch(console.error)
|
|
276
284
|
} else if (cmd === 'dev') {
|
|
277
285
|
cmdDev()
|
|
278
286
|
} else if (cmd === 'generate' || cmd === 'g') {
|
|
279
|
-
const
|
|
280
|
-
|
|
287
|
+
const { positionals } = parseArgs({
|
|
288
|
+
args: process.argv.slice(3),
|
|
289
|
+
options: {},
|
|
290
|
+
strict: false,
|
|
291
|
+
allowPositionals: true,
|
|
292
|
+
})
|
|
293
|
+
const type = positionals[0]
|
|
294
|
+
const name = positionals[1]
|
|
281
295
|
if (!type || !name) {
|
|
282
296
|
console.error('Usage: npx weifuwu generate module <name>')
|
|
283
297
|
process.exit(1)
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Context, Middleware } from './types.ts';
|
|
2
|
+
import type { Redis } from './vendor.ts';
|
|
3
|
+
export interface CachedResponse {
|
|
4
|
+
status: number;
|
|
5
|
+
statusText: string;
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
body: string;
|
|
8
|
+
createdAt: number;
|
|
9
|
+
tags: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface CacheStore {
|
|
12
|
+
get(key: string): Promise<CachedResponse | null>;
|
|
13
|
+
set(key: string, entry: CachedResponse, ttl: number): Promise<void>;
|
|
14
|
+
delete(key: string): Promise<void>;
|
|
15
|
+
invalidate(tag: string): Promise<void>;
|
|
16
|
+
flush(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface CacheOptions {
|
|
19
|
+
/** TTL in milliseconds. Default: 300_000 (5 min). */
|
|
20
|
+
ttl?: number;
|
|
21
|
+
/** Cache store. 'memory' (default) or 'redis'. */
|
|
22
|
+
store?: 'memory' | 'redis' | CacheStore;
|
|
23
|
+
/** Redis client (required when store: 'redis'). */
|
|
24
|
+
redis?: Redis;
|
|
25
|
+
/** Custom cache key function. Default: SHA256 of method + URL. */
|
|
26
|
+
key?: (req: Request) => string;
|
|
27
|
+
/** Tag function for grouped invalidation. Called after handler runs (ctx available). */
|
|
28
|
+
tag?: (req: Request, ctx: Context) => string | string[] | undefined;
|
|
29
|
+
/** Whether to cache responses with Set-Cookie. Default: false. */
|
|
30
|
+
cacheCookies?: boolean;
|
|
31
|
+
/** Status codes to cache. Default: [200]. */
|
|
32
|
+
cacheStatus?: number[];
|
|
33
|
+
/** Maximum number of bytes per cached body. Default: 1MB. Larger bodies are skipped. */
|
|
34
|
+
maxBodySize?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface CacheMiddleware extends Middleware {
|
|
37
|
+
/** Invalidate all entries with a given tag. */
|
|
38
|
+
invalidate(tag: string): Promise<void>;
|
|
39
|
+
/** Flush all cached entries. */
|
|
40
|
+
flush(): Promise<void>;
|
|
41
|
+
/** Cleanup. */
|
|
42
|
+
close(): void;
|
|
43
|
+
/** Store reference (for testing). */
|
|
44
|
+
store: CacheStore;
|
|
45
|
+
}
|
|
46
|
+
export declare class MemoryCache implements CacheStore {
|
|
47
|
+
private store;
|
|
48
|
+
private tagIndex;
|
|
49
|
+
private interval;
|
|
50
|
+
constructor(cleanupMs?: number);
|
|
51
|
+
get(key: string): Promise<CachedResponse | null>;
|
|
52
|
+
set(key: string, data: CachedResponse, ttl: number): Promise<void>;
|
|
53
|
+
delete(key: string): Promise<void>;
|
|
54
|
+
invalidate(tag: string): Promise<void>;
|
|
55
|
+
flush(): Promise<void>;
|
|
56
|
+
private cleanup;
|
|
57
|
+
close(): void;
|
|
58
|
+
/** Testing only. */
|
|
59
|
+
get size(): number;
|
|
60
|
+
}
|
|
61
|
+
export declare class RedisCache implements CacheStore {
|
|
62
|
+
private redis;
|
|
63
|
+
private prefix;
|
|
64
|
+
private tagPrefix;
|
|
65
|
+
constructor(redis: Redis, prefix?: string);
|
|
66
|
+
private key;
|
|
67
|
+
private tagKey;
|
|
68
|
+
get(key: string): Promise<CachedResponse | null>;
|
|
69
|
+
set(key: string, entry: CachedResponse, ttl: number): Promise<void>;
|
|
70
|
+
delete(key: string): Promise<void>;
|
|
71
|
+
invalidate(tag: string): Promise<void>;
|
|
72
|
+
flush(): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
export declare function cache(options?: CacheOptions): CacheMiddleware;
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { execSync } from "node:child_process";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join, dirname, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { parseArgs } from "node:util";
|
|
10
11
|
var __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
var __dirname = dirname(__filename);
|
|
12
13
|
var pkgRoot = existsSync(join(__dirname, "package.json")) ? __dirname : resolve(__dirname, "..");
|
|
@@ -44,7 +45,6 @@ async function cmdInit(name, opts) {
|
|
|
44
45
|
await writeFile(uiPage, content);
|
|
45
46
|
}
|
|
46
47
|
if (opts.minimal) {
|
|
47
|
-
await cp(join(templateDir, "..", "..", ".."), "/dev/null");
|
|
48
48
|
try {
|
|
49
49
|
await rmrf(join(targetDir, "ui"));
|
|
50
50
|
} catch {
|
|
@@ -213,30 +213,44 @@ var HELP = `
|
|
|
213
213
|
weifuwu \u2014 Web-standard HTTP framework for Node.js
|
|
214
214
|
|
|
215
215
|
Usage:
|
|
216
|
-
npx weifuwu init <name>
|
|
217
|
-
npx weifuwu init <name> --minimal
|
|
218
|
-
npx weifuwu
|
|
219
|
-
npx weifuwu
|
|
220
|
-
npx weifuwu
|
|
216
|
+
npx weifuwu init <name> Create a new project (SSR + i18n + theme)
|
|
217
|
+
npx weifuwu init <name> --minimal Create a minimal HTTP project
|
|
218
|
+
npx weifuwu init <name> --skip-install Create project, skip npm install
|
|
219
|
+
npx weifuwu dev Start dev server in current directory
|
|
220
|
+
npx weifuwu generate module <name> Scaffold a new module
|
|
221
|
+
npx weifuwu version Print version
|
|
221
222
|
`;
|
|
222
223
|
if (cmd === "version" || cmd === "-v" || cmd === "--version") {
|
|
223
224
|
cmdVersion().catch(console.error);
|
|
224
225
|
} else if (cmd === "skill") {
|
|
225
226
|
cmdSkill().catch(console.error);
|
|
226
227
|
} else if (cmd === "init") {
|
|
227
|
-
const
|
|
228
|
+
const { values, positionals } = parseArgs({
|
|
229
|
+
args: process.argv.slice(3),
|
|
230
|
+
options: {
|
|
231
|
+
minimal: { type: "boolean", short: "m" },
|
|
232
|
+
"skip-install": { type: "boolean" }
|
|
233
|
+
},
|
|
234
|
+
strict: false,
|
|
235
|
+
allowPositionals: true
|
|
236
|
+
});
|
|
237
|
+
const name = positionals[0];
|
|
228
238
|
if (!name) {
|
|
229
|
-
console.error("Usage: npx weifuwu init <name> [--minimal]");
|
|
239
|
+
console.error("Usage: npx weifuwu init <name> [--minimal] [--skip-install]");
|
|
230
240
|
process.exit(1);
|
|
231
241
|
}
|
|
232
|
-
|
|
233
|
-
const skipInstall = process.argv.includes("--skip-install");
|
|
234
|
-
cmdInit(name, { minimal, skipInstall }).catch(console.error);
|
|
242
|
+
cmdInit(name, { minimal: !!values.minimal, skipInstall: !!values["skip-install"] }).catch(console.error);
|
|
235
243
|
} else if (cmd === "dev") {
|
|
236
244
|
cmdDev();
|
|
237
245
|
} else if (cmd === "generate" || cmd === "g") {
|
|
238
|
-
const
|
|
239
|
-
|
|
246
|
+
const { positionals } = parseArgs({
|
|
247
|
+
args: process.argv.slice(3),
|
|
248
|
+
options: {},
|
|
249
|
+
strict: false,
|
|
250
|
+
allowPositionals: true
|
|
251
|
+
});
|
|
252
|
+
const type = positionals[0];
|
|
253
|
+
const name = positionals[1];
|
|
240
254
|
if (!type || !name) {
|
|
241
255
|
console.error("Usage: npx weifuwu generate module <name>");
|
|
242
256
|
process.exit(1);
|
package/dist/fts.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Sql } from './vendor.ts';
|
|
2
|
+
import type { BoundTable } from './postgres/schema/index.ts';
|
|
3
|
+
export interface FTSSearchResult {
|
|
4
|
+
/** Primary key value */
|
|
5
|
+
id: unknown;
|
|
6
|
+
/** Relevance score (0–1) */
|
|
7
|
+
rank: number;
|
|
8
|
+
/** The row data (all columns) */
|
|
9
|
+
row: Record<string, unknown>;
|
|
10
|
+
/** ts_headline highlighted snippet, if requested */
|
|
11
|
+
headline?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FTSCreateIndexOptions {
|
|
14
|
+
language?: string;
|
|
15
|
+
indexName?: string;
|
|
16
|
+
indexType?: 'gin' | 'gist';
|
|
17
|
+
}
|
|
18
|
+
export interface FTSSearchOptions {
|
|
19
|
+
fields?: string[];
|
|
20
|
+
limit?: number;
|
|
21
|
+
offset?: number;
|
|
22
|
+
headline?: boolean;
|
|
23
|
+
language?: string;
|
|
24
|
+
rankColumn?: string;
|
|
25
|
+
minRank?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function createIndex(sql: Sql<{}>, table: BoundTable<any>, fields: string[], options?: FTSCreateIndexOptions): Promise<void>;
|
|
28
|
+
export declare function dropIndex(sql: Sql<{}>, table: BoundTable<any>, options?: {
|
|
29
|
+
indexName?: string;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
export declare function search<T extends Record<string, unknown>>(sql: Sql<{}>, table: BoundTable<T>, query: string, options?: FTSSearchOptions): Promise<FTSSearchResult[]>;
|
|
32
|
+
export declare function suggest(sql: Sql<{}>, table: BoundTable<any>, prefix: string, options?: {
|
|
33
|
+
field?: string;
|
|
34
|
+
language?: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
}): Promise<string[]>;
|
package/dist/index.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ export { requestId } from './request-id.ts';
|
|
|
31
31
|
export type { RequestIdOptions } from './request-id.ts';
|
|
32
32
|
export { createSSEStream, formatSSE, formatSSEData } from './sse.ts';
|
|
33
33
|
export type { SSEEvent } from './sse.ts';
|
|
34
|
-
export { testApp, TestApp, TestRequest } from './test-utils.ts';
|
|
35
|
-
export type { TestResponse } from './test-utils.ts';
|
|
34
|
+
export { testApp, TestApp, TestRequest, createTestDb, withTestDb } from './test-utils.ts';
|
|
35
|
+
export type { TestResponse, TestDb } from './test-utils.ts';
|
|
36
36
|
export { graphql } from './graphql.ts';
|
|
37
37
|
export type { GraphQLOptions, GraphQLHandler } from './graphql.ts';
|
|
38
38
|
export { aiStream } from './ai.ts';
|
|
@@ -77,3 +77,10 @@ export type { LogdbOptions, LogdbModule, LogEntry, LogEntryInput, } from './logd
|
|
|
77
77
|
export { iii, createWorker, registerWorker } from './iii/index.ts';
|
|
78
78
|
export type { IIIModule, IIIOptions, WorkerInfo, FunctionInfo, TriggerInfo, FunctionHandler, FunctionContext, TriggerInput, RemoteWorker, TriggerRequest, } from './iii/types.ts';
|
|
79
79
|
export { ssr } from './ssr.ts';
|
|
80
|
+
export { session, MemoryStore, RedisStore } from './session.ts';
|
|
81
|
+
export type { Session, SessionOptions, SessionStore, SessionData, SessionInjected } from './session.ts';
|
|
82
|
+
export { cache, MemoryCache, RedisCache } from './cache.ts';
|
|
83
|
+
export type { CacheOptions, CacheStore, CacheMiddleware, CachedResponse } from './cache.ts';
|
|
84
|
+
export { webhook } from './webhook.ts';
|
|
85
|
+
export type { WebhookOptions, WebhookModule, WebhookEvent, WebhookHandler, PlatformConfig, CustomVerifierConfig } from './webhook.ts';
|
|
86
|
+
export * as fts from './fts.ts';
|