weifuwu 0.25.2 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +291 -2489
- package/ai/provider.ts +129 -0
- package/ai/stream.ts +63 -0
- package/{dist/cli.d.ts → cli.js} +1 -1
- package/cli.ts +55 -257
- package/core/cookie.ts +114 -0
- package/core/env.ts +142 -0
- package/core/logger.ts +72 -0
- package/core/router.ts +795 -0
- package/core/serve.ts +294 -0
- package/core/sse.ts +85 -0
- package/core/trace.ts +146 -0
- package/graphql.ts +267 -0
- package/hub.ts +133 -0
- package/index.ts +71 -0
- package/mailer.ts +81 -0
- package/middleware/compress.ts +103 -0
- package/middleware/cors.ts +81 -0
- package/middleware/csrf.ts +112 -0
- package/middleware/flash.ts +144 -0
- package/middleware/health.ts +44 -0
- package/middleware/helmet.ts +98 -0
- package/middleware/i18n.ts +175 -0
- package/middleware/rate-limit.ts +167 -0
- package/middleware/request-id.ts +60 -0
- package/middleware/static.ts +149 -0
- package/middleware/theme.ts +84 -0
- package/middleware/upload.ts +168 -0
- package/middleware/validate.ts +186 -0
- package/package.json +15 -36
- package/postgres/client.ts +132 -0
- package/postgres/index.ts +4 -0
- package/postgres/module.ts +37 -0
- package/postgres/schema/columns.ts +186 -0
- package/postgres/schema/index.ts +36 -0
- package/postgres/schema/sql.ts +39 -0
- package/postgres/schema/table.ts +548 -0
- package/postgres/schema/where.ts +99 -0
- package/postgres/types.ts +48 -0
- package/queue/cron.ts +90 -0
- package/queue/index.ts +654 -0
- package/queue/types.ts +60 -0
- package/redis/client.ts +24 -0
- package/{dist/redis/index.d.ts → redis/index.ts} +2 -2
- package/redis/types.ts +28 -0
- package/types.ts +78 -0
- package/cli/template/app.ts +0 -22
- package/cli/template/index.ts +0 -10
- package/cli/template/locales/en.json +0 -13
- package/cli/template/locales/zh-CN.json +0 -13
- package/cli/template/locales/zh-TW.json +0 -13
- package/cli/template/locales/zh.json +0 -13
- package/cli/template/ui/app/globals.css +0 -2
- package/cli/template/ui/app/layout.tsx +0 -15
- package/cli/template/ui/app/page.tsx +0 -124
- package/cli/template/ui/components/Greeting.tsx +0 -3
- package/dist/agent/client.d.ts +0 -2
- package/dist/agent/index.d.ts +0 -2
- package/dist/agent/rest.d.ts +0 -14
- package/dist/agent/run.d.ts +0 -19
- package/dist/agent/types.d.ts +0 -55
- package/dist/ai/provider.d.ts +0 -45
- package/dist/ai/utils.d.ts +0 -5
- package/dist/ai/workflow.d.ts +0 -17
- package/dist/ai-sdk.d.ts +0 -2
- package/dist/ai.d.ts +0 -13
- package/dist/analytics.d.ts +0 -45
- package/dist/auth.d.ts +0 -22
- package/dist/cache.d.ts +0 -74
- package/dist/cli.js +0 -302
- package/dist/client-locale.d.ts +0 -25
- package/dist/client-pref.d.ts +0 -3
- package/dist/client-router.d.ts +0 -300
- package/dist/client-state.d.ts +0 -22
- package/dist/client-theme.d.ts +0 -36
- package/dist/compile.d.ts +0 -15
- package/dist/compress.d.ts +0 -20
- package/dist/cookie.d.ts +0 -36
- package/dist/cors.d.ts +0 -25
- package/dist/cron-utils.d.ts +0 -73
- package/dist/csrf.d.ts +0 -47
- package/dist/deploy/config.d.ts +0 -2
- package/dist/deploy/gateway.d.ts +0 -2
- package/dist/deploy/index.d.ts +0 -4
- package/dist/deploy/manager.d.ts +0 -16
- package/dist/deploy/process.d.ts +0 -14
- package/dist/deploy/types.d.ts +0 -53
- package/dist/env.d.ts +0 -69
- package/dist/error-boundary.d.ts +0 -2
- package/dist/flash.d.ts +0 -90
- package/dist/fts.d.ts +0 -36
- package/dist/graphql.d.ts +0 -16
- package/dist/head.d.ts +0 -6
- package/dist/health.d.ts +0 -24
- package/dist/helmet.d.ts +0 -33
- package/dist/html-shell.d.ts +0 -1
- package/dist/hub.d.ts +0 -37
- package/dist/i18n.d.ts +0 -39
- package/dist/iii/client.d.ts +0 -2
- package/dist/iii/index.d.ts +0 -4
- package/dist/iii/register-worker.d.ts +0 -9
- package/dist/iii/rest.d.ts +0 -3
- package/dist/iii/stream.d.ts +0 -82
- package/dist/iii/types.d.ts +0 -121
- package/dist/iii/worker.d.ts +0 -2
- package/dist/iii/ws.d.ts +0 -22
- package/dist/index.d.ts +0 -101
- package/dist/index.js +0 -12752
- package/dist/kb/index.d.ts +0 -3
- package/dist/kb/types.d.ts +0 -72
- package/dist/layout.d.ts +0 -2
- package/dist/live.d.ts +0 -7
- package/dist/logdb/client.d.ts +0 -2
- package/dist/logdb/index.d.ts +0 -2
- package/dist/logdb/rest.d.ts +0 -5
- package/dist/logdb/types.d.ts +0 -27
- package/dist/logger.d.ts +0 -16
- package/dist/mailer.d.ts +0 -51
- package/dist/mcp.d.ts +0 -34
- package/dist/messager/agent.d.ts +0 -11
- package/dist/messager/client.d.ts +0 -2
- package/dist/messager/index.d.ts +0 -2
- package/dist/messager/rest.d.ts +0 -15
- package/dist/messager/types.d.ts +0 -57
- package/dist/messager/ws.d.ts +0 -14
- package/dist/module-server.d.ts +0 -9
- package/dist/not-found.d.ts +0 -2
- package/dist/notifier/client.d.ts +0 -2
- package/dist/notifier/index.d.ts +0 -2
- package/dist/notifier/types.d.ts +0 -105
- package/dist/opencode/client.d.ts +0 -2
- package/dist/opencode/index.d.ts +0 -2
- package/dist/opencode/permissions.d.ts +0 -5
- package/dist/opencode/prompt.d.ts +0 -8
- package/dist/opencode/rest.d.ts +0 -16
- package/dist/opencode/run.d.ts +0 -13
- package/dist/opencode/session.d.ts +0 -26
- package/dist/opencode/skills.d.ts +0 -4
- package/dist/opencode/tools/bash.d.ts +0 -6
- package/dist/opencode/tools/edit.d.ts +0 -19
- package/dist/opencode/tools/glob.d.ts +0 -9
- package/dist/opencode/tools/grep.d.ts +0 -17
- package/dist/opencode/tools/index.d.ts +0 -12
- package/dist/opencode/tools/question.d.ts +0 -5
- package/dist/opencode/tools/read.d.ts +0 -16
- package/dist/opencode/tools/skill.d.ts +0 -18
- package/dist/opencode/tools/web.d.ts +0 -18
- package/dist/opencode/tools/write.d.ts +0 -13
- package/dist/opencode/types.d.ts +0 -90
- package/dist/opencode/ws.d.ts +0 -21
- package/dist/permissions.d.ts +0 -51
- package/dist/postgres/client.d.ts +0 -4
- package/dist/postgres/index.d.ts +0 -4
- package/dist/postgres/module.d.ts +0 -17
- package/dist/postgres/schema/columns.d.ts +0 -99
- package/dist/postgres/schema/index.d.ts +0 -6
- package/dist/postgres/schema/sql.d.ts +0 -22
- package/dist/postgres/schema/table.d.ts +0 -141
- package/dist/postgres/schema/where.d.ts +0 -29
- package/dist/postgres/types.d.ts +0 -50
- package/dist/queue/index.d.ts +0 -2
- package/dist/queue/types.d.ts +0 -62
- package/dist/rate-limit.d.ts +0 -45
- package/dist/react.d.ts +0 -14
- package/dist/react.js +0 -751
- package/dist/redis/client.d.ts +0 -2
- package/dist/redis/types.d.ts +0 -18
- package/dist/request-id.d.ts +0 -40
- package/dist/router.d.ts +0 -73
- package/dist/s3.d.ts +0 -68
- package/dist/seo.d.ts +0 -104
- package/dist/serve.d.ts +0 -38
- package/dist/server-registry.d.ts +0 -10
- package/dist/session.d.ts +0 -117
- package/dist/sse.d.ts +0 -47
- package/dist/ssr-entries.d.ts +0 -4
- package/dist/ssr.d.ts +0 -11
- package/dist/static.d.ts +0 -23
- package/dist/stream.d.ts +0 -24
- package/dist/tailwind.d.ts +0 -15
- package/dist/tenant/client.d.ts +0 -2
- package/dist/tenant/graphql.d.ts +0 -3
- package/dist/tenant/index.d.ts +0 -2
- package/dist/tenant/rest.d.ts +0 -3
- package/dist/tenant/schema.d.ts +0 -5
- package/dist/tenant/types.d.ts +0 -48
- package/dist/tenant/utils.d.ts +0 -9
- package/dist/test-utils.d.ts +0 -194
- package/dist/theme.d.ts +0 -31
- package/dist/trace.d.ts +0 -95
- package/dist/tsx-context.d.ts +0 -32
- package/dist/types.d.ts +0 -47
- package/dist/upload.d.ts +0 -55
- package/dist/use-action.d.ts +0 -42
- package/dist/use-agent-stream.d.ts +0 -49
- package/dist/use-flash-message.d.ts +0 -17
- package/dist/use-websocket.d.ts +0 -42
- package/dist/user/client.d.ts +0 -30
- package/dist/user/index.d.ts +0 -2
- package/dist/user/oauth-login.d.ts +0 -21
- package/dist/user/oauth2.d.ts +0 -31
- package/dist/user/types.d.ts +0 -178
- package/dist/validate.d.ts +0 -32
- package/dist/vendor.d.ts +0 -7
- package/dist/webhook.d.ts +0 -79
- package/opencode/ui/app/globals.css +0 -1
- package/opencode/ui/app/layout.tsx +0 -13
- package/opencode/ui/app/page.tsx +0 -523
package/README.md
CHANGED
|
@@ -1,2722 +1,524 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: weifuwu
|
|
3
|
-
description: Web-standard HTTP framework for Node.js — (req, ctx) => Response
|
|
4
|
-
---
|
|
5
|
-
|
|
6
1
|
# weifuwu
|
|
7
2
|
|
|
8
|
-
**Web-standard HTTP
|
|
9
|
-
|
|
10
|
-
## Quick Start
|
|
3
|
+
**Web-standard HTTP microframework for Node.js** — `(req, ctx) => Response`
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
import { serve } from 'weifuwu'
|
|
14
|
-
serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
|
|
15
|
-
```
|
|
5
|
+
Pure Node.js, no build step. Native TypeScript via Node.js 24+.
|
|
16
6
|
|
|
17
|
-
```ts
|
|
18
|
-
import { serve, Router, ssr } from 'weifuwu'
|
|
19
|
-
const app = new Router()
|
|
20
|
-
app.use('/', ssr({ dir: './ui' }))
|
|
21
|
-
serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
|
|
22
7
|
```
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npx weifuwu init my-app && cd my-app && npm run dev
|
|
8
|
+
npm install weifuwu
|
|
26
9
|
```
|
|
27
10
|
|
|
28
|
-
|
|
11
|
+
---
|
|
29
12
|
|
|
30
|
-
|
|
13
|
+
## Quick start
|
|
31
14
|
|
|
32
15
|
```ts
|
|
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'
|
|
16
|
+
import { serve, Router } from 'weifuwu'
|
|
47
17
|
|
|
48
18
|
const app = new Router()
|
|
19
|
+
app.get('/', () => new Response('Hello world!'))
|
|
20
|
+
app.get('/api/ping', () => Response.json({ pong: true }))
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
app.use(logger())
|
|
52
|
-
|
|
53
|
-
// 2. UX middleware — single-line auto-registers middleware + routes
|
|
54
|
-
app.use(theme())
|
|
55
|
-
app.use(i18n({ default: 'zh', dir: './locales' }))
|
|
56
|
-
app.use(flash())
|
|
57
|
-
|
|
58
|
-
// 3. Database
|
|
59
|
-
const pg = postgres()
|
|
60
|
-
app.use(pg)
|
|
61
|
-
|
|
62
|
-
// 4. Session & Auth
|
|
63
|
-
app.use(session({ store: 'redis', redis: myRedis }))
|
|
64
|
-
const auth = user({ pg, jwtSecret: process.env.JWT_SECRET })
|
|
65
|
-
await auth.migrate()
|
|
66
|
-
app.use(auth) // auto-registers middleware + /register, /login
|
|
67
|
-
app.use('/auth', auth) // explicit path mounts for more control
|
|
68
|
-
|
|
69
|
-
// 5. API protection
|
|
70
|
-
app.use('/api', rateLimit({ max: 60, window: 60_000 }))
|
|
71
|
-
|
|
72
|
-
// 6. AI
|
|
73
|
-
app.use(aiProvider()) // ctx.ai
|
|
74
|
-
|
|
75
|
-
// 7. SSR
|
|
76
|
-
app.use('/', ssr({ dir: './ui' }))
|
|
77
|
-
|
|
78
|
-
// 8. REST API
|
|
79
|
-
app.get('/api/ping', () => Response.json({ ok: true }))
|
|
80
|
-
app.post('/api/chat', async (req, ctx) => {
|
|
81
|
-
const { prompt } = await req.json()
|
|
82
|
-
const result = await ctx.ai.generateText({ prompt })
|
|
83
|
-
return Response.json(result)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
// 9. Start
|
|
87
|
-
const server = serve(app.handler(), { port: 3000 })
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
npx weifuwu init my-app # Full project (SSR + i18n + theme + WS demo)
|
|
92
|
-
npx weifuwu init my-api --minimal # Minimal HTTP project (2 files)
|
|
93
|
-
npx weifuwu init my-api --skip-install # Skip npm install
|
|
94
|
-
npx weifuwu dev # Start dev server (auto-detect index.ts)
|
|
95
|
-
npx weifuwu generate module my-mod # Scaffold middleware module + test
|
|
96
|
-
npx weifuwu version # Print version
|
|
22
|
+
serve(app.handler(), { port: 3000 })
|
|
97
23
|
```
|
|
98
24
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
## Core Concepts
|
|
102
|
-
|
|
103
|
-
### serve()
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
const server = serve(handler, { port: 3000 })
|
|
107
|
-
await server.ready
|
|
108
|
-
```
|
|
25
|
+
## Core concepts
|
|
109
26
|
|
|
110
|
-
|
|
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 |
|
|
27
|
+
### Handler
|
|
121
28
|
|
|
122
29
|
```ts
|
|
123
|
-
|
|
124
|
-
stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
|
|
125
|
-
readonly port: number
|
|
126
|
-
readonly hostname: string
|
|
127
|
-
ready: Promise<void>
|
|
128
|
-
}
|
|
129
|
-
const { server, url } = await createTestServer(handler)
|
|
30
|
+
type Handler = (req: Request, ctx: Context) => Response | Promise<Response>
|
|
130
31
|
```
|
|
131
32
|
|
|
132
|
-
`
|
|
133
|
-
closes idle keep-alive sockets, then waits for in-flight requests to complete.
|
|
134
|
-
If they don't finish within `timeoutMs` (default 10 seconds), remaining connections
|
|
135
|
-
are forcibly closed. SIGTERM/SIGINT use the same graceful pattern.
|
|
33
|
+
Standard `Request` in, standard `Response` out. No framework-specific request/response objects.
|
|
136
34
|
|
|
137
35
|
### Router
|
|
138
36
|
|
|
139
37
|
```ts
|
|
140
38
|
const app = new Router()
|
|
141
|
-
app.get('/
|
|
142
|
-
app.post('/
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
})
|
|
146
|
-
app.use('/admin', authMW) // path-scoped middleware
|
|
147
|
-
app.use('/admin', adminRouter) // sub-router (flattened into parent trie)
|
|
148
|
-
app.ws('/echo', {
|
|
149
|
-
open(ws, ctx) {
|
|
150
|
-
ctx.ws.json({ type: 'connected' })
|
|
151
|
-
},
|
|
152
|
-
message(ws, ctx, data) {
|
|
153
|
-
ctx.ws.json({ echo: data.toString() })
|
|
154
|
-
},
|
|
155
|
-
})
|
|
156
|
-
app.ws('/chat', {
|
|
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
|
-
},
|
|
163
|
-
})
|
|
164
|
-
app.onError((err, req, ctx) => Response.json({ error: err.message }, { status: 500 }))
|
|
165
|
-
|
|
166
|
-
// Debug: list all registered routes
|
|
167
|
-
console.log(app.routes())
|
|
168
|
-
// [ 'GET /hello/:name', 'POST /data', 'WS /echo', 'WS /chat' ]
|
|
169
|
-
|
|
170
|
-
// Cross-process WebSocket broadcast (Redis)
|
|
171
|
-
import { createHub } from 'weifuwu'
|
|
172
|
-
app.wsHub(createHub({ redis: redis() }))
|
|
173
|
-
|
|
174
|
-
const handler = app.handler()
|
|
175
|
-
const wsHandler = app.websocketHandler()
|
|
176
|
-
serve(handler, { port: 3000, websocket: wsHandler })
|
|
177
|
-
```
|
|
178
|
-
|
|
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` |
|
|
184
|
-
|
|
185
|
-
Query params → `ctx.query`.
|
|
186
|
-
|
|
187
|
-
### Request lifecycle
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
Request → serve() → app.handler() → global middleware × N → path middleware × N → route handler → Response
|
|
191
|
-
↑
|
|
192
|
-
mountPath set by sub-router
|
|
39
|
+
app.get('/users', handler)
|
|
40
|
+
app.post('/users', handler)
|
|
41
|
+
app.get('/users/:id', handler)
|
|
42
|
+
app.ws('/chat', { message(ws, ctx, data) { ... } })
|
|
193
43
|
```
|
|
194
44
|
|
|
195
|
-
|
|
196
|
-
2. `app.handler()` creates `ctx = { params, query }` and routes to the matching trie node
|
|
197
|
-
3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
|
|
198
|
-
4. **Path‑scoped middleware** runs for matching paths (e.g. `app.use('/admin', authMW)`)
|
|
199
|
-
5. **Route‑level middleware** runs (e.g. `app.get('/admin', validate(...), handler)`)
|
|
200
|
-
6. **Route handler** returns `Response` — middleware chain unwinds
|
|
201
|
-
|
|
202
|
-
Sub-routers (`app.use('/admin', adminRouter)`) are **flattened** into the parent trie. The sub-router's global middleware merges with the parent's. `ctx.mountPath` is set when entering a sub-router, allowing each module to derive its own paths.
|
|
45
|
+
Returns a `handler()` function compatible with `serve()`.
|
|
203
46
|
|
|
204
47
|
### Middleware
|
|
205
48
|
|
|
206
49
|
```ts
|
|
207
50
|
type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>
|
|
208
|
-
app.use(mw) // global
|
|
209
|
-
app.use('/admin', mw) // path-scoped
|
|
210
|
-
app.get('/admin', mw, handler) // route-level
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### Middleware Dependency Checking
|
|
214
|
-
|
|
215
|
-
Middleware factories can declare what ctx fields they inject and depend on via
|
|
216
|
-
`__meta`. The Router warns at registration time if a dependency is unsatisfied.
|
|
217
|
-
|
|
218
|
-
```ts
|
|
219
|
-
// postgres() declares: __meta = { injects: ['sql'], depends: [] }
|
|
220
|
-
// session() declares: __meta = { injects: ['session'], depends: [] }
|
|
221
|
-
// user() declares: __meta = { injects: ['user'], depends: ['sql', 'session'] }
|
|
222
|
-
|
|
223
|
-
const app = new Router()
|
|
224
|
-
app.use(user()) // ⚠️ Warns: depends on 'sql' and 'session' but they aren't registered
|
|
225
|
-
// → "[weifuwu] Middleware at "global" depends on ctx.sql but it hasn't been registered yet."
|
|
226
|
-
// → "Register the provider before this middleware: app.use(sql())"
|
|
227
|
-
|
|
228
|
-
// Correct order:
|
|
229
|
-
app.use(postgres())
|
|
230
|
-
app.use(session())
|
|
231
|
-
app.use(user())
|
|
232
51
|
```
|
|
233
52
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
```ts
|
|
237
|
-
function myMiddleware() {
|
|
238
|
-
const mw = async (req, ctx, next) => {
|
|
239
|
-
ctx.myField = await setup()
|
|
240
|
-
return next(req, ctx)
|
|
241
|
-
}
|
|
242
|
-
mw.__meta = { injects: ['myField'], depends: ['sql'] }
|
|
243
|
-
return mw
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
The check is purely advisory — warnings go to `console.warn`, no errors are thrown. Built-in
|
|
248
|
-
middleware (`postgres`, `redis`, `session`, `aiProvider`, `rateLimit`) all have `__meta` pre-attached.
|
|
249
|
-
|
|
250
|
-
_New in v0.25._
|
|
251
|
-
|
|
252
|
-
### Context
|
|
253
|
-
|
|
254
|
-
The `ctx` object accumulates properties as it passes through the middleware chain. Below are all documented properties:
|
|
255
|
-
|
|
256
|
-
| Property | Set by | Type | Description |
|
|
257
|
-
| ------------- | -------------------------------- | ------------------------- | ------------------------------------ |
|
|
258
|
-
| `params` | Router | `Record<string, string>` | URL path parameters |
|
|
259
|
-
| `query` | Router | `Record<string, string>` | URL query parameters |
|
|
260
|
-
| `mountPath` | Router | `string` | Current sub-router mount prefix |
|
|
261
|
-
| `env` | `loadEnv()` | `Record<string, string>` | Public env vars (`WEIFUWU_PUBLIC_*`) |
|
|
262
|
-
| `csrf.token` | `csrf()` | `string` | CSRF token (namespace) |
|
|
263
|
-
| `requestId` | `requestId()` | `string` | Request ID |
|
|
264
|
-
| `session` | `session()` | `Session` | Session data object |
|
|
265
|
-
| `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
|
|
266
|
-
| `redis` | `redis()` | `Redis` | Redis client |
|
|
267
|
-
| `ai` | `aiProvider()` | `AIProvider` | AI model & embedding |
|
|
268
|
-
| `queue` | `queue()` | `Queue` | Job queue |
|
|
269
|
-
| `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
|
|
270
|
-
| `permissions` | `permissions()` | `{ roles, permissions }` | RBAC roles & permissions sets |
|
|
271
|
-
| `theme` | `theme()` | `{ value, set }` | Current theme + switcher |
|
|
272
|
-
| `i18n` | `i18n()` | `{ locale, t, set }` | Locale, translation, switcher |
|
|
273
|
-
| `flash` | `flash()` | `{ value, set }` | Flash message + setter |
|
|
274
|
-
| `tailwind` | `tailwindContext()` | `{ css, url }` | Compiled Tailwind CSS |
|
|
275
|
-
| `tenant` | `tenant()` | `TenantContext` | Current tenant info |
|
|
276
|
-
| `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
|
|
277
|
-
| `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
|
|
278
|
-
| `notifier` | `notifier()` | `Notifier` | Multi-channel notification system |
|
|
279
|
-
| `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
|
|
280
|
-
| `mountPath` | `Router` | `string` | Sub-router mount path |
|
|
281
|
-
| `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
|
|
282
|
-
|
|
283
|
-
### Type-Safe Context
|
|
284
|
-
|
|
285
|
-
Middleware-injected properties are **automatically typed** through chained `use()` calls:
|
|
53
|
+
Middleware enriches `ctx` with additional properties:
|
|
286
54
|
|
|
287
55
|
```ts
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
app.
|
|
294
|
-
ctx.csrf.token // ✅ string (IDE autocomplete)
|
|
295
|
-
ctx.requestId // ✅ string
|
|
296
|
-
ctx.sql`SELECT 1` // ✅ Sql<{}>
|
|
297
|
-
})
|
|
56
|
+
app.use(postgres()) // → ctx.sql
|
|
57
|
+
app.use(redis()) // → ctx.redis
|
|
58
|
+
app.use(aiProvider()) // → ctx.ai
|
|
59
|
+
app.use(queue()) // → ctx.queue
|
|
60
|
+
app.use(cors())
|
|
61
|
+
app.use(rateLimit({ window: 60 }))
|
|
298
62
|
```
|
|
299
63
|
|
|
300
|
-
Each module exports an `XxxInjected` type (e.g. `PostgresInjected`, `UserInjected`) for composing custom context types. `Context` is an interface — modules augment it via `declare module` for ambient compatibility.
|
|
301
|
-
|
|
302
64
|
---
|
|
303
65
|
|
|
304
|
-
##
|
|
305
|
-
|
|
306
|
-
All modules follow one of **4 patterns** — learn these and you know every module.
|
|
307
|
-
|
|
308
|
-
| Pattern | How to mount | Example |
|
|
309
|
-
| ------- | ---------------------------------------- | ------------------------------------------------------ |
|
|
310
|
-
| `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
|
|
311
|
-
| `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
|
|
312
|
-
| `[γ]` | Import and call directly | `mailer()`, `fts`, `cron-utils` |
|
|
313
|
-
| `[δ]` | `import { useXxx } from 'weifuwu/react'` | `useTheme()`, `useLocale()`, `useWebsocket()` |
|
|
314
|
-
|
|
315
|
-
### Pattern α — Middleware
|
|
316
|
-
|
|
317
|
-
```ts
|
|
318
|
-
app.use(compress()) // basic
|
|
319
|
-
const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
|
|
320
|
-
app.use(pg)
|
|
321
|
-
app.use(rateLimit({ max: 100 })) // with .close()
|
|
322
|
-
```
|
|
66
|
+
## Public API
|
|
323
67
|
|
|
324
|
-
###
|
|
68
|
+
### serve
|
|
325
69
|
|
|
326
70
|
```ts
|
|
327
|
-
|
|
328
|
-
app.use('/graphql', graphql(handler))
|
|
329
|
-
app.use('/logs', logdb({ pg })) // with .log(), .migrate()
|
|
330
|
-
app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
|
|
331
|
-
app.ws('/ws', messager({ pg }).wsHandler())
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
β modules that need **separate middleware** use `.middleware()`. Most can auto-register both middleware and routes in one call:
|
|
71
|
+
import { serve } from 'weifuwu'
|
|
335
72
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// Explicit form when more control is needed:
|
|
343
|
-
const a = analytics()
|
|
344
|
-
app.use(a.middleware()) // tracking only
|
|
345
|
-
app.use('/', a) // dashboard at custom path
|
|
73
|
+
const server = serve(handler, {
|
|
74
|
+
port: 3000, // default: 3000
|
|
75
|
+
websocket: wsHandler, // optional WebSocket handler from Router
|
|
76
|
+
shutdown: true, // graceful shutdown on SIGTERM/SIGINT (default: true)
|
|
77
|
+
maxBody: 1024 * 1024, // max request body size (default: 1MB)
|
|
78
|
+
})
|
|
346
79
|
```
|
|
347
80
|
|
|
348
|
-
|
|
81
|
+
Returns `{ close(): Promise<void> }`.
|
|
349
82
|
|
|
350
|
-
|
|
83
|
+
### Router
|
|
351
84
|
|
|
352
85
|
```ts
|
|
353
|
-
import {
|
|
86
|
+
import { Router } from 'weifuwu'
|
|
354
87
|
|
|
355
|
-
const
|
|
356
|
-
await email.send({ to: 'user@test.com', subject: 'Hello', text: 'Body' })
|
|
88
|
+
const r = new Router()
|
|
357
89
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
import { useTheme, useLocale, useWebsocket } from 'weifuwu/react'
|
|
90
|
+
// HTTP methods
|
|
91
|
+
r.get(path, handler)
|
|
92
|
+
r.post(path, handler)
|
|
93
|
+
r.put(path, handler)
|
|
94
|
+
r.patch(path, handler)
|
|
95
|
+
r.delete(path, handler)
|
|
96
|
+
r.head(path, handler)
|
|
97
|
+
r.options(path, handler)
|
|
367
98
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
```
|
|
99
|
+
// Middleware (applied to all routes)
|
|
100
|
+
r.use(middleware) // global middleware
|
|
101
|
+
r.use('/prefix', middleware) // scoped to prefix
|
|
373
102
|
|
|
374
|
-
|
|
103
|
+
// WebSocket
|
|
104
|
+
r.ws(path, {
|
|
105
|
+
open(ws, ctx) { ... },
|
|
106
|
+
message(ws, ctx, data) { ... },
|
|
107
|
+
close(ws, ctx) { ... },
|
|
108
|
+
})
|
|
375
109
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
graph TD
|
|
380
|
-
serve --> Router
|
|
381
|
-
Router --> postgres
|
|
382
|
-
Router --> redis
|
|
383
|
-
Router --> aiProvider
|
|
384
|
-
|
|
385
|
-
subgraph "DB-Dependent Modules"
|
|
386
|
-
user --> postgres
|
|
387
|
-
session --> postgres
|
|
388
|
-
session -.-> redis
|
|
389
|
-
queue --> postgres
|
|
390
|
-
queue -.-> redis
|
|
391
|
-
permissions --> postgres
|
|
392
|
-
analytics --> postgres
|
|
393
|
-
logdb --> postgres
|
|
394
|
-
tenant --> postgres
|
|
395
|
-
messager --> postgres
|
|
396
|
-
messager -.-> redis
|
|
397
|
-
agent --> postgres
|
|
398
|
-
kb --> postgres
|
|
399
|
-
iii --> postgres
|
|
400
|
-
iii -.-> redis
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
subgraph "AI-Dependent Modules"
|
|
404
|
-
agent --> aiProvider
|
|
405
|
-
kb --> aiProvider
|
|
406
|
-
aiStream --> aiProvider
|
|
407
|
-
opencode --> aiProvider
|
|
408
|
-
runWorkflow --> aiProvider
|
|
409
|
-
end
|
|
110
|
+
// Compose
|
|
111
|
+
r.handler() // → (req, ctx) => Response (for serve())
|
|
112
|
+
r.websocketHandler() // → WebSocket upgrade handler
|
|
410
113
|
```
|
|
411
114
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
| What do you want to do? | Module | Pattern |
|
|
415
|
-
| -------------------------------- | ----------------------------------------------- | ---------------------- |
|
|
416
|
-
| **User registration / login** | `user()` | β |
|
|
417
|
-
| **Simple token/header auth** | `auth()` | α |
|
|
418
|
-
| **JWT verification** | `user().middleware()` | α |
|
|
419
|
-
| **Role-based access control** | `permissions()` | α |
|
|
420
|
-
| **AI chat / generate / stream** | `ctx.ai.generateText()` / `ctx.ai.streamText()` | α (via `aiProvider()`) |
|
|
421
|
-
| **AI agent with knowledge** | `agent()` + `knowledgeBase()` | β |
|
|
422
|
-
| **Send email** | `mailer()` | γ |
|
|
423
|
-
| **File upload** | `upload()` | α |
|
|
424
|
-
| **Object storage (S3/MinIO)** | `s3()` | α |
|
|
425
|
-
| **Rate limiting** | `rateLimit()` | α |
|
|
426
|
-
| **Response caching** | `cache()` | α |
|
|
427
|
-
| **Periodic / delayed jobs** | `queue()` | α |
|
|
428
|
-
| **Page view analytics** | `analytics()` | β |
|
|
429
|
-
| **Structured logging** | `logdb()` | β |
|
|
430
|
-
| **Real-time chat / messager** | `messager()` | β |
|
|
431
|
-
| **Full-text search** | `fts` | γ |
|
|
432
|
-
| **Theme switching** | `theme()` | α |
|
|
433
|
-
| **i18n / localization** | `i18n()` | α |
|
|
434
|
-
| **Flash messages** | `flash()` | α |
|
|
435
|
-
| **Server-Sent Events** | `createSSEStream()` | γ |
|
|
436
|
-
| **GraphQL endpoint** | `graphql()` | β |
|
|
437
|
-
| **Webhook receiver** | `webhook()` | β |
|
|
438
|
-
| **SSR with React** | `ssr()` | β |
|
|
439
|
-
| **Health check** | `health()` | β |
|
|
440
|
-
| **SEO (robots.txt, sitemap)** | `seo()` | β |
|
|
441
|
-
| **Multi-process deploy** | `deploy()` | γ |
|
|
442
|
-
| **Distributed functions (iii)** | `iii()` | β |
|
|
443
|
-
| **Multi-tenant BaaS** | `tenant()` | β |
|
|
444
|
-
| **Client-side routing** | `useNavigate()`, `<Link>` | δ |
|
|
445
|
-
| **WebSocket in React** | `useWebsocket()` | δ |
|
|
446
|
-
| **Compression (brotli/gzip)** | `compress()` | α |
|
|
447
|
-
| **Security headers (CSP, HSTS)** | `helmet()` | α |
|
|
448
|
-
| **CORS** | `cors()` | α |
|
|
449
|
-
| **CSRF protection** | `csrf()` | α |
|
|
450
|
-
| **Request ID tracing** | `requestId()` | α |
|
|
451
|
-
| **Environment variables** | `env()` / `loadEnv()` | α |
|
|
452
|
-
| **Static file serving** | `serveStatic()` | α |
|
|
453
|
-
| **Object storage (S3/MinIO)** | `s3()` | α |
|
|
454
|
-
| **Send email** | `mailer()` | γ |
|
|
455
|
-
| **Scheduled / cron tasks** | `cron-utils` (`cronNext()`) | γ |
|
|
456
|
-
| **Server-Sent Events** | `createSSEStream()` | γ |
|
|
457
|
-
| **Multi-process deploy** | `deploy()` | γ |
|
|
458
|
-
| **Distributed functions (iii)** | `iii()` | β |
|
|
459
|
-
| **Webhook receiver** | `webhook()` | β |
|
|
460
|
-
| **MCP tool integration** | `mcpClient()` | γ |
|
|
461
|
-
| **Notifications** | `notifier()` | α |
|
|
462
|
-
| **API Key management** | `user({ apiKeys: true })` | β |
|
|
463
|
-
| **WebSocket testing** | `testApp().wsReq()` | — |
|
|
464
|
-
| **Social login (OAuth)** | `user({ oauthLogin })` | β |
|
|
465
|
-
| **Database migrations** | `pg.migrate()` | — |
|
|
466
|
-
|
|
467
|
-
---
|
|
468
|
-
|
|
469
|
-
## Request Tracing & Logging
|
|
470
|
-
|
|
471
|
-
Every request gets a **trace ID** via `AsyncLocalStorage`, injected into responses as `X-Trace-Id`. W3C `traceparent` headers are forwarded.
|
|
115
|
+
### Context
|
|
472
116
|
|
|
473
117
|
```ts
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
**Structured logging** — `logger({ format: 'json' })` outputs JSON to stderr with `traceId`, `timestamp`, `elapsed_ms`:
|
|
482
|
-
|
|
483
|
-
```json
|
|
484
|
-
{
|
|
485
|
-
"level": "info",
|
|
486
|
-
"message": "request",
|
|
487
|
-
"method": "GET",
|
|
488
|
-
"path": "/api/users",
|
|
489
|
-
"status": 200,
|
|
490
|
-
"elapsed_ms": 42,
|
|
491
|
-
"traceId": "f240a3f3-...",
|
|
492
|
-
"timestamp": "2025-01-15T10:30:00.000Z"
|
|
118
|
+
interface Context {
|
|
119
|
+
params: Record<string, string> // URL parameters
|
|
120
|
+
query: Record<string, string> // query string
|
|
121
|
+
mountPath?: string // prefix path if mounted under Router.use()
|
|
122
|
+
[key: string]: unknown // middleware-injected fields
|
|
493
123
|
}
|
|
494
124
|
```
|
|
495
125
|
|
|
496
|
-
|
|
126
|
+
### Middleware modules
|
|
497
127
|
|
|
498
|
-
|
|
128
|
+
#### postgres()
|
|
499
129
|
|
|
500
|
-
|
|
130
|
+
```ts
|
|
131
|
+
import { postgres } from 'weifuwu'
|
|
501
132
|
|
|
502
|
-
|
|
133
|
+
app.use(postgres({ connection: process.env.DATABASE_URL }))
|
|
134
|
+
// ctx.sql → SqlClient
|
|
503
135
|
|
|
136
|
+
const rows = await ctx.sql`SELECT * FROM users WHERE id = ${id}`
|
|
504
137
|
```
|
|
505
|
-
GET /agents/:id/runs?days=7 → [{ input, output, tokens_in, tokens_out, elapsed_ms, status, trace_id, ... }]
|
|
506
|
-
GET /agents/:id/runs/summary?days=7 → { total, success, error, success_rate, tokens_in, tokens_out, avg_elapsed_ms, p95_elapsed_ms }
|
|
507
|
-
GET /opencode/sessions/:id/usage → { message_count, tokens_in, tokens_out, tokens_total }
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
Non-streaming runs log full token data; streaming runs log `status: 'stream'`.
|
|
511
|
-
|
|
512
|
-
---
|
|
513
|
-
|
|
514
|
-
## Agent ↔ Messager Streaming
|
|
515
138
|
|
|
516
|
-
|
|
139
|
+
Includes table builder and migrations:
|
|
517
140
|
|
|
518
141
|
```ts
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
// Agent replies stream to: hub.broadcast({ type: 'agent_stream', data: { token, full } })
|
|
142
|
+
const { sql, migrate } = postgres({ connection: '...' })
|
|
143
|
+
await migrate() // run all pending migrations
|
|
144
|
+
await sql.close()
|
|
523
145
|
```
|
|
524
146
|
|
|
525
|
-
|
|
526
|
-
// Frontend — React hook
|
|
527
|
-
import { useAgentStream } from 'weifuwu/react'
|
|
147
|
+
Options: `connection`, `max`, `ssl`, `idle_timeout`, `connect_timeout`, `statementTimeout`, `onQuery`, `signal`, `closeTimeout`
|
|
528
148
|
|
|
529
|
-
|
|
530
|
-
wsPath: '/ws',
|
|
531
|
-
channelId: 1,
|
|
532
|
-
})
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
Multi-round conversation context: the last 10 channel messages are automatically injected into agent calls.
|
|
536
|
-
|
|
537
|
-
---
|
|
149
|
+
Types:
|
|
538
150
|
|
|
539
|
-
|
|
151
|
+
- `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `SqlClient`, `Sql`
|
|
540
152
|
|
|
541
|
-
|
|
153
|
+
#### redis()
|
|
542
154
|
|
|
543
155
|
```ts
|
|
544
|
-
import {
|
|
545
|
-
|
|
546
|
-
const app = testApp()
|
|
547
|
-
app.use(postgres({ connection: TEST_DB }))
|
|
548
|
-
app.get('/users/:id', (req, ctx) => Response.json({ id: ctx.params.id, user: ctx.user }))
|
|
156
|
+
import { redis } from 'weifuwu'
|
|
549
157
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
.withUser({ id: 1 })
|
|
553
|
-
.header('X-Custom', 'val')
|
|
554
|
-
.body({ data: 'test' })
|
|
555
|
-
.send()
|
|
158
|
+
app.use(redis({ url: process.env.REDIS_URL }))
|
|
159
|
+
// ctx.redis → Redis client
|
|
556
160
|
|
|
557
|
-
|
|
558
|
-
|
|
161
|
+
await ctx.redis.set('key', 'value')
|
|
162
|
+
const val = await ctx.redis.get('key')
|
|
559
163
|
```
|
|
560
164
|
|
|
561
|
-
|
|
562
|
-
| ------------------------------------------------------------ | ----------------------------------------------------- |
|
|
563
|
-
| `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request |
|
|
564
|
-
| `.withUser(u)` `.withTenant(t)` `.with(ctx)` | Simulate middleware injection |
|
|
565
|
-
| `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
|
|
566
|
-
| `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
|
|
567
|
-
|
|
568
|
-
**WebSocket testing** (new in v0.25) — `app.ws()` + `app.wsReq()`:
|
|
165
|
+
Options: `url`, `host`, `port`, `password`, `db`, `keyPrefix`, `maxRetriesPerRequest`, `enableReadyCheck`, `lazyConnect`, `retryStrategy`
|
|
569
166
|
|
|
570
|
-
|
|
571
|
-
const app = testApp()
|
|
572
|
-
app.ws('/echo', {
|
|
573
|
-
open(ws) {
|
|
574
|
-
ws.send(JSON.stringify({ type: 'connected' }))
|
|
575
|
-
},
|
|
576
|
-
message(ws, ctx, data) {
|
|
577
|
-
ws.send('echo: ' + data.toString())
|
|
578
|
-
},
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
// Connect via WebSocket
|
|
582
|
-
const conn = await app.wsReq('/echo').connect()
|
|
583
|
-
|
|
584
|
-
// Wait for the open message
|
|
585
|
-
const openMsg = await conn.receiveJson()
|
|
586
|
-
assert.equal(openMsg.type, 'connected')
|
|
167
|
+
Types: `RedisOptions`, `RedisClient`, `RedisInjected`, `Redis`
|
|
587
168
|
|
|
588
|
-
|
|
589
|
-
conn.send('hello')
|
|
590
|
-
const reply = await conn.receive()
|
|
591
|
-
assert.equal(reply, 'echo: hello')
|
|
169
|
+
#### aiProvider()
|
|
592
170
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
| Method | Description |
|
|
598
|
-
| ------------------------------------------ | ------------------------------------------- |
|
|
599
|
-
| `app.ws(path, handler)` | Register a WebSocket handler |
|
|
600
|
-
| `app.wsReq(path)` | Start building a WebSocket connection |
|
|
601
|
-
| `.timeout(ms)` | Set connection timeout (default: 5000) |
|
|
602
|
-
| `.connect()` → `TestWSConnection` | Connect and return a connection handle |
|
|
603
|
-
| `conn.send(data)` / `conn.json(obj)` | Send a message |
|
|
604
|
-
| `conn.receive()` / `conn.receiveJson<T>()` | Wait for the next message |
|
|
605
|
-
| `conn.expectSilent(ms)` | Assert no message arrives within the period |
|
|
606
|
-
| `conn.close()` | Close the connection |
|
|
607
|
-
| `app.close()` | Close all connections and stop the server |
|
|
171
|
+
```ts
|
|
172
|
+
import { aiProvider } from 'weifuwu'
|
|
608
173
|
|
|
609
|
-
|
|
174
|
+
app.use(aiProvider())
|
|
175
|
+
// ctx.ai → AIProvider
|
|
610
176
|
|
|
611
|
-
|
|
612
|
-
|
|
177
|
+
app.get('/ask', async (req, ctx) => {
|
|
178
|
+
const result = await ctx.ai.generateText({
|
|
179
|
+
prompt: 'Explain quantum computing',
|
|
180
|
+
})
|
|
181
|
+
return Response.json(result)
|
|
182
|
+
})
|
|
613
183
|
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
await db.destroy() // DROP SCHEMA ... CASCADE
|
|
619
|
-
|
|
620
|
-
// Transaction rollback — all changes are rolled back after callback
|
|
621
|
-
await withTestDb(async (sql) => {
|
|
622
|
-
await sql`INSERT INTO users ...`
|
|
623
|
-
// Automatically rolled back
|
|
184
|
+
// Streaming
|
|
185
|
+
app.get('/stream', async (req, ctx) => {
|
|
186
|
+
const result = ctx.ai.streamText({ prompt: 'Tell me a story' })
|
|
187
|
+
return result.toTextStreamResponse()
|
|
624
188
|
})
|
|
625
189
|
```
|
|
626
190
|
|
|
627
|
-
|
|
628
|
-
| ---------------------- | --------------------------------------------------------------- |
|
|
629
|
-
| `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
|
|
630
|
-
| `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback |
|
|
631
|
-
|
|
632
|
-
Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset.
|
|
633
|
-
|
|
634
|
-
---
|
|
635
|
-
|
|
636
|
-
## Module Reference
|
|
637
|
-
|
|
638
|
-
Modules are organized alphabetically. Each module shows its pattern badge (`[α]` Middleware, `[β]` Router, `[γ]` Standalone, `[δ]` Client-side) and category.
|
|
191
|
+
Configured via environment variables:
|
|
639
192
|
|
|
640
|
-
|
|
193
|
+
- `OPENAI_API_KEY` — API key (default: `ollama`)
|
|
194
|
+
- `OPENAI_BASE_URL` — API base URL (default: `http://localhost:11434/v1`)
|
|
195
|
+
- `OPENAI_MODEL` — model name (default: `gpt-4o`)
|
|
641
196
|
|
|
642
|
-
|
|
197
|
+
Types: `AIProviderOptions`, `AIProvider`, `AIProviderInjected`
|
|
643
198
|
|
|
644
|
-
|
|
199
|
+
Also exports the raw SDK functions:
|
|
645
200
|
|
|
646
201
|
```ts
|
|
647
|
-
|
|
648
|
-
const a = agent({ pg, provider })
|
|
649
|
-
await a.migrate()
|
|
650
|
-
app.use('/api', a)
|
|
651
|
-
await a.addKnowledge(agentId, 'Title', 'some knowledge content')
|
|
652
|
-
a.run(agentId, { input: 'summarize the data', stream: true })
|
|
202
|
+
import { streamText, generateText, embed, embedMany, tool, openai } from 'weifuwu'
|
|
653
203
|
```
|
|
654
204
|
|
|
655
|
-
|
|
656
|
-
| -------------------- | ------------ | ------------------------- | --------------------------------------------- |
|
|
657
|
-
| `pg` | `object` | — | PostgreSQL client |
|
|
658
|
-
| `provider` | `AIProvider` | `aiProvider()` (from env) | AI provider for model & embedding resolution |
|
|
659
|
-
| `model` | `object` | — | Explicit AI model (overrides provider) |
|
|
660
|
-
| `embeddingModel` | `object` | — | Explicit embedding model (overrides provider) |
|
|
661
|
-
| `embeddingDimension` | `number` | `provider.dimension` | Embedding vector dimension |
|
|
662
|
-
| `tools` | `object[]` | — | Custom tool definitions |
|
|
663
|
-
|
|
664
|
-
| Method | Description |
|
|
665
|
-
| ---------------------------------------------- | ------------------------ |
|
|
666
|
-
| `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
|
|
667
|
-
| `.addKnowledge(agentId, title, content)` | Add knowledge document |
|
|
668
|
-
| `.migrate()` | DB setup |
|
|
669
|
-
| `.close()` | Cleanup |
|
|
205
|
+
#### queue()
|
|
670
206
|
|
|
671
|
-
|
|
207
|
+
```ts
|
|
208
|
+
import { queue } from 'weifuwu'
|
|
672
209
|
|
|
673
|
-
|
|
210
|
+
app.use(queue({ store: 'memory' }))
|
|
211
|
+
// ctx.queue → Queue
|
|
674
212
|
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
const chat = await aiStream(async (req) => ({ messages: (await req.json()).messages }), provider)
|
|
678
|
-
app.use('/chat', chat)
|
|
679
|
-
```
|
|
213
|
+
// In-memory queue (default)
|
|
214
|
+
const q = queue()
|
|
680
215
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
| `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
|
|
684
|
-
| `provider` | `AIProvider` | Optional. If provided and handler omits `model`, `provider.model()` is used as default |
|
|
216
|
+
// Redis-backed queue
|
|
217
|
+
const q = queue({ store: 'redis', redis: ctx.redis })
|
|
685
218
|
|
|
686
|
-
|
|
219
|
+
// PostgreSQL-backed queue
|
|
220
|
+
const q = queue({ store: 'pg', pg: { sql: ctx.sql } })
|
|
687
221
|
|
|
688
|
-
|
|
222
|
+
q.process('email', async (job) => {
|
|
223
|
+
await sendEmail(job.payload)
|
|
224
|
+
})
|
|
689
225
|
|
|
690
|
-
|
|
691
|
-
const a = analytics()
|
|
692
|
-
app.use(a.middleware())
|
|
693
|
-
app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
|
|
226
|
+
await q.add('email', { to: 'user@example.com', subject: 'Hello' })
|
|
694
227
|
```
|
|
695
228
|
|
|
696
|
-
|
|
697
|
-
| ---------- | ---------- | --------------------------------------- | --------------------------------- |
|
|
698
|
-
| `pg` | `object` | — | PostgreSQL client for persistence |
|
|
699
|
-
| `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip |
|
|
229
|
+
Methods: `add(type, payload, opts?)`, `process(type, handler)`, `cron(pattern, handler)`, `run()`, `stats()`, `jobs(limit?)`, `failedJobs(limit?)`, `retryFailed(jobId)`, `retryAllFailed(type?)`, `close()`, `dashboard()`, `migrate()`
|
|
700
230
|
|
|
701
|
-
|
|
702
|
-
// With PostgreSQL
|
|
703
|
-
const a = analytics({ pg })
|
|
704
|
-
await a.migrate()
|
|
705
|
-
app.use(a.middleware())
|
|
706
|
-
app.use('/', a) // dashboard routes
|
|
707
|
-
```
|
|
231
|
+
Types: `QueueOptions`, `Queue`, `QueueJob`, `QueueInjected`
|
|
708
232
|
|
|
709
|
-
|
|
233
|
+
#### graphql()
|
|
710
234
|
|
|
711
235
|
```ts
|
|
712
|
-
|
|
713
|
-
app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
|
|
714
|
-
app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
|
|
715
|
-
app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
|
|
236
|
+
import { graphql } from 'weifuwu'
|
|
716
237
|
|
|
717
|
-
// Session-based auth (must be placed after session() middleware)
|
|
718
|
-
app.use(session())
|
|
719
238
|
app.use(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
},
|
|
239
|
+
'/graphql',
|
|
240
|
+
graphql({
|
|
241
|
+
schema: `
|
|
242
|
+
type Query { hello: String }
|
|
243
|
+
`,
|
|
244
|
+
resolvers: { Query: { hello: () => 'world' } },
|
|
727
245
|
}),
|
|
728
246
|
)
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
| Option | Type | Default | Description |
|
|
732
|
-
| ------------- | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------- |
|
|
733
|
-
| `token` | `string` | — | Static token to match |
|
|
734
|
-
| `header` | `string` | `'Authorization'` | Header name |
|
|
735
|
-
| `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
|
|
736
|
-
| `proxy` | `string` | — | Auth service URL to proxy requests to |
|
|
737
|
-
| `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
|
|
738
|
-
| `resolveUser` | `(userId) => object\|null` | — | Load user from userId (called when `session: true`). Return falsy to reject + auto-destroy stale session |
|
|
739
|
-
|
|
740
|
-
When `session: true`, auth checks `ctx.session.userId` before the
|
|
741
|
-
Authorization header. This lets logged-in users authenticate via their
|
|
742
|
-
session cookie without sending a token. Falls back to header/token auth
|
|
743
|
-
if no session userId is present.
|
|
744
247
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
248
|
+
// With GraphiQL IDE:
|
|
249
|
+
app.use(
|
|
250
|
+
'/graphql',
|
|
251
|
+
graphql({
|
|
252
|
+
schema: `type Query { hello: String }`,
|
|
253
|
+
graphiql: true,
|
|
254
|
+
}),
|
|
255
|
+
)
|
|
750
256
|
```
|
|
751
257
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
| `level` | `number` | `6` | Compression level (zlib) |
|
|
258
|
+
Options: `schema`, `rootValue`, `resolvers`, `context`, `graphiql`, `maxDepth`, `timeout`
|
|
259
|
+
|
|
260
|
+
Types: `GraphQLOptions`, `GraphQLHandler`
|
|
756
261
|
|
|
757
|
-
|
|
262
|
+
#### cors()
|
|
758
263
|
|
|
759
264
|
```ts
|
|
760
|
-
|
|
761
|
-
app.use(cors({ origin:
|
|
762
|
-
app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
|
|
763
|
-
app.use(cors({ credentials: true, maxAge: 3600 }))
|
|
265
|
+
import { cors } from 'weifuwu'
|
|
266
|
+
app.use(cors({ origin: 'https://myapp.com' }))
|
|
764
267
|
```
|
|
765
268
|
|
|
766
|
-
|
|
767
|
-
| ---------------- | ---------------------------- | -------------------------------------------------------- | ---------------------------------- |
|
|
768
|
-
| `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
|
|
769
|
-
| `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
|
|
770
|
-
| `allowedHeaders` | `string[]` | — | Custom allowed headers |
|
|
771
|
-
| `exposedHeaders` | `string[]` | — | Response headers exposed to client |
|
|
772
|
-
| `credentials` | `boolean` | `false` | Allow cookies/credentials |
|
|
773
|
-
| `maxAge` | `number` | — | Preflight cache duration (seconds) |
|
|
269
|
+
Options: `origin`, `methods`, `allowedHeaders`, `exposedHeaders`, `credentials`, `maxAge`
|
|
774
270
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
Cookie-based flash message. Read from request, write via redirect.
|
|
271
|
+
#### compress()
|
|
778
272
|
|
|
779
273
|
```ts
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
app.get('/', (req, ctx) => {
|
|
783
|
-
const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
|
|
784
|
-
})
|
|
785
|
-
|
|
786
|
-
app.post('/save', (req, ctx) => {
|
|
787
|
-
return ctx.flash.set({ type: 'success', text: 'Saved!' }, '/articles')
|
|
788
|
-
})
|
|
274
|
+
import { compress } from 'weifuwu'
|
|
275
|
+
app.use(compress({ threshold: 1024, brotli: true }))
|
|
789
276
|
```
|
|
790
277
|
|
|
791
|
-
|
|
792
|
-
| ------ | -------- | --------- | ----------- |
|
|
793
|
-
| `name` | `string` | `'flash'` | Cookie name |
|
|
278
|
+
Options: `threshold`, `brotli`, `level`
|
|
794
279
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
|
|
280
|
+
#### helmet()
|
|
798
281
|
|
|
799
282
|
```ts
|
|
800
|
-
|
|
801
|
-
app.use(
|
|
802
|
-
app.use(
|
|
803
|
-
cache({
|
|
804
|
-
ttl: 30_000,
|
|
805
|
-
tag: (req, ctx) => (ctx.user ? `user:${ctx.user.id}` : undefined), // per-user invalidation
|
|
806
|
-
}),
|
|
807
|
-
)
|
|
808
|
-
|
|
809
|
-
// Programmatic invalidation
|
|
810
|
-
const c = cache({ store: 'redis', redis: ctx.redis })
|
|
811
|
-
app.use(c)
|
|
812
|
-
await c.invalidate('users') // invalidate all entries tagged with 'users'
|
|
813
|
-
await c.flush() // clear entire cache
|
|
283
|
+
import { helmet } from 'weifuwu'
|
|
284
|
+
app.use(helmet())
|
|
814
285
|
```
|
|
815
286
|
|
|
816
|
-
|
|
817
|
-
| -------------- | ----------------------------------- | ------------------ | --------------------------------------------- |
|
|
818
|
-
| `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
|
|
819
|
-
| `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
|
|
820
|
-
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
821
|
-
| `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
|
|
822
|
-
| `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
|
|
823
|
-
| `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
|
|
824
|
-
| `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
|
|
825
|
-
| `maxBodySize` | `number` | `1048576` (1MB) | Max body bytes to cache |
|
|
287
|
+
Sets security headers (CSP, HSTS, X-Frame-Options, etc.).
|
|
826
288
|
|
|
827
|
-
|
|
289
|
+
#### rateLimit()
|
|
828
290
|
|
|
829
291
|
```ts
|
|
830
|
-
import {
|
|
831
|
-
|
|
832
|
-
const mem = new MemoryCache()
|
|
833
|
-
await mem.set(
|
|
834
|
-
'key',
|
|
835
|
-
{ status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] },
|
|
836
|
-
300_000,
|
|
837
|
-
)
|
|
838
|
-
mem.close()
|
|
839
|
-
```
|
|
292
|
+
import { rateLimit } from 'weifuwu'
|
|
840
293
|
|
|
841
|
-
|
|
294
|
+
// In-memory (default)
|
|
295
|
+
app.use(rateLimit({ window: 60, max: 100 }))
|
|
842
296
|
|
|
843
|
-
|
|
844
|
-
app.use(
|
|
845
|
-
// ctx.csrf.token — set on GET/HEAD/OPTIONS
|
|
846
|
-
// Auto-validates x-csrf-token or x-xsrf-token header on POST/PUT/DELETE/PATCH
|
|
847
|
-
// Falls back to body field matching the key name
|
|
297
|
+
// Redis-backed
|
|
298
|
+
app.use(rateLimit({ window: 60, max: 100, redis: ctx.redis }))
|
|
848
299
|
```
|
|
849
300
|
|
|
850
|
-
|
|
851
|
-
| ---------------- | -------------------------- | ----------------------------------------- |
|
|
852
|
-
| `cookie` | `'_csrf'` | Cookie name |
|
|
853
|
-
| `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
|
|
854
|
-
| `key` | `'_csrf'` | Body field fallback |
|
|
855
|
-
| `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
|
|
856
|
-
|
|
857
|
-
### deploy [β] [Networking]
|
|
301
|
+
Options: `window`, `max`, `redis`, `key`, `statusCode`, `message`
|
|
858
302
|
|
|
859
|
-
|
|
303
|
+
#### validate()
|
|
860
304
|
|
|
861
305
|
```ts
|
|
862
|
-
import {
|
|
306
|
+
import { validate } from 'weifuwu'
|
|
307
|
+
import { z } from 'zod'
|
|
863
308
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
309
|
+
app.post(
|
|
310
|
+
'/users',
|
|
311
|
+
validate({
|
|
312
|
+
body: z.object({ name: z.string() }),
|
|
313
|
+
query: z.object({ ref: z.string().optional() }),
|
|
314
|
+
params: z.object({}),
|
|
315
|
+
headers: z.object({ authorization: z.string() }),
|
|
868
316
|
}),
|
|
317
|
+
handler,
|
|
869
318
|
)
|
|
319
|
+
// ctx.parsed → { body, query, params, headers }
|
|
870
320
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
domain: 'example.com',
|
|
875
|
-
deployToken: process.env.DEPLOY_TOKEN,
|
|
876
|
-
apps: { blog: {}, api: {} },
|
|
877
|
-
}),
|
|
878
|
-
)
|
|
321
|
+
function handler(req, ctx) {
|
|
322
|
+
const { name } = ctx.parsed.body
|
|
323
|
+
}
|
|
879
324
|
```
|
|
880
325
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
| Field | Default | Rule |
|
|
884
|
-
| ------- | ------------ | -------------------------- |
|
|
885
|
-
| `dir` | App key | `blog` → `'./blog'` |
|
|
886
|
-
| `entry` | `'index.ts'` | Default entry file |
|
|
887
|
-
| `port` | `3001+` | Auto-incremented from 3001 |
|
|
888
|
-
| `path` | `'/key'` | Only for localhost domain |
|
|
889
|
-
|
|
890
|
-
Override any field explicitly:
|
|
326
|
+
#### upload()
|
|
891
327
|
|
|
892
328
|
```ts
|
|
893
|
-
|
|
894
|
-
apps: {
|
|
895
|
-
blog: { dir: '../packages/blog', entry: 'server.ts', port: 8080, path: '/blog' },
|
|
896
|
-
},
|
|
897
|
-
})
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
**Routing** — match priority: explicit path > app key > defaultApp.
|
|
329
|
+
import { upload } from 'weifuwu'
|
|
901
330
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
api: { path: '/api' }, // example.com/api or localhost:3000/api
|
|
905
|
-
blog: {}, // blog.example.com or localhost:3000/blog
|
|
906
|
-
}
|
|
331
|
+
app.post('/files', upload({ maxFiles: 5, maxSize: 10 * 1024 * 1024 }), handler)
|
|
332
|
+
// ctx.parsed → { files: UploadedFile[], fields: Record<string, string> }
|
|
907
333
|
```
|
|
908
334
|
|
|
909
|
-
|
|
335
|
+
Options: `maxFiles`, `maxSize`, `allowedTypes`, `keepExtensions`
|
|
336
|
+
|
|
337
|
+
#### static()
|
|
910
338
|
|
|
911
339
|
```ts
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
ports: [3001, 3002]
|
|
915
|
-
}
|
|
916
|
-
}
|
|
340
|
+
import { serveStatic } from 'weifuwu'
|
|
341
|
+
app.use('/assets', serveStatic({ root: './public', index: 'index.html' }))
|
|
917
342
|
```
|
|
918
343
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
**Process watchdog** — auto-restarts with exponential backoff on unexpected exit.
|
|
922
|
-
|
|
923
|
-
**Management API** — all endpoints require `Authorization: Bearer <deployToken>`:
|
|
924
|
-
|
|
925
|
-
| Endpoint | Method | Description |
|
|
926
|
-
| ----------------------------- | ------ | -------------- |
|
|
927
|
-
| `/_deploy/apps` | GET | List apps |
|
|
928
|
-
| `/_deploy/apps/:name` | GET | App details |
|
|
929
|
-
| `/_deploy/apps/:name/deploy` | POST | Restart |
|
|
930
|
-
| `/_deploy/apps/:name/restart` | POST | Restart |
|
|
931
|
-
| `/_deploy/apps/:name/stop` | POST | Stop |
|
|
932
|
-
| `/_deploy/apps/:name/start` | POST | Start |
|
|
933
|
-
| `/_deploy/apps/:name/logs` | GET | SSE log stream |
|
|
934
|
-
|
|
935
|
-
```bash
|
|
936
|
-
curl -H "Authorization: Bearer my-token" http://localhost:3000/_deploy/apps
|
|
937
|
-
```
|
|
344
|
+
Options: `root`, `index`, `maxAge`, `immutable`, `brotli`, `headers`
|
|
938
345
|
|
|
939
|
-
|
|
346
|
+
#### csrf()
|
|
940
347
|
|
|
941
|
-
```
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
Restart=always
|
|
348
|
+
```ts
|
|
349
|
+
import { csrf } from 'weifuwu'
|
|
350
|
+
app.use(csrf())
|
|
351
|
+
// ctx.csrf.token → string (for forms)
|
|
946
352
|
```
|
|
947
353
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
| Option | Default | Description |
|
|
951
|
-
| ------------- | ------------- | ------------------------------- |
|
|
952
|
-
| `domain` | `'localhost'` | Root domain |
|
|
953
|
-
| `port` | `3000` | Gateway port |
|
|
954
|
-
| `deployToken` | — | Bearer token for management API |
|
|
955
|
-
| `defaultApp` | — | Fallback route |
|
|
956
|
-
| `apps` | — | `Record<string, AppConfig>` |
|
|
957
|
-
|
|
958
|
-
**AppConfig:**
|
|
959
|
-
|
|
960
|
-
| Field | Default | Description |
|
|
961
|
-
| ---------------- | ---------------- | ------------------------------- |
|
|
962
|
-
| `dir` | App key | Directory containing the app |
|
|
963
|
-
| `port` | Auto (3001+) | Internal port |
|
|
964
|
-
| `entry` | `'index.ts'` | Entry file |
|
|
965
|
-
| `path` | `'/key'` (local) | URL path prefix |
|
|
966
|
-
| `env` | — | Environment variables |
|
|
967
|
-
| `healthEndpoint` | `/` | Health check path |
|
|
968
|
-
| `buildCommand` | — | Build command |
|
|
969
|
-
| `ports` | — | `[port, port+1]` for blue-green |
|
|
970
|
-
|
|
971
|
-
### env [α] [DevTools]
|
|
972
|
-
|
|
973
|
-
Environment variable middleware. Injects `ctx.env` with all `WEIFUWU_PUBLIC_*` variables (prefix stripped).
|
|
974
|
-
Safe to expose to the client.
|
|
975
|
-
|
|
976
|
-
```ts
|
|
977
|
-
import { env, loadEnv } from 'weifuwu'
|
|
978
|
-
loadEnv() // Load .env into process.env
|
|
979
|
-
app.use(env()) // → ctx.env
|
|
354
|
+
Protects POST/PUT/DELETE endpoints by requiring a valid CSRF token in `X-CSRF-Token` header.
|
|
980
355
|
|
|
981
|
-
|
|
982
|
-
return Response.json({ apiUrl: ctx.env.API_URL })
|
|
983
|
-
})
|
|
984
|
-
```
|
|
356
|
+
Options: `secret`, `cookie`, `header`
|
|
985
357
|
|
|
986
|
-
|
|
358
|
+
#### flash()
|
|
987
359
|
|
|
988
360
|
```ts
|
|
989
|
-
import {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
isBundled() // Running from compiled dist/index.js?
|
|
994
|
-
getPublicEnv() // { API_URL: '...' } — no middleware needed
|
|
361
|
+
import { flash } from 'weifuwu'
|
|
362
|
+
app.use(flash())
|
|
363
|
+
// ctx.flash.value → string | undefined (read-once)
|
|
364
|
+
// ctx.flash.set('success', 'Saved!')
|
|
995
365
|
```
|
|
996
366
|
|
|
997
|
-
|
|
998
|
-
| ---------------- | ---------------------------------------------------------------- |
|
|
999
|
-
| `loadEnv(path?)` | Load `.env` file into `process.env` (does not override existing) |
|
|
1000
|
-
| `env()` | Middleware — injects `ctx.env` with public vars |
|
|
1001
|
-
| `getPublicEnv()` | Returns `WEIFUWU_PUBLIC_*` vars with prefix stripped |
|
|
1002
|
-
| `isDev()` | `true` when `NODE_ENV === 'development'` |
|
|
1003
|
-
| `isProd()` | `true` when `NODE_ENV === 'production'` |
|
|
1004
|
-
| `isBundled()` | `true` when running from compiled bundle |
|
|
367
|
+
Options: `cookie`, `maxAge`
|
|
1005
368
|
|
|
1006
|
-
|
|
369
|
+
#### requestId()
|
|
1007
370
|
|
|
1008
371
|
```ts
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
maxDepth: 10, // max query nesting (default 10, 0 = disable)
|
|
1014
|
-
timeout: 30_000, // execution timeout in ms
|
|
1015
|
-
})
|
|
1016
|
-
app.use('/graphql', graphql(handler))
|
|
372
|
+
import { requestId } from 'weifuwu'
|
|
373
|
+
app.use(requestId())
|
|
374
|
+
// ctx.requestId → string (UUID)
|
|
375
|
+
// Response gets X-Request-Id header
|
|
1017
376
|
```
|
|
1018
377
|
|
|
1019
|
-
|
|
1020
|
-
| ----------- | ------------------------- | -------- | ------------------------------ |
|
|
1021
|
-
| `schema` | `string \| GraphQLSchema` | — | SDL string or pre-built schema |
|
|
1022
|
-
| `resolvers` | `object` | — | Resolver map |
|
|
1023
|
-
| `rootValue` | `any` | — | Root value for queries |
|
|
1024
|
-
| `context` | `(req, ctx) => object` | — | Per-request context factory |
|
|
1025
|
-
| `graphiql` | `boolean` | `false` | Serve GraphiQL IDE at GET / |
|
|
1026
|
-
| `maxDepth` | `number` | `10` | Max query nesting depth |
|
|
1027
|
-
| `timeout` | `number` | `30_000` | Execution timeout (ms) |
|
|
378
|
+
Options: `header`, `generator`
|
|
1028
379
|
|
|
1029
|
-
|
|
380
|
+
#### health()
|
|
1030
381
|
|
|
1031
382
|
```ts
|
|
383
|
+
import { health } from 'weifuwu'
|
|
1032
384
|
app.use('/health', health())
|
|
1033
|
-
//
|
|
385
|
+
// GET /health → { status: 'ok', uptime: 12345 }
|
|
1034
386
|
```
|
|
1035
387
|
|
|
1036
|
-
|
|
1037
|
-
| ------- | --------------------- | ----------- | ---------------------------- |
|
|
1038
|
-
| `path` | `string` | `'/health'` | Health check endpoint |
|
|
1039
|
-
| `check` | `() => Promise<void>` | — | Async function; throws → 503 |
|
|
1040
|
-
|
|
1041
|
-
### helmet [α] [Security]
|
|
1042
|
-
|
|
1043
|
-
15 security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
|
|
388
|
+
#### theme()
|
|
1044
389
|
|
|
1045
390
|
```ts
|
|
1046
|
-
|
|
1047
|
-
app.use(
|
|
391
|
+
import { theme } from 'weifuwu'
|
|
392
|
+
app.use(theme({ cookie: 'theme' }))
|
|
393
|
+
// ctx.theme → { value: 'light' | 'dark', set(newValue) }
|
|
1048
394
|
```
|
|
1049
395
|
|
|
1050
|
-
|
|
1051
|
-
| --------------------------- | --------------------------------------- | -------------------------- |
|
|
1052
|
-
| `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
|
|
1053
|
-
| `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
|
|
1054
|
-
| `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
|
|
1055
|
-
| `referrerPolicy` | `'no-referrer'` | Referrer header |
|
|
1056
|
-
| `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
|
|
1057
|
-
| `permissionsPolicy` | — | Feature permissions policy |
|
|
1058
|
-
| `crossOriginEmbedderPolicy` | — | COEP header |
|
|
1059
|
-
| `crossOriginOpenerPolicy` | — | COOP header |
|
|
1060
|
-
| `crossOriginResourcePolicy` | — | CORP header |
|
|
1061
|
-
|
|
1062
|
-
### iii [β] — Worker / Function / Trigger [API]
|
|
396
|
+
Options: `cookie`, `default`, `param`
|
|
1063
397
|
|
|
1064
|
-
|
|
398
|
+
#### i18n()
|
|
1065
399
|
|
|
1066
400
|
```ts
|
|
1067
|
-
import {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
app.ws('/iii', engine.wsHandler())
|
|
1071
|
-
|
|
1072
|
-
const w = createWorker('orders')
|
|
1073
|
-
w.registerFunction('orders::create', async (payload) =>
|
|
1074
|
-
db.query('INSERT INTO orders ...', [payload.items]),
|
|
1075
|
-
)
|
|
1076
|
-
engine.addWorker(w)
|
|
1077
|
-
await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
|
|
401
|
+
import { i18n } from 'weifuwu'
|
|
402
|
+
app.use(i18n({ dir: './locales', defaultLocale: 'en' }))
|
|
403
|
+
// ctx.i18n → { locale: 'en', t(key), set(locale) }
|
|
1078
404
|
```
|
|
1079
405
|
|
|
1080
|
-
|
|
1081
|
-
| ----------- | -------- | ------- | --------------------------------------------- |
|
|
1082
|
-
| `pg` | `object` | — | PostgreSQL client for persistent triggers |
|
|
1083
|
-
| `redis` | `object` | — | Redis client for streams |
|
|
1084
|
-
| `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
|
|
1085
|
-
|
|
1086
|
-
| Method | Description |
|
|
1087
|
-
| ---------------------------------------------------------- | ------------------------- |
|
|
1088
|
-
| `.addWorker(w)` | Register a worker |
|
|
1089
|
-
| `.removeWorker(w)` | Remove a worker |
|
|
1090
|
-
| `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
|
|
1091
|
-
| `.listWorkers()` | List registered workers |
|
|
1092
|
-
| `.listFunctions()` | List registered functions |
|
|
1093
|
-
| `.listTriggers()` | List registered triggers |
|
|
1094
|
-
| `.wsHandler()` | WebSocket handler |
|
|
1095
|
-
| `.migrate()` | DB setup |
|
|
1096
|
-
| `.shutdown()` | Clean shutdown |
|
|
1097
|
-
|
|
1098
|
-
### knowledgeBase [β] — RAG with pgvector [AI]
|
|
406
|
+
Options: `dir`, `defaultLocale`, `cookie`, `param`, `header`
|
|
1099
407
|
|
|
1100
|
-
|
|
1101
|
-
import { knowledgeBase, aiProvider } from 'weifuwu'
|
|
408
|
+
### Standalone utilities
|
|
1102
409
|
|
|
1103
|
-
|
|
1104
|
-
pg: postgres(),
|
|
1105
|
-
provider: aiProvider(),
|
|
1106
|
-
table: 'my_docs',
|
|
1107
|
-
})
|
|
410
|
+
#### mailer()
|
|
1108
411
|
|
|
1109
|
-
|
|
1110
|
-
|
|
412
|
+
```ts
|
|
413
|
+
import { mailer } from 'weifuwu'
|
|
1111
414
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
415
|
+
const m = mailer({
|
|
416
|
+
host: 'smtp.example.com',
|
|
417
|
+
port: 587,
|
|
418
|
+
auth: { user: '...', pass: '...' },
|
|
419
|
+
from: 'noreply@example.com',
|
|
1116
420
|
})
|
|
1117
421
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
// → [{ key, title, content, score: 0.92, metadata }, ...]
|
|
1121
|
-
|
|
1122
|
-
// Delete
|
|
1123
|
-
await kb.delete('docs/outdated.md')
|
|
1124
|
-
|
|
1125
|
-
// List all documents
|
|
1126
|
-
const entries = await kb.list()
|
|
1127
|
-
// → [{ key, title, chunks: 3 }, ...]
|
|
1128
|
-
|
|
1129
|
-
// Use as middleware (injects ctx.kb.search)
|
|
1130
|
-
app.use(kb.middleware())
|
|
1131
|
-
app.get('/search', async (req, ctx) => {
|
|
1132
|
-
const results = await ctx.kb.search(ctx.query.q)
|
|
1133
|
-
return Response.json(results)
|
|
1134
|
-
})
|
|
422
|
+
await m.send({ to: 'user@example.com', subject: 'Hello', text: '...' })
|
|
423
|
+
await m.close()
|
|
1135
424
|
```
|
|
1136
425
|
|
|
1137
|
-
|
|
1138
|
-
| ----------------- | ---------------- | ------------ | --------------------------------------- |
|
|
1139
|
-
| `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
|
|
1140
|
-
| `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
|
|
1141
|
-
| `table` | `string` | `'_kb_docs'` | Database table name |
|
|
1142
|
-
| `chunkSize` | `number` | `512` | Max characters per chunk |
|
|
1143
|
-
| `chunkOverlap` | `number` | `64` | Overlap between chunks |
|
|
1144
|
-
| `searchLimit` | `number` | `5` | Default search result count |
|
|
1145
|
-
| `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
|
|
1146
|
-
|
|
1147
|
-
Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
|
|
1148
|
-
replaces old chunks. Provider's `embed()` is used automatically.
|
|
1149
|
-
The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
|
|
426
|
+
Options: `host`, `port`, `auth`, `from`, `secure`
|
|
1150
427
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
PostgreSQL structured event logging with monthly partitioning.
|
|
428
|
+
#### SSE
|
|
1154
429
|
|
|
1155
430
|
```ts
|
|
1156
|
-
|
|
1157
|
-
await logger.migrate()
|
|
1158
|
-
app.use('/logs', logger)
|
|
1159
|
-
await logger.clean(12) // drop partitions older than 12 months
|
|
1160
|
-
await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
| Option | Type | Default | Description |
|
|
1164
|
-
| ------- | -------- | ---------------- | ----------------- |
|
|
1165
|
-
| `pg` | `object` | — | PostgreSQL client |
|
|
1166
|
-
| `table` | `string` | `'_log_entries'` | Table name |
|
|
431
|
+
import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
|
|
1167
432
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
| GET | `/:id` | Get single entry |
|
|
433
|
+
const stream = createSSEStream()
|
|
434
|
+
stream.write(formatSSE('eventType', { data: 'hello' }))
|
|
435
|
+
stream.end()
|
|
436
|
+
```
|
|
1173
437
|
|
|
1174
|
-
|
|
438
|
+
#### Hub (pub/sub)
|
|
1175
439
|
|
|
1176
440
|
```ts
|
|
1177
|
-
|
|
1178
|
-
app.use(logger({ format: 'combined' })) // with query params
|
|
1179
|
-
```
|
|
441
|
+
import { createHub } from 'weifuwu'
|
|
1180
442
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
443
|
+
const hub = createHub({ redis: optionalRedisClient })
|
|
444
|
+
hub.join('room:1', ws)
|
|
445
|
+
hub.sendRoom('room:1', { type: 'message', text: 'hello' })
|
|
446
|
+
hub.leave(ws)
|
|
447
|
+
```
|
|
1184
448
|
|
|
1185
|
-
|
|
449
|
+
#### Cookie helpers
|
|
1186
450
|
|
|
1187
451
|
```ts
|
|
1188
|
-
|
|
1189
|
-
from: 'noreply@example.com',
|
|
1190
|
-
transport: 'smtp://user:pass@smtp.example.com:587',
|
|
1191
|
-
})
|
|
1192
|
-
await mail.send({
|
|
1193
|
-
to: 'user@test.com',
|
|
1194
|
-
subject: 'Hello',
|
|
1195
|
-
text: 'Body',
|
|
1196
|
-
html: '<p>Body</p>',
|
|
1197
|
-
cc: 'admin@test.com',
|
|
1198
|
-
})
|
|
452
|
+
import { getCookies, setCookie, deleteCookie } from 'weifuwu'
|
|
1199
453
|
```
|
|
1200
454
|
|
|
1201
|
-
|
|
1202
|
-
| ----------- | ---------------- | ------- | ------------------------------------------------ |
|
|
1203
|
-
| `transport` | `string\|object` | — | Nodemailer transport config or connection string |
|
|
1204
|
-
| `from` | `string` | — | Default sender address |
|
|
1205
|
-
| `send` | `function` | — | Custom send function (alternative to transport) |
|
|
1206
|
-
|
|
1207
|
-
### mcpClient [γ] — MCP Server integration [AI]
|
|
1208
|
-
|
|
1209
|
-
[Model Context Protocol](https://modelcontextprotocol.io) client. Spawns MCP server
|
|
1210
|
-
subprocesses and exposes their tools as AI SDK-compatible tool objects.
|
|
455
|
+
### Test utilities
|
|
1211
456
|
|
|
1212
457
|
```ts
|
|
1213
|
-
import {
|
|
1214
|
-
|
|
1215
|
-
const fsMcp = mcpClient({
|
|
1216
|
-
command: 'npx',
|
|
1217
|
-
args: ['@modelcontextprotocol/server-filesystem', '/workspace'],
|
|
1218
|
-
})
|
|
1219
|
-
|
|
1220
|
-
const tools = await fsMcp.getTools()
|
|
458
|
+
import { testApp, TestApp, createTestDb, withTestDb } from 'weifuwu'
|
|
1221
459
|
|
|
1222
|
-
const
|
|
1223
|
-
await
|
|
460
|
+
const app = new Router().handler()
|
|
461
|
+
const res = await testApp(app, new Request('http://localhost/'))
|
|
462
|
+
// res.status, res.headers, await res.json()
|
|
1224
463
|
|
|
1225
|
-
//
|
|
1226
|
-
await
|
|
1227
|
-
|
|
1228
|
-
// Or call a tool directly
|
|
1229
|
-
const result = await fsMcp.callTool('echo', { text: 'hello' })
|
|
1230
|
-
|
|
1231
|
-
await fsMcp.close() // shutdown the MCP server process
|
|
464
|
+
// With database
|
|
465
|
+
const db = await createTestDb()
|
|
466
|
+
// db.sql, db.close()
|
|
1232
467
|
```
|
|
1233
468
|
|
|
1234
|
-
|
|
1235
|
-
| ----------------- | ---------- | ------- | ------------------------------------------------------- |
|
|
1236
|
-
| `command` | `string` | — | **Required.** Command to spawn (e.g. `'npx'`, `'node'`) |
|
|
1237
|
-
| `args` | `string[]` | `[]` | Arguments passed to the command |
|
|
1238
|
-
| `env` | `object` | — | Extra environment variables |
|
|
1239
|
-
| `timeout` | `number` | `15000` | Handshake/response timeout (ms) |
|
|
1240
|
-
| `maxResponseSize` | `number` | `10MB` | Max tool response body size |
|
|
1241
|
-
|
|
1242
|
-
| Method | Description |
|
|
1243
|
-
| ------------ | ------------------------------------------------------------------------- |
|
|
1244
|
-
| `getTools()` | Fetch tool definitions, returns `Record<string, Tool>`-compatible objects |
|
|
1245
|
-
| `refresh()` | Re-fetch tool definitions from the server |
|
|
1246
|
-
| `callTool()` | Call a tool by name directly |
|
|
1247
|
-
| `close()` | Shutdown the MCP server process |
|
|
1248
|
-
|
|
1249
|
-
Tool schemas (JSON Schema) are automatically converted to Zod schemas for AI SDK compatibility.
|
|
1250
|
-
Responses are concatenated from text content items, with size limiting.
|
|
1251
|
-
|
|
1252
|
-
### oauthLogin (via user()) — Social login (OAuth 2.0 client) [Security]
|
|
1253
|
-
|
|
1254
|
-
Social login is built into the [`user()`](#user-β) module via the `oauthLogin` option — no separate import needed.
|
|
469
|
+
### Environment
|
|
1255
470
|
|
|
1256
471
|
```ts
|
|
1257
|
-
|
|
1258
|
-
const u = user({
|
|
1259
|
-
pg,
|
|
1260
|
-
jwtSecret: process.env.JWT_SECRET!,
|
|
1261
|
-
oauthLogin: {
|
|
1262
|
-
redirectUrl: '/dashboard',
|
|
1263
|
-
providers: {
|
|
1264
|
-
google: {
|
|
1265
|
-
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
1266
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
1267
|
-
},
|
|
1268
|
-
github: {
|
|
1269
|
-
clientId: process.env.GITHUB_CLIENT_ID,
|
|
1270
|
-
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
1271
|
-
},
|
|
1272
|
-
},
|
|
1273
|
-
},
|
|
1274
|
-
})
|
|
1275
|
-
await u.migrate()
|
|
1276
|
-
app.use(u) // POST /register, POST /login, GET /auth/:provider, GET /auth/:provider/callback
|
|
1277
|
-
```
|
|
472
|
+
import { loadEnv, isDev, isProd, env } from 'weifuwu'
|
|
1278
473
|
|
|
1279
|
-
|
|
474
|
+
loadEnv() // loads .env file
|
|
475
|
+
isDev() // NODE_ENV === 'development'
|
|
476
|
+
isProd() // NODE_ENV === 'production'
|
|
477
|
+
env('MY_VAR', 'default')
|
|
478
|
+
getPublicEnv() // env vars starting with PUBLIC_
|
|
479
|
+
```
|
|
1280
480
|
|
|
1281
|
-
|
|
481
|
+
### Tracing
|
|
1282
482
|
|
|
1283
483
|
```ts
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
providers: {
|
|
1289
|
-
discord: {
|
|
1290
|
-
clientId: process.env.DISCORD_CLIENT_ID,
|
|
1291
|
-
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
1292
|
-
authUrl: 'https://discord.com/api/oauth2/authorize',
|
|
1293
|
-
tokenUrl: 'https://discord.com/api/oauth2/token',
|
|
1294
|
-
userUrl: 'https://discord.com/api/users/@me',
|
|
1295
|
-
parseUser: (data) => ({
|
|
1296
|
-
id: data.id,
|
|
1297
|
-
email: data.email ?? '',
|
|
1298
|
-
name: data.global_name ?? data.username,
|
|
1299
|
-
avatarUrl: data.avatar
|
|
1300
|
-
? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
|
|
1301
|
-
: '',
|
|
1302
|
-
}),
|
|
1303
|
-
},
|
|
1304
|
-
},
|
|
1305
|
-
},
|
|
484
|
+
import { trace, currentTraceId } from 'weifuwu'
|
|
485
|
+
|
|
486
|
+
trace('fetch-user', async () => {
|
|
487
|
+
// auto-tracked with trace ID
|
|
1306
488
|
})
|
|
489
|
+
currentTraceId() // get current trace ID
|
|
1307
490
|
```
|
|
1308
491
|
|
|
1309
|
-
|
|
1310
|
-
| ------------------- | ------------------------------------- | ------- | ------------------------------------------------------------------- |
|
|
1311
|
-
| `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
|
|
1312
|
-
| `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
|
|
1313
|
-
|
|
1314
|
-
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.
|
|
1315
|
-
|
|
1316
|
-
### messager [β] [Networking]
|
|
1317
|
-
|
|
1318
|
-
Real-time chat with channels, WebSocket, agent routing.
|
|
492
|
+
### Error handling
|
|
1319
493
|
|
|
1320
494
|
```ts
|
|
1321
|
-
|
|
1322
|
-
await msg.migrate()
|
|
1323
|
-
app.use('/api', msg)
|
|
1324
|
-
app.ws('/ws', msg.wsHandler())
|
|
1325
|
-
await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
|
|
1326
|
-
```
|
|
495
|
+
import { HttpError } from 'weifuwu'
|
|
1327
496
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
| `pg` | `object` | — | PostgreSQL client |
|
|
1331
|
-
| `agents` | `AgentModule` | — | Agent module for routing |
|
|
1332
|
-
| `webhookTimeout` | `number` | — | Webhook timeout |
|
|
1333
|
-
| `redis` | `object` | — | Redis client |
|
|
497
|
+
throw new HttpError('Not found', 404) // caught by serve(), returns 404
|
|
498
|
+
```
|
|
1334
499
|
|
|
1335
|
-
|
|
1336
|
-
| -------------------------------- | --------------------------------------------------- |
|
|
1337
|
-
| `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
|
|
1338
|
-
| `.send(channel, content, opts?)` | Send message to channel |
|
|
1339
|
-
| `.close()` | Cleanup |
|
|
500
|
+
---
|
|
1340
501
|
|
|
1341
|
-
|
|
502
|
+
## CLI
|
|
1342
503
|
|
|
1343
|
-
|
|
1344
|
-
|
|
504
|
+
```bash
|
|
505
|
+
npx weifuwu init my-api
|
|
506
|
+
cd my-api
|
|
507
|
+
npm run dev
|
|
508
|
+
```
|
|
1345
509
|
|
|
1346
|
-
|
|
1347
|
-
import { notifier, mailer } from 'weifuwu'
|
|
510
|
+
Creates a minimal API project with `app.ts`, `index.ts`, and TypeScript config.
|
|
1348
511
|
|
|
1349
|
-
|
|
1350
|
-
const n = notifier({ sql: pg.sql, mailer: mail })
|
|
1351
|
-
await n.migrate()
|
|
1352
|
-
app.use(n) // injects ctx.notifier
|
|
512
|
+
---
|
|
1353
513
|
|
|
1354
|
-
|
|
1355
|
-
await ctx.notifier.send(
|
|
1356
|
-
{ userId: 42, email: 'user@example.com' },
|
|
1357
|
-
{ title: 'Welcome!', body: 'Thanks for joining', type: 'onboarding' },
|
|
1358
|
-
)
|
|
1359
|
-
|
|
1360
|
-
// Broadcast to all users with inbox enabled
|
|
1361
|
-
await ctx.notifier.broadcast({
|
|
1362
|
-
title: 'System maintenance tonight',
|
|
1363
|
-
body: 'The system will be down from 2-4 AM',
|
|
1364
|
-
})
|
|
1365
|
-
|
|
1366
|
-
// Check unread count
|
|
1367
|
-
const count = await ctx.notifier.unreadCount(userId)
|
|
1368
|
-
|
|
1369
|
-
// List notifications (newest first)
|
|
1370
|
-
const notifications = await ctx.notifier.list(userId, { limit: 10 })
|
|
1371
|
-
|
|
1372
|
-
// Mark as read
|
|
1373
|
-
await ctx.notifier.markRead(userId, [notifId])
|
|
1374
|
-
await ctx.notifier.markRead(userId) // mark ALL as read
|
|
1375
|
-
|
|
1376
|
-
// User preferences
|
|
1377
|
-
await ctx.notifier.setPreferences(userId, { channels: ['inbox', 'email'] })
|
|
1378
|
-
const prefs = await ctx.notifier.getPreferences(userId)
|
|
1379
|
-
// → { channels: ['inbox', 'email'] }
|
|
1380
|
-
```
|
|
1381
|
-
|
|
1382
|
-
| Option | Type | Default | Description |
|
|
1383
|
-
| ---------- | ----------- | ------------------ | ------------------------------- |
|
|
1384
|
-
| `sql` | `SqlClient` | — | **Required.** PostgreSQL client |
|
|
1385
|
-
| `mailer` | `Mailer` | — | Mailer for email channel |
|
|
1386
|
-
| `hub` | `Hub` | — | Pub/sub hub for WebSocket push |
|
|
1387
|
-
| `table` | `string` | `'_notifications'` | Notifications table name |
|
|
1388
|
-
| `pageSize` | `number` | `50` | Default page size for list() |
|
|
1389
|
-
|
|
1390
|
-
| Method | Description |
|
|
1391
|
-
| -------------------------------- | --------------------------------------------- |
|
|
1392
|
-
| `.send(to, message)` | Send notification (routes by user preference) |
|
|
1393
|
-
| `.broadcast(message)` | Send to all users with inbox enabled |
|
|
1394
|
-
| `.unreadCount(userId)` | Count unread notifications |
|
|
1395
|
-
| `.count(userId, unreadOnly?)` | Total or unread count |
|
|
1396
|
-
| `.markRead(userId, ids?)` | Mark notification(s) as read |
|
|
1397
|
-
| `.list(userId, opts?)` | List notifications (paginated) |
|
|
1398
|
-
| `.getPreferences(userId)` | Get user's channel preferences |
|
|
1399
|
-
| `.setPreferences(userId, prefs)` | Set user's channel preferences |
|
|
1400
|
-
| `.migrate()` | Create tables |
|
|
1401
|
-
| `.clean(days)` | Delete notifications older than N days |
|
|
1402
|
-
|
|
1403
|
-
**Channel routing:** Each user has channel preferences (default: `['inbox']`). When
|
|
1404
|
-
`sending`, the notification is delivered to each enabled channel. Email requires
|
|
1405
|
-
`mailer` to be configured. WebSocket requires `hub` (e.g. from `messager.wsHandler()`).
|
|
1406
|
-
|
|
1407
|
-
### opencode [β] [AI]
|
|
1408
|
-
|
|
1409
|
-
AI programming assistant.
|
|
1410
|
-
|
|
1411
|
-
```ts
|
|
1412
|
-
const oc = await opencode({
|
|
1413
|
-
pg,
|
|
1414
|
-
model: openai('gpt-4o'),
|
|
1415
|
-
workspace: '/home/user/project',
|
|
1416
|
-
permissions: { bash: { allow: true }, write: { allow: false } },
|
|
1417
|
-
})
|
|
1418
|
-
await oc.migrate()
|
|
1419
|
-
app.use('/opencode', oc)
|
|
1420
|
-
app.ws('/opencode', oc.wsHandler())
|
|
1421
|
-
```
|
|
1422
|
-
|
|
1423
|
-
| Option | Type | Default | Description |
|
|
1424
|
-
| -------------- | ---------- | ------- | ------------------------------------------------------ |
|
|
1425
|
-
| `pg` | `object` | — | PostgreSQL client |
|
|
1426
|
-
| `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
|
|
1427
|
-
| `baseURL` | `string` | — | OpenAI-compatible API base URL |
|
|
1428
|
-
| `apiKey` | `string` | — | API key for the model |
|
|
1429
|
-
| `workspace` | `string` | — | Project directory |
|
|
1430
|
-
| `systemPrompt` | `string` | — | Custom system prompt |
|
|
1431
|
-
| `skills` | `object[]` | — | Custom skill definitions |
|
|
1432
|
-
| `permissions` | `object` | — | Tool permission rules |
|
|
1433
|
-
|
|
1434
|
-
### postgres [α] [Database]
|
|
1435
|
-
|
|
1436
|
-
Type-safe PostgreSQL client with schema builder, CRUD, migrations, soft delete, and JSONB/vector support.
|
|
1437
|
-
|
|
1438
|
-
```ts
|
|
1439
|
-
const pg = postgres() // reads DATABASE_URL
|
|
1440
|
-
app.use(pg) // injects ctx.sql
|
|
1441
|
-
```
|
|
1442
|
-
|
|
1443
|
-
| Option | Type | Default | Description |
|
|
1444
|
-
| ------------------ | --------------------------- | ------------------ | --------------------------------------- |
|
|
1445
|
-
| `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
|
|
1446
|
-
| `max` | `number` | `10` | Max pool connections |
|
|
1447
|
-
| `ssl` | `boolean\|object` | — | SSL options |
|
|
1448
|
-
| `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
|
|
1449
|
-
| `connect_timeout` | `number` | `30` | Connection timeout |
|
|
1450
|
-
| `statementTimeout` | `number` | `30_000` | Per-statement timeout (ms, 0 = disable) |
|
|
1451
|
-
| `onQuery` | `(query, ms, rows) => void` | — | Query logging callback |
|
|
1452
|
-
|
|
1453
|
-
```ts
|
|
1454
|
-
// Raw SQL via tagged template
|
|
1455
|
-
await pg.sql`SELECT * FROM users WHERE email = ${email}`
|
|
1456
|
-
|
|
1457
|
-
// Define a table — one API, sql pre-bound
|
|
1458
|
-
import { serial, text, boolean, timestamps } from 'weifuwu'
|
|
1459
|
-
|
|
1460
|
-
const users = pg.table('_users', {
|
|
1461
|
-
id: serial('id').primaryKey(),
|
|
1462
|
-
name: text('name').notNull(),
|
|
1463
|
-
email: text('email').unique().notNull(),
|
|
1464
|
-
active: boolean('active').default(true),
|
|
1465
|
-
...timestamps(),
|
|
1466
|
-
})
|
|
1467
|
-
await users.create() // DDL — no need to pass sql
|
|
1468
|
-
await users.createIndex('email')
|
|
1469
|
-
|
|
1470
|
-
// CRUD — sql already bound
|
|
1471
|
-
await users.insert({ name: 'Alice' })
|
|
1472
|
-
const { count, data } = await users.readMany(
|
|
1473
|
-
{ role: 'admin' },
|
|
1474
|
-
{ orderBy: { name: 'asc' }, limit: 10 },
|
|
1475
|
-
)
|
|
1476
|
-
await users.upsert({ email: 'alice@test.com' }, 'email')
|
|
1477
|
-
|
|
1478
|
-
// Reuse schema without redefining fields
|
|
1479
|
-
import { pgTable } from 'weifuwu'
|
|
1480
|
-
const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
|
|
1481
|
-
const users = pg.table(usersSchema) // bind — no field duplication
|
|
1482
|
-
|
|
1483
|
-
// Transactions — with auto-retry on deadlock/serialization failure
|
|
1484
|
-
await pg.transaction(
|
|
1485
|
-
async (sql) => {
|
|
1486
|
-
const txUsers = users.withSql(sql)
|
|
1487
|
-
return txUsers.insert({ name: 'Bob' })
|
|
1488
|
-
},
|
|
1489
|
-
{ maxRetries: 3 },
|
|
1490
|
-
)
|
|
1491
|
-
|
|
1492
|
-
// Soft delete — automatic if deleted_at column exists
|
|
1493
|
-
await users.delete(1) // SET deleted_at = NOW()
|
|
1494
|
-
await users.hardDelete(1) // DELETE FROM
|
|
1495
|
-
await users.read(1) // auto-filters deleted_at IS NULL (use withDeleted: true to include)
|
|
1496
|
-
|
|
1497
|
-
// JSONB queries
|
|
1498
|
-
const logs = pg.table('logs', { meta: jsonb<{ service: string }>('meta') })
|
|
1499
|
-
await logs.readMany(contains('meta', { service: 'auth' }))
|
|
1500
|
-
|
|
1501
|
-
// Connection pool visibility
|
|
1502
|
-
console.log(pg.poolStats()) // { active: 3, idle: 7, waiting: 0, max: 10 }
|
|
1503
|
-
|
|
1504
|
-
// Migration tracking
|
|
1505
|
-
await pg.migrate() // creates _weifuwu_migrations
|
|
1506
|
-
await pg.markMigrated('myModule') // idempotent
|
|
1507
|
-
const done = await pg.isMigrated('myModule')
|
|
1508
|
-
|
|
1509
|
-
// Partitioned tables
|
|
1510
|
-
await logs.create({ partitionBy: partitionBy('range', 'created_at') })
|
|
1511
|
-
```
|
|
1512
|
-
|
|
1513
|
-
**When to use pgTable vs pg.table:**
|
|
1514
|
-
| API | Use when |
|
|
1515
|
-
|-----|---------|
|
|
1516
|
-
| `pg.table('t', cols)` | You have `pg` available (factory, handler, migrate) |
|
|
1517
|
-
| `pg.table(schema)` | Reusing a schema without duplicating field definitions |
|
|
1518
|
-
| `pgTable('t', cols)` | No `pg` reference (utility modules, standalone schema files) |
|
|
1519
|
-
|
|
1520
|
-
| Column builder | Type | Notes |
|
|
1521
|
-
| ---------------------------------- | ---------- | ------------------------------- |
|
|
1522
|
-
| `serial(name)` | `number` | Auto-increment |
|
|
1523
|
-
| `uuid(name)` | `string` | — |
|
|
1524
|
-
| `text(name)` | `string` | — |
|
|
1525
|
-
| `integer(name)` | `number` | — |
|
|
1526
|
-
| `boolean(name)` / `boolean_(name)` | `boolean` | `_` suffix for JS reserved word |
|
|
1527
|
-
| `timestamptz(name)` | `string` | — |
|
|
1528
|
-
| `jsonb<T>(name)` | `T` | Generic for typed JSONB access |
|
|
1529
|
-
| `textArray(name)` | `string[]` | TEXT[] |
|
|
1530
|
-
| `vector(name, dims)` | `number[]` | pgvector support |
|
|
1531
|
-
|
|
1532
|
-
**Column modifiers:** `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(val)`, `.unique()`, `.references(table, column?, onDelete?)`.
|
|
1533
|
-
|
|
1534
|
-
**CRUD methods:**
|
|
1535
|
-
|
|
1536
|
-
| Method | Description |
|
|
1537
|
-
| ----------------------------- | ----------------------------------------------------------------- |
|
|
1538
|
-
| `insert(data)` | INSERT + RETURNING \*, returns the inserted row |
|
|
1539
|
-
| `insertMany(data)` | Bulk INSERT + RETURNING \*, returns rows |
|
|
1540
|
-
| `read(id, opts?)` | SELECT by detected primary key + auto soft-delete filter |
|
|
1541
|
-
| `readMany(where?, opts?)` | Filtered query with `{ count, data }` — auto-filters soft-deleted |
|
|
1542
|
-
| `update(id, data)` | UPDATE by primary key + RETURNING \*, returns updated row |
|
|
1543
|
-
| `updateMany(where, data)` | Bulk UPDATE, returns affected row count |
|
|
1544
|
-
| `delete(id)` | Soft delete if `deleted_at` exists, else hard delete |
|
|
1545
|
-
| `hardDelete(id)` | Always DELETE FROM |
|
|
1546
|
-
| `deleteMany(where)` | Soft bulk delete if `deleted_at` exists |
|
|
1547
|
-
| `hardDeleteMany(where)` | Always DELETE FROM |
|
|
1548
|
-
| `upsert(data, conflict)` | INSERT ON CONFLICT DO UPDATE, returns row |
|
|
1549
|
-
| `count(where?)` | SELECT COUNT(\*) — auto-filters soft-deleted |
|
|
1550
|
-
| `create(opts?)` | CREATE TABLE IF NOT EXISTS |
|
|
1551
|
-
| `drop(opts?)` | DROP TABLE IF EXISTS |
|
|
1552
|
-
| `createIndex(columns, opts?)` | CREATE INDEX |
|
|
1553
|
-
| `createUniqueIndex(columns)` | CREATE UNIQUE INDEX |
|
|
1554
|
-
| `withSql(sql)` | Returns copy bound to a different sql (for transactions) |
|
|
1555
|
-
|
|
1556
|
-
**Where helpers** — composable query conditions:
|
|
1557
|
-
|
|
1558
|
-
| Helper | SQL |
|
|
1559
|
-
| ----------------------------------- | ------------------------- |
|
|
1560
|
-
| `eq(col, val)` | `"col" = val` |
|
|
1561
|
-
| `ne(col, val)` | `"col" != val` |
|
|
1562
|
-
| `gt` / `gte` / `lt` / `lte` | Comparison operators |
|
|
1563
|
-
| `isNull(col)` / `isNotNull(col)` | `IS NULL` / `IS NOT NULL` |
|
|
1564
|
-
| `like(col, pattern)` | `LIKE` |
|
|
1565
|
-
| `contains(col, val)` | `@>` JSONB containment |
|
|
1566
|
-
| `in_(col, vals)` | `= ANY(...)` |
|
|
1567
|
-
| `and(...)` / `or(...)` / `not(...)` | Boolean composition |
|
|
1568
|
-
|
|
1569
|
-
**PgModule** — base class for modules that need DB access:
|
|
1570
|
-
|
|
1571
|
-
```ts
|
|
1572
|
-
class MyModule extends PgModule {
|
|
1573
|
-
async migrate() {
|
|
1574
|
-
/* run DDL */
|
|
1575
|
-
}
|
|
1576
|
-
async getUsers() {
|
|
1577
|
-
return this.table('users', {}).readMany()
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
```
|
|
1581
|
-
|
|
1582
|
-
Where helpers + `and`/`or`/`not` can be imported from `'weifuwu'` alongside `postgres`. Full column builders and table helpers are in the same barrel.
|
|
1583
|
-
|
|
1584
|
-
### cron-utils [γ] [DevTools]
|
|
1585
|
-
|
|
1586
|
-
Shared cron expression parsing utilities. All functions operate in **local timezone**.
|
|
1587
|
-
|
|
1588
|
-
```ts
|
|
1589
|
-
import { cronNext } from 'weifuwu'
|
|
1590
|
-
|
|
1591
|
-
// Next weekday at 09:00
|
|
1592
|
-
const next = cronNext('0 9 * * 1-5')
|
|
1593
|
-
console.log(new Date(next))
|
|
1594
|
-
```
|
|
1595
|
-
|
|
1596
|
-
| Function | Description |
|
|
1597
|
-
| ----------------------- | ---------------------------------------------------------- |
|
|
1598
|
-
| `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]` |
|
|
1599
|
-
| `matches(fields, date)` | Check if a date matches a parsed pattern |
|
|
1600
|
-
| `cronNext(expr, from?)` | Calculate next matching timestamp (`from` defaults to now) |
|
|
1601
|
-
|
|
1602
|
-
### fts — Full-Text Search (PostgreSQL) [Database]
|
|
1603
|
-
|
|
1604
|
-
Utilities for PostgreSQL full-text search: create GIN indexes, search with ranking, and generate highlighted snippets.
|
|
1605
|
-
|
|
1606
|
-
```ts
|
|
1607
|
-
import { fts } from 'weifuwu'
|
|
1608
|
-
|
|
1609
|
-
const articles = pg.table('articles', {
|
|
1610
|
-
id: serial('id').primaryKey(),
|
|
1611
|
-
title: text('title'),
|
|
1612
|
-
body: text('body'),
|
|
1613
|
-
})
|
|
1614
|
-
|
|
1615
|
-
// Create search index
|
|
1616
|
-
await fts.createIndex(pg.sql, articles, ['title', 'body'], { language: 'english' })
|
|
1617
|
-
|
|
1618
|
-
// Search with ranking
|
|
1619
|
-
const results = await fts.search(pg.sql, articles, 'node.js framework', {
|
|
1620
|
-
fields: ['title', 'body'],
|
|
1621
|
-
limit: 20,
|
|
1622
|
-
headline: true, // highlighted snippets via ts_headline
|
|
1623
|
-
})
|
|
1624
|
-
// → [{ id, rank: 0.8, row: { title, body, ... }, headline: '...<b>Node.js</b> framework...' }]
|
|
1625
|
-
|
|
1626
|
-
// Drop index
|
|
1627
|
-
await fts.dropIndex(pg.sql, articles)
|
|
1628
|
-
```
|
|
1629
|
-
|
|
1630
|
-
| Function | Description |
|
|
1631
|
-
| ---------------------------------------- | ------------------------------ |
|
|
1632
|
-
| `createIndex(sql, table, fields, opts?)` | Create GIN/GiST tsvector index |
|
|
1633
|
-
| `search(sql, table, query, opts?)` | Search with ts_rank ordering |
|
|
1634
|
-
| `dropIndex(sql, table, opts?)` | Drop the index |
|
|
1635
|
-
|
|
1636
|
-
Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
|
|
1637
|
-
|
|
1638
|
-
### theme [α] [UX]
|
|
1639
|
-
|
|
1640
|
-
```ts
|
|
1641
|
-
// Single line — auto-registers middleware + /__theme/:value route
|
|
1642
|
-
app.use(theme({ default: 'dark' }))
|
|
1643
|
-
|
|
1644
|
-
// ctx.theme = { value: 'dark', set: fn }
|
|
1645
|
-
// ctx.theme.value — 'dark'
|
|
1646
|
-
// ctx.theme.set('light', '/settings') — 302 + Set-Cookie
|
|
1647
|
-
|
|
1648
|
-
// Explicit form for more control:
|
|
1649
|
-
// const t = theme()
|
|
1650
|
-
// app.use(t.middleware())
|
|
1651
|
-
// app.use('/', t)
|
|
1652
|
-
```
|
|
1653
|
-
|
|
1654
|
-
| Option | Type | Default | Description |
|
|
1655
|
-
| --------- | -------- | ---------- | ------------------------------ |
|
|
1656
|
-
| `default` | `string` | `'system'` | Default theme |
|
|
1657
|
-
| `cookie` | `string` | `'theme'` | Cookie name (empty to disable) |
|
|
1658
|
-
|
|
1659
|
-
```ts
|
|
1660
|
-
// Server-side switching
|
|
1661
|
-
app.post('/settings', async (req, ctx) => {
|
|
1662
|
-
const { theme } = await req.json()
|
|
1663
|
-
return ctx.theme.set(theme, '/settings')
|
|
1664
|
-
})
|
|
1665
|
-
```
|
|
1666
|
-
|
|
1667
|
-
See [`useTheme()`](#usetheme) for client-side usage.
|
|
1668
|
-
|
|
1669
|
-
### i18n [α] [UX]
|
|
1670
|
-
|
|
1671
|
-
```ts
|
|
1672
|
-
// Single line — auto-registers middleware + /__lang/:locale route
|
|
1673
|
-
app.use(i18n({ default: 'zh', dir: './locales' }))
|
|
1674
|
-
|
|
1675
|
-
// ctx.i18n = { locale: 'zh', t, set }
|
|
1676
|
-
// ctx.i18n.t('welcome') → '欢迎'
|
|
1677
|
-
// ctx.i18n.locale → 'zh'
|
|
1678
|
-
// ctx.i18n.set('en', '/settings') — 302 + Set-Cookie
|
|
1679
|
-
|
|
1680
|
-
// Explicit form for more control:
|
|
1681
|
-
// const l = i18n()
|
|
1682
|
-
// app.use(l.middleware())
|
|
1683
|
-
// app.use('/', l)
|
|
1684
|
-
```
|
|
1685
|
-
|
|
1686
|
-
| Option | Type | Default | Description |
|
|
1687
|
-
| -------------------- | --------- | ---------- | -------------------------------------------------- |
|
|
1688
|
-
| `default` | `string` | `'en'` | Default locale |
|
|
1689
|
-
| `dir` | `string` | — | Directory with `{locale}.json` files |
|
|
1690
|
-
| `messages` | `object` | — | Inline translations: `{ zh: { welcome: '欢迎' } }` |
|
|
1691
|
-
| `cookie` | `string` | `'locale'` | Cookie name (empty to disable) |
|
|
1692
|
-
| `fromAcceptLanguage` | `boolean` | `true` | Detect from Accept-Language header |
|
|
1693
|
-
|
|
1694
|
-
```ts
|
|
1695
|
-
// Handler
|
|
1696
|
-
app.get('/greet', async (req, ctx) => {
|
|
1697
|
-
const greeting = ctx.i18n?.t('welcome', { name: 'Alice' })
|
|
1698
|
-
return Response.json({ greeting, locale: ctx.i18n?.locale })
|
|
1699
|
-
})
|
|
1700
|
-
```
|
|
1701
|
-
|
|
1702
|
-
**Client-side:** import `useLocale` from `weifuwu/react`, `useTheme` from `weifuwu/react`.
|
|
1703
|
-
|
|
1704
|
-
### queue [α] [Database]
|
|
1705
|
-
|
|
1706
|
-
Async job queue. Supports immediate, delayed, and recurring (cron) tasks with three backends:
|
|
1707
|
-
|
|
1708
|
-
- `{ store: 'memory' }` — in-memory, zero dependency, suitable for dev & cron-like tasks
|
|
1709
|
-
- `{ store: 'pg', pg }` — PostgreSQL-backed, persistent, multi-instance safe via `FOR UPDATE SKIP LOCKED`
|
|
1710
|
-
- `{ store: 'redis', redis }` — Redis-backed, production-grade, distributed
|
|
1711
|
-
|
|
1712
|
-
```ts
|
|
1713
|
-
// Create queue
|
|
1714
|
-
const q = queue({ store: 'memory' })
|
|
1715
|
-
// const q = queue({ store: 'pg', pg })
|
|
1716
|
-
// const q = queue({ store: 'redis', redis })
|
|
1717
|
-
|
|
1718
|
-
// Register cron job (uses the same backend for persistence)
|
|
1719
|
-
q.cron('*/5 * * * *', async () => {
|
|
1720
|
-
await cleanCache()
|
|
1721
|
-
})
|
|
1722
|
-
|
|
1723
|
-
// Or use process/add for full queue semantics
|
|
1724
|
-
q.process('send-email', async (job) => {
|
|
1725
|
-
await sendMail(job.payload)
|
|
1726
|
-
})
|
|
1727
|
-
|
|
1728
|
-
// Immediate
|
|
1729
|
-
q.add('send-email', { to: 'user@test.com' })
|
|
1730
|
-
|
|
1731
|
-
// Delayed
|
|
1732
|
-
q.add('send-email', { to: 'user@test.com' }, { delay: 60_000 })
|
|
1733
|
-
|
|
1734
|
-
// Scheduled (cron) — re-queues automatically
|
|
1735
|
-
q.add('weekly-report', {}, { schedule: '0 9 * * 1' })
|
|
1736
|
-
|
|
1737
|
-
q.run()
|
|
1738
|
-
```
|
|
1739
|
-
|
|
1740
|
-
| Option | Type | Default | Description |
|
|
1741
|
-
| -------------- | ----------------------------- | ---------- | ----------------------------------------------- |
|
|
1742
|
-
| `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
|
|
1743
|
-
| `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
|
|
1744
|
-
| `url` | `string` | — | Redis URL (alternative to client) |
|
|
1745
|
-
| `pg` | `object` | — | PostgreSQL client (required when `store: 'pg'`) |
|
|
1746
|
-
| `prefix` | `string` | `'queue'` | Key/table prefix |
|
|
1747
|
-
| `pollInterval` | `number` | `200` | Poll interval (ms) |
|
|
1748
|
-
|
|
1749
|
-
| Method | Description |
|
|
1750
|
-
| ---------------------------- | --------------------------------------------------- |
|
|
1751
|
-
| `.cron(pattern, handler)` | Register a cron job (uses process + add internally) |
|
|
1752
|
-
| `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`) |
|
|
1753
|
-
| `.process(type, handler)` | Register job processor |
|
|
1754
|
-
| `.run()` | Start processing |
|
|
1755
|
-
| `.stop()` | Stop processing |
|
|
1756
|
-
| `.jobs(limit?)` | List pending jobs |
|
|
1757
|
-
| `.failedJobs(limit?)` | List failed jobs with error messages |
|
|
1758
|
-
| `.retryFailed(jobId)` | Retry a specific failed job |
|
|
1759
|
-
| `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
|
|
1760
|
-
| `.dashboard()` | Returns a Router with management endpoints |
|
|
1761
|
-
| `.close()` | Cleanup |
|
|
1762
|
-
|
|
1763
|
-
**Schedule (cron) field reference:**
|
|
1764
|
-
|
|
1765
|
-
| Field | Range |
|
|
1766
|
-
| ------------ | -------------- |
|
|
1767
|
-
| minute | 0–59 |
|
|
1768
|
-
| hour | 0–23 |
|
|
1769
|
-
| day of month | 1–31 |
|
|
1770
|
-
| month | 1–12 |
|
|
1771
|
-
| day of week | 0–6 (0=Sunday) |
|
|
1772
|
-
|
|
1773
|
-
Supported cron syntax: `*` (any), `*/n` (every n), `n-m` (range), `n,m,o` (list), `n` (exact).
|
|
1774
|
-
|
|
1775
|
-
**Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
|
|
1776
|
-
|
|
1777
|
-
| Method | Path | Description |
|
|
1778
|
-
| ------ | --------------- | ------------------------------------------- |
|
|
1779
|
-
| GET | `/` | Queue stats + pending/failed counts by type |
|
|
1780
|
-
| GET | `/:type/failed` | List failed jobs for a type |
|
|
1781
|
-
| POST | `/:type/retry` | Retry all failed jobs of a type |
|
|
1782
|
-
| POST | `/retry/:id` | Retry a specific failed job by ID |
|
|
1783
|
-
|
|
1784
|
-
### rateLimit [α] [Security]
|
|
1785
|
-
|
|
1786
|
-
```ts
|
|
1787
|
-
app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
|
|
1788
|
-
app.get('/api', rateLimit({ max: 10 }), handler) // per-route
|
|
1789
|
-
app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
|
|
1790
|
-
|
|
1791
|
-
// Multi-process: Redis-backed rate limiting
|
|
1792
|
-
app.use(rateLimit({ max: 100, store: 'redis', redis: ctx.redis }))
|
|
1793
|
-
|
|
1794
|
-
// Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After headers
|
|
1795
|
-
// m.stop() — clear interval (memory) or Redis cleanup
|
|
1796
|
-
```
|
|
1797
|
-
|
|
1798
|
-
| Option | Type | Default | Description |
|
|
1799
|
-
| --------- | --------------------- | --------------------- | --------------------------------------------- |
|
|
1800
|
-
| `max` | `number` | `100` | Max requests per window |
|
|
1801
|
-
| `window` | `number` | `60_000` | Window duration (ms) |
|
|
1802
|
-
| `key` | `(req) => string` | IP-based | Key function |
|
|
1803
|
-
| `message` | `string` | `'Too Many Requests'` | 429 response body |
|
|
1804
|
-
| `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
|
|
1805
|
-
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
1806
|
-
| `prefix` | `string` | `'ratelimit:'` | Redis key prefix |
|
|
1807
|
-
|
|
1808
|
-
Redis mode uses `INCR` + `EXPIRE` for atomic counting, enabling accurate rate limiting across multiple server processes. Memory mode is ideal for single-process deployments.
|
|
1809
|
-
|
|
1810
|
-
### redis [α] [Database]
|
|
1811
|
-
|
|
1812
|
-
```ts
|
|
1813
|
-
const r = redis() // reads REDIS_URL
|
|
1814
|
-
app.use(r) // injects ctx.redis
|
|
1815
|
-
await ctx.redis.set('key', 'value')
|
|
1816
|
-
// r.close() — cleanup
|
|
1817
|
-
```
|
|
1818
|
-
|
|
1819
|
-
| Option | Type | Default | Description |
|
|
1820
|
-
| --------------------- | -------- | --------------- | -------------------------- |
|
|
1821
|
-
| `url` | `string` | `REDIS_URL` env | Redis connection string |
|
|
1822
|
-
| (all ioredis options) | — | — | Passed directly to ioredis |
|
|
1823
|
-
|
|
1824
|
-
### requestId [α] [DevTools]
|
|
1825
|
-
|
|
1826
|
-
```ts
|
|
1827
|
-
app.use(requestId())
|
|
1828
|
-
app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID() }))
|
|
1829
|
-
// Sets X-Request-ID header on responses, available as ctx.requestId
|
|
1830
|
-
```
|
|
1831
|
-
|
|
1832
|
-
| Option | Type | Default | Description |
|
|
1833
|
-
| ----------- | -------------- | --------------------- | ------------------------- |
|
|
1834
|
-
| `header` | `string` | `'X-Request-ID'` | Header name to read/write |
|
|
1835
|
-
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
|
|
1836
|
-
|
|
1837
|
-
### trace [α] [DevTools]
|
|
1838
|
-
|
|
1839
|
-
Request-scoped tracing via `AsyncLocalStorage`. Use as middleware to inject `ctx.trace`:
|
|
1840
|
-
|
|
1841
|
-
```ts
|
|
1842
|
-
import { trace } from 'weifuwu'
|
|
1843
|
-
app.use(trace()) // → ctx.trace
|
|
1844
|
-
app.use(trace({ header: 'X-Trace-Id' })) // custom header
|
|
1845
|
-
|
|
1846
|
-
app.get('/', (req, ctx) => {
|
|
1847
|
-
console.log(ctx.trace.requestId) // 550e8400-e29b-...
|
|
1848
|
-
console.log(ctx.trace.traceId) // trace UUID
|
|
1849
|
-
console.log(ctx.trace.elapsed()) // ms since request start
|
|
1850
|
-
})
|
|
1851
|
-
```
|
|
1852
|
-
|
|
1853
|
-
| Option | Type | Default | Description |
|
|
1854
|
-
| ----------- | -------------- | --------------------- | ---------------------- |
|
|
1855
|
-
| `header` | `string` | `'X-Request-ID'` | Request ID header name |
|
|
1856
|
-
| `generator` | `() => string` | `crypto.randomUUID()` | Custom ID generator |
|
|
1857
|
-
|
|
1858
|
-
Utility functions (also available standalone):
|
|
1859
|
-
|
|
1860
|
-
```ts
|
|
1861
|
-
import { currentTraceId, runWithTrace, traceElapsed, currentTrace } from 'weifuwu'
|
|
1862
|
-
|
|
1863
|
-
const traceId = currentTraceId() // UUID or incoming X-Trace-Id
|
|
1864
|
-
const elapsed = traceElapsed() // ms since request started
|
|
1865
|
-
runWithTrace(incomingId, () => { ... }) // manual scope
|
|
1866
|
-
```
|
|
1867
|
-
|
|
1868
|
-
| Function | Description |
|
|
1869
|
-
| --------------------------- | ---------------------------------------------------------- |
|
|
1870
|
-
| `currentTraceId()` | Current request trace ID, or `undefined` outside a request |
|
|
1871
|
-
| `currentTrace()` | Full `{ traceId, startTime }` context |
|
|
1872
|
-
| `traceElapsed()` | Milliseconds elapsed since the trace started |
|
|
1873
|
-
| `runWithTrace(traceId, fn)` | Execute `fn` inside a trace scope |
|
|
1874
|
-
|
|
1875
|
-
### s3 [α] — S3-compatible object storage [Networking]
|
|
1876
|
-
|
|
1877
|
-
```ts
|
|
1878
|
-
import { s3 } from 'weifuwu'
|
|
1879
|
-
|
|
1880
|
-
app.use(
|
|
1881
|
-
s3({
|
|
1882
|
-
bucket: 'my-app',
|
|
1883
|
-
region: 'us-east-1',
|
|
1884
|
-
endpoint: process.env.S3_URL, // MinIO / R2 / AWS
|
|
1885
|
-
forcePathStyle: true, // required for MinIO
|
|
1886
|
-
credentials: {
|
|
1887
|
-
accessKeyId: process.env.S3_ACCESS_KEY,
|
|
1888
|
-
secretAccessKey: process.env.S3_SECRET_KEY,
|
|
1889
|
-
},
|
|
1890
|
-
publicUrl: 'https://cdn.example.com', // for unsigned public URLs
|
|
1891
|
-
}),
|
|
1892
|
-
)
|
|
1893
|
-
```
|
|
1894
|
-
|
|
1895
|
-
Injects `ctx.s3` with methods for S3-compatible object storage.
|
|
1896
|
-
|
|
1897
|
-
```ts
|
|
1898
|
-
// Upload
|
|
1899
|
-
await ctx.s3.put('images/logo.png', buffer, { contentType: 'image/png' })
|
|
1900
|
-
|
|
1901
|
-
// Download
|
|
1902
|
-
const buf = await ctx.s3.get('images/logo.png') // Buffer | null
|
|
1903
|
-
|
|
1904
|
-
// Delete
|
|
1905
|
-
await ctx.s3.delete('images/logo.png')
|
|
1906
|
-
|
|
1907
|
-
// Check existence
|
|
1908
|
-
if (await ctx.s3.exists('images/logo.png')) { ... }
|
|
1909
|
-
|
|
1910
|
-
// Signed URL (expires in 1 hour by default)
|
|
1911
|
-
const url = await ctx.s3.url('images/logo.png')
|
|
1912
|
-
const shortUrl = await ctx.s3.url('images/logo.png', { expiresIn: 60 })
|
|
1913
|
-
|
|
1914
|
-
// Public URL (requires publicUrl in options)
|
|
1915
|
-
const publicUrl = await ctx.s3.url('images/logo.png', { expiresIn: 0 })
|
|
1916
|
-
|
|
1917
|
-
// List objects under a prefix
|
|
1918
|
-
const keys = await ctx.s3.list('images/')
|
|
1919
|
-
```
|
|
1920
|
-
|
|
1921
|
-
| Option | Type | Default | Description |
|
|
1922
|
-
| ---------------- | ---------------------------------- | ------------- | ------------------------------------------------------------------ |
|
|
1923
|
-
| `bucket` | `string` | — | **Required.** S3 bucket name |
|
|
1924
|
-
| `region` | `string` | `'us-east-1'` | AWS region |
|
|
1925
|
-
| `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
|
|
1926
|
-
| `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
|
|
1927
|
-
| `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
|
|
1928
|
-
| `publicUrl` | `string` | — | Base URL for unsigned public URLs via `url(key, { expiresIn: 0 })` |
|
|
1929
|
-
|
|
1930
|
-
Credentials can be omitted to use AWS environment variables (`AWS_ACCESS_KEY_ID`,
|
|
1931
|
-
`AWS_SECRET_ACCESS_KEY`) or IAM roles (EC2, ECS, Lambda).
|
|
1932
|
-
|
|
1933
|
-
The module can also be used standalone without the middleware:
|
|
1934
|
-
|
|
1935
|
-
```ts
|
|
1936
|
-
const storage = s3({ bucket: 'my-app', endpoint: '...' })
|
|
1937
|
-
await storage.put('key', body)
|
|
1938
|
-
const data = await storage.get('key')
|
|
1939
|
-
```
|
|
1940
|
-
|
|
1941
|
-
Requires MinIO or another S3-compatible service for local development.
|
|
1942
|
-
Add to `docker-compose.yml`:
|
|
1943
|
-
|
|
1944
|
-
```yml
|
|
1945
|
-
minio:
|
|
1946
|
-
image: minio/minio
|
|
1947
|
-
ports:
|
|
1948
|
-
- '9000:9000'
|
|
1949
|
-
environment:
|
|
1950
|
-
MINIO_ROOT_USER: minioadmin
|
|
1951
|
-
MINIO_ROOT_PASSWORD: minioadmin
|
|
1952
|
-
command: server /data
|
|
1953
|
-
```
|
|
1954
|
-
|
|
1955
|
-
### seo [β] + seoMiddleware [α] [API]
|
|
1956
|
-
|
|
1957
|
-
```ts
|
|
1958
|
-
app.use(
|
|
1959
|
-
'/',
|
|
1960
|
-
seo({
|
|
1961
|
-
baseUrl: 'https://example.com',
|
|
1962
|
-
robots: [{ userAgent: '*', allow: '/' }],
|
|
1963
|
-
sitemap: { urls: [{ loc: '/' }] },
|
|
1964
|
-
}),
|
|
1965
|
-
)
|
|
1966
|
-
// GET /robots.txt, GET /sitemap.xml
|
|
1967
|
-
|
|
1968
|
-
app.use(
|
|
1969
|
-
seoMiddleware({
|
|
1970
|
-
headers: { 'X-Robots-Tag': (path) => (path.startsWith('/admin') ? 'noindex' : undefined) },
|
|
1971
|
-
}),
|
|
1972
|
-
)
|
|
1973
|
-
```
|
|
1974
|
-
|
|
1975
|
-
Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
|
|
1976
|
-
|
|
1977
|
-
| Option | Type | Default | Description |
|
|
1978
|
-
| --------- | ------------------ | ---------------------------------- | ----------------------------------------------- |
|
|
1979
|
-
| `baseUrl` | `string` | — | Base URL for sitemap URLs |
|
|
1980
|
-
| `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
|
|
1981
|
-
| `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
|
|
1982
|
-
| `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
|
|
1983
|
-
|
|
1984
|
-
### session [α] [Security]
|
|
1985
|
-
|
|
1986
|
-
Cookie-based server-side session management with memory and Redis stores.
|
|
1987
|
-
|
|
1988
|
-
```ts
|
|
1989
|
-
app.use(session()) // in-memory store (default)
|
|
1990
|
-
app.use(session({ store: 'redis', redis: ctx.redis })) // Redis store
|
|
1991
|
-
app.use(session({ store: 'redis', redis, ttl: 30 * 60_000, cookieName: 'sid' }))
|
|
1992
|
-
|
|
1993
|
-
app.get('/login', async (req, ctx) => {
|
|
1994
|
-
ctx.session.userId = 42
|
|
1995
|
-
ctx.session.role = 'admin'
|
|
1996
|
-
// Auto-saved on response — cookie set automatically
|
|
1997
|
-
return Response.json({ ok: true })
|
|
1998
|
-
})
|
|
1999
|
-
|
|
2000
|
-
app.get('/logout', async (req, ctx) => {
|
|
2001
|
-
ctx.session.destroy() // or ctx.session = null
|
|
2002
|
-
return Response.json({ ok: true })
|
|
2003
|
-
})
|
|
2004
|
-
|
|
2005
|
-
// ctx.session.id — readonly session ID
|
|
2006
|
-
// ctx.session.save() — explicit dirty mark (for deep mutations)
|
|
2007
|
-
// ctx.session.destroy() — clear session + remove cookie
|
|
2008
|
-
// Session mutations are auto-detected on property set/delete
|
|
2009
|
-
```
|
|
2010
|
-
|
|
2011
|
-
| Option | Type | Default | Description |
|
|
2012
|
-
| ----------------- | ------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
2013
|
-
| `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
|
|
2014
|
-
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
2015
|
-
| `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
|
|
2016
|
-
| `cookieName` | `string` | `'__session'` | Cookie name |
|
|
2017
|
-
| `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
|
|
2018
|
-
| `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
|
|
2019
|
-
| `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
|
|
2020
|
-
| `cookie.path` | `string` | `'/'` | Cookie path |
|
|
2021
|
-
| `cookie.domain` | `string` | — | Cookie domain |
|
|
2022
|
-
| `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
|
|
2023
|
-
| `rotateInterval` | `number` | `900000` (15min) | Auto-rotate session ID to prevent fixation attacks. Set `0` to disable |
|
|
2024
|
-
|
|
2025
|
-
When `secret` is set, the cookie value is signed with HMAC-SHA256:
|
|
2026
|
-
`uuid.base64url(hmac)`. Tampered cookies are rejected and treated as new
|
|
2027
|
-
sessions (no error message, no data leak).
|
|
2028
|
-
|
|
2029
|
-
Session ID auto-rotation copies data to a new ID and deletes the old one
|
|
2030
|
-
from the store. Rotation happens transparently on the next request after
|
|
2031
|
-
`rotateInterval` has elapsed.
|
|
2032
|
-
|
|
2033
|
-
**Stores** are also exported for standalone use:
|
|
2034
|
-
|
|
2035
|
-
```ts
|
|
2036
|
-
import { MemoryStore, RedisStore } from 'weifuwu'
|
|
2037
|
-
|
|
2038
|
-
const mem = new MemoryStore() // auto-cleanup every 60s
|
|
2039
|
-
await mem.set('sid', { userId: 1 }, 86400000)
|
|
2040
|
-
mem.close()
|
|
2041
|
-
|
|
2042
|
-
const redis = new RedisStore(redisClient, 'myapp:session:')
|
|
2043
|
-
await redis.destroy('sid')
|
|
2044
|
-
```
|
|
2045
|
-
|
|
2046
|
-
### ssr({ dir }) [β] [SSR]
|
|
2047
|
-
|
|
2048
|
-
One-stop Server-Side Rendering. Accepts a directory and returns a Router that handles all SSR routes, tailwind CSS, hydration, and livereload — using Next.js-style file conventions.
|
|
2049
|
-
|
|
2050
|
-
```ts
|
|
2051
|
-
import { Router, ssr } from 'weifuwu'
|
|
2052
|
-
const app = new Router()
|
|
2053
|
-
app.use('/', ssr({ dir: './ui' }))
|
|
2054
|
-
```
|
|
2055
|
-
|
|
2056
|
-
**Directory conventions (Next.js-style):**
|
|
2057
|
-
|
|
2058
|
-
```
|
|
2059
|
-
./ui/
|
|
2060
|
-
├── app/ ← only this directory affects routing
|
|
2061
|
-
│ ├── globals.css ← tailwind CSS + CSS variables (optional)
|
|
2062
|
-
│ ├── layout.tsx → root layout (wraps all pages)
|
|
2063
|
-
│ ├── page.tsx → GET /
|
|
2064
|
-
│ ├── not-found.tsx → 404 page (optional)
|
|
2065
|
-
│ ├── error.tsx → error boundary (optional)
|
|
2066
|
-
│ ├── about/
|
|
2067
|
-
│ │ ├── page.tsx → GET /about
|
|
2068
|
-
│ │ └── layout.tsx → group layout
|
|
2069
|
-
│ └── posts/
|
|
2070
|
-
│ ├── page.tsx → GET /posts
|
|
2071
|
-
│ └── [id]/
|
|
2072
|
-
│ └── page.tsx → GET /posts/:id
|
|
2073
|
-
├── components/ ← shared components (does not affect routing)
|
|
2074
|
-
└── lib/ ← utilities (does not affect routing)
|
|
2075
|
-
```
|
|
2076
|
-
|
|
2077
|
-
| Location | Route |
|
|
2078
|
-
| ---------------------- | -------------------------------------------------------- |
|
|
2079
|
-
| `app/page.tsx` | `GET /` |
|
|
2080
|
-
| `app/[param]/page.tsx` | `GET /:param` |
|
|
2081
|
-
| `app/layout.tsx` | Root layout (wraps all pages in its subtree) |
|
|
2082
|
-
| `app/not-found.tsx` | 404 fallback for that subtree |
|
|
2083
|
-
| `app/error.tsx` | Error boundary for that subtree |
|
|
2084
|
-
| `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
|
|
2085
|
-
|
|
2086
|
-
**How hydration works:**
|
|
2087
|
-
|
|
2088
|
-
- Each page is lazy-resolved on first request — only the `page.tsx` and its layout chain are compiled
|
|
2089
|
-
- An inline `<script type="module">` in the HTML handles hydration
|
|
2090
|
-
- It imports `{ setCtx, TsxContext }` from the vendor bundle (`/__wfw/v/bundle`) via importmap
|
|
2091
|
-
- Then dynamically imports the page component: `await import('/__ssr/[hash].js')`
|
|
2092
|
-
- The vendor bundle (react + react-dom + weifuwu client libs) is compiled once and cached
|
|
2093
|
-
- Page components are pre-compiled to `/__ssr/{hash}.js` — no runtime esbuild after first request
|
|
2094
|
-
- **Dev:** `createRoot` + render; **Production:** `hydrateRoot` (reuses SSR DOM)
|
|
2095
|
-
- The hydration script and page component share the same SSR context store — data flows seamlessly from server to client
|
|
2096
|
-
- Tailwind CSS served at `/__wfw/style/{hash}.css` (cached, content-hashed)
|
|
2097
|
-
- Dev mode extras: HMR WebSocket, file watcher, hot component replacement
|
|
2098
|
-
|
|
2099
|
-
```ts
|
|
2100
|
-
// Multiple independent SSR directories
|
|
2101
|
-
app.use('/', ssr({ dir: './www' }))
|
|
2102
|
-
app.use('/admin', ssr({ dir: './admin' }))
|
|
2103
|
-
|
|
2104
|
-
// API routes coexist normally
|
|
2105
|
-
app.get('/api/ping', () => Response.json({ pong: true }))
|
|
2106
|
-
```
|
|
2107
|
-
|
|
2108
|
-
Layout components receive `{ children }` and wrap from outer to inner:
|
|
2109
|
-
|
|
2110
|
-
### tenant [β] [Networking]
|
|
2111
|
-
|
|
2112
|
-
Multi-tenant BaaS with dynamic table API and GraphQL.
|
|
2113
|
-
|
|
2114
|
-
```ts
|
|
2115
|
-
const t = tenant({ pg, usersTable: '_users' })
|
|
2116
|
-
await t.migrate()
|
|
2117
|
-
app.use('/api', t.middleware()) // → ctx.tenant
|
|
2118
|
-
app.use('/api', t) // dynamic CRUD
|
|
2119
|
-
app.use('/graphql', t.graphql()) // dynamic GraphQL
|
|
2120
|
-
```
|
|
2121
|
-
|
|
2122
|
-
| Option | Type | Default | Description |
|
|
2123
|
-
| ------------ | -------- | ------- | --------------------------------------------- |
|
|
2124
|
-
| `pg` | `object` | — | PostgreSQL client |
|
|
2125
|
-
| `usersTable` | `string` | — | Users table name for tenant membership lookup |
|
|
2126
|
-
|
|
2127
|
-
### upload [α] [DevTools]
|
|
2128
|
-
|
|
2129
|
-
```ts
|
|
2130
|
-
app.post(
|
|
2131
|
-
'/upload',
|
|
2132
|
-
upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }),
|
|
2133
|
-
(req, ctx) => {
|
|
2134
|
-
// ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
|
|
2135
|
-
// Multiple files with same field name → array
|
|
2136
|
-
// ctx.parsed.fields.title → 'hello'
|
|
2137
|
-
},
|
|
2138
|
-
)
|
|
2139
|
-
```
|
|
2140
|
-
|
|
2141
|
-
| Option | Type | Default | Description |
|
|
2142
|
-
| -------------- | ---------- | ------- | ---------------------------------------- |
|
|
2143
|
-
| `dir` | `string` | — | Write files to disk (omit for in-memory) |
|
|
2144
|
-
| `maxFileSize` | `number` | — | Max bytes per file |
|
|
2145
|
-
| `allowedTypes` | `string[]` | — | Allowed MIME types |
|
|
2146
|
-
|
|
2147
|
-
### user [β] [Security]
|
|
2148
|
-
|
|
2149
|
-
Authentication: register, login, JWT, OAuth2 服务端, 社会化登录.
|
|
2150
|
-
|
|
2151
|
-
```ts
|
|
2152
|
-
const u = user({
|
|
2153
|
-
pg,
|
|
2154
|
-
jwtSecret: process.env.JWT_SECRET!,
|
|
2155
|
-
oauthLogin: {
|
|
2156
|
-
// 可选 — 社会化登录
|
|
2157
|
-
providers: {
|
|
2158
|
-
github: { clientId: '...', clientSecret: '...' },
|
|
2159
|
-
google: { clientId: '...', clientSecret: '...' },
|
|
2160
|
-
},
|
|
2161
|
-
},
|
|
2162
|
-
})
|
|
2163
|
-
await u.migrate()
|
|
2164
|
-
app.use(u) // POST /register, POST /login
|
|
2165
|
-
app.use(u.middleware()) // ctx.user
|
|
2166
|
-
// GET /auth/github, GET /auth/github/callback (如配置 oauthLogin)
|
|
2167
|
-
```
|
|
2168
|
-
|
|
2169
|
-
| Option | Type | Default | Description |
|
|
2170
|
-
| ------------ | -------- | ---------- | ------------------------------------------------------------------------------ |
|
|
2171
|
-
| `pg` | `object` | — | PostgreSQL client |
|
|
2172
|
-
| `jwtSecret` | `string` | — | JWT signing secret |
|
|
2173
|
-
| `table` | `string` | `'_users'` | Users table name |
|
|
2174
|
-
| `expiresIn` | `string` | `'24h'` | JWT expiration |
|
|
2175
|
-
| `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
|
|
2176
|
-
| `oauthLogin` | `object` | — | 社会化登录: `{ providers: Record<string, OAuthProviderConfig>, redirectUrl? }` |
|
|
2177
|
-
|
|
2178
|
-
| Method | Description |
|
|
2179
|
-
| ----------------- | --------------------------------------- |
|
|
2180
|
-
| `.register(data)` | Register a new user programmatically |
|
|
2181
|
-
| `.login(data)` | Log in programmatically |
|
|
2182
|
-
| `.verify(token)` | Verify JWT token |
|
|
2183
|
-
| `.middleware()` | JWT verify middleware — sets `ctx.user` |
|
|
2184
|
-
|
|
2185
|
-
**API Key management** — enable via `user({ apiKeys: true })`:
|
|
2186
|
-
|
|
2187
|
-
```ts
|
|
2188
|
-
const auth = user({ pg, jwtSecret: process.env.JWT_SECRET, apiKeys: true })
|
|
2189
|
-
await auth.migrate()
|
|
2190
|
-
app.use(auth)
|
|
2191
|
-
|
|
2192
|
-
// Create an API key (server-side)
|
|
2193
|
-
const { id, key } = await auth.createApiKey(userId, 'Deploy Key', ['read', 'deploy'])
|
|
2194
|
-
// key → 'sk_live_abc123...' (only shown once!)
|
|
2195
|
-
|
|
2196
|
-
// List keys (masked)
|
|
2197
|
-
const keys = await auth.listApiKeys(userId)
|
|
2198
|
-
// → [{ id, name, prefix: 'sk_live_abc...f3a2', scopes: ['read','deploy'], last_used_at, revoked }]
|
|
2199
|
-
|
|
2200
|
-
// Revoke a key
|
|
2201
|
-
await auth.revokeApiKey(userId, keyId)
|
|
2202
|
-
|
|
2203
|
-
// REST API (auto-mounted when routes are registered)
|
|
2204
|
-
// POST /api-keys → Create (requires auth)
|
|
2205
|
-
// GET /api-keys → List (requires auth)
|
|
2206
|
-
// DELETE /api-keys/:id → Revoke (requires auth)
|
|
2207
|
-
```
|
|
2208
|
-
|
|
2209
|
-
API keys start with `sk_live_` and are hashed with SHA256 before storage.
|
|
2210
|
-
The middleware resolves API keys automatically — use them with `Authorization: Bearer sk_live_...`.
|
|
2211
|
-
|
|
2212
|
-
_New in v0.25._
|
|
2213
|
-
|
|
2214
|
-
### permissions [α] — RBAC [Security]
|
|
2215
|
-
|
|
2216
|
-
Role-based access control.
|
|
2217
|
-
|
|
2218
|
-
```ts
|
|
2219
|
-
const perm = permissions({ pg })
|
|
2220
|
-
await perm.migrate()
|
|
2221
|
-
|
|
2222
|
-
// Assign roles & permissions
|
|
2223
|
-
await perm.assignRole(userId, 'admin')
|
|
2224
|
-
await perm.grantPermission('admin', 'posts:create')
|
|
2225
|
-
await perm.grantPermission('admin', 'posts:edit')
|
|
2226
|
-
await perm.grantPermission('admin', '*') // wildcard — all permissions
|
|
2227
|
-
|
|
2228
|
-
// Use as middleware
|
|
2229
|
-
app.use((req, ctx, next) => {
|
|
2230
|
-
ctx.user = { id: userId }
|
|
2231
|
-
return next(req, ctx)
|
|
2232
|
-
})
|
|
2233
|
-
app.use(perm) // → ctx.roles, ctx.permissions
|
|
2234
|
-
|
|
2235
|
-
// Route guards
|
|
2236
|
-
app.get('/admin', perm.requireRole('admin'), adminHandler)
|
|
2237
|
-
app.post('/posts', perm.requirePermission('posts:create'), createHandler)
|
|
2238
|
-
|
|
2239
|
-
// Handler-level check
|
|
2240
|
-
app.get('/posts/:id', async (req, ctx) => {
|
|
2241
|
-
if (!ctx.permissions.has('posts:read')) {
|
|
2242
|
-
return Response.json({ error: 'forbidden' }, { status: 403 })
|
|
2243
|
-
}
|
|
2244
|
-
return Response.json(post)
|
|
2245
|
-
})
|
|
2246
|
-
```
|
|
2247
|
-
|
|
2248
|
-
| Option | Type | Default | Description |
|
|
2249
|
-
| -------- | -------- | ------- | --------------------------------------------- |
|
|
2250
|
-
| `pg` | `object` | — | PostgreSQL client |
|
|
2251
|
-
| `prefix` | `string` | `''` | Table prefix (e.g. `'myapp'` → `myapp_roles`) |
|
|
2252
|
-
|
|
2253
|
-
| Method | Description |
|
|
2254
|
-
| ------------------------------------- | --------------------------------------------------- |
|
|
2255
|
-
| `.assignRole(userId, role)` | Assign role to user (creates role if missing) |
|
|
2256
|
-
| `.removeRole(userId, role)` | Remove role from user |
|
|
2257
|
-
| `.grantPermission(role, permission)` | Grant permission to role |
|
|
2258
|
-
| `.revokePermission(role, permission)` | Revoke permission from role |
|
|
2259
|
-
| `.getUserRoles(userId)` | List user's roles |
|
|
2260
|
-
| `.getUserPermissions(userId)` | List user's permissions (union of all roles) |
|
|
2261
|
-
| `.requireRole(...roles)` | Middleware — rejects if user lacks any of the roles |
|
|
2262
|
-
| `.requirePermission(...perms)` | Middleware — rejects if user lacks any permission |
|
|
2263
|
-
| `.migrate()` | Create tables |
|
|
2264
|
-
|
|
2265
|
-
### validate [α] [DevTools]
|
|
2266
|
-
|
|
2267
|
-
````ts
|
|
2268
|
-
import { z } from 'zod'
|
|
2269
|
-
const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
|
|
2270
|
-
app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string().optional() }) }), (req, ctx) => {
|
|
2271
|
-
// ctx.parsed.body — typed & validated
|
|
2272
|
-
// ctx.parsed.query — typed & validated
|
|
2273
|
-
// ctx.parsed.params — typed & validated (for dynamic routes)
|
|
2274
|
-
// ctx.parsed.headers — typed & validated
|
|
2275
|
-
})
|
|
2276
|
-
// Validation failure: returns 400 with { error: 'Validation failed', issues: [...] }
|
|
2277
|
-
|
|
2278
|
-
**Form body auto-parsing** — `application/x-www-form-urlencoded` bodies are automatically parsed into `Record<string, string>` via `URLSearchParams`, even without a Zod schema:
|
|
2279
|
-
|
|
2280
|
-
```ts
|
|
2281
|
-
// No schema needed — just parse the form
|
|
2282
|
-
app.post('/contact', validate(), (req, ctx) => {
|
|
2283
|
-
const email = ctx.parsed.body.email // string
|
|
2284
|
-
const msg = ctx.parsed.body.message // string
|
|
2285
|
-
return Response.json({ received: true })
|
|
2286
|
-
})
|
|
2287
|
-
|
|
2288
|
-
// Or validate with Zod
|
|
2289
|
-
app.post('/contact', validate({ body: z.object({ email: z.string().email() }) }), handler)
|
|
2290
|
-
````
|
|
2291
|
-
|
|
2292
|
-
| Option | Type | Default | Description |
|
|
2293
|
-
| --------- | ----------- | ------- | ------------------------------------- |
|
|
2294
|
-
| `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
|
|
2295
|
-
| `query` | `ZodSchema` | — | Query validation schema |
|
|
2296
|
-
| `params` | `ZodSchema` | — | URL params validation schema |
|
|
2297
|
-
| `headers` | `ZodSchema` | — | Header validation schema |
|
|
2298
|
-
|
|
2299
|
-
### webhook [β] [API]
|
|
2300
|
-
|
|
2301
|
-
Webhook receiver with built-in signature verification for Stripe, GitHub, and Slack. Event-based dispatch with replay protection.
|
|
2302
|
-
|
|
2303
|
-
```ts
|
|
2304
|
-
import { webhook } from 'weifuwu'
|
|
2305
|
-
|
|
2306
|
-
const wh = webhook({
|
|
2307
|
-
stripe: { secret: process.env.STRIPE_WEBHOOK_SECRET! },
|
|
2308
|
-
github: { secret: process.env.GITHUB_WEBHOOK_SECRET! },
|
|
2309
|
-
slack: { secret: process.env.SLACK_WEBHOOK_SECRET! },
|
|
2310
|
-
})
|
|
2311
|
-
|
|
2312
|
-
app.use('/webhooks', wh)
|
|
2313
|
-
|
|
2314
|
-
wh.on('checkout.session.completed', async (event, ctx) => {
|
|
2315
|
-
await fulfillOrder(event.payload.data.object)
|
|
2316
|
-
})
|
|
2317
|
-
|
|
2318
|
-
wh.on('push', async (event, ctx) => {
|
|
2319
|
-
await triggerCI(event.payload)
|
|
2320
|
-
})
|
|
2321
|
-
|
|
2322
|
-
wh.on('*', (event) => {
|
|
2323
|
-
console.log(`Received ${event.provider}.${event.event}`)
|
|
2324
|
-
})
|
|
2325
|
-
```
|
|
2326
|
-
|
|
2327
|
-
| Option | Type | Default | Description |
|
|
2328
|
-
| ------------------ | ------------------------ | --------- | ----------------------------------- |
|
|
2329
|
-
| `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
|
|
2330
|
-
| `github` | `PlatformConfig` | — | GitHub webhook config |
|
|
2331
|
-
| `slack` | `PlatformConfig` | — | Slack webhook config |
|
|
2332
|
-
| `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
|
|
2333
|
-
| `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
|
|
2334
|
-
| `idempotencyTTL` | `number` | `3600000` | Dedup TTL (ms) |
|
|
2335
|
-
|
|
2336
|
-
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.
|
|
2337
|
-
|
|
2338
|
-
### Client-side navigation [δ] [Client]
|
|
2339
|
-
|
|
2340
|
-
```tsx
|
|
2341
|
-
import { Link, navigate, useNavigate, useNavigating } from 'weifuwu/react'
|
|
2342
|
-
;<Link href="/about" prefetch>
|
|
2343
|
-
About
|
|
2344
|
-
</Link> // client-side nav + prefetch on hover/visible
|
|
2345
|
-
const n = useNavigate() // hook: n('/contact')
|
|
2346
|
-
navigate('/contact') // bare function (no hook needed)
|
|
2347
|
-
const loading = useNavigating() // reactive loading state
|
|
2348
|
-
```
|
|
2349
|
-
|
|
2350
|
-
`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.
|
|
2351
|
-
|
|
2352
|
-
**Preference URLs** (`/__lang/`, `/__theme/`) are intercepted by modular interceptors registered via `addInterceptor()` — no page reload needed. Importing `useLocale` or `useTheme` registers the interceptor automatically.
|
|
2353
|
-
|
|
2354
|
-
### Client-side hooks [δ] [Client]
|
|
2355
|
-
|
|
2356
|
-
```tsx
|
|
2357
|
-
import { useWebsocket, useAction, useFetch, useQueryState, createStore, Head } from 'weifuwu/react'
|
|
2358
|
-
import { useLocale, useTheme, applyTheme, addInterceptor, useLoaderData, useFlashMessage } from 'weifuwu/react'
|
|
2359
|
-
|
|
2360
|
-
// WebSocket — auto-reconnecting
|
|
2361
|
-
const { send, lastMessage, readyState, close, reconnect } = useWebsocket('/ws/chat', {
|
|
2362
|
-
onMessage: (d) => console.log(d),
|
|
2363
|
-
reconnect: { maxRetries: 10, delay: 3000 },
|
|
2364
|
-
protocols: [], // optional sub-protocols
|
|
2365
|
-
enabled: true, // pause/resume connection
|
|
2366
|
-
})
|
|
2367
|
-
|
|
2368
|
-
// Form action
|
|
2369
|
-
const { submit, data, error, pending, reset } = useAction('/api/feedback', {
|
|
2370
|
-
method: 'POST',
|
|
2371
|
-
headers: { 'X-Custom': 'value' },
|
|
2372
|
-
onSuccess: (data) => console.log(data),
|
|
2373
|
-
onError: (err) => console.error(err),
|
|
2374
|
-
})
|
|
2375
|
-
// Auto-reads _csrf cookie, sends as x-csrf-token or x-xsrf-token
|
|
2376
|
-
|
|
2377
|
-
// Data fetching — cache + dedup + mutate
|
|
2378
|
-
const { data, error, loading, mutate } = useFetch('/api/posts', { fallback: loadData, ttl: 30_000 })
|
|
2379
|
-
|
|
2380
|
-
// URL query state
|
|
2381
|
-
const [q, setQ] = useQueryState('q', '')
|
|
2382
|
-
const [page, setPage] = useQueryState('page', '1')
|
|
2383
|
-
|
|
2384
|
-
// Shared state — persists across client navs
|
|
2385
|
-
const useStore = createStore({ count: 0 })
|
|
2386
|
-
const count = useStore(s => s.count)
|
|
2387
|
-
|
|
2388
|
-
// Per-page meta tags
|
|
2389
|
-
<Head><title>Page Title</title><meta name="description" content="..." /></Head>
|
|
2390
|
-
```
|
|
2391
|
-
|
|
2392
|
-
**`TsxContext`** — React context holding page data (`params`, `query`, `user`, `parsed`, `theme`, `i18n`, `flash`, `loaderData`, `env`). Used internally by hooks; rarely needed directly.
|
|
2393
|
-
|
|
2394
|
-
### Locale & Theme [δ] [Client]
|
|
2395
|
-
|
|
2396
|
-
```tsx
|
|
2397
|
-
import { useLocale } from 'weifuwu/react'
|
|
2398
|
-
function LangSwitch() {
|
|
2399
|
-
const { locale, setLocale, t } = useLocale()
|
|
2400
|
-
return <button onClick={() => setLocale('zh-CN')}>{t('switch_lang')}</button>
|
|
2401
|
-
}
|
|
2402
|
-
```
|
|
2403
|
-
|
|
2404
|
-
| Return | Description |
|
|
2405
|
-
| ------------------- | ----------------------------------------------------- |
|
|
2406
|
-
| `locale` | Current locale string (from `ctx.i18n.locale`) |
|
|
2407
|
-
| `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
|
|
2408
|
-
| `t` | Translate a key using loaded locale messages |
|
|
2409
|
-
|
|
2410
|
-
```tsx
|
|
2411
|
-
import { useTheme } from 'weifuwu/react'
|
|
2412
|
-
function ThemeToggle() {
|
|
2413
|
-
const { theme, resolvedTheme, setTheme } = useTheme()
|
|
2414
|
-
return (
|
|
2415
|
-
<>
|
|
2416
|
-
<span>Current: {resolvedTheme}</span> {/* 'dark' | 'light' — never 'system' */}
|
|
2417
|
-
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
|
|
2418
|
-
<option value="light">☀ Light</option>
|
|
2419
|
-
<option value="dark">🌙 Dark</option>
|
|
2420
|
-
<option value="system">💻 System</option>
|
|
2421
|
-
</select>
|
|
2422
|
-
</>
|
|
2423
|
-
)
|
|
2424
|
-
}
|
|
2425
|
-
```
|
|
2426
|
-
|
|
2427
|
-
| Return | Description |
|
|
2428
|
-
| ----------------- | ---------------------------------------------------------------- |
|
|
2429
|
-
| `theme` | Raw preference (`'light'` \| `'dark'` \| `'system'`) |
|
|
2430
|
-
| `resolvedTheme` | Resolved value (`'light'` \| `'dark'`) — `'system'` → matchMedia |
|
|
2431
|
-
| `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`) |
|
|
2432
|
-
|
|
2433
|
-
**`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
|
|
2434
|
-
|
|
2435
|
-
**`useLoaderData()`** — Returns middleware-injected data from the request context. Works identically on server (SSR) and client (hydration/SPA). Re-renders on SPA navigation.
|
|
2436
|
-
|
|
2437
|
-
```tsx
|
|
2438
|
-
import { useLoaderData } from 'weifuwu/react'
|
|
2439
|
-
function Page() {
|
|
2440
|
-
const data = useLoaderData<{ posts: Post[] }>()
|
|
2441
|
-
return (
|
|
2442
|
-
<ul>
|
|
2443
|
-
{data.posts.map((p) => (
|
|
2444
|
-
<li key={p.id}>{p.title}</li>
|
|
2445
|
-
))}
|
|
2446
|
-
</ul>
|
|
2447
|
-
)
|
|
2448
|
-
}
|
|
2449
|
-
```
|
|
2450
|
-
|
|
2451
|
-
On the server, data flows from middleware → `ctx` → `setCtx(ctxValue)` (serialized via JSON). On the client, the hydration script calls `setCtx(ctxData)` which populates the shared context store. `useLoaderData()` reads from the snapshot via `useSyncExternalStore` — no SSR-specific code needed in your components.
|
|
2452
|
-
|
|
2453
|
-
**`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
|
|
2454
|
-
|
|
2455
|
-
```ts
|
|
2456
|
-
import { addInterceptor } from 'weifuwu/react'
|
|
2457
|
-
addInterceptor(async (url) => {
|
|
2458
|
-
if (!url.pathname.startsWith('/__custom/')) return false
|
|
2459
|
-
// handle without page reload
|
|
2460
|
-
return true
|
|
2461
|
-
})
|
|
2462
|
-
```
|
|
2463
|
-
|
|
2464
|
-
### Flash messages [δ] [Client]
|
|
2465
|
-
|
|
2466
|
-
```ts
|
|
2467
|
-
import { flash } from 'weifuwu'
|
|
2468
|
-
|
|
2469
|
-
app.use(flash())
|
|
2470
|
-
|
|
2471
|
-
// Read flash
|
|
2472
|
-
app.get('/', (req, ctx) => {
|
|
2473
|
-
const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
|
|
2474
|
-
})
|
|
2475
|
-
|
|
2476
|
-
// Set flash + redirect
|
|
2477
|
-
app.post('/save', async (req, ctx) => {
|
|
2478
|
-
await saveArticle()
|
|
2479
|
-
return ctx.flash.set({ type: 'success', text: '已保存' }, '/articles')
|
|
2480
|
-
// → 302 /articles + Set-Cookie flash=...
|
|
2481
|
-
})
|
|
2482
|
-
```
|
|
2483
|
-
|
|
2484
|
-
```tsx
|
|
2485
|
-
// Client
|
|
2486
|
-
import { useFlashMessage } from 'weifuwu/react'
|
|
2487
|
-
|
|
2488
|
-
function Toast() {
|
|
2489
|
-
const flash = useFlashMessage<{ type: string; text: string }>()
|
|
2490
|
-
if (!flash) return null
|
|
2491
|
-
return <div className={`toast toast-${flash.type}`}>{flash.text}</div>
|
|
2492
|
-
}
|
|
2493
|
-
```
|
|
2494
|
-
|
|
2495
|
-
| Option | Type | Default | Description |
|
|
2496
|
-
| ------ | -------- | --------- | ----------- |
|
|
2497
|
-
| `name` | `string` | `'flash'` | Cookie name |
|
|
2498
|
-
|
|
2499
|
-
### Dev mode [δ] [Client]
|
|
2500
|
-
|
|
2501
|
-
Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically registers importmap, vendor bundle, HMR WebSocket, and file watcher. No explicit setup needed.
|
|
2502
|
-
|
|
2503
|
-
- Inline hydration script uses `createRoot` + render (replaces SSR DOM)
|
|
2504
|
-
- Vendor bundle served at `/__wfw/v/bundle?h=<hash>` — compiled from source, unminified
|
|
2505
|
-
- Hot component replacement: file changes → WebSocket message → browser imports hot bundle → component refreshed in place — `useState` values preserved
|
|
2506
|
-
- Tailwind CSS hot-reloads without page refresh
|
|
2507
|
-
- Layout changes trigger a full page reload
|
|
2508
|
-
|
|
2509
|
-
---
|
|
2510
|
-
|
|
2511
|
-
## AI
|
|
2512
|
-
|
|
2513
|
-
```ts
|
|
2514
|
-
import {
|
|
2515
|
-
openai,
|
|
2516
|
-
streamText,
|
|
2517
|
-
generateText,
|
|
2518
|
-
streamObject,
|
|
2519
|
-
generateObject,
|
|
2520
|
-
tool,
|
|
2521
|
-
embed,
|
|
2522
|
-
embedMany,
|
|
2523
|
-
aiProvider,
|
|
2524
|
-
} from 'weifuwu'
|
|
2525
|
-
import { runWorkflow } from 'weifuwu'
|
|
2526
|
-
|
|
2527
|
-
const provider = aiProvider()
|
|
2528
|
-
```
|
|
2529
|
-
|
|
2530
|
-
For AI streaming endpoints see [`aiStream`](#aistream-β). For AI agent APIs see [`agent`](#agent-β).
|
|
2531
|
-
|
|
2532
|
-
### aiProvider [α] — AI model & embedding configuration [AI]
|
|
2533
|
-
|
|
2534
|
-
```ts
|
|
2535
|
-
const provider = aiProvider() // auto from env
|
|
2536
|
-
app.use(provider) // → ctx.ai
|
|
2537
|
-
|
|
2538
|
-
// Handler
|
|
2539
|
-
app.post('/ask', async (req, ctx) => {
|
|
2540
|
-
const { text } = await ctx.ai.generateText({ prompt: 'hello' })
|
|
2541
|
-
const vec = await ctx.ai.embed('some text')
|
|
2542
|
-
const stream = ctx.ai.streamText({ system: 'assistant', messages: [...] })
|
|
2543
|
-
return stream.toTextStreamResponse()
|
|
2544
|
-
})
|
|
2545
|
-
```
|
|
2546
|
-
|
|
2547
|
-
| Option | Type | Default | Description |
|
|
2548
|
-
| -------------------- | -------- | -------------------------------------------------------- | -------------------- |
|
|
2549
|
-
| `baseURL` | `string` | `OPENAI_BASE_URL` env or `http://localhost:11434/v1` | API base URL |
|
|
2550
|
-
| `apiKey` | `string` | `OPENAI_API_KEY` env or `'ollama'` | API key |
|
|
2551
|
-
| `model` | `string` | `OPENAI_MODEL` env or `'qwen3:0.6b'` | Chat model name |
|
|
2552
|
-
| `embeddingModel` | `string` | `OPENAI_EMBEDDING_MODEL` env or `'qwen3-embedding:0.6b'` | Embedding model name |
|
|
2553
|
-
| `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024` | Vector dimension |
|
|
2554
|
-
|
|
2555
|
-
| Method | Description |
|
|
2556
|
-
| ------------------------ | --------------------------------------- |
|
|
2557
|
-
| `.model(name?)` | Get `LanguageModel` instance |
|
|
2558
|
-
| `.embeddingModel(name?)` | Get `EmbeddingModel` instance |
|
|
2559
|
-
| `.embed(text)` | Embed single text → `Promise<number[]>` |
|
|
2560
|
-
| `.embedMany(texts)` | Batch embed → `Promise<number[][]>` |
|
|
2561
|
-
| `.generateText(params)` | Generate text (model auto-injected) |
|
|
2562
|
-
| `.streamText(params)` | Stream text (model auto-injected) |
|
|
2563
|
-
| `.dimension` | Configured embedding dimension |
|
|
2564
|
-
|
|
2565
|
-
### DAG Workflow [AI]
|
|
2566
|
-
|
|
2567
|
-
```ts
|
|
2568
|
-
const tools = { queryUser: tool({ ... }) }
|
|
2569
|
-
|
|
2570
|
-
// Via provider:
|
|
2571
|
-
const wf = runWorkflow({ tools, provider })
|
|
2572
|
-
|
|
2573
|
-
// Or explicit model:
|
|
2574
|
-
const wf = runWorkflow({ tools, model: openai('gpt-4o') })
|
|
2575
|
-
```
|
|
2576
|
-
|
|
2577
|
-
| Option | Type | Default | Description |
|
|
2578
|
-
| ---------- | --------------- | ------- | ---------------------------------------------------------------- |
|
|
2579
|
-
| `tools` | `object` | — | Registered tool definitions |
|
|
2580
|
-
| `provider` | `AIProvider` | — | AI provider (uses `provider.model()` for LLM-generated workflow) |
|
|
2581
|
-
| `model` | `LanguageModel` | — | Explicit model (overrides provider) |
|
|
2582
|
-
| `maxSteps` | `number` | `200` | Max execution steps |
|
|
2583
|
-
|
|
2584
|
-
---
|
|
2585
|
-
|
|
2586
|
-
## Server-Sent Events
|
|
2587
|
-
|
|
2588
|
-
```ts
|
|
2589
|
-
import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
|
|
2590
|
-
async function* events() {
|
|
2591
|
-
yield formatSSE('chat', 'Hello')
|
|
2592
|
-
yield formatSSE('chat', 'World')
|
|
2593
|
-
}
|
|
2594
|
-
app.get('/stream', (req, ctx) => createSSEStream(events()))
|
|
2595
|
-
```
|
|
2596
|
-
|
|
2597
|
-
---
|
|
2598
|
-
|
|
2599
|
-
## Complete export index
|
|
2600
|
-
|
|
2601
|
-
Every public symbol can be imported from `'weifuwu'`:
|
|
2602
|
-
|
|
2603
|
-
### Core
|
|
2604
|
-
|
|
2605
|
-
```ts
|
|
2606
|
-
serve, createTestServer, Router, ssr,
|
|
2607
|
-
Context, Handler, Middleware, ErrorHandler, ServeOptions, Server,
|
|
2608
|
-
loadEnv, env, isDev, isProd, isBundled, getPublicEnv,
|
|
2609
|
-
currentTraceId, currentTrace, runWithTrace, traceElapsed, trace, TraceContext,
|
|
2610
|
-
testApp, TestApp, TestRequest, TestResponse,
|
|
2611
|
-
createTestDb, withTestDb,
|
|
2612
|
-
getCookies, setCookie, deleteCookie,
|
|
2613
|
-
createSSEStream, formatSSE, formatSSEData, SSEEvent,
|
|
2614
|
-
DEFAULT_MAX_BODY, MIGRATIONS_TABLE,
|
|
2615
|
-
```
|
|
2616
|
-
|
|
2617
|
-
### Middleware / DevTools
|
|
2618
|
-
|
|
2619
|
-
```ts
|
|
2620
|
-
logger, cors, compress, helmet,
|
|
2621
|
-
rateLimit, requestId, validate, upload,
|
|
2622
|
-
csrf, session, MemoryStore, RedisStore, SessionStore,
|
|
2623
|
-
cache, MemoryCache, RedisCache, CacheStore,
|
|
2624
|
-
flash, permissions,
|
|
2625
|
-
serveStatic, s3,
|
|
2626
|
-
```
|
|
2627
|
-
|
|
2628
|
-
### Database
|
|
2629
|
-
|
|
2630
|
-
```ts
|
|
2631
|
-
postgres, PostgresOptions, PostgresClient,
|
|
2632
|
-
redis, RedisOptions, RedisClient,
|
|
2633
|
-
queue, QueueOptions, QueueJob, Queue,
|
|
2634
|
-
PostgresInjected, RedisInjected, QueueInjected,
|
|
2635
|
-
// Schema helpers:
|
|
2636
|
-
pgTable, SQL, sql,
|
|
2637
|
-
ColumnBuilder, serial, uuid, text, integer, boolean, boolean_, timestamptz, jsonb, textArray, vector,
|
|
2638
|
-
partitionBy, timestamps, toDDL, PartitionByDef,
|
|
2639
|
-
Table, BoundTable, IndexOptions, FindOptions, CreateOptions,
|
|
2640
|
-
eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not,
|
|
2641
|
-
fts,
|
|
2642
|
-
```
|
|
2643
|
-
|
|
2644
|
-
### Security / Auth
|
|
2645
|
-
|
|
2646
|
-
```ts
|
|
2647
|
-
auth,
|
|
2648
|
-
user, UserModule, UserData, UserOptions, UserInjected, OAuthProviderConfig, OAuth2Client,
|
|
2649
|
-
permissions, PermissionsModule, PermissionsOptions,
|
|
2650
|
-
csrf, CsrfOptions, CsrfInjected,
|
|
2651
|
-
helmet, HelmetOptions,
|
|
2652
|
-
session, SessionStore, SessionOptions, SessionData, SessionInjected,
|
|
2653
|
-
rateLimit, RateLimitOptions,
|
|
2654
|
-
```
|
|
2655
|
-
|
|
2656
|
-
### UX Middleware
|
|
2657
|
-
|
|
2658
|
-
```ts
|
|
2659
|
-
theme, ThemeOptions, ThemeInjected,
|
|
2660
|
-
i18n, I18nOptions, I18nInjected,
|
|
2661
|
-
flash, FlashOptions, FlashInjected,
|
|
2662
|
-
```
|
|
2663
|
-
|
|
2664
|
-
### AI
|
|
2665
|
-
|
|
2666
|
-
```ts
|
|
2667
|
-
aiProvider, AIProvider, AIProviderOptions, AIProviderInjected,
|
|
2668
|
-
streamText, generateText, streamObject, generateObject,
|
|
2669
|
-
tool, embed, embedMany, smoothStream,
|
|
2670
|
-
openai, createOpenAI,
|
|
2671
|
-
aiStream, AIHandler,
|
|
2672
|
-
runWorkflow,
|
|
2673
|
-
agent, AgentModule, AgentOptions,
|
|
2674
|
-
knowledgeBase, KBModule, KBOptions,
|
|
2675
|
-
opencode, OpencodeModule, OpencodeOptions,
|
|
2676
|
-
```
|
|
2677
|
-
|
|
2678
|
-
### API / Routing
|
|
2679
|
-
|
|
2680
|
-
```ts
|
|
2681
|
-
analytics, AnalyticsModule, AnalyticsOptions,
|
|
2682
|
-
health, HealthOptions,
|
|
2683
|
-
graphql, GraphQLOptions, GraphQLHandler,
|
|
2684
|
-
logdb, LogdbModule, LogdbOptions,
|
|
2685
|
-
seo, seoMiddleware, seoTags, SeoOptions,
|
|
2686
|
-
webhook, WebhookModule, WebhookOptions,
|
|
2687
|
-
iii, createWorker, registerWorker, IIIModule, IIIOptions,
|
|
2688
|
-
```
|
|
2689
|
-
|
|
2690
|
-
### Networking / Storage
|
|
2691
|
-
|
|
2692
|
-
```ts
|
|
2693
|
-
s3, S3Options, S3Module, S3Body,
|
|
2694
|
-
mailer, MailerOptions, Mailer,
|
|
2695
|
-
messager, MessagerModule, MessagerOptions,
|
|
2696
|
-
hub, createHub, Hub, HubOptions,
|
|
2697
|
-
deploy, defineConfig, DeployConfig, AppConfig,
|
|
2698
|
-
tenant, TenantModule, TenantOptions, TenantContext,
|
|
2699
|
-
```
|
|
2700
|
-
|
|
2701
|
-
### Client-side (from `'weifuwu/react'`)
|
|
2702
|
-
|
|
2703
|
-
```ts
|
|
2704
|
-
TsxContext, setCtx, useCtx, addCtxRebuilder, useLoaderData,
|
|
2705
|
-
useWebsocket, useAction, useFetch, useQueryState, createStore,
|
|
2706
|
-
Link, useNavigate, useNavigating, addInterceptor,
|
|
2707
|
-
useLocale, useTheme, applyTheme, useFlashMessage,
|
|
2708
|
-
useAgentStream,
|
|
2709
|
-
Head,
|
|
2710
|
-
|
|
2711
|
-
// Types:
|
|
2712
|
-
StoreApi,
|
|
2713
|
-
UseActionOptions, UseActionReturn,
|
|
2714
|
-
UseWebsocketOptions, UseWebsocketReturn,
|
|
2715
|
-
UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState,
|
|
2716
|
-
```
|
|
2717
|
-
|
|
2718
|
-
---
|
|
514
|
+
## Dependencies
|
|
2719
515
|
|
|
2720
|
-
|
|
516
|
+
- `postgres` — PostgreSQL client
|
|
517
|
+
- `ioredis` — Redis client
|
|
518
|
+
- `ai`, `@ai-sdk/openai` — AI SDK
|
|
519
|
+
- `graphql`, `@graphql-tools/schema` — GraphQL
|
|
520
|
+
- `ws` — WebSocket
|
|
521
|
+
- `zod` — Schema validation
|
|
522
|
+
- `nodemailer` — Email
|
|
2721
523
|
|
|
2722
|
-
|
|
524
|
+
Zero build tools. Zero frontend framework dependencies.
|