weifuwu 0.24.1 → 0.24.2
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 +818 -712
- package/cli/template/app.ts +5 -1
- package/cli/template/index.ts +4 -1
- package/cli/template/locales/en.json +6 -1
- package/cli/template/locales/zh-CN.json +6 -1
- package/cli/template/locales/zh-TW.json +6 -1
- package/cli/template/locales/zh.json +6 -1
- package/cli/template/ui/app/globals.css +1 -1
- package/cli/template/ui/app/page.tsx +55 -16
- package/cli.ts +148 -104
- package/dist/agent/rest.d.ts +1 -1
- package/dist/agent/run.d.ts +2 -2
- package/dist/ai/workflow.d.ts +1 -1
- package/dist/ai-sdk.d.ts +1 -1
- package/dist/cli.js +135 -97
- package/dist/cookie.d.ts +24 -0
- package/dist/fts.d.ts +5 -5
- package/dist/iii/index.d.ts +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +787 -346
- package/dist/live.d.ts +2 -3
- package/dist/logdb/rest.d.ts +1 -1
- package/dist/mailer.d.ts +1 -1
- package/dist/messager/agent.d.ts +2 -2
- package/dist/messager/rest.d.ts +3 -3
- package/dist/messager/ws.d.ts +3 -3
- package/dist/opencode/index.d.ts +1 -1
- package/dist/opencode/permissions.d.ts +1 -1
- package/dist/opencode/run.d.ts +1 -1
- package/dist/opencode/session.d.ts +9 -9
- package/dist/opencode/tools/web.d.ts +1 -1
- package/dist/opencode/ws.d.ts +1 -2
- package/dist/permissions.d.ts +2 -2
- package/dist/postgres/module.d.ts +3 -3
- package/dist/postgres/schema/index.d.ts +1 -1
- package/dist/postgres/schema/table.d.ts +22 -20
- package/dist/postgres/types.d.ts +4 -4
- package/dist/queue/types.d.ts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +135 -90
- package/dist/router.d.ts +10 -10
- package/dist/session.d.ts +1 -2
- package/dist/tenant/graphql.d.ts +2 -2
- package/dist/tenant/index.d.ts +1 -1
- package/dist/tenant/rest.d.ts +2 -2
- package/dist/test-utils.d.ts +3 -3
- package/dist/user/index.d.ts +1 -1
- package/dist/user/oauth-login.d.ts +2 -2
- package/dist/vendor.d.ts +4 -0
- package/opencode/ui/app/globals.css +1 -1
- package/opencode/ui/app/layout.tsx +2 -3
- package/opencode/ui/app/page.tsx +302 -73
- package/package.json +26 -3
- package/cli/template/.weifuwu/ssr/2e3a7e60.js +0 -112
package/README.md
CHANGED
|
@@ -30,7 +30,20 @@ npx weifuwu init my-app && cd my-app && npm run dev
|
|
|
30
30
|
### Typical Full App
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
serve,
|
|
35
|
+
Router,
|
|
36
|
+
postgres,
|
|
37
|
+
session,
|
|
38
|
+
user,
|
|
39
|
+
aiProvider,
|
|
40
|
+
ssr,
|
|
41
|
+
flash,
|
|
42
|
+
i18n,
|
|
43
|
+
theme,
|
|
44
|
+
logger,
|
|
45
|
+
rateLimit,
|
|
46
|
+
} from 'weifuwu'
|
|
34
47
|
|
|
35
48
|
const app = new Router()
|
|
36
49
|
|
|
@@ -50,14 +63,14 @@ app.use(pg)
|
|
|
50
63
|
app.use(session({ store: 'redis', redis: myRedis }))
|
|
51
64
|
const auth = user({ pg, jwtSecret: process.env.JWT_SECRET })
|
|
52
65
|
await auth.migrate()
|
|
53
|
-
app.use(auth)
|
|
54
|
-
app.use('/auth', auth)
|
|
66
|
+
app.use(auth) // auto-registers middleware + /register, /login
|
|
67
|
+
app.use('/auth', auth) // explicit path mounts for more control
|
|
55
68
|
|
|
56
69
|
// 5. API protection
|
|
57
70
|
app.use('/api', rateLimit({ max: 60, window: 60_000 }))
|
|
58
71
|
|
|
59
72
|
// 6. AI
|
|
60
|
-
app.use(aiProvider())
|
|
73
|
+
app.use(aiProvider()) // ctx.ai
|
|
61
74
|
|
|
62
75
|
// 7. SSR
|
|
63
76
|
app.use('/', ssr({ dir: './ui' }))
|
|
@@ -94,21 +107,21 @@ const server = serve(handler, { port: 3000 })
|
|
|
94
107
|
await server.ready
|
|
95
108
|
```
|
|
96
109
|
|
|
97
|
-
| Option
|
|
98
|
-
|
|
99
|
-
| `port`
|
|
100
|
-
| `hostname`
|
|
101
|
-
| `signal`
|
|
102
|
-
| `websocket`
|
|
103
|
-
| `maxBodySize`
|
|
104
|
-
| `timeout`
|
|
105
|
-
| `keepAliveTimeout` | `number`
|
|
106
|
-
| `headersTimeout`
|
|
107
|
-
| `shutdown`
|
|
110
|
+
| Option | Type | Default | Description |
|
|
111
|
+
| ------------------ | ------------------ | ----------- | ------------------------------ |
|
|
112
|
+
| `port` | `number` | `0` | Listen port |
|
|
113
|
+
| `hostname` | `string` | `'0.0.0.0'` | Listen address |
|
|
114
|
+
| `signal` | `AbortSignal` | — | Shutdown on abort |
|
|
115
|
+
| `websocket` | `WsUpgradeHandler` | — | WebSocket upgrade handler |
|
|
116
|
+
| `maxBodySize` | `number` | `10MB` | Max body bytes (0 = unlimited) |
|
|
117
|
+
| `timeout` | `number` | `30_000` | Socket inactivity timeout (ms) |
|
|
118
|
+
| `keepAliveTimeout` | `number` | `5_000` | Keep-Alive idle timeout (ms) |
|
|
119
|
+
| `headersTimeout` | `number` | `6_000` | Headers read timeout (ms) |
|
|
120
|
+
| `shutdown` | `boolean` | `true` | Auto SIGTERM/SIGINT |
|
|
108
121
|
|
|
109
122
|
```ts
|
|
110
123
|
interface Server {
|
|
111
|
-
stop: (timeoutMs?: number) => Promise<void>
|
|
124
|
+
stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
|
|
112
125
|
readonly port: number
|
|
113
126
|
readonly hostname: string
|
|
114
127
|
ready: Promise<void>
|
|
@@ -126,16 +139,27 @@ are forcibly closed. SIGTERM/SIGINT use the same graceful pattern.
|
|
|
126
139
|
```ts
|
|
127
140
|
const app = new Router()
|
|
128
141
|
app.get('/hello/:name', (req, ctx) => Response.json({ message: `Hello, ${ctx.params.name}!` }))
|
|
129
|
-
app.post('/data', async (req, ctx) => {
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
app.post('/data', async (req, ctx) => {
|
|
143
|
+
const body = await req.json()
|
|
144
|
+
return Response.json(body, { status: 201 })
|
|
145
|
+
})
|
|
146
|
+
app.use('/admin', authMW) // path-scoped middleware
|
|
147
|
+
app.use('/admin', adminRouter) // sub-router (flattened into parent trie)
|
|
132
148
|
app.ws('/echo', {
|
|
133
|
-
open(ws, ctx) {
|
|
134
|
-
|
|
149
|
+
open(ws, ctx) {
|
|
150
|
+
ctx.ws.json({ type: 'connected' })
|
|
151
|
+
},
|
|
152
|
+
message(ws, ctx, data) {
|
|
153
|
+
ctx.ws.json({ echo: data.toString() })
|
|
154
|
+
},
|
|
135
155
|
})
|
|
136
156
|
app.ws('/chat', {
|
|
137
|
-
open(ws, ctx) {
|
|
138
|
-
|
|
157
|
+
open(ws, ctx) {
|
|
158
|
+
ctx.ws.join('room')
|
|
159
|
+
},
|
|
160
|
+
message(ws, ctx, data) {
|
|
161
|
+
ctx.ws.sendRoom('room', JSON.parse(data.toString()))
|
|
162
|
+
},
|
|
139
163
|
})
|
|
140
164
|
app.onError((err, req, ctx) => Response.json({ error: err.message }, { status: 500 }))
|
|
141
165
|
|
|
@@ -152,11 +176,11 @@ const wsHandler = app.websocketHandler()
|
|
|
152
176
|
serve(handler, { port: 3000, websocket: wsHandler })
|
|
153
177
|
```
|
|
154
178
|
|
|
155
|
-
| Pattern
|
|
156
|
-
|
|
157
|
-
| Static
|
|
158
|
-
| Param
|
|
159
|
-
| Wildcard | `/static/*`
|
|
179
|
+
| Pattern | Example | Match |
|
|
180
|
+
| -------- | ------------ | ----------------------------- |
|
|
181
|
+
| Static | `/about` | exact |
|
|
182
|
+
| Param | `/users/:id` | `/users/42` → `ctx.params.id` |
|
|
183
|
+
| Wildcard | `/static/*` | `/static/js/app.js` |
|
|
160
184
|
|
|
161
185
|
Query params → `ctx.query`.
|
|
162
186
|
|
|
@@ -170,7 +194,7 @@ Request → serve() → app.handler() → global middleware × N → path middle
|
|
|
170
194
|
|
|
171
195
|
1. `serve()` receives HTTP request
|
|
172
196
|
2. `app.handler()` creates `ctx = { params, query }` and routes to the matching trie node
|
|
173
|
-
3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
|
|
197
|
+
3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
|
|
174
198
|
4. **Path‑scoped middleware** runs for matching paths (e.g. `app.use('/admin', authMW)`)
|
|
175
199
|
5. **Route‑level middleware** runs (e.g. `app.get('/admin', validate(...), handler)`)
|
|
176
200
|
6. **Route handler** returns `Response` — middleware chain unwinds
|
|
@@ -181,40 +205,40 @@ Sub-routers (`app.use('/admin', adminRouter)`) are **flattened** into the parent
|
|
|
181
205
|
|
|
182
206
|
```ts
|
|
183
207
|
type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>
|
|
184
|
-
app.use(mw)
|
|
185
|
-
app.use('/admin', mw)
|
|
186
|
-
app.get('/admin', mw, handler)
|
|
208
|
+
app.use(mw) // global
|
|
209
|
+
app.use('/admin', mw) // path-scoped
|
|
210
|
+
app.get('/admin', mw, handler) // route-level
|
|
187
211
|
```
|
|
188
212
|
|
|
189
213
|
### Context
|
|
190
214
|
|
|
191
215
|
The `ctx` object accumulates properties as it passes through the middleware chain. Below are all documented properties:
|
|
192
216
|
|
|
193
|
-
| Property
|
|
194
|
-
|
|
195
|
-
| `params`
|
|
196
|
-
| `query`
|
|
197
|
-
| `mountPath`
|
|
198
|
-
| `env`
|
|
199
|
-
| `csrf.token`
|
|
200
|
-
| `requestId`
|
|
201
|
-
| `session`
|
|
202
|
-
| `sql`
|
|
203
|
-
| `redis`
|
|
204
|
-
| `ai`
|
|
205
|
-
| `queue`
|
|
206
|
-
| `user`
|
|
207
|
-
| `permissions` | `permissions()`
|
|
208
|
-
| `theme`
|
|
209
|
-
| `i18n`
|
|
210
|
-
| `flash`
|
|
211
|
-
| `tailwind`
|
|
212
|
-
| `tenant`
|
|
213
|
-
| `parsed`
|
|
214
|
-
| `layoutStack` | `ssr()` internal
|
|
215
|
-
| `loaderData`
|
|
216
|
-
| `mountPath`
|
|
217
|
-
| `deploy`
|
|
217
|
+
| Property | Set by | Type | Description |
|
|
218
|
+
| ------------- | -------------------------------- | ------------------------- | ------------------------------------ |
|
|
219
|
+
| `params` | Router | `Record<string, string>` | URL path parameters |
|
|
220
|
+
| `query` | Router | `Record<string, string>` | URL query parameters |
|
|
221
|
+
| `mountPath` | Router | `string` | Current sub-router mount prefix |
|
|
222
|
+
| `env` | `loadEnv()` | `Record<string, string>` | Public env vars (`WEIFUWU_PUBLIC_*`) |
|
|
223
|
+
| `csrf.token` | `csrf()` | `string` | CSRF token (namespace) |
|
|
224
|
+
| `requestId` | `requestId()` | `string` | Request ID |
|
|
225
|
+
| `session` | `session()` | `Session` | Session data object |
|
|
226
|
+
| `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
|
|
227
|
+
| `redis` | `redis()` | `Redis` | Redis client |
|
|
228
|
+
| `ai` | `aiProvider()` | `AIProvider` | AI model & embedding |
|
|
229
|
+
| `queue` | `queue()` | `Queue` | Job queue |
|
|
230
|
+
| `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
|
|
231
|
+
| `permissions` | `permissions()` | `{ roles, permissions }` | RBAC roles & permissions sets |
|
|
232
|
+
| `theme` | `theme()` | `{ value, set }` | Current theme + switcher |
|
|
233
|
+
| `i18n` | `i18n()` | `{ locale, t, set }` | Locale, translation, switcher |
|
|
234
|
+
| `flash` | `flash()` | `{ value, set }` | Flash message + setter |
|
|
235
|
+
| `tailwind` | `tailwindContext()` | `{ css, url }` | Compiled Tailwind CSS |
|
|
236
|
+
| `tenant` | `tenant()` | `TenantContext` | Current tenant info |
|
|
237
|
+
| `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
|
|
238
|
+
| `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
|
|
239
|
+
| `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
|
|
240
|
+
| `mountPath` | `Router` | `string` | Sub-router mount path |
|
|
241
|
+
| `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
|
|
218
242
|
|
|
219
243
|
### Type-Safe Context
|
|
220
244
|
|
|
@@ -222,13 +246,13 @@ Middleware-injected properties are **automatically typed** through chained `use(
|
|
|
222
246
|
|
|
223
247
|
```ts
|
|
224
248
|
const app = new Router()
|
|
225
|
-
.use(csrf())
|
|
226
|
-
.use(requestId())
|
|
227
|
-
.use(postgres())
|
|
249
|
+
.use(csrf()) // → Router<Context & { csrf: { token: string } }>
|
|
250
|
+
.use(requestId()) // → Router<Context & { csrf: ..., requestId }>
|
|
251
|
+
.use(postgres()) // → Router<Context & { csrf: ..., requestId, sql }>
|
|
228
252
|
|
|
229
253
|
app.get('/me', (_req, ctx) => {
|
|
230
|
-
ctx.csrf.token
|
|
231
|
-
ctx.requestId
|
|
254
|
+
ctx.csrf.token // ✅ string (IDE autocomplete)
|
|
255
|
+
ctx.requestId // ✅ string
|
|
232
256
|
ctx.sql`SELECT 1` // ✅ Sql<{}>
|
|
233
257
|
})
|
|
234
258
|
```
|
|
@@ -241,43 +265,44 @@ Each module exports an `XxxInjected` type (e.g. `PostgresInjected`, `UserInjecte
|
|
|
241
265
|
|
|
242
266
|
All modules follow one of **4 patterns** — learn these and you know every module.
|
|
243
267
|
|
|
244
|
-
| Pattern | How to mount
|
|
245
|
-
|
|
246
|
-
| `[α]`
|
|
247
|
-
| `[β]`
|
|
248
|
-
| `[γ]`
|
|
249
|
-
| `[δ]`
|
|
268
|
+
| Pattern | How to mount | Example |
|
|
269
|
+
| ------- | ---------------------------------------- | ------------------------------------------------------ |
|
|
270
|
+
| `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
|
|
271
|
+
| `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
|
|
272
|
+
| `[γ]` | Import and call directly | `mailer()`, `fts`, `cron-utils` |
|
|
273
|
+
| `[δ]` | `import { useXxx } from 'weifuwu/react'` | `useTheme()`, `useLocale()`, `useWebsocket()` |
|
|
250
274
|
|
|
251
275
|
### Pattern α — Middleware
|
|
252
276
|
|
|
253
277
|
```ts
|
|
254
|
-
app.use(compress())
|
|
255
|
-
const pg = postgres()
|
|
278
|
+
app.use(compress()) // basic
|
|
279
|
+
const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
|
|
256
280
|
app.use(pg)
|
|
257
|
-
app.use(rateLimit({ max: 100 }))
|
|
281
|
+
app.use(rateLimit({ max: 100 })) // with .close()
|
|
258
282
|
```
|
|
259
283
|
|
|
260
284
|
### Pattern β — Router
|
|
261
285
|
|
|
262
286
|
```ts
|
|
263
|
-
app.use('/health', health())
|
|
287
|
+
app.use('/health', health()) // with path
|
|
264
288
|
app.use('/graphql', graphql(handler))
|
|
265
|
-
app.use('/logs', logdb({ pg }))
|
|
266
|
-
app.use('/auth', user({ pg, jwtSecret }))
|
|
289
|
+
app.use('/logs', logdb({ pg })) // with .log(), .migrate()
|
|
290
|
+
app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
|
|
267
291
|
app.ws('/ws', messager({ pg }).wsHandler())
|
|
268
292
|
```
|
|
269
293
|
|
|
270
294
|
β modules that need **separate middleware** use `.middleware()`. Most can auto-register both middleware and routes in one call:
|
|
295
|
+
|
|
271
296
|
```ts
|
|
272
|
-
app.use(theme())
|
|
273
|
-
app.use(i18n({ dir: './locales' }))
|
|
297
|
+
app.use(theme()) // auto: middleware + /__theme/:value
|
|
298
|
+
app.use(i18n({ dir: './locales' })) // auto: middleware + /__lang/:locale
|
|
274
299
|
app.use(analytics({ pg })) // auto: middleware + /__analytics
|
|
275
|
-
app.use(auth)
|
|
300
|
+
app.use(auth) // auto: middleware + /register, /login (user())
|
|
276
301
|
|
|
277
302
|
// Explicit form when more control is needed:
|
|
278
303
|
const a = analytics()
|
|
279
|
-
app.use(a.middleware())
|
|
280
|
-
app.use('/', a)
|
|
304
|
+
app.use(a.middleware()) // tracking only
|
|
305
|
+
app.use('/', a) // dashboard at custom path
|
|
281
306
|
```
|
|
282
307
|
|
|
283
308
|
### Pattern γ — Standalone
|
|
@@ -290,7 +315,7 @@ import { mailer, cronNext, fts } from 'weifuwu'
|
|
|
290
315
|
const email = mailer({ transport: 'smtp://...', from: 'noreply@example.com' })
|
|
291
316
|
await email.send({ to: 'user@test.com', subject: 'Hello', text: 'Body' })
|
|
292
317
|
|
|
293
|
-
const next = cronNext('0 9 * * 1-5')
|
|
318
|
+
const next = cronNext('0 9 * * 1-5') // next weekday at 09:00
|
|
294
319
|
```
|
|
295
320
|
|
|
296
321
|
### Pattern δ — Client-side
|
|
@@ -346,54 +371,54 @@ graph TD
|
|
|
346
371
|
|
|
347
372
|
## Quick Module Selection
|
|
348
373
|
|
|
349
|
-
| What do you want to do?
|
|
350
|
-
|
|
351
|
-
| **User registration / login**
|
|
352
|
-
| **Simple token/header auth**
|
|
353
|
-
| **JWT verification**
|
|
354
|
-
| **Role-based access control**
|
|
355
|
-
| **AI chat / generate / stream**
|
|
356
|
-
| **AI agent with knowledge**
|
|
357
|
-
| **Send email**
|
|
358
|
-
| **File upload**
|
|
359
|
-
| **Object storage (S3/MinIO)**
|
|
360
|
-
| **Rate limiting**
|
|
361
|
-
| **Response caching**
|
|
362
|
-
| **Periodic / delayed jobs**
|
|
363
|
-
| **Page view analytics**
|
|
364
|
-
| **Structured logging**
|
|
365
|
-
| **Real-time chat / messager**
|
|
366
|
-
| **Full-text search**
|
|
367
|
-
| **Theme switching**
|
|
368
|
-
| **i18n / localization**
|
|
369
|
-
| **Flash messages**
|
|
370
|
-
| **Server-Sent Events**
|
|
371
|
-
| **GraphQL endpoint**
|
|
372
|
-
| **Webhook receiver**
|
|
373
|
-
| **SSR with React**
|
|
374
|
-
| **Health check**
|
|
375
|
-
| **SEO (robots.txt, sitemap)**
|
|
376
|
-
| **Multi-process deploy**
|
|
377
|
-
| **Distributed functions (iii)**
|
|
378
|
-
| **Multi-tenant BaaS**
|
|
379
|
-
| **Client-side routing**
|
|
380
|
-
| **WebSocket in React**
|
|
381
|
-
| **Compression (brotli/gzip)**
|
|
382
|
-
| **Security headers (CSP, HSTS)** | `helmet()`
|
|
383
|
-
| **CORS**
|
|
384
|
-
| **CSRF protection**
|
|
385
|
-
| **Request ID tracing**
|
|
386
|
-
| **Environment variables**
|
|
387
|
-
| **Static file serving**
|
|
388
|
-
| **Object storage (S3/MinIO)**
|
|
389
|
-
| **Send email**
|
|
390
|
-
| **Scheduled / cron tasks**
|
|
391
|
-
| **Server-Sent Events**
|
|
392
|
-
| **Multi-process deploy**
|
|
393
|
-
| **Distributed functions (iii)**
|
|
394
|
-
| **Webhook receiver**
|
|
395
|
-
| **Social login (OAuth)**
|
|
396
|
-
| **Database migrations**
|
|
374
|
+
| What do you want to do? | Module | Pattern |
|
|
375
|
+
| -------------------------------- | ----------------------------------------------- | ---------------------- |
|
|
376
|
+
| **User registration / login** | `user()` | β |
|
|
377
|
+
| **Simple token/header auth** | `auth()` | α |
|
|
378
|
+
| **JWT verification** | `user().middleware()` | α |
|
|
379
|
+
| **Role-based access control** | `permissions()` | α |
|
|
380
|
+
| **AI chat / generate / stream** | `ctx.ai.generateText()` / `ctx.ai.streamText()` | α (via `aiProvider()`) |
|
|
381
|
+
| **AI agent with knowledge** | `agent()` + `knowledgeBase()` | β |
|
|
382
|
+
| **Send email** | `mailer()` | γ |
|
|
383
|
+
| **File upload** | `upload()` | α |
|
|
384
|
+
| **Object storage (S3/MinIO)** | `s3()` | α |
|
|
385
|
+
| **Rate limiting** | `rateLimit()` | α |
|
|
386
|
+
| **Response caching** | `cache()` | α |
|
|
387
|
+
| **Periodic / delayed jobs** | `queue()` | α |
|
|
388
|
+
| **Page view analytics** | `analytics()` | β |
|
|
389
|
+
| **Structured logging** | `logdb()` | β |
|
|
390
|
+
| **Real-time chat / messager** | `messager()` | β |
|
|
391
|
+
| **Full-text search** | `fts` | γ |
|
|
392
|
+
| **Theme switching** | `theme()` | α |
|
|
393
|
+
| **i18n / localization** | `i18n()` | α |
|
|
394
|
+
| **Flash messages** | `flash()` | α |
|
|
395
|
+
| **Server-Sent Events** | `createSSEStream()` | γ |
|
|
396
|
+
| **GraphQL endpoint** | `graphql()` | β |
|
|
397
|
+
| **Webhook receiver** | `webhook()` | β |
|
|
398
|
+
| **SSR with React** | `ssr()` | β |
|
|
399
|
+
| **Health check** | `health()` | β |
|
|
400
|
+
| **SEO (robots.txt, sitemap)** | `seo()` | β |
|
|
401
|
+
| **Multi-process deploy** | `deploy()` | γ |
|
|
402
|
+
| **Distributed functions (iii)** | `iii()` | β |
|
|
403
|
+
| **Multi-tenant BaaS** | `tenant()` | β |
|
|
404
|
+
| **Client-side routing** | `useNavigate()`, `<Link>` | δ |
|
|
405
|
+
| **WebSocket in React** | `useWebsocket()` | δ |
|
|
406
|
+
| **Compression (brotli/gzip)** | `compress()` | α |
|
|
407
|
+
| **Security headers (CSP, HSTS)** | `helmet()` | α |
|
|
408
|
+
| **CORS** | `cors()` | α |
|
|
409
|
+
| **CSRF protection** | `csrf()` | α |
|
|
410
|
+
| **Request ID tracing** | `requestId()` | α |
|
|
411
|
+
| **Environment variables** | `env()` / `loadEnv()` | α |
|
|
412
|
+
| **Static file serving** | `serveStatic()` | α |
|
|
413
|
+
| **Object storage (S3/MinIO)** | `s3()` | α |
|
|
414
|
+
| **Send email** | `mailer()` | γ |
|
|
415
|
+
| **Scheduled / cron tasks** | `cron-utils` (`cronNext()`) | γ |
|
|
416
|
+
| **Server-Sent Events** | `createSSEStream()` | γ |
|
|
417
|
+
| **Multi-process deploy** | `deploy()` | γ |
|
|
418
|
+
| **Distributed functions (iii)** | `iii()` | β |
|
|
419
|
+
| **Webhook receiver** | `webhook()` | β |
|
|
420
|
+
| **Social login (OAuth)** | `user({ oauthLogin })` | β |
|
|
421
|
+
| **Database migrations** | `pg.migrate()` | — |
|
|
397
422
|
|
|
398
423
|
---
|
|
399
424
|
|
|
@@ -412,7 +437,16 @@ app.get('/api', (req, ctx) => {
|
|
|
412
437
|
**Structured logging** — `logger({ format: 'json' })` outputs JSON to stderr with `traceId`, `timestamp`, `elapsed_ms`:
|
|
413
438
|
|
|
414
439
|
```json
|
|
415
|
-
{
|
|
440
|
+
{
|
|
441
|
+
"level": "info",
|
|
442
|
+
"message": "request",
|
|
443
|
+
"method": "GET",
|
|
444
|
+
"path": "/api/users",
|
|
445
|
+
"status": 200,
|
|
446
|
+
"elapsed_ms": 42,
|
|
447
|
+
"traceId": "f240a3f3-...",
|
|
448
|
+
"timestamp": "2025-01-15T10:30:00.000Z"
|
|
449
|
+
}
|
|
416
450
|
```
|
|
417
451
|
|
|
418
452
|
Default format is `'short'` (human-readable). `'combined'` includes query strings.
|
|
@@ -480,12 +514,12 @@ assert.equal(res.status, 200)
|
|
|
480
514
|
assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
|
|
481
515
|
```
|
|
482
516
|
|
|
483
|
-
| Method
|
|
484
|
-
|
|
485
|
-
| `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request
|
|
486
|
-
| `.withUser(u)` `.withTenant(t)` `.with(ctx)`
|
|
487
|
-
| `.header(k,v)` `.body(data)` `.rawBody(str)`
|
|
488
|
-
| `.send()` → `TestResponse`
|
|
517
|
+
| Method | Description |
|
|
518
|
+
| ------------------------------------------------------------ | ----------------------------------------------------- |
|
|
519
|
+
| `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request |
|
|
520
|
+
| `.withUser(u)` `.withTenant(t)` `.with(ctx)` | Simulate middleware injection |
|
|
521
|
+
| `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
|
|
522
|
+
| `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
|
|
489
523
|
|
|
490
524
|
### Database test isolation
|
|
491
525
|
|
|
@@ -496,7 +530,7 @@ import { createTestDb, withTestDb } from 'weifuwu'
|
|
|
496
530
|
const db = await createTestDb()
|
|
497
531
|
await db.sql`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
|
|
498
532
|
await db.sql`INSERT INTO users (name) VALUES ('Alice')`
|
|
499
|
-
await db.destroy()
|
|
533
|
+
await db.destroy() // DROP SCHEMA ... CASCADE
|
|
500
534
|
|
|
501
535
|
// Transaction rollback — all changes are rolled back after callback
|
|
502
536
|
await withTestDb(async (sql) => {
|
|
@@ -505,10 +539,10 @@ await withTestDb(async (sql) => {
|
|
|
505
539
|
})
|
|
506
540
|
```
|
|
507
541
|
|
|
508
|
-
| Function
|
|
509
|
-
|
|
510
|
-
| `createTestDb(opts?)`
|
|
511
|
-
| `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback
|
|
542
|
+
| Function | Description |
|
|
543
|
+
| ---------------------- | --------------------------------------------------------------- |
|
|
544
|
+
| `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
|
|
545
|
+
| `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback |
|
|
512
546
|
|
|
513
547
|
Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset.
|
|
514
548
|
|
|
@@ -533,21 +567,21 @@ await a.addKnowledge(agentId, 'Title', 'some knowledge content')
|
|
|
533
567
|
a.run(agentId, { input: 'summarize the data', stream: true })
|
|
534
568
|
```
|
|
535
569
|
|
|
536
|
-
| Option
|
|
537
|
-
|
|
538
|
-
| `pg`
|
|
539
|
-
| `provider`
|
|
540
|
-
| `model`
|
|
541
|
-
| `embeddingModel`
|
|
542
|
-
| `embeddingDimension` | `number`
|
|
543
|
-
| `tools`
|
|
570
|
+
| Option | Type | Default | Description |
|
|
571
|
+
| -------------------- | ------------ | ------------------------- | --------------------------------------------- |
|
|
572
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
573
|
+
| `provider` | `AIProvider` | `aiProvider()` (from env) | AI provider for model & embedding resolution |
|
|
574
|
+
| `model` | `object` | — | Explicit AI model (overrides provider) |
|
|
575
|
+
| `embeddingModel` | `object` | — | Explicit embedding model (overrides provider) |
|
|
576
|
+
| `embeddingDimension` | `number` | `provider.dimension` | Embedding vector dimension |
|
|
577
|
+
| `tools` | `object[]` | — | Custom tool definitions |
|
|
544
578
|
|
|
545
|
-
| Method
|
|
546
|
-
|
|
579
|
+
| Method | Description |
|
|
580
|
+
| ---------------------------------------------- | ------------------------ |
|
|
547
581
|
| `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
|
|
548
|
-
| `.addKnowledge(agentId, title, content)`
|
|
549
|
-
| `.migrate()`
|
|
550
|
-
| `.close()`
|
|
582
|
+
| `.addKnowledge(agentId, title, content)` | Add knowledge document |
|
|
583
|
+
| `.migrate()` | DB setup |
|
|
584
|
+
| `.close()` | Cleanup |
|
|
551
585
|
|
|
552
586
|
### aiStream [β] [AI]
|
|
553
587
|
|
|
@@ -559,10 +593,10 @@ const chat = await aiStream(async (req) => ({ messages: (await req.json()).messa
|
|
|
559
593
|
app.use('/chat', chat)
|
|
560
594
|
```
|
|
561
595
|
|
|
562
|
-
| Param
|
|
563
|
-
|
|
564
|
-
| `handler`
|
|
565
|
-
| `provider` | `AIProvider`
|
|
596
|
+
| Param | Type | Description |
|
|
597
|
+
| ---------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
598
|
+
| `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
|
|
599
|
+
| `provider` | `AIProvider` | Optional. If provided and handler omits `model`, `provider.model()` is used as default |
|
|
566
600
|
|
|
567
601
|
### analytics [β] [API]
|
|
568
602
|
|
|
@@ -571,49 +605,52 @@ In-memory or PostgreSQL page view tracking with built-in dashboard.
|
|
|
571
605
|
```ts
|
|
572
606
|
const a = analytics()
|
|
573
607
|
app.use(a.middleware())
|
|
574
|
-
app.use('/', a)
|
|
608
|
+
app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
|
|
575
609
|
```
|
|
576
610
|
|
|
577
|
-
| Option
|
|
578
|
-
|
|
579
|
-
| `pg`
|
|
580
|
-
| `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip
|
|
611
|
+
| Option | Type | Default | Description |
|
|
612
|
+
| ---------- | ---------- | --------------------------------------- | --------------------------------- |
|
|
613
|
+
| `pg` | `object` | — | PostgreSQL client for persistence |
|
|
614
|
+
| `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip |
|
|
581
615
|
|
|
582
616
|
```ts
|
|
583
617
|
// With PostgreSQL
|
|
584
618
|
const a = analytics({ pg })
|
|
585
619
|
await a.migrate()
|
|
586
620
|
app.use(a.middleware())
|
|
587
|
-
app.use('/', a)
|
|
621
|
+
app.use('/', a) // dashboard routes
|
|
588
622
|
```
|
|
589
623
|
|
|
590
624
|
### auth [α] [Security]
|
|
591
625
|
|
|
592
626
|
```ts
|
|
593
|
-
app.use(auth({ token: 'sk-123' }))
|
|
594
|
-
app.use(auth({ header: 'X-API-Key', token: 'my-key' }))
|
|
627
|
+
app.use(auth({ token: 'sk-123' })) // static token
|
|
628
|
+
app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
|
|
595
629
|
app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
|
|
596
630
|
app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
|
|
597
631
|
|
|
598
632
|
// Session-based auth (must be placed after session() middleware)
|
|
599
633
|
app.use(session())
|
|
600
|
-
app.use(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
634
|
+
app.use(
|
|
635
|
+
auth({
|
|
636
|
+
session: true,
|
|
637
|
+
resolveUser: async (userId) => {
|
|
638
|
+
// load user from DB
|
|
639
|
+
const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
|
|
640
|
+
return user ?? null // null → destroy stale session
|
|
641
|
+
},
|
|
642
|
+
}),
|
|
643
|
+
)
|
|
607
644
|
```
|
|
608
645
|
|
|
609
|
-
| Option
|
|
610
|
-
|
|
611
|
-
| `token`
|
|
612
|
-
| `header`
|
|
613
|
-
| `verify`
|
|
614
|
-
| `proxy`
|
|
615
|
-
| `session`
|
|
616
|
-
| `resolveUser` | `(userId) => object\|null`
|
|
646
|
+
| Option | Type | Default | Description |
|
|
647
|
+
| ------------- | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------- |
|
|
648
|
+
| `token` | `string` | — | Static token to match |
|
|
649
|
+
| `header` | `string` | `'Authorization'` | Header name |
|
|
650
|
+
| `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
|
|
651
|
+
| `proxy` | `string` | — | Auth service URL to proxy requests to |
|
|
652
|
+
| `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
|
|
653
|
+
| `resolveUser` | `(userId) => object\|null` | — | Load user from userId (called when `session: true`). Return falsy to reject + auto-destroy stale session |
|
|
617
654
|
|
|
618
655
|
When `session: true`, auth checks `ctx.session.userId` before the
|
|
619
656
|
Authorization header. This lets logged-in users authenticate via their
|
|
@@ -623,32 +660,32 @@ if no session userId is present.
|
|
|
623
660
|
### compress [α] [DevTools]
|
|
624
661
|
|
|
625
662
|
```ts
|
|
626
|
-
app.use(compress())
|
|
627
|
-
app.use(compress({ threshold: 2048, level: 4 }))
|
|
663
|
+
app.use(compress()) // brotli > gzip > deflate (min 1KB)
|
|
664
|
+
app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and level
|
|
628
665
|
```
|
|
629
666
|
|
|
630
|
-
| Option
|
|
631
|
-
|
|
632
|
-
| `threshold` | `number` | `1024`
|
|
633
|
-
| `level`
|
|
667
|
+
| Option | Type | Default | Description |
|
|
668
|
+
| ----------- | -------- | ------- | ----------------------------- |
|
|
669
|
+
| `threshold` | `number` | `1024` | Minimum byte size to compress |
|
|
670
|
+
| `level` | `number` | `6` | Compression level (zlib) |
|
|
634
671
|
|
|
635
672
|
### cors [α] [DevTools]
|
|
636
673
|
|
|
637
674
|
```ts
|
|
638
|
-
app.use(cors())
|
|
639
|
-
app.use(cors({ origin: ['https://example.com'] }))
|
|
675
|
+
app.use(cors()) // allow all
|
|
676
|
+
app.use(cors({ origin: ['https://example.com'] })) // whitelist
|
|
640
677
|
app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
|
|
641
678
|
app.use(cors({ credentials: true, maxAge: 3600 }))
|
|
642
679
|
```
|
|
643
680
|
|
|
644
|
-
| Option
|
|
645
|
-
|
|
646
|
-
| `origin`
|
|
647
|
-
| `methods`
|
|
648
|
-
| `allowedHeaders` | `string[]`
|
|
649
|
-
| `exposedHeaders` | `string[]`
|
|
650
|
-
| `credentials`
|
|
651
|
-
| `maxAge`
|
|
681
|
+
| Option | Type | Default | Description |
|
|
682
|
+
| ---------------- | ---------------------------- | -------------------------------------------------------- | ---------------------------------- |
|
|
683
|
+
| `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
|
|
684
|
+
| `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
|
|
685
|
+
| `allowedHeaders` | `string[]` | — | Custom allowed headers |
|
|
686
|
+
| `exposedHeaders` | `string[]` | — | Response headers exposed to client |
|
|
687
|
+
| `credentials` | `boolean` | `false` | Allow cookies/credentials |
|
|
688
|
+
| `maxAge` | `number` | — | Preflight cache duration (seconds) |
|
|
652
689
|
|
|
653
690
|
### flash [α] [UX]
|
|
654
691
|
|
|
@@ -658,7 +695,7 @@ Cookie-based flash message. Read from request, write via redirect.
|
|
|
658
695
|
app.use(flash())
|
|
659
696
|
|
|
660
697
|
app.get('/', (req, ctx) => {
|
|
661
|
-
const msg = ctx.flash.value
|
|
698
|
+
const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
|
|
662
699
|
})
|
|
663
700
|
|
|
664
701
|
app.post('/save', (req, ctx) => {
|
|
@@ -666,8 +703,8 @@ app.post('/save', (req, ctx) => {
|
|
|
666
703
|
})
|
|
667
704
|
```
|
|
668
705
|
|
|
669
|
-
| Option | Type
|
|
670
|
-
|
|
706
|
+
| Option | Type | Default | Description |
|
|
707
|
+
| ------ | -------- | --------- | ----------- |
|
|
671
708
|
| `name` | `string` | `'flash'` | Cookie name |
|
|
672
709
|
|
|
673
710
|
### cache [α] [DevTools]
|
|
@@ -675,30 +712,32 @@ app.post('/save', (req, ctx) => {
|
|
|
675
712
|
Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
|
|
676
713
|
|
|
677
714
|
```ts
|
|
678
|
-
app.use(cache())
|
|
679
|
-
app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis }))
|
|
680
|
-
app.use(
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
})
|
|
715
|
+
app.use(cache()) // in-memory, 5min TTL
|
|
716
|
+
app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
|
|
717
|
+
app.use(
|
|
718
|
+
cache({
|
|
719
|
+
ttl: 30_000,
|
|
720
|
+
tag: (req, ctx) => (ctx.user ? `user:${ctx.user.id}` : undefined), // per-user invalidation
|
|
721
|
+
}),
|
|
722
|
+
)
|
|
684
723
|
|
|
685
724
|
// Programmatic invalidation
|
|
686
725
|
const c = cache({ store: 'redis', redis: ctx.redis })
|
|
687
726
|
app.use(c)
|
|
688
|
-
await c.invalidate('users')
|
|
689
|
-
await c.flush()
|
|
727
|
+
await c.invalidate('users') // invalidate all entries tagged with 'users'
|
|
728
|
+
await c.flush() // clear entire cache
|
|
690
729
|
```
|
|
691
730
|
|
|
692
|
-
| Option
|
|
693
|
-
|
|
694
|
-
| `ttl`
|
|
695
|
-
| `store`
|
|
696
|
-
| `redis`
|
|
697
|
-
| `key`
|
|
698
|
-
| `tag`
|
|
699
|
-
| `cacheCookies` | `boolean`
|
|
700
|
-
| `cacheStatus`
|
|
701
|
-
| `maxBodySize`
|
|
731
|
+
| Option | Type | Default | Description |
|
|
732
|
+
| -------------- | ----------------------------------- | ------------------ | --------------------------------------------- |
|
|
733
|
+
| `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
|
|
734
|
+
| `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
|
|
735
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
736
|
+
| `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
|
|
737
|
+
| `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
|
|
738
|
+
| `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
|
|
739
|
+
| `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
|
|
740
|
+
| `maxBodySize` | `number` | `1048576` (1MB) | Max body bytes to cache |
|
|
702
741
|
|
|
703
742
|
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.
|
|
704
743
|
|
|
@@ -706,7 +745,11 @@ Cached responses include `X-Cache: HIT` and `Age` headers. Requests with `Author
|
|
|
706
745
|
import { MemoryCache, RedisCache } from 'weifuwu'
|
|
707
746
|
|
|
708
747
|
const mem = new MemoryCache()
|
|
709
|
-
await mem.set(
|
|
748
|
+
await mem.set(
|
|
749
|
+
'key',
|
|
750
|
+
{ status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] },
|
|
751
|
+
300_000,
|
|
752
|
+
)
|
|
710
753
|
mem.close()
|
|
711
754
|
```
|
|
712
755
|
|
|
@@ -719,12 +762,12 @@ app.use(csrf())
|
|
|
719
762
|
// Falls back to body field matching the key name
|
|
720
763
|
```
|
|
721
764
|
|
|
722
|
-
| Option
|
|
723
|
-
|
|
724
|
-
| `cookie`
|
|
725
|
-
| `header`
|
|
726
|
-
| `key`
|
|
727
|
-
| `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation
|
|
765
|
+
| Option | Default | Description |
|
|
766
|
+
| ---------------- | -------------------------- | ----------------------------------------- |
|
|
767
|
+
| `cookie` | `'_csrf'` | Cookie name |
|
|
768
|
+
| `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
|
|
769
|
+
| `key` | `'_csrf'` | Body field fallback |
|
|
770
|
+
| `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
|
|
728
771
|
|
|
729
772
|
### deploy [β] [Networking]
|
|
730
773
|
|
|
@@ -734,26 +777,30 @@ Multi-process manager with reverse proxy, health checks, auto-restart, and zero-
|
|
|
734
777
|
import { deploy, defineConfig } from 'weifuwu'
|
|
735
778
|
|
|
736
779
|
// Local
|
|
737
|
-
await deploy(
|
|
738
|
-
|
|
739
|
-
}
|
|
780
|
+
await deploy(
|
|
781
|
+
defineConfig({
|
|
782
|
+
apps: { blog: {}, api: {} },
|
|
783
|
+
}),
|
|
784
|
+
)
|
|
740
785
|
|
|
741
786
|
// Production
|
|
742
|
-
await deploy(
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
787
|
+
await deploy(
|
|
788
|
+
defineConfig({
|
|
789
|
+
domain: 'example.com',
|
|
790
|
+
deployToken: process.env.DEPLOY_TOKEN,
|
|
791
|
+
apps: { blog: {}, api: {} },
|
|
792
|
+
}),
|
|
793
|
+
)
|
|
747
794
|
```
|
|
748
795
|
|
|
749
796
|
**Auto-derived defaults** — each app key derives `dir`, `port`, `entry`, and `path`:
|
|
750
797
|
|
|
751
|
-
| Field
|
|
752
|
-
|
|
753
|
-
| `dir`
|
|
754
|
-
| `entry` | `'index.ts'` | Default entry file
|
|
755
|
-
| `port`
|
|
756
|
-
| `path`
|
|
798
|
+
| Field | Default | Rule |
|
|
799
|
+
| ------- | ------------ | -------------------------- |
|
|
800
|
+
| `dir` | App key | `blog` → `'./blog'` |
|
|
801
|
+
| `entry` | `'index.ts'` | Default entry file |
|
|
802
|
+
| `port` | `3001+` | Auto-incremented from 3001 |
|
|
803
|
+
| `path` | `'/key'` | Only for localhost domain |
|
|
757
804
|
|
|
758
805
|
Override any field explicitly:
|
|
759
806
|
|
|
@@ -777,7 +824,11 @@ apps: {
|
|
|
777
824
|
**Blue-green** — zero-downtime via `ports`:
|
|
778
825
|
|
|
779
826
|
```ts
|
|
780
|
-
apps: {
|
|
827
|
+
apps: {
|
|
828
|
+
blog: {
|
|
829
|
+
ports: [3001, 3002]
|
|
830
|
+
}
|
|
831
|
+
}
|
|
781
832
|
```
|
|
782
833
|
|
|
783
834
|
**WebSocket** — automatically bridged through the gateway.
|
|
@@ -786,15 +837,15 @@ apps: { blog: { ports: [3001, 3002] } }
|
|
|
786
837
|
|
|
787
838
|
**Management API** — all endpoints require `Authorization: Bearer <deployToken>`:
|
|
788
839
|
|
|
789
|
-
| Endpoint
|
|
790
|
-
|
|
791
|
-
| `/_deploy/apps`
|
|
792
|
-
| `/_deploy/apps/:name`
|
|
793
|
-
| `/_deploy/apps/:name/deploy`
|
|
794
|
-
| `/_deploy/apps/:name/restart` | POST
|
|
795
|
-
| `/_deploy/apps/:name/stop`
|
|
796
|
-
| `/_deploy/apps/:name/start`
|
|
797
|
-
| `/_deploy/apps/:name/logs`
|
|
840
|
+
| Endpoint | Method | Description |
|
|
841
|
+
| ----------------------------- | ------ | -------------- |
|
|
842
|
+
| `/_deploy/apps` | GET | List apps |
|
|
843
|
+
| `/_deploy/apps/:name` | GET | App details |
|
|
844
|
+
| `/_deploy/apps/:name/deploy` | POST | Restart |
|
|
845
|
+
| `/_deploy/apps/:name/restart` | POST | Restart |
|
|
846
|
+
| `/_deploy/apps/:name/stop` | POST | Stop |
|
|
847
|
+
| `/_deploy/apps/:name/start` | POST | Start |
|
|
848
|
+
| `/_deploy/apps/:name/logs` | GET | SSE log stream |
|
|
798
849
|
|
|
799
850
|
```bash
|
|
800
851
|
curl -H "Authorization: Bearer my-token" http://localhost:3000/_deploy/apps
|
|
@@ -811,26 +862,26 @@ Restart=always
|
|
|
811
862
|
|
|
812
863
|
**DeployConfig:**
|
|
813
864
|
|
|
814
|
-
| Option
|
|
815
|
-
|
|
816
|
-
| `domain`
|
|
817
|
-
| `port`
|
|
818
|
-
| `deployToken` | —
|
|
819
|
-
| `defaultApp`
|
|
820
|
-
| `apps`
|
|
865
|
+
| Option | Default | Description |
|
|
866
|
+
| ------------- | ------------- | ------------------------------- |
|
|
867
|
+
| `domain` | `'localhost'` | Root domain |
|
|
868
|
+
| `port` | `3000` | Gateway port |
|
|
869
|
+
| `deployToken` | — | Bearer token for management API |
|
|
870
|
+
| `defaultApp` | — | Fallback route |
|
|
871
|
+
| `apps` | — | `Record<string, AppConfig>` |
|
|
821
872
|
|
|
822
873
|
**AppConfig:**
|
|
823
874
|
|
|
824
|
-
| Field
|
|
825
|
-
|
|
826
|
-
| `dir`
|
|
827
|
-
| `port`
|
|
828
|
-
| `entry`
|
|
829
|
-
| `path`
|
|
830
|
-
| `env`
|
|
831
|
-
| `healthEndpoint` | `/`
|
|
832
|
-
| `buildCommand`
|
|
833
|
-
| `ports`
|
|
875
|
+
| Field | Default | Description |
|
|
876
|
+
| ---------------- | ---------------- | ------------------------------- |
|
|
877
|
+
| `dir` | App key | Directory containing the app |
|
|
878
|
+
| `port` | Auto (3001+) | Internal port |
|
|
879
|
+
| `entry` | `'index.ts'` | Entry file |
|
|
880
|
+
| `path` | `'/key'` (local) | URL path prefix |
|
|
881
|
+
| `env` | — | Environment variables |
|
|
882
|
+
| `healthEndpoint` | `/` | Health check path |
|
|
883
|
+
| `buildCommand` | — | Build command |
|
|
884
|
+
| `ports` | — | `[port, port+1]` for blue-green |
|
|
834
885
|
|
|
835
886
|
### env [α] [DevTools]
|
|
836
887
|
|
|
@@ -839,8 +890,8 @@ Safe to expose to the client.
|
|
|
839
890
|
|
|
840
891
|
```ts
|
|
841
892
|
import { env, loadEnv } from 'weifuwu'
|
|
842
|
-
loadEnv()
|
|
843
|
-
app.use(env())
|
|
893
|
+
loadEnv() // Load .env into process.env
|
|
894
|
+
app.use(env()) // → ctx.env
|
|
844
895
|
|
|
845
896
|
app.get('/config', (req, ctx) => {
|
|
846
897
|
return Response.json({ apiUrl: ctx.env.API_URL })
|
|
@@ -852,20 +903,20 @@ Helper utilities:
|
|
|
852
903
|
```ts
|
|
853
904
|
import { isDev, isProd, isBundled, getPublicEnv } from 'weifuwu'
|
|
854
905
|
|
|
855
|
-
isDev()
|
|
856
|
-
isProd()
|
|
857
|
-
isBundled()
|
|
858
|
-
getPublicEnv()
|
|
906
|
+
isDev() // NODE_ENV === 'development'
|
|
907
|
+
isProd() // NODE_ENV === 'production'
|
|
908
|
+
isBundled() // Running from compiled dist/index.js?
|
|
909
|
+
getPublicEnv() // { API_URL: '...' } — no middleware needed
|
|
859
910
|
```
|
|
860
911
|
|
|
861
|
-
| Function
|
|
862
|
-
|
|
912
|
+
| Function | Description |
|
|
913
|
+
| ---------------- | ---------------------------------------------------------------- |
|
|
863
914
|
| `loadEnv(path?)` | Load `.env` file into `process.env` (does not override existing) |
|
|
864
|
-
| `env()`
|
|
865
|
-
| `getPublicEnv()` | Returns `WEIFUWU_PUBLIC_*` vars with prefix stripped
|
|
866
|
-
| `isDev()`
|
|
867
|
-
| `isProd()`
|
|
868
|
-
| `isBundled()`
|
|
915
|
+
| `env()` | Middleware — injects `ctx.env` with public vars |
|
|
916
|
+
| `getPublicEnv()` | Returns `WEIFUWU_PUBLIC_*` vars with prefix stripped |
|
|
917
|
+
| `isDev()` | `true` when `NODE_ENV === 'development'` |
|
|
918
|
+
| `isProd()` | `true` when `NODE_ENV === 'production'` |
|
|
919
|
+
| `isBundled()` | `true` when running from compiled bundle |
|
|
869
920
|
|
|
870
921
|
### graphql [β] [API]
|
|
871
922
|
|
|
@@ -873,22 +924,22 @@ getPublicEnv() // { API_URL: '...' } — no middleware needed
|
|
|
873
924
|
const handler: GraphQLHandler = () => ({
|
|
874
925
|
schema: `type Query { hello: String }`,
|
|
875
926
|
resolvers: { Query: { hello: () => 'world' } },
|
|
876
|
-
graphiql: true,
|
|
877
|
-
maxDepth: 10,
|
|
878
|
-
timeout: 30_000,
|
|
927
|
+
graphiql: true, // GET / returns GraphiQL IDE
|
|
928
|
+
maxDepth: 10, // max query nesting (default 10, 0 = disable)
|
|
929
|
+
timeout: 30_000, // execution timeout in ms
|
|
879
930
|
})
|
|
880
931
|
app.use('/graphql', graphql(handler))
|
|
881
932
|
```
|
|
882
933
|
|
|
883
|
-
| Option
|
|
884
|
-
|
|
885
|
-
| `schema`
|
|
886
|
-
| `resolvers` | `object`
|
|
887
|
-
| `rootValue` | `any`
|
|
888
|
-
| `context`
|
|
889
|
-
| `graphiql`
|
|
890
|
-
| `maxDepth`
|
|
891
|
-
| `timeout`
|
|
934
|
+
| Option | Type | Default | Description |
|
|
935
|
+
| ----------- | ------------------------- | -------- | ------------------------------ |
|
|
936
|
+
| `schema` | `string \| GraphQLSchema` | — | SDL string or pre-built schema |
|
|
937
|
+
| `resolvers` | `object` | — | Resolver map |
|
|
938
|
+
| `rootValue` | `any` | — | Root value for queries |
|
|
939
|
+
| `context` | `(req, ctx) => object` | — | Per-request context factory |
|
|
940
|
+
| `graphiql` | `boolean` | `false` | Serve GraphiQL IDE at GET / |
|
|
941
|
+
| `maxDepth` | `number` | `10` | Max query nesting depth |
|
|
942
|
+
| `timeout` | `number` | `30_000` | Execution timeout (ms) |
|
|
892
943
|
|
|
893
944
|
### health [β] [API]
|
|
894
945
|
|
|
@@ -897,10 +948,10 @@ app.use('/health', health())
|
|
|
897
948
|
// Returns 200 on success, 503 when check throws
|
|
898
949
|
```
|
|
899
950
|
|
|
900
|
-
| Option
|
|
901
|
-
|
|
902
|
-
| `path`
|
|
903
|
-
| `check` | `() => Promise<void>` | —
|
|
951
|
+
| Option | Type | Default | Description |
|
|
952
|
+
| ------- | --------------------- | ----------- | ---------------------------- |
|
|
953
|
+
| `path` | `string` | `'/health'` | Health check endpoint |
|
|
954
|
+
| `check` | `() => Promise<void>` | — | Async function; throws → 503 |
|
|
904
955
|
|
|
905
956
|
### helmet [α] [Security]
|
|
906
957
|
|
|
@@ -911,17 +962,17 @@ app.use(helmet())
|
|
|
911
962
|
app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))
|
|
912
963
|
```
|
|
913
964
|
|
|
914
|
-
| Option
|
|
915
|
-
|
|
916
|
-
| `contentSecurityPolicy`
|
|
917
|
-
| `xFrameOptions`
|
|
918
|
-
| `strictTransportSecurity`
|
|
919
|
-
| `referrerPolicy`
|
|
920
|
-
| `xContentTypeOptions`
|
|
921
|
-
| `permissionsPolicy`
|
|
922
|
-
| `crossOriginEmbedderPolicy` | —
|
|
923
|
-
| `crossOriginOpenerPolicy`
|
|
924
|
-
| `crossOriginResourcePolicy` | —
|
|
965
|
+
| Option | Default | Description |
|
|
966
|
+
| --------------------------- | --------------------------------------- | -------------------------- |
|
|
967
|
+
| `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
|
|
968
|
+
| `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
|
|
969
|
+
| `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
|
|
970
|
+
| `referrerPolicy` | `'no-referrer'` | Referrer header |
|
|
971
|
+
| `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
|
|
972
|
+
| `permissionsPolicy` | — | Feature permissions policy |
|
|
973
|
+
| `crossOriginEmbedderPolicy` | — | COEP header |
|
|
974
|
+
| `crossOriginOpenerPolicy` | — | COOP header |
|
|
975
|
+
| `crossOriginResourcePolicy` | — | CORP header |
|
|
925
976
|
|
|
926
977
|
### iii [β] — Worker / Function / Trigger [API]
|
|
927
978
|
|
|
@@ -934,30 +985,30 @@ app.use('/iii', engine)
|
|
|
934
985
|
app.ws('/iii', engine.wsHandler())
|
|
935
986
|
|
|
936
987
|
const w = createWorker('orders')
|
|
937
|
-
w.registerFunction('orders::create', async (payload) =>
|
|
988
|
+
w.registerFunction('orders::create', async (payload) =>
|
|
989
|
+
db.query('INSERT INTO orders ...', [payload.items]),
|
|
990
|
+
)
|
|
938
991
|
engine.addWorker(w)
|
|
939
992
|
await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
|
|
940
993
|
```
|
|
941
994
|
|
|
942
|
-
| Option
|
|
943
|
-
|
|
944
|
-
| `pg`
|
|
945
|
-
| `redis`
|
|
946
|
-
| `streamTTL` | `number` | `3600`
|
|
947
|
-
|
|
948
|
-
| Method
|
|
949
|
-
|
|
950
|
-
| `.addWorker(w)`
|
|
951
|
-
| `.removeWorker(w)`
|
|
952
|
-
| `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function
|
|
953
|
-
| `.listWorkers()`
|
|
954
|
-
| `.listFunctions()`
|
|
955
|
-
| `.listTriggers()`
|
|
956
|
-
| `.wsHandler()`
|
|
957
|
-
| `.migrate()`
|
|
958
|
-
| `.shutdown()`
|
|
959
|
-
|
|
960
|
-
|
|
995
|
+
| Option | Type | Default | Description |
|
|
996
|
+
| ----------- | -------- | ------- | --------------------------------------------- |
|
|
997
|
+
| `pg` | `object` | — | PostgreSQL client for persistent triggers |
|
|
998
|
+
| `redis` | `object` | — | Redis client for streams |
|
|
999
|
+
| `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
|
|
1000
|
+
|
|
1001
|
+
| Method | Description |
|
|
1002
|
+
| ---------------------------------------------------------- | ------------------------- |
|
|
1003
|
+
| `.addWorker(w)` | Register a worker |
|
|
1004
|
+
| `.removeWorker(w)` | Remove a worker |
|
|
1005
|
+
| `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
|
|
1006
|
+
| `.listWorkers()` | List registered workers |
|
|
1007
|
+
| `.listFunctions()` | List registered functions |
|
|
1008
|
+
| `.listTriggers()` | List registered triggers |
|
|
1009
|
+
| `.wsHandler()` | WebSocket handler |
|
|
1010
|
+
| `.migrate()` | DB setup |
|
|
1011
|
+
| `.shutdown()` | Clean shutdown |
|
|
961
1012
|
|
|
962
1013
|
### knowledgeBase [β] — RAG with pgvector [AI]
|
|
963
1014
|
|
|
@@ -998,21 +1049,20 @@ app.get('/search', async (req, ctx) => {
|
|
|
998
1049
|
})
|
|
999
1050
|
```
|
|
1000
1051
|
|
|
1001
|
-
| Option
|
|
1002
|
-
|
|
1003
|
-
| `pg`
|
|
1004
|
-
| `provider`
|
|
1005
|
-
| `table`
|
|
1006
|
-
| `chunkSize`
|
|
1007
|
-
| `chunkOverlap`
|
|
1008
|
-
| `searchLimit`
|
|
1009
|
-
| `searchThreshold` | `number`
|
|
1052
|
+
| Option | Type | Default | Description |
|
|
1053
|
+
| ----------------- | ---------------- | ------------ | --------------------------------------- |
|
|
1054
|
+
| `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
|
|
1055
|
+
| `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
|
|
1056
|
+
| `table` | `string` | `'_kb_docs'` | Database table name |
|
|
1057
|
+
| `chunkSize` | `number` | `512` | Max characters per chunk |
|
|
1058
|
+
| `chunkOverlap` | `number` | `64` | Overlap between chunks |
|
|
1059
|
+
| `searchLimit` | `number` | `5` | Default search result count |
|
|
1060
|
+
| `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
|
|
1010
1061
|
|
|
1011
1062
|
Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
|
|
1012
1063
|
replaces old chunks. Provider's `embed()` is used automatically.
|
|
1013
1064
|
The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
|
|
1014
1065
|
|
|
1015
|
-
|
|
1016
1066
|
### logdb [β] [API]
|
|
1017
1067
|
|
|
1018
1068
|
PostgreSQL structured event logging with monthly partitioning.
|
|
@@ -1021,53 +1071,60 @@ PostgreSQL structured event logging with monthly partitioning.
|
|
|
1021
1071
|
const logger = logdb({ pg })
|
|
1022
1072
|
await logger.migrate()
|
|
1023
1073
|
app.use('/logs', logger)
|
|
1024
|
-
await logger.clean(12)
|
|
1074
|
+
await logger.clean(12) // drop partitions older than 12 months
|
|
1025
1075
|
await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
|
|
1026
1076
|
```
|
|
1027
1077
|
|
|
1028
|
-
| Option
|
|
1029
|
-
|
|
1030
|
-
| `pg`
|
|
1031
|
-
| `table` | `string` | `'_log_entries'` | Table name
|
|
1078
|
+
| Option | Type | Default | Description |
|
|
1079
|
+
| ------- | -------- | ---------------- | ----------------- |
|
|
1080
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1081
|
+
| `table` | `string` | `'_log_entries'` | Table name |
|
|
1032
1082
|
|
|
1033
|
-
| Method | Path
|
|
1034
|
-
|
|
1035
|
-
| POST
|
|
1036
|
-
| GET
|
|
1037
|
-
| GET
|
|
1083
|
+
| Method | Path | Description |
|
|
1084
|
+
| ------ | ------ | ---------------------------------------------------------------- |
|
|
1085
|
+
| POST | `/` | Create log entry |
|
|
1086
|
+
| GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
|
|
1087
|
+
| GET | `/:id` | Get single entry |
|
|
1038
1088
|
|
|
1039
1089
|
### logger [α] [DevTools]
|
|
1040
1090
|
|
|
1041
1091
|
```ts
|
|
1042
|
-
app.use(logger())
|
|
1043
|
-
app.use(logger({ format: 'combined' }))
|
|
1092
|
+
app.use(logger()) // GET /hello 200 5ms
|
|
1093
|
+
app.use(logger({ format: 'combined' })) // with query params
|
|
1044
1094
|
```
|
|
1045
1095
|
|
|
1046
|
-
| Option
|
|
1047
|
-
|
|
1096
|
+
| Option | Type | Default | Description |
|
|
1097
|
+
| -------- | --------------------------------- | --------- | ------------------------------------------------------------- |
|
|
1048
1098
|
| `format` | `'short' \| 'combined' \| 'json'` | `'short'` | Log format: path only, path + query params, or JSON to stderr |
|
|
1049
1099
|
|
|
1050
1100
|
### mailer [γ] [Networking]
|
|
1051
1101
|
|
|
1052
1102
|
```ts
|
|
1053
|
-
const mail = mailer({
|
|
1054
|
-
|
|
1103
|
+
const mail = mailer({
|
|
1104
|
+
from: 'noreply@example.com',
|
|
1105
|
+
transport: 'smtp://user:pass@smtp.example.com:587',
|
|
1106
|
+
})
|
|
1107
|
+
await mail.send({
|
|
1108
|
+
to: 'user@test.com',
|
|
1109
|
+
subject: 'Hello',
|
|
1110
|
+
text: 'Body',
|
|
1111
|
+
html: '<p>Body</p>',
|
|
1112
|
+
cc: 'admin@test.com',
|
|
1113
|
+
})
|
|
1055
1114
|
```
|
|
1056
1115
|
|
|
1057
|
-
| Option
|
|
1058
|
-
|
|
1059
|
-
| `transport` | `string\|object` | —
|
|
1060
|
-
| `from`
|
|
1061
|
-
| `send`
|
|
1062
|
-
|
|
1063
|
-
|
|
1116
|
+
| Option | Type | Default | Description |
|
|
1117
|
+
| ----------- | ---------------- | ------- | ------------------------------------------------ |
|
|
1118
|
+
| `transport` | `string\|object` | — | Nodemailer transport config or connection string |
|
|
1119
|
+
| `from` | `string` | — | Default sender address |
|
|
1120
|
+
| `send` | `function` | — | Custom send function (alternative to transport) |
|
|
1064
1121
|
|
|
1065
1122
|
### oauthLogin (via user()) — Social login (OAuth 2.0 client) [Security]
|
|
1066
1123
|
|
|
1067
1124
|
Social login is built into the [`user()`](#user-β) module via the `oauthLogin` option — no separate import needed.
|
|
1068
1125
|
|
|
1069
1126
|
```ts
|
|
1070
|
-
app.use(session())
|
|
1127
|
+
app.use(session()) // required — stores OAuth state
|
|
1071
1128
|
const u = user({
|
|
1072
1129
|
pg,
|
|
1073
1130
|
jwtSecret: process.env.JWT_SECRET!,
|
|
@@ -1086,7 +1143,7 @@ const u = user({
|
|
|
1086
1143
|
},
|
|
1087
1144
|
})
|
|
1088
1145
|
await u.migrate()
|
|
1089
|
-
app.use(u)
|
|
1146
|
+
app.use(u) // POST /register, POST /login, GET /auth/:provider, GET /auth/:provider/callback
|
|
1090
1147
|
```
|
|
1091
1148
|
|
|
1092
1149
|
**Flow:** User clicks "Login with Google" → redirected to Google → back to app → user created/linked in database → JWT signed → session created → redirected to `redirectUrl` with `?token=` (or JSON response for API clients).
|
|
@@ -1119,14 +1176,13 @@ const u = user({
|
|
|
1119
1176
|
})
|
|
1120
1177
|
```
|
|
1121
1178
|
|
|
1122
|
-
| Option (oauthLogin) | Type
|
|
1123
|
-
|
|
1124
|
-
| `providers`
|
|
1125
|
-
| `redirectUrl`
|
|
1179
|
+
| Option (oauthLogin) | Type | Default | Description |
|
|
1180
|
+
| ------------------- | ------------------------------------- | ------- | ------------------------------------------------------------------- |
|
|
1181
|
+
| `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
|
|
1182
|
+
| `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
|
|
1126
1183
|
|
|
1127
1184
|
Built-in providers (Google, GitHub) have preset URLs — you only need to provide `clientId` and `clientSecret`. The module auto-creates a `_auth_providers` table on first request.
|
|
1128
1185
|
|
|
1129
|
-
|
|
1130
1186
|
### messager [β] [Networking]
|
|
1131
1187
|
|
|
1132
1188
|
Real-time chat with channels, WebSocket, agent routing.
|
|
@@ -1139,20 +1195,18 @@ app.ws('/ws', msg.wsHandler())
|
|
|
1139
1195
|
await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
|
|
1140
1196
|
```
|
|
1141
1197
|
|
|
1142
|
-
| Option
|
|
1143
|
-
|
|
1144
|
-
| `pg`
|
|
1145
|
-
| `agents`
|
|
1146
|
-
| `webhookTimeout` | `number`
|
|
1147
|
-
| `redis`
|
|
1148
|
-
|
|
1149
|
-
| Method | Description |
|
|
1150
|
-
|--------|-------------|
|
|
1151
|
-
| `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
|
|
1152
|
-
| `.send(channel, content, opts?)` | Send message to channel |
|
|
1153
|
-
| `.close()` | Cleanup |
|
|
1154
|
-
|
|
1198
|
+
| Option | Type | Default | Description |
|
|
1199
|
+
| ---------------- | ------------- | ------- | ------------------------ |
|
|
1200
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1201
|
+
| `agents` | `AgentModule` | — | Agent module for routing |
|
|
1202
|
+
| `webhookTimeout` | `number` | — | Webhook timeout |
|
|
1203
|
+
| `redis` | `object` | — | Redis client |
|
|
1155
1204
|
|
|
1205
|
+
| Method | Description |
|
|
1206
|
+
| -------------------------------- | --------------------------------------------------- |
|
|
1207
|
+
| `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
|
|
1208
|
+
| `.send(channel, content, opts?)` | Send message to channel |
|
|
1209
|
+
| `.close()` | Cleanup |
|
|
1156
1210
|
|
|
1157
1211
|
### opencode [β] [AI]
|
|
1158
1212
|
|
|
@@ -1170,35 +1224,35 @@ app.use('/opencode', oc)
|
|
|
1170
1224
|
app.ws('/opencode', oc.wsHandler())
|
|
1171
1225
|
```
|
|
1172
1226
|
|
|
1173
|
-
| Option
|
|
1174
|
-
|
|
1175
|
-
| `pg`
|
|
1176
|
-
| `model`
|
|
1177
|
-
| `baseURL`
|
|
1178
|
-
| `apiKey`
|
|
1179
|
-
| `workspace`
|
|
1180
|
-
| `systemPrompt` | `string`
|
|
1181
|
-
| `skills`
|
|
1182
|
-
| `permissions`
|
|
1227
|
+
| Option | Type | Default | Description |
|
|
1228
|
+
| -------------- | ---------- | ------- | ------------------------------------------------------ |
|
|
1229
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1230
|
+
| `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
|
|
1231
|
+
| `baseURL` | `string` | — | OpenAI-compatible API base URL |
|
|
1232
|
+
| `apiKey` | `string` | — | API key for the model |
|
|
1233
|
+
| `workspace` | `string` | — | Project directory |
|
|
1234
|
+
| `systemPrompt` | `string` | — | Custom system prompt |
|
|
1235
|
+
| `skills` | `object[]` | — | Custom skill definitions |
|
|
1236
|
+
| `permissions` | `object` | — | Tool permission rules |
|
|
1183
1237
|
|
|
1184
1238
|
### postgres [α] [Database]
|
|
1185
1239
|
|
|
1186
1240
|
Type-safe PostgreSQL client with schema builder, CRUD, migrations, soft delete, and JSONB/vector support.
|
|
1187
1241
|
|
|
1188
1242
|
```ts
|
|
1189
|
-
const pg = postgres()
|
|
1190
|
-
app.use(pg)
|
|
1243
|
+
const pg = postgres() // reads DATABASE_URL
|
|
1244
|
+
app.use(pg) // injects ctx.sql
|
|
1191
1245
|
```
|
|
1192
1246
|
|
|
1193
|
-
| Option
|
|
1194
|
-
|
|
1195
|
-
| `connection`
|
|
1196
|
-
| `max`
|
|
1197
|
-
| `ssl`
|
|
1198
|
-
| `idle_timeout`
|
|
1199
|
-
| `connect_timeout`
|
|
1200
|
-
| `statementTimeout` | `number`
|
|
1201
|
-
| `onQuery`
|
|
1247
|
+
| Option | Type | Default | Description |
|
|
1248
|
+
| ------------------ | --------------------------- | ------------------ | --------------------------------------- |
|
|
1249
|
+
| `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
|
|
1250
|
+
| `max` | `number` | `10` | Max pool connections |
|
|
1251
|
+
| `ssl` | `boolean\|object` | — | SSL options |
|
|
1252
|
+
| `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
|
|
1253
|
+
| `connect_timeout` | `number` | `30` | Connection timeout |
|
|
1254
|
+
| `statementTimeout` | `number` | `30_000` | Per-statement timeout (ms, 0 = disable) |
|
|
1255
|
+
| `onQuery` | `(query, ms, rows) => void` | — | Query logging callback |
|
|
1202
1256
|
|
|
1203
1257
|
```ts
|
|
1204
1258
|
// Raw SQL via tagged template
|
|
@@ -1214,40 +1268,46 @@ const users = pg.table('_users', {
|
|
|
1214
1268
|
active: boolean('active').default(true),
|
|
1215
1269
|
...timestamps(),
|
|
1216
1270
|
})
|
|
1217
|
-
await users.create()
|
|
1271
|
+
await users.create() // DDL — no need to pass sql
|
|
1218
1272
|
await users.createIndex('email')
|
|
1219
1273
|
|
|
1220
1274
|
// CRUD — sql already bound
|
|
1221
1275
|
await users.insert({ name: 'Alice' })
|
|
1222
|
-
const { count, data } = await users.readMany(
|
|
1276
|
+
const { count, data } = await users.readMany(
|
|
1277
|
+
{ role: 'admin' },
|
|
1278
|
+
{ orderBy: { name: 'asc' }, limit: 10 },
|
|
1279
|
+
)
|
|
1223
1280
|
await users.upsert({ email: 'alice@test.com' }, 'email')
|
|
1224
1281
|
|
|
1225
1282
|
// Reuse schema without redefining fields
|
|
1226
1283
|
import { pgTable } from 'weifuwu'
|
|
1227
|
-
const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') })
|
|
1228
|
-
const users = pg.table(usersSchema)
|
|
1284
|
+
const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
|
|
1285
|
+
const users = pg.table(usersSchema) // bind — no field duplication
|
|
1229
1286
|
|
|
1230
1287
|
// Transactions — with auto-retry on deadlock/serialization failure
|
|
1231
|
-
await pg.transaction(
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1288
|
+
await pg.transaction(
|
|
1289
|
+
async (sql) => {
|
|
1290
|
+
const txUsers = users.withSql(sql)
|
|
1291
|
+
return txUsers.insert({ name: 'Bob' })
|
|
1292
|
+
},
|
|
1293
|
+
{ maxRetries: 3 },
|
|
1294
|
+
)
|
|
1235
1295
|
|
|
1236
1296
|
// Soft delete — automatic if deleted_at column exists
|
|
1237
|
-
await users.delete(1)
|
|
1238
|
-
await users.hardDelete(1)
|
|
1239
|
-
await users.read(1)
|
|
1297
|
+
await users.delete(1) // SET deleted_at = NOW()
|
|
1298
|
+
await users.hardDelete(1) // DELETE FROM
|
|
1299
|
+
await users.read(1) // auto-filters deleted_at IS NULL (use withDeleted: true to include)
|
|
1240
1300
|
|
|
1241
1301
|
// JSONB queries
|
|
1242
1302
|
const logs = pg.table('logs', { meta: jsonb<{ service: string }>('meta') })
|
|
1243
1303
|
await logs.readMany(contains('meta', { service: 'auth' }))
|
|
1244
1304
|
|
|
1245
1305
|
// Connection pool visibility
|
|
1246
|
-
console.log(pg.poolStats())
|
|
1306
|
+
console.log(pg.poolStats()) // { active: 3, idle: 7, waiting: 0, max: 10 }
|
|
1247
1307
|
|
|
1248
1308
|
// Migration tracking
|
|
1249
|
-
await pg.migrate()
|
|
1250
|
-
await pg.markMigrated('myModule')
|
|
1309
|
+
await pg.migrate() // creates _weifuwu_migrations
|
|
1310
|
+
await pg.markMigrated('myModule') // idempotent
|
|
1251
1311
|
const done = await pg.isMigrated('myModule')
|
|
1252
1312
|
|
|
1253
1313
|
// Partitioned tables
|
|
@@ -1261,61 +1321,65 @@ await logs.create({ partitionBy: partitionBy('range', 'created_at') })
|
|
|
1261
1321
|
| `pg.table(schema)` | Reusing a schema without duplicating field definitions |
|
|
1262
1322
|
| `pgTable('t', cols)` | No `pg` reference (utility modules, standalone schema files) |
|
|
1263
1323
|
|
|
1264
|
-
| Column builder
|
|
1265
|
-
|
|
1266
|
-
| `serial(name)`
|
|
1267
|
-
| `uuid(name)`
|
|
1268
|
-
| `text(name)`
|
|
1269
|
-
| `integer(name)`
|
|
1270
|
-
| `boolean(name)` / `boolean_(name)` | `boolean`
|
|
1271
|
-
| `timestamptz(name)`
|
|
1272
|
-
| `jsonb<T>(name)`
|
|
1273
|
-
| `textArray(name)`
|
|
1274
|
-
| `vector(name, dims)`
|
|
1324
|
+
| Column builder | Type | Notes |
|
|
1325
|
+
| ---------------------------------- | ---------- | ------------------------------- |
|
|
1326
|
+
| `serial(name)` | `number` | Auto-increment |
|
|
1327
|
+
| `uuid(name)` | `string` | — |
|
|
1328
|
+
| `text(name)` | `string` | — |
|
|
1329
|
+
| `integer(name)` | `number` | — |
|
|
1330
|
+
| `boolean(name)` / `boolean_(name)` | `boolean` | `_` suffix for JS reserved word |
|
|
1331
|
+
| `timestamptz(name)` | `string` | — |
|
|
1332
|
+
| `jsonb<T>(name)` | `T` | Generic for typed JSONB access |
|
|
1333
|
+
| `textArray(name)` | `string[]` | TEXT[] |
|
|
1334
|
+
| `vector(name, dims)` | `number[]` | pgvector support |
|
|
1275
1335
|
|
|
1276
1336
|
**Column modifiers:** `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(val)`, `.unique()`, `.references(table, column?, onDelete?)`.
|
|
1277
1337
|
|
|
1278
1338
|
**CRUD methods:**
|
|
1279
1339
|
|
|
1280
|
-
| Method
|
|
1281
|
-
|
|
1282
|
-
| `insert(data)`
|
|
1283
|
-
| `insertMany(data)`
|
|
1284
|
-
| `read(id, opts?)`
|
|
1285
|
-
| `readMany(where?, opts?)`
|
|
1286
|
-
| `update(id, data)`
|
|
1287
|
-
| `updateMany(where, data)`
|
|
1288
|
-
| `delete(id)`
|
|
1289
|
-
| `hardDelete(id)`
|
|
1290
|
-
| `deleteMany(where)`
|
|
1291
|
-
| `hardDeleteMany(where)`
|
|
1292
|
-
| `upsert(data, conflict)`
|
|
1293
|
-
| `count(where?)`
|
|
1294
|
-
| `create(opts?)`
|
|
1295
|
-
| `drop(opts?)`
|
|
1296
|
-
| `createIndex(columns, opts?)` | CREATE INDEX
|
|
1297
|
-
| `createUniqueIndex(columns)`
|
|
1298
|
-
| `withSql(sql)`
|
|
1340
|
+
| Method | Description |
|
|
1341
|
+
| ----------------------------- | ----------------------------------------------------------------- |
|
|
1342
|
+
| `insert(data)` | INSERT + RETURNING \*, returns the inserted row |
|
|
1343
|
+
| `insertMany(data)` | Bulk INSERT + RETURNING \*, returns rows |
|
|
1344
|
+
| `read(id, opts?)` | SELECT by detected primary key + auto soft-delete filter |
|
|
1345
|
+
| `readMany(where?, opts?)` | Filtered query with `{ count, data }` — auto-filters soft-deleted |
|
|
1346
|
+
| `update(id, data)` | UPDATE by primary key + RETURNING \*, returns updated row |
|
|
1347
|
+
| `updateMany(where, data)` | Bulk UPDATE, returns affected row count |
|
|
1348
|
+
| `delete(id)` | Soft delete if `deleted_at` exists, else hard delete |
|
|
1349
|
+
| `hardDelete(id)` | Always DELETE FROM |
|
|
1350
|
+
| `deleteMany(where)` | Soft bulk delete if `deleted_at` exists |
|
|
1351
|
+
| `hardDeleteMany(where)` | Always DELETE FROM |
|
|
1352
|
+
| `upsert(data, conflict)` | INSERT ON CONFLICT DO UPDATE, returns row |
|
|
1353
|
+
| `count(where?)` | SELECT COUNT(\*) — auto-filters soft-deleted |
|
|
1354
|
+
| `create(opts?)` | CREATE TABLE IF NOT EXISTS |
|
|
1355
|
+
| `drop(opts?)` | DROP TABLE IF EXISTS |
|
|
1356
|
+
| `createIndex(columns, opts?)` | CREATE INDEX |
|
|
1357
|
+
| `createUniqueIndex(columns)` | CREATE UNIQUE INDEX |
|
|
1358
|
+
| `withSql(sql)` | Returns copy bound to a different sql (for transactions) |
|
|
1299
1359
|
|
|
1300
1360
|
**Where helpers** — composable query conditions:
|
|
1301
1361
|
|
|
1302
|
-
| Helper
|
|
1303
|
-
|
|
1304
|
-
| `eq(col, val)`
|
|
1305
|
-
| `ne(col, val)`
|
|
1306
|
-
| `gt` / `gte` / `lt` / `lte`
|
|
1307
|
-
| `isNull(col)` / `isNotNull(col)`
|
|
1308
|
-
| `like(col, pattern)`
|
|
1309
|
-
| `contains(col, val)`
|
|
1310
|
-
| `in_(col, vals)`
|
|
1311
|
-
| `and(...)` / `or(...)` / `not(...)` | Boolean composition
|
|
1362
|
+
| Helper | SQL |
|
|
1363
|
+
| ----------------------------------- | ------------------------- |
|
|
1364
|
+
| `eq(col, val)` | `"col" = val` |
|
|
1365
|
+
| `ne(col, val)` | `"col" != val` |
|
|
1366
|
+
| `gt` / `gte` / `lt` / `lte` | Comparison operators |
|
|
1367
|
+
| `isNull(col)` / `isNotNull(col)` | `IS NULL` / `IS NOT NULL` |
|
|
1368
|
+
| `like(col, pattern)` | `LIKE` |
|
|
1369
|
+
| `contains(col, val)` | `@>` JSONB containment |
|
|
1370
|
+
| `in_(col, vals)` | `= ANY(...)` |
|
|
1371
|
+
| `and(...)` / `or(...)` / `not(...)` | Boolean composition |
|
|
1312
1372
|
|
|
1313
1373
|
**PgModule** — base class for modules that need DB access:
|
|
1314
1374
|
|
|
1315
1375
|
```ts
|
|
1316
1376
|
class MyModule extends PgModule {
|
|
1317
|
-
async migrate() {
|
|
1318
|
-
|
|
1377
|
+
async migrate() {
|
|
1378
|
+
/* run DDL */
|
|
1379
|
+
}
|
|
1380
|
+
async getUsers() {
|
|
1381
|
+
return this.table('users', {}).readMany()
|
|
1382
|
+
}
|
|
1319
1383
|
}
|
|
1320
1384
|
```
|
|
1321
1385
|
|
|
@@ -1333,10 +1397,10 @@ const next = cronNext('0 9 * * 1-5')
|
|
|
1333
1397
|
console.log(new Date(next))
|
|
1334
1398
|
```
|
|
1335
1399
|
|
|
1336
|
-
| Function
|
|
1337
|
-
|
|
1338
|
-
| `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]`
|
|
1339
|
-
| `matches(fields, date)` | Check if a date matches a parsed pattern
|
|
1400
|
+
| Function | Description |
|
|
1401
|
+
| ----------------------- | ---------------------------------------------------------- |
|
|
1402
|
+
| `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]` |
|
|
1403
|
+
| `matches(fields, date)` | Check if a date matches a parsed pattern |
|
|
1340
1404
|
| `cronNext(expr, from?)` | Calculate next matching timestamp (`from` defaults to now) |
|
|
1341
1405
|
|
|
1342
1406
|
### fts — Full-Text Search (PostgreSQL) [Database]
|
|
@@ -1367,11 +1431,11 @@ const results = await fts.search(pg.sql, articles, 'node.js framework', {
|
|
|
1367
1431
|
await fts.dropIndex(pg.sql, articles)
|
|
1368
1432
|
```
|
|
1369
1433
|
|
|
1370
|
-
| Function
|
|
1371
|
-
|
|
1434
|
+
| Function | Description |
|
|
1435
|
+
| ---------------------------------------- | ------------------------------ |
|
|
1372
1436
|
| `createIndex(sql, table, fields, opts?)` | Create GIN/GiST tsvector index |
|
|
1373
|
-
| `search(sql, table, query, opts?)`
|
|
1374
|
-
| `dropIndex(sql, table, opts?)`
|
|
1437
|
+
| `search(sql, table, query, opts?)` | Search with ts_rank ordering |
|
|
1438
|
+
| `dropIndex(sql, table, opts?)` | Drop the index |
|
|
1375
1439
|
|
|
1376
1440
|
Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
|
|
1377
1441
|
|
|
@@ -1391,10 +1455,10 @@ app.use(theme({ default: 'dark' }))
|
|
|
1391
1455
|
// app.use('/', t)
|
|
1392
1456
|
```
|
|
1393
1457
|
|
|
1394
|
-
| Option
|
|
1395
|
-
|
|
1396
|
-
| `default` | `string` | `'system'` | Default theme
|
|
1397
|
-
| `cookie`
|
|
1458
|
+
| Option | Type | Default | Description |
|
|
1459
|
+
| --------- | -------- | ---------- | ------------------------------ |
|
|
1460
|
+
| `default` | `string` | `'system'` | Default theme |
|
|
1461
|
+
| `cookie` | `string` | `'theme'` | Cookie name (empty to disable) |
|
|
1398
1462
|
|
|
1399
1463
|
```ts
|
|
1400
1464
|
// Server-side switching
|
|
@@ -1423,13 +1487,13 @@ app.use(i18n({ default: 'zh', dir: './locales' }))
|
|
|
1423
1487
|
// app.use('/', l)
|
|
1424
1488
|
```
|
|
1425
1489
|
|
|
1426
|
-
| Option
|
|
1427
|
-
|
|
1428
|
-
| `default`
|
|
1429
|
-
| `dir`
|
|
1430
|
-
| `messages`
|
|
1431
|
-
| `cookie`
|
|
1432
|
-
| `fromAcceptLanguage` | `boolean` | `true`
|
|
1490
|
+
| Option | Type | Default | Description |
|
|
1491
|
+
| -------------------- | --------- | ---------- | -------------------------------------------------- |
|
|
1492
|
+
| `default` | `string` | `'en'` | Default locale |
|
|
1493
|
+
| `dir` | `string` | — | Directory with `{locale}.json` files |
|
|
1494
|
+
| `messages` | `object` | — | Inline translations: `{ zh: { welcome: '欢迎' } }` |
|
|
1495
|
+
| `cookie` | `string` | `'locale'` | Cookie name (empty to disable) |
|
|
1496
|
+
| `fromAcceptLanguage` | `boolean` | `true` | Detect from Accept-Language header |
|
|
1433
1497
|
|
|
1434
1498
|
```ts
|
|
1435
1499
|
// Handler
|
|
@@ -1456,7 +1520,9 @@ const q = queue({ store: 'memory' })
|
|
|
1456
1520
|
// const q = queue({ store: 'redis', redis })
|
|
1457
1521
|
|
|
1458
1522
|
// Register cron job (uses the same backend for persistence)
|
|
1459
|
-
q.cron('*/5 * * * *', async () => {
|
|
1523
|
+
q.cron('*/5 * * * *', async () => {
|
|
1524
|
+
await cleanCache()
|
|
1525
|
+
})
|
|
1460
1526
|
|
|
1461
1527
|
// Or use process/add for full queue semantics
|
|
1462
1528
|
q.process('send-email', async (job) => {
|
|
@@ -1475,55 +1541,55 @@ q.add('weekly-report', {}, { schedule: '0 9 * * 1' })
|
|
|
1475
1541
|
q.run()
|
|
1476
1542
|
```
|
|
1477
1543
|
|
|
1478
|
-
| Option
|
|
1479
|
-
|
|
1480
|
-
| `store`
|
|
1481
|
-
| `redis`
|
|
1482
|
-
| `url`
|
|
1483
|
-
| `pg`
|
|
1484
|
-
| `prefix`
|
|
1485
|
-
| `pollInterval` | `number`
|
|
1486
|
-
|
|
1487
|
-
| Method
|
|
1488
|
-
|
|
1489
|
-
| `.cron(pattern, handler)`
|
|
1490
|
-
| `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`)
|
|
1491
|
-
| `.process(type, handler)`
|
|
1492
|
-
| `.run()`
|
|
1493
|
-
| `.stop()`
|
|
1494
|
-
| `.jobs(limit?)`
|
|
1495
|
-
| `.failedJobs(limit?)`
|
|
1496
|
-
| `.retryFailed(jobId)`
|
|
1497
|
-
| `.retryAllFailed(type?)`
|
|
1498
|
-
| `.dashboard()`
|
|
1499
|
-
| `.close()`
|
|
1544
|
+
| Option | Type | Default | Description |
|
|
1545
|
+
| -------------- | ----------------------------- | ---------- | ----------------------------------------------- |
|
|
1546
|
+
| `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
|
|
1547
|
+
| `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
|
|
1548
|
+
| `url` | `string` | — | Redis URL (alternative to client) |
|
|
1549
|
+
| `pg` | `object` | — | PostgreSQL client (required when `store: 'pg'`) |
|
|
1550
|
+
| `prefix` | `string` | `'queue'` | Key/table prefix |
|
|
1551
|
+
| `pollInterval` | `number` | `200` | Poll interval (ms) |
|
|
1552
|
+
|
|
1553
|
+
| Method | Description |
|
|
1554
|
+
| ---------------------------- | --------------------------------------------------- |
|
|
1555
|
+
| `.cron(pattern, handler)` | Register a cron job (uses process + add internally) |
|
|
1556
|
+
| `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`) |
|
|
1557
|
+
| `.process(type, handler)` | Register job processor |
|
|
1558
|
+
| `.run()` | Start processing |
|
|
1559
|
+
| `.stop()` | Stop processing |
|
|
1560
|
+
| `.jobs(limit?)` | List pending jobs |
|
|
1561
|
+
| `.failedJobs(limit?)` | List failed jobs with error messages |
|
|
1562
|
+
| `.retryFailed(jobId)` | Retry a specific failed job |
|
|
1563
|
+
| `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
|
|
1564
|
+
| `.dashboard()` | Returns a Router with management endpoints |
|
|
1565
|
+
| `.close()` | Cleanup |
|
|
1500
1566
|
|
|
1501
1567
|
**Schedule (cron) field reference:**
|
|
1502
1568
|
|
|
1503
|
-
| Field
|
|
1504
|
-
|
|
1505
|
-
| minute
|
|
1506
|
-
| hour
|
|
1507
|
-
| day of month | 1–31
|
|
1508
|
-
| month
|
|
1509
|
-
| day of week
|
|
1569
|
+
| Field | Range |
|
|
1570
|
+
| ------------ | -------------- |
|
|
1571
|
+
| minute | 0–59 |
|
|
1572
|
+
| hour | 0–23 |
|
|
1573
|
+
| day of month | 1–31 |
|
|
1574
|
+
| month | 1–12 |
|
|
1575
|
+
| day of week | 0–6 (0=Sunday) |
|
|
1510
1576
|
|
|
1511
1577
|
Supported cron syntax: `*` (any), `*/n` (every n), `n-m` (range), `n,m,o` (list), `n` (exact).
|
|
1512
1578
|
|
|
1513
1579
|
**Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
|
|
1514
1580
|
|
|
1515
|
-
| Method | Path
|
|
1516
|
-
|
|
1517
|
-
| GET
|
|
1518
|
-
| GET
|
|
1519
|
-
| POST
|
|
1520
|
-
| POST
|
|
1581
|
+
| Method | Path | Description |
|
|
1582
|
+
| ------ | --------------- | ------------------------------------------- |
|
|
1583
|
+
| GET | `/` | Queue stats + pending/failed counts by type |
|
|
1584
|
+
| GET | `/:type/failed` | List failed jobs for a type |
|
|
1585
|
+
| POST | `/:type/retry` | Retry all failed jobs of a type |
|
|
1586
|
+
| POST | `/retry/:id` | Retry a specific failed job by ID |
|
|
1521
1587
|
|
|
1522
1588
|
### rateLimit [α] [Security]
|
|
1523
1589
|
|
|
1524
1590
|
```ts
|
|
1525
|
-
app.use(rateLimit({ max: 100, window: 60_000 }))
|
|
1526
|
-
app.get('/api', rateLimit({ max: 10 }), handler)
|
|
1591
|
+
app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
|
|
1592
|
+
app.get('/api', rateLimit({ max: 10 }), handler) // per-route
|
|
1527
1593
|
app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
|
|
1528
1594
|
|
|
1529
1595
|
// Multi-process: Redis-backed rate limiting
|
|
@@ -1533,31 +1599,31 @@ app.use(rateLimit({ max: 100, store: 'redis', redis: ctx.redis }))
|
|
|
1533
1599
|
// m.stop() — clear interval (memory) or Redis cleanup
|
|
1534
1600
|
```
|
|
1535
1601
|
|
|
1536
|
-
| Option
|
|
1537
|
-
|
|
1538
|
-
| `max`
|
|
1539
|
-
| `window`
|
|
1540
|
-
| `key`
|
|
1541
|
-
| `message` | `string`
|
|
1542
|
-
| `store`
|
|
1543
|
-
| `redis`
|
|
1544
|
-
| `prefix`
|
|
1602
|
+
| Option | Type | Default | Description |
|
|
1603
|
+
| --------- | --------------------- | --------------------- | --------------------------------------------- |
|
|
1604
|
+
| `max` | `number` | `100` | Max requests per window |
|
|
1605
|
+
| `window` | `number` | `60_000` | Window duration (ms) |
|
|
1606
|
+
| `key` | `(req) => string` | IP-based | Key function |
|
|
1607
|
+
| `message` | `string` | `'Too Many Requests'` | 429 response body |
|
|
1608
|
+
| `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
|
|
1609
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
1610
|
+
| `prefix` | `string` | `'ratelimit:'` | Redis key prefix |
|
|
1545
1611
|
|
|
1546
1612
|
Redis mode uses `INCR` + `EXPIRE` for atomic counting, enabling accurate rate limiting across multiple server processes. Memory mode is ideal for single-process deployments.
|
|
1547
1613
|
|
|
1548
1614
|
### redis [α] [Database]
|
|
1549
1615
|
|
|
1550
1616
|
```ts
|
|
1551
|
-
const r = redis()
|
|
1552
|
-
app.use(r)
|
|
1617
|
+
const r = redis() // reads REDIS_URL
|
|
1618
|
+
app.use(r) // injects ctx.redis
|
|
1553
1619
|
await ctx.redis.set('key', 'value')
|
|
1554
1620
|
// r.close() — cleanup
|
|
1555
1621
|
```
|
|
1556
1622
|
|
|
1557
|
-
| Option
|
|
1558
|
-
|
|
1559
|
-
| `url`
|
|
1560
|
-
| (all ioredis options) | —
|
|
1623
|
+
| Option | Type | Default | Description |
|
|
1624
|
+
| --------------------- | -------- | --------------- | -------------------------- |
|
|
1625
|
+
| `url` | `string` | `REDIS_URL` env | Redis connection string |
|
|
1626
|
+
| (all ioredis options) | — | — | Passed directly to ioredis |
|
|
1561
1627
|
|
|
1562
1628
|
### requestId [α] [DevTools]
|
|
1563
1629
|
|
|
@@ -1567,10 +1633,10 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
|
|
|
1567
1633
|
// Sets X-Request-ID header on responses, available as ctx.requestId
|
|
1568
1634
|
```
|
|
1569
1635
|
|
|
1570
|
-
| Option
|
|
1571
|
-
|
|
1572
|
-
| `header`
|
|
1573
|
-
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator
|
|
1636
|
+
| Option | Type | Default | Description |
|
|
1637
|
+
| ----------- | -------------- | --------------------- | ------------------------- |
|
|
1638
|
+
| `header` | `string` | `'X-Request-ID'` | Header name to read/write |
|
|
1639
|
+
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
|
|
1574
1640
|
|
|
1575
1641
|
### trace [α] [DevTools]
|
|
1576
1642
|
|
|
@@ -1578,20 +1644,20 @@ Request-scoped tracing via `AsyncLocalStorage`. Use as middleware to inject `ctx
|
|
|
1578
1644
|
|
|
1579
1645
|
```ts
|
|
1580
1646
|
import { trace } from 'weifuwu'
|
|
1581
|
-
app.use(trace())
|
|
1582
|
-
app.use(trace({ header: 'X-Trace-Id' }))
|
|
1647
|
+
app.use(trace()) // → ctx.trace
|
|
1648
|
+
app.use(trace({ header: 'X-Trace-Id' })) // custom header
|
|
1583
1649
|
|
|
1584
1650
|
app.get('/', (req, ctx) => {
|
|
1585
|
-
console.log(ctx.trace.requestId)
|
|
1586
|
-
console.log(ctx.trace.traceId)
|
|
1587
|
-
console.log(ctx.trace.elapsed())
|
|
1651
|
+
console.log(ctx.trace.requestId) // 550e8400-e29b-...
|
|
1652
|
+
console.log(ctx.trace.traceId) // trace UUID
|
|
1653
|
+
console.log(ctx.trace.elapsed()) // ms since request start
|
|
1588
1654
|
})
|
|
1589
1655
|
```
|
|
1590
1656
|
|
|
1591
|
-
| Option
|
|
1592
|
-
|
|
1593
|
-
| `header`
|
|
1594
|
-
| `generator` | `() => string` | `crypto.randomUUID()` | Custom ID generator
|
|
1657
|
+
| Option | Type | Default | Description |
|
|
1658
|
+
| ----------- | -------------- | --------------------- | ---------------------- |
|
|
1659
|
+
| `header` | `string` | `'X-Request-ID'` | Request ID header name |
|
|
1660
|
+
| `generator` | `() => string` | `crypto.randomUUID()` | Custom ID generator |
|
|
1595
1661
|
|
|
1596
1662
|
Utility functions (also available standalone):
|
|
1597
1663
|
|
|
@@ -1603,29 +1669,31 @@ const elapsed = traceElapsed() // ms since request started
|
|
|
1603
1669
|
runWithTrace(incomingId, () => { ... }) // manual scope
|
|
1604
1670
|
```
|
|
1605
1671
|
|
|
1606
|
-
| Function
|
|
1607
|
-
|
|
1608
|
-
| `currentTraceId()`
|
|
1609
|
-
| `currentTrace()`
|
|
1610
|
-
| `traceElapsed()`
|
|
1611
|
-
| `runWithTrace(traceId, fn)` | Execute `fn` inside a trace scope
|
|
1672
|
+
| Function | Description |
|
|
1673
|
+
| --------------------------- | ---------------------------------------------------------- |
|
|
1674
|
+
| `currentTraceId()` | Current request trace ID, or `undefined` outside a request |
|
|
1675
|
+
| `currentTrace()` | Full `{ traceId, startTime }` context |
|
|
1676
|
+
| `traceElapsed()` | Milliseconds elapsed since the trace started |
|
|
1677
|
+
| `runWithTrace(traceId, fn)` | Execute `fn` inside a trace scope |
|
|
1612
1678
|
|
|
1613
1679
|
### s3 [α] — S3-compatible object storage [Networking]
|
|
1614
1680
|
|
|
1615
1681
|
```ts
|
|
1616
1682
|
import { s3 } from 'weifuwu'
|
|
1617
1683
|
|
|
1618
|
-
app.use(
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1684
|
+
app.use(
|
|
1685
|
+
s3({
|
|
1686
|
+
bucket: 'my-app',
|
|
1687
|
+
region: 'us-east-1',
|
|
1688
|
+
endpoint: process.env.S3_URL, // MinIO / R2 / AWS
|
|
1689
|
+
forcePathStyle: true, // required for MinIO
|
|
1690
|
+
credentials: {
|
|
1691
|
+
accessKeyId: process.env.S3_ACCESS_KEY,
|
|
1692
|
+
secretAccessKey: process.env.S3_SECRET_KEY,
|
|
1693
|
+
},
|
|
1694
|
+
publicUrl: 'https://cdn.example.com', // for unsigned public URLs
|
|
1695
|
+
}),
|
|
1696
|
+
)
|
|
1629
1697
|
```
|
|
1630
1698
|
|
|
1631
1699
|
Injects `ctx.s3` with methods for S3-compatible object storage.
|
|
@@ -1654,14 +1722,14 @@ const publicUrl = await ctx.s3.url('images/logo.png', { expiresIn: 0 })
|
|
|
1654
1722
|
const keys = await ctx.s3.list('images/')
|
|
1655
1723
|
```
|
|
1656
1724
|
|
|
1657
|
-
| Option
|
|
1658
|
-
|
|
1659
|
-
| `bucket`
|
|
1660
|
-
| `region`
|
|
1661
|
-
| `endpoint`
|
|
1662
|
-
| `forcePathStyle` | `boolean`
|
|
1663
|
-
| `credentials`
|
|
1664
|
-
| `publicUrl`
|
|
1725
|
+
| Option | Type | Default | Description |
|
|
1726
|
+
| ---------------- | ---------------------------------- | ------------- | ------------------------------------------------------------------ |
|
|
1727
|
+
| `bucket` | `string` | — | **Required.** S3 bucket name |
|
|
1728
|
+
| `region` | `string` | `'us-east-1'` | AWS region |
|
|
1729
|
+
| `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
|
|
1730
|
+
| `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
|
|
1731
|
+
| `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
|
|
1732
|
+
| `publicUrl` | `string` | — | Base URL for unsigned public URLs via `url(key, { expiresIn: 0 })` |
|
|
1665
1733
|
|
|
1666
1734
|
Credentials can be omitted to use AWS environment variables (`AWS_ACCESS_KEY_ID`,
|
|
1667
1735
|
`AWS_SECRET_ACCESS_KEY`) or IAM roles (EC2, ECS, Lambda).
|
|
@@ -1688,32 +1756,42 @@ minio:
|
|
|
1688
1756
|
command: server /data
|
|
1689
1757
|
```
|
|
1690
1758
|
|
|
1691
|
-
|
|
1692
1759
|
### seo [β] + seoMiddleware [α] [API]
|
|
1693
1760
|
|
|
1694
1761
|
```ts
|
|
1695
|
-
app.use(
|
|
1762
|
+
app.use(
|
|
1763
|
+
'/',
|
|
1764
|
+
seo({
|
|
1765
|
+
baseUrl: 'https://example.com',
|
|
1766
|
+
robots: [{ userAgent: '*', allow: '/' }],
|
|
1767
|
+
sitemap: { urls: [{ loc: '/' }] },
|
|
1768
|
+
}),
|
|
1769
|
+
)
|
|
1696
1770
|
// GET /robots.txt, GET /sitemap.xml
|
|
1697
1771
|
|
|
1698
|
-
app.use(
|
|
1772
|
+
app.use(
|
|
1773
|
+
seoMiddleware({
|
|
1774
|
+
headers: { 'X-Robots-Tag': (path) => (path.startsWith('/admin') ? 'noindex' : undefined) },
|
|
1775
|
+
}),
|
|
1776
|
+
)
|
|
1699
1777
|
```
|
|
1700
1778
|
|
|
1701
1779
|
Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
|
|
1702
1780
|
|
|
1703
|
-
| Option
|
|
1704
|
-
|
|
1705
|
-
| `baseUrl` | `string`
|
|
1706
|
-
| `robots`
|
|
1707
|
-
| `sitemap` | `SitemapConfig`
|
|
1708
|
-
| `headers` | `SeoHeadersConfig` | —
|
|
1781
|
+
| Option | Type | Default | Description |
|
|
1782
|
+
| --------- | ------------------ | ---------------------------------- | ----------------------------------------------- |
|
|
1783
|
+
| `baseUrl` | `string` | — | Base URL for sitemap URLs |
|
|
1784
|
+
| `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
|
|
1785
|
+
| `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
|
|
1786
|
+
| `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
|
|
1709
1787
|
|
|
1710
1788
|
### session [α] [Security]
|
|
1711
1789
|
|
|
1712
1790
|
Cookie-based server-side session management with memory and Redis stores.
|
|
1713
1791
|
|
|
1714
1792
|
```ts
|
|
1715
|
-
app.use(session())
|
|
1716
|
-
app.use(session({ store: 'redis', redis: ctx.redis }))
|
|
1793
|
+
app.use(session()) // in-memory store (default)
|
|
1794
|
+
app.use(session({ store: 'redis', redis: ctx.redis })) // Redis store
|
|
1717
1795
|
app.use(session({ store: 'redis', redis, ttl: 30 * 60_000, cookieName: 'sid' }))
|
|
1718
1796
|
|
|
1719
1797
|
app.get('/login', async (req, ctx) => {
|
|
@@ -1724,7 +1802,7 @@ app.get('/login', async (req, ctx) => {
|
|
|
1724
1802
|
})
|
|
1725
1803
|
|
|
1726
1804
|
app.get('/logout', async (req, ctx) => {
|
|
1727
|
-
ctx.session.destroy()
|
|
1805
|
+
ctx.session.destroy() // or ctx.session = null
|
|
1728
1806
|
return Response.json({ ok: true })
|
|
1729
1807
|
})
|
|
1730
1808
|
|
|
@@ -1734,19 +1812,19 @@ app.get('/logout', async (req, ctx) => {
|
|
|
1734
1812
|
// Session mutations are auto-detected on property set/delete
|
|
1735
1813
|
```
|
|
1736
1814
|
|
|
1737
|
-
| Option
|
|
1738
|
-
|
|
1739
|
-
| `store`
|
|
1740
|
-
| `redis`
|
|
1741
|
-
| `ttl`
|
|
1742
|
-
| `cookieName`
|
|
1743
|
-
| `cookie.httpOnly` | `boolean`
|
|
1744
|
-
| `cookie.secure`
|
|
1745
|
-
| `cookie.sameSite` | `string`
|
|
1746
|
-
| `cookie.path`
|
|
1747
|
-
| `cookie.domain`
|
|
1748
|
-
| `secret`
|
|
1749
|
-
| `rotateInterval`
|
|
1815
|
+
| Option | Type | Default | Description |
|
|
1816
|
+
| ----------------- | ------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
1817
|
+
| `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
|
|
1818
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
1819
|
+
| `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
|
|
1820
|
+
| `cookieName` | `string` | `'__session'` | Cookie name |
|
|
1821
|
+
| `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
|
|
1822
|
+
| `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
|
|
1823
|
+
| `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
|
|
1824
|
+
| `cookie.path` | `string` | `'/'` | Cookie path |
|
|
1825
|
+
| `cookie.domain` | `string` | — | Cookie domain |
|
|
1826
|
+
| `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
|
|
1827
|
+
| `rotateInterval` | `number` | `900000` (15min) | Auto-rotate session ID to prevent fixation attacks. Set `0` to disable |
|
|
1750
1828
|
|
|
1751
1829
|
When `secret` is set, the cookie value is signed with HMAC-SHA256:
|
|
1752
1830
|
`uuid.base64url(hmac)`. Tampered cookies are rejected and treated as new
|
|
@@ -1761,7 +1839,7 @@ from the store. Rotation happens transparently on the next request after
|
|
|
1761
1839
|
```ts
|
|
1762
1840
|
import { MemoryStore, RedisStore } from 'weifuwu'
|
|
1763
1841
|
|
|
1764
|
-
const mem = new MemoryStore()
|
|
1842
|
+
const mem = new MemoryStore() // auto-cleanup every 60s
|
|
1765
1843
|
await mem.set('sid', { userId: 1 }, 86400000)
|
|
1766
1844
|
mem.close()
|
|
1767
1845
|
|
|
@@ -1800,14 +1878,14 @@ app.use('/', ssr({ dir: './ui' }))
|
|
|
1800
1878
|
└── lib/ ← utilities (does not affect routing)
|
|
1801
1879
|
```
|
|
1802
1880
|
|
|
1803
|
-
| Location
|
|
1804
|
-
|
|
1805
|
-
| `app/page.tsx`
|
|
1806
|
-
| `app/[param]/page.tsx` | `GET /:param`
|
|
1807
|
-
| `app/layout.tsx`
|
|
1808
|
-
| `app/not-found.tsx`
|
|
1809
|
-
| `app/error.tsx`
|
|
1810
|
-
| `app/globals.css`
|
|
1881
|
+
| Location | Route |
|
|
1882
|
+
| ---------------------- | -------------------------------------------------------- |
|
|
1883
|
+
| `app/page.tsx` | `GET /` |
|
|
1884
|
+
| `app/[param]/page.tsx` | `GET /:param` |
|
|
1885
|
+
| `app/layout.tsx` | Root layout (wraps all pages in its subtree) |
|
|
1886
|
+
| `app/not-found.tsx` | 404 fallback for that subtree |
|
|
1887
|
+
| `app/error.tsx` | Error boundary for that subtree |
|
|
1888
|
+
| `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
|
|
1811
1889
|
|
|
1812
1890
|
**How hydration works:**
|
|
1813
1891
|
|
|
@@ -1824,7 +1902,7 @@ app.use('/', ssr({ dir: './ui' }))
|
|
|
1824
1902
|
|
|
1825
1903
|
```ts
|
|
1826
1904
|
// Multiple independent SSR directories
|
|
1827
|
-
app.use('/',
|
|
1905
|
+
app.use('/', ssr({ dir: './www' }))
|
|
1828
1906
|
app.use('/admin', ssr({ dir: './admin' }))
|
|
1829
1907
|
|
|
1830
1908
|
// API routes coexist normally
|
|
@@ -1840,31 +1918,35 @@ Multi-tenant BaaS with dynamic table API and GraphQL.
|
|
|
1840
1918
|
```ts
|
|
1841
1919
|
const t = tenant({ pg, usersTable: '_users' })
|
|
1842
1920
|
await t.migrate()
|
|
1843
|
-
app.use('/api', t.middleware())
|
|
1844
|
-
app.use('/api', t)
|
|
1921
|
+
app.use('/api', t.middleware()) // → ctx.tenant
|
|
1922
|
+
app.use('/api', t) // dynamic CRUD
|
|
1845
1923
|
app.use('/graphql', t.graphql()) // dynamic GraphQL
|
|
1846
1924
|
```
|
|
1847
1925
|
|
|
1848
|
-
| Option
|
|
1849
|
-
|
|
1850
|
-
| `pg`
|
|
1851
|
-
| `usersTable` | `string` | —
|
|
1926
|
+
| Option | Type | Default | Description |
|
|
1927
|
+
| ------------ | -------- | ------- | --------------------------------------------- |
|
|
1928
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1929
|
+
| `usersTable` | `string` | — | Users table name for tenant membership lookup |
|
|
1852
1930
|
|
|
1853
1931
|
### upload [α] [DevTools]
|
|
1854
1932
|
|
|
1855
1933
|
```ts
|
|
1856
|
-
app.post(
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
})
|
|
1934
|
+
app.post(
|
|
1935
|
+
'/upload',
|
|
1936
|
+
upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }),
|
|
1937
|
+
(req, ctx) => {
|
|
1938
|
+
// ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
|
|
1939
|
+
// Multiple files with same field name → array
|
|
1940
|
+
// ctx.parsed.fields.title → 'hello'
|
|
1941
|
+
},
|
|
1942
|
+
)
|
|
1861
1943
|
```
|
|
1862
1944
|
|
|
1863
|
-
| Option
|
|
1864
|
-
|
|
1865
|
-
| `dir`
|
|
1866
|
-
| `maxFileSize`
|
|
1867
|
-
| `allowedTypes` | `string[]` | —
|
|
1945
|
+
| Option | Type | Default | Description |
|
|
1946
|
+
| -------------- | ---------- | ------- | ---------------------------------------- |
|
|
1947
|
+
| `dir` | `string` | — | Write files to disk (omit for in-memory) |
|
|
1948
|
+
| `maxFileSize` | `number` | — | Max bytes per file |
|
|
1949
|
+
| `allowedTypes` | `string[]` | — | Allowed MIME types |
|
|
1868
1950
|
|
|
1869
1951
|
### user [β] [Security]
|
|
1870
1952
|
|
|
@@ -1874,7 +1956,8 @@ Authentication: register, login, JWT, OAuth2 服务端, 社会化登录.
|
|
|
1874
1956
|
const u = user({
|
|
1875
1957
|
pg,
|
|
1876
1958
|
jwtSecret: process.env.JWT_SECRET!,
|
|
1877
|
-
oauthLogin: {
|
|
1959
|
+
oauthLogin: {
|
|
1960
|
+
// 可选 — 社会化登录
|
|
1878
1961
|
providers: {
|
|
1879
1962
|
github: { clientId: '...', clientSecret: '...' },
|
|
1880
1963
|
google: { clientId: '...', clientSecret: '...' },
|
|
@@ -1882,26 +1965,26 @@ const u = user({
|
|
|
1882
1965
|
},
|
|
1883
1966
|
})
|
|
1884
1967
|
await u.migrate()
|
|
1885
|
-
app.use(u)
|
|
1886
|
-
app.use(u.middleware())
|
|
1968
|
+
app.use(u) // POST /register, POST /login
|
|
1969
|
+
app.use(u.middleware()) // ctx.user
|
|
1887
1970
|
// GET /auth/github, GET /auth/github/callback (如配置 oauthLogin)
|
|
1888
1971
|
```
|
|
1889
1972
|
|
|
1890
|
-
| Option
|
|
1891
|
-
|
|
1892
|
-
| `pg`
|
|
1893
|
-
| `jwtSecret`
|
|
1894
|
-
| `table`
|
|
1895
|
-
| `expiresIn`
|
|
1896
|
-
| `oauth2`
|
|
1897
|
-
| `oauthLogin` | `object` | —
|
|
1973
|
+
| Option | Type | Default | Description |
|
|
1974
|
+
| ------------ | -------- | ---------- | ------------------------------------------------------------------------------ |
|
|
1975
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1976
|
+
| `jwtSecret` | `string` | — | JWT signing secret |
|
|
1977
|
+
| `table` | `string` | `'_users'` | Users table name |
|
|
1978
|
+
| `expiresIn` | `string` | `'24h'` | JWT expiration |
|
|
1979
|
+
| `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
|
|
1980
|
+
| `oauthLogin` | `object` | — | 社会化登录: `{ providers: Record<string, OAuthProviderConfig>, redirectUrl? }` |
|
|
1898
1981
|
|
|
1899
|
-
| Method
|
|
1900
|
-
|
|
1901
|
-
| `.register(data)` | Register a new user programmatically
|
|
1902
|
-
| `.login(data)`
|
|
1903
|
-
| `.verify(token)`
|
|
1904
|
-
| `.middleware()`
|
|
1982
|
+
| Method | Description |
|
|
1983
|
+
| ----------------- | --------------------------------------- |
|
|
1984
|
+
| `.register(data)` | Register a new user programmatically |
|
|
1985
|
+
| `.login(data)` | Log in programmatically |
|
|
1986
|
+
| `.verify(token)` | Verify JWT token |
|
|
1987
|
+
| `.middleware()` | JWT verify middleware — sets `ctx.user` |
|
|
1905
1988
|
|
|
1906
1989
|
### permissions [α] — RBAC [Security]
|
|
1907
1990
|
|
|
@@ -1915,11 +1998,14 @@ await perm.migrate()
|
|
|
1915
1998
|
await perm.assignRole(userId, 'admin')
|
|
1916
1999
|
await perm.grantPermission('admin', 'posts:create')
|
|
1917
2000
|
await perm.grantPermission('admin', 'posts:edit')
|
|
1918
|
-
await perm.grantPermission('admin', '*')
|
|
2001
|
+
await perm.grantPermission('admin', '*') // wildcard — all permissions
|
|
1919
2002
|
|
|
1920
2003
|
// Use as middleware
|
|
1921
|
-
app.use((req, ctx, next) => {
|
|
1922
|
-
|
|
2004
|
+
app.use((req, ctx, next) => {
|
|
2005
|
+
ctx.user = { id: userId }
|
|
2006
|
+
return next(req, ctx)
|
|
2007
|
+
})
|
|
2008
|
+
app.use(perm) // → ctx.roles, ctx.permissions
|
|
1923
2009
|
|
|
1924
2010
|
// Route guards
|
|
1925
2011
|
app.get('/admin', perm.requireRole('admin'), adminHandler)
|
|
@@ -1934,26 +2020,26 @@ app.get('/posts/:id', async (req, ctx) => {
|
|
|
1934
2020
|
})
|
|
1935
2021
|
```
|
|
1936
2022
|
|
|
1937
|
-
| Option
|
|
1938
|
-
|
|
1939
|
-
| `pg`
|
|
1940
|
-
| `prefix` | `string` | `''`
|
|
2023
|
+
| Option | Type | Default | Description |
|
|
2024
|
+
| -------- | -------- | ------- | --------------------------------------------- |
|
|
2025
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
2026
|
+
| `prefix` | `string` | `''` | Table prefix (e.g. `'myapp'` → `myapp_roles`) |
|
|
1941
2027
|
|
|
1942
|
-
| Method
|
|
1943
|
-
|
|
1944
|
-
| `.assignRole(userId, role)`
|
|
1945
|
-
| `.removeRole(userId, role)`
|
|
1946
|
-
| `.grantPermission(role, permission)`
|
|
1947
|
-
| `.revokePermission(role, permission)` | Revoke permission from role
|
|
1948
|
-
| `.getUserRoles(userId)`
|
|
1949
|
-
| `.getUserPermissions(userId)`
|
|
1950
|
-
| `.requireRole(...roles)`
|
|
1951
|
-
| `.requirePermission(...perms)`
|
|
1952
|
-
| `.migrate()`
|
|
2028
|
+
| Method | Description |
|
|
2029
|
+
| ------------------------------------- | --------------------------------------------------- |
|
|
2030
|
+
| `.assignRole(userId, role)` | Assign role to user (creates role if missing) |
|
|
2031
|
+
| `.removeRole(userId, role)` | Remove role from user |
|
|
2032
|
+
| `.grantPermission(role, permission)` | Grant permission to role |
|
|
2033
|
+
| `.revokePermission(role, permission)` | Revoke permission from role |
|
|
2034
|
+
| `.getUserRoles(userId)` | List user's roles |
|
|
2035
|
+
| `.getUserPermissions(userId)` | List user's permissions (union of all roles) |
|
|
2036
|
+
| `.requireRole(...roles)` | Middleware — rejects if user lacks any of the roles |
|
|
2037
|
+
| `.requirePermission(...perms)` | Middleware — rejects if user lacks any permission |
|
|
2038
|
+
| `.migrate()` | Create tables |
|
|
1953
2039
|
|
|
1954
2040
|
### validate [α] [DevTools]
|
|
1955
2041
|
|
|
1956
|
-
|
|
2042
|
+
````ts
|
|
1957
2043
|
import { z } from 'zod'
|
|
1958
2044
|
const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
|
|
1959
2045
|
app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string().optional() }) }), (req, ctx) => {
|
|
@@ -1976,14 +2062,14 @@ app.post('/contact', validate(), (req, ctx) => {
|
|
|
1976
2062
|
|
|
1977
2063
|
// Or validate with Zod
|
|
1978
2064
|
app.post('/contact', validate({ body: z.object({ email: z.string().email() }) }), handler)
|
|
1979
|
-
|
|
2065
|
+
````
|
|
1980
2066
|
|
|
1981
|
-
| Option
|
|
1982
|
-
|
|
1983
|
-
| `body`
|
|
1984
|
-
| `query`
|
|
1985
|
-
| `params`
|
|
1986
|
-
| `headers` | `ZodSchema` | —
|
|
2067
|
+
| Option | Type | Default | Description |
|
|
2068
|
+
| --------- | ----------- | ------- | ------------------------------------- |
|
|
2069
|
+
| `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
|
|
2070
|
+
| `query` | `ZodSchema` | — | Query validation schema |
|
|
2071
|
+
| `params` | `ZodSchema` | — | URL params validation schema |
|
|
2072
|
+
| `headers` | `ZodSchema` | — | Header validation schema |
|
|
1987
2073
|
|
|
1988
2074
|
### webhook [β] [API]
|
|
1989
2075
|
|
|
@@ -2013,14 +2099,14 @@ wh.on('*', (event) => {
|
|
|
2013
2099
|
})
|
|
2014
2100
|
```
|
|
2015
2101
|
|
|
2016
|
-
| Option
|
|
2017
|
-
|
|
2018
|
-
| `stripe`
|
|
2019
|
-
| `github`
|
|
2020
|
-
| `slack`
|
|
2021
|
-
| `custom`
|
|
2022
|
-
| `replayProtection` | `boolean`
|
|
2023
|
-
| `idempotencyTTL`
|
|
2102
|
+
| Option | Type | Default | Description |
|
|
2103
|
+
| ------------------ | ------------------------ | --------- | ----------------------------------- |
|
|
2104
|
+
| `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
|
|
2105
|
+
| `github` | `PlatformConfig` | — | GitHub webhook config |
|
|
2106
|
+
| `slack` | `PlatformConfig` | — | Slack webhook config |
|
|
2107
|
+
| `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
|
|
2108
|
+
| `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
|
|
2109
|
+
| `idempotencyTTL` | `number` | `3600000` | Dedup TTL (ms) |
|
|
2024
2110
|
|
|
2025
2111
|
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.
|
|
2026
2112
|
|
|
@@ -2028,11 +2114,12 @@ Built-in verifiers handle HMAC-SHA256, timestamp validation (Slack's 5-min windo
|
|
|
2028
2114
|
|
|
2029
2115
|
```tsx
|
|
2030
2116
|
import { Link, navigate, useNavigate, useNavigating } from 'weifuwu/react'
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2117
|
+
;<Link href="/about" prefetch>
|
|
2118
|
+
About
|
|
2119
|
+
</Link> // client-side nav + prefetch on hover/visible
|
|
2120
|
+
const n = useNavigate() // hook: n('/contact')
|
|
2121
|
+
navigate('/contact') // bare function (no hook needed)
|
|
2122
|
+
const loading = useNavigating() // reactive loading state
|
|
2036
2123
|
```
|
|
2037
2124
|
|
|
2038
2125
|
`navigate()` fetches the SSR page, extracts the root container content, and replaces it in-place. Middleware runs on server each nav — data is always fresh.
|
|
@@ -2089,11 +2176,11 @@ function LangSwitch() {
|
|
|
2089
2176
|
}
|
|
2090
2177
|
```
|
|
2091
2178
|
|
|
2092
|
-
| Return
|
|
2093
|
-
|
|
2094
|
-
| `locale`
|
|
2179
|
+
| Return | Description |
|
|
2180
|
+
| ------------------- | ----------------------------------------------------- |
|
|
2181
|
+
| `locale` | Current locale string (from `ctx.i18n.locale`) |
|
|
2095
2182
|
| `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
|
|
2096
|
-
| `t`
|
|
2183
|
+
| `t` | Translate a key using loaded locale messages |
|
|
2097
2184
|
|
|
2098
2185
|
```tsx
|
|
2099
2186
|
import { useTheme } from 'weifuwu/react'
|
|
@@ -2101,8 +2188,8 @@ function ThemeToggle() {
|
|
|
2101
2188
|
const { theme, resolvedTheme, setTheme } = useTheme()
|
|
2102
2189
|
return (
|
|
2103
2190
|
<>
|
|
2104
|
-
<span>Current: {resolvedTheme}</span>
|
|
2105
|
-
<select value={theme} onChange={e => setTheme(e.target.value)}>
|
|
2191
|
+
<span>Current: {resolvedTheme}</span> {/* 'dark' | 'light' — never 'system' */}
|
|
2192
|
+
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
|
|
2106
2193
|
<option value="light">☀ Light</option>
|
|
2107
2194
|
<option value="dark">🌙 Dark</option>
|
|
2108
2195
|
<option value="system">💻 System</option>
|
|
@@ -2112,11 +2199,11 @@ function ThemeToggle() {
|
|
|
2112
2199
|
}
|
|
2113
2200
|
```
|
|
2114
2201
|
|
|
2115
|
-
| Return
|
|
2116
|
-
|
|
2117
|
-
| `theme`
|
|
2118
|
-
| `resolvedTheme`
|
|
2119
|
-
| `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`)
|
|
2202
|
+
| Return | Description |
|
|
2203
|
+
| ----------------- | ---------------------------------------------------------------- |
|
|
2204
|
+
| `theme` | Raw preference (`'light'` \| `'dark'` \| `'system'`) |
|
|
2205
|
+
| `resolvedTheme` | Resolved value (`'light'` \| `'dark'`) — `'system'` → matchMedia |
|
|
2206
|
+
| `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`) |
|
|
2120
2207
|
|
|
2121
2208
|
**`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
|
|
2122
2209
|
|
|
@@ -2126,7 +2213,13 @@ function ThemeToggle() {
|
|
|
2126
2213
|
import { useLoaderData } from 'weifuwu/react'
|
|
2127
2214
|
function Page() {
|
|
2128
2215
|
const data = useLoaderData<{ posts: Post[] }>()
|
|
2129
|
-
return
|
|
2216
|
+
return (
|
|
2217
|
+
<ul>
|
|
2218
|
+
{data.posts.map((p) => (
|
|
2219
|
+
<li key={p.id}>{p.title}</li>
|
|
2220
|
+
))}
|
|
2221
|
+
</ul>
|
|
2222
|
+
)
|
|
2130
2223
|
}
|
|
2131
2224
|
```
|
|
2132
2225
|
|
|
@@ -2152,7 +2245,7 @@ app.use(flash())
|
|
|
2152
2245
|
|
|
2153
2246
|
// Read flash
|
|
2154
2247
|
app.get('/', (req, ctx) => {
|
|
2155
|
-
const msg = ctx.flash.value
|
|
2248
|
+
const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
|
|
2156
2249
|
})
|
|
2157
2250
|
|
|
2158
2251
|
// Set flash + redirect
|
|
@@ -2174,8 +2267,8 @@ function Toast() {
|
|
|
2174
2267
|
}
|
|
2175
2268
|
```
|
|
2176
2269
|
|
|
2177
|
-
| Option | Type
|
|
2178
|
-
|
|
2270
|
+
| Option | Type | Default | Description |
|
|
2271
|
+
| ------ | -------- | --------- | ----------- |
|
|
2179
2272
|
| `name` | `string` | `'flash'` | Cookie name |
|
|
2180
2273
|
|
|
2181
2274
|
### Dev mode [δ] [Client]
|
|
@@ -2193,7 +2286,17 @@ Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically regi
|
|
|
2193
2286
|
## AI
|
|
2194
2287
|
|
|
2195
2288
|
```ts
|
|
2196
|
-
import {
|
|
2289
|
+
import {
|
|
2290
|
+
openai,
|
|
2291
|
+
streamText,
|
|
2292
|
+
generateText,
|
|
2293
|
+
streamObject,
|
|
2294
|
+
generateObject,
|
|
2295
|
+
tool,
|
|
2296
|
+
embed,
|
|
2297
|
+
embedMany,
|
|
2298
|
+
aiProvider,
|
|
2299
|
+
} from 'weifuwu'
|
|
2197
2300
|
import { runWorkflow } from 'weifuwu'
|
|
2198
2301
|
|
|
2199
2302
|
const provider = aiProvider()
|
|
@@ -2216,23 +2319,23 @@ app.post('/ask', async (req, ctx) => {
|
|
|
2216
2319
|
})
|
|
2217
2320
|
```
|
|
2218
2321
|
|
|
2219
|
-
| Option
|
|
2220
|
-
|
|
2221
|
-
| `baseURL`
|
|
2222
|
-
| `apiKey`
|
|
2223
|
-
| `model`
|
|
2224
|
-
| `embeddingModel`
|
|
2225
|
-
| `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024`
|
|
2226
|
-
|
|
2227
|
-
| Method
|
|
2228
|
-
|
|
2229
|
-
| `.model(name?)`
|
|
2230
|
-
| `.embeddingModel(name?)` | Get `EmbeddingModel` instance
|
|
2231
|
-
| `.embed(text)`
|
|
2232
|
-
| `.embedMany(texts)`
|
|
2233
|
-
| `.generateText(params)`
|
|
2234
|
-
| `.streamText(params)`
|
|
2235
|
-
| `.dimension`
|
|
2322
|
+
| Option | Type | Default | Description |
|
|
2323
|
+
| -------------------- | -------- | -------------------------------------------------------- | -------------------- |
|
|
2324
|
+
| `baseURL` | `string` | `OPENAI_BASE_URL` env or `http://localhost:11434/v1` | API base URL |
|
|
2325
|
+
| `apiKey` | `string` | `OPENAI_API_KEY` env or `'ollama'` | API key |
|
|
2326
|
+
| `model` | `string` | `OPENAI_MODEL` env or `'qwen3:0.6b'` | Chat model name |
|
|
2327
|
+
| `embeddingModel` | `string` | `OPENAI_EMBEDDING_MODEL` env or `'qwen3-embedding:0.6b'` | Embedding model name |
|
|
2328
|
+
| `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024` | Vector dimension |
|
|
2329
|
+
|
|
2330
|
+
| Method | Description |
|
|
2331
|
+
| ------------------------ | --------------------------------------- |
|
|
2332
|
+
| `.model(name?)` | Get `LanguageModel` instance |
|
|
2333
|
+
| `.embeddingModel(name?)` | Get `EmbeddingModel` instance |
|
|
2334
|
+
| `.embed(text)` | Embed single text → `Promise<number[]>` |
|
|
2335
|
+
| `.embedMany(texts)` | Batch embed → `Promise<number[][]>` |
|
|
2336
|
+
| `.generateText(params)` | Generate text (model auto-injected) |
|
|
2337
|
+
| `.streamText(params)` | Stream text (model auto-injected) |
|
|
2338
|
+
| `.dimension` | Configured embedding dimension |
|
|
2236
2339
|
|
|
2237
2340
|
### DAG Workflow [AI]
|
|
2238
2341
|
|
|
@@ -2246,12 +2349,12 @@ const wf = runWorkflow({ tools, provider })
|
|
|
2246
2349
|
const wf = runWorkflow({ tools, model: openai('gpt-4o') })
|
|
2247
2350
|
```
|
|
2248
2351
|
|
|
2249
|
-
| Option
|
|
2250
|
-
|
|
2251
|
-
| `tools`
|
|
2252
|
-
| `provider` | `AIProvider`
|
|
2253
|
-
| `model`
|
|
2254
|
-
| `maxSteps` | `number`
|
|
2352
|
+
| Option | Type | Default | Description |
|
|
2353
|
+
| ---------- | --------------- | ------- | ---------------------------------------------------------------- |
|
|
2354
|
+
| `tools` | `object` | — | Registered tool definitions |
|
|
2355
|
+
| `provider` | `AIProvider` | — | AI provider (uses `provider.model()` for LLM-generated workflow) |
|
|
2356
|
+
| `model` | `LanguageModel` | — | Explicit model (overrides provider) |
|
|
2357
|
+
| `maxSteps` | `number` | `200` | Max execution steps |
|
|
2255
2358
|
|
|
2256
2359
|
---
|
|
2257
2360
|
|
|
@@ -2259,7 +2362,10 @@ const wf = runWorkflow({ tools, model: openai('gpt-4o') })
|
|
|
2259
2362
|
|
|
2260
2363
|
```ts
|
|
2261
2364
|
import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
|
|
2262
|
-
async function* events() {
|
|
2365
|
+
async function* events() {
|
|
2366
|
+
yield formatSSE('chat', 'Hello')
|
|
2367
|
+
yield formatSSE('chat', 'World')
|
|
2368
|
+
}
|
|
2263
2369
|
app.get('/stream', (req, ctx) => createSSEStream(events()))
|
|
2264
2370
|
```
|
|
2265
2371
|
|