weifuwu 0.17.26 → 0.18.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 +335 -93
- package/cli.ts +4 -0
- package/dist/agent/types.d.ts +2 -2
- package/dist/ai.d.ts +1 -3
- package/dist/analytics.d.ts +1 -2
- package/dist/cli.js +4 -0
- package/dist/graphql.d.ts +1 -3
- package/dist/iii/types.d.ts +1 -2
- package/dist/index.js +255 -243
- package/dist/logdb/types.d.ts +1 -2
- package/dist/messager/types.d.ts +2 -2
- package/dist/opencode/types.d.ts +1 -2
- package/dist/react.d.ts +1 -0
- package/dist/react.js +13 -0
- package/dist/tenant/types.d.ts +2 -2
- package/dist/types.d.ts +1 -0
- package/dist/use-flash-message.d.ts +1 -0
- package/dist/user/types.d.ts +1 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,45 +88,89 @@ app.get('/admin', mw, handler) // route-level
|
|
|
88
88
|
|
|
89
89
|
## Module Patterns
|
|
90
90
|
|
|
91
|
-
All modules follow one of
|
|
91
|
+
All modules follow one of **2 patterns** — learn these and you know every module.
|
|
92
92
|
|
|
93
93
|
| Pattern | How to mount | Example |
|
|
94
94
|
|---------|-------------|---------|
|
|
95
|
-
| `[
|
|
96
|
-
| `[
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
| `[α]` | `app.use(mod())` | `compress()`, `preferences()`, `postgres()` |
|
|
96
|
+
| `[β]` | `app.use('/path', mod())` | `health()`, `graphql(handler)`, `user()` |
|
|
97
|
+
|
|
98
|
+
### Pattern α — Middleware
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
app.use(compress()) // basic
|
|
102
|
+
const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
|
|
103
|
+
app.use(pg)
|
|
104
|
+
app.use(rateLimit({ max: 100 })) // with .stop()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Pattern β — Router
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
app.use('/health', health()) // no extras
|
|
111
|
+
app.use('/graphql', graphql(handler))
|
|
112
|
+
app.use('/logs', logdb({ pg })) // with .log(), .migrate()
|
|
113
|
+
app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
|
|
114
|
+
app.ws('/ws', messager({ pg }).wsHandler())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
β modules that need **separate middleware** use `.middleware()`:
|
|
118
|
+
```ts
|
|
119
|
+
const a = analytics()
|
|
120
|
+
app.use(a.middleware()) // tracking
|
|
121
|
+
app.use('/', a) // dashboard
|
|
122
|
+
```
|
|
100
123
|
|
|
101
124
|
---
|
|
102
125
|
|
|
103
126
|
## Module Reference
|
|
104
127
|
|
|
105
|
-
### agent [
|
|
128
|
+
### agent [β]
|
|
106
129
|
|
|
107
130
|
```ts
|
|
108
|
-
const a = agent({ pg })
|
|
131
|
+
const a = agent({ pg, model: openai('gpt-4o'), embeddingModel: openai.embedding('text-embedding-3-small') })
|
|
109
132
|
await a.migrate()
|
|
110
|
-
app.use('/api', a
|
|
111
|
-
await a.addKnowledge(agentId, 'Title', '
|
|
112
|
-
a.run(agentId, {
|
|
133
|
+
app.use('/api', a)
|
|
134
|
+
await a.addKnowledge(agentId, 'Title', 'some knowledge content')
|
|
135
|
+
a.run(agentId, { input: 'summarize the data', stream: true })
|
|
113
136
|
```
|
|
114
137
|
|
|
115
|
-
|
|
138
|
+
| Option | Type | Default | Description |
|
|
139
|
+
|--------|------|---------|-------------|
|
|
140
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
141
|
+
| `model` | `object` | — | AI model (e.g. `openai('gpt-4o')`) |
|
|
142
|
+
| `embeddingModel` | `object` | — | Embedding model for knowledge search |
|
|
143
|
+
| `embeddingDimension` | `number` | `1536` | Embedding vector dimension |
|
|
144
|
+
| `tools` | `object[]` | — | Custom tool definitions |
|
|
145
|
+
|
|
146
|
+
| Method | Description |
|
|
147
|
+
|--------|-------------|
|
|
148
|
+
| `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
|
|
149
|
+
| `.addKnowledge(agentId, title, content)` | Add knowledge document |
|
|
150
|
+
| `.migrate()` | DB setup |
|
|
151
|
+
| `.close()` | Cleanup |
|
|
152
|
+
|
|
153
|
+
### aiStream [β]
|
|
154
|
+
|
|
155
|
+
Creates an AI streaming chat endpoint using the Vercel AI SDK.
|
|
116
156
|
|
|
117
157
|
```ts
|
|
118
158
|
const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
|
|
119
|
-
app.use('/chat', chat
|
|
159
|
+
app.use('/chat', chat)
|
|
120
160
|
```
|
|
121
161
|
|
|
122
|
-
|
|
162
|
+
| Param | Type | Description |
|
|
163
|
+
|-------|------|-------------|
|
|
164
|
+
| `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
|
|
165
|
+
|
|
166
|
+
### analytics [β]
|
|
123
167
|
|
|
124
168
|
In-memory or PostgreSQL page view tracking with built-in dashboard.
|
|
125
169
|
|
|
126
170
|
```ts
|
|
127
171
|
const a = analytics()
|
|
128
172
|
app.use(a.middleware())
|
|
129
|
-
app.use('/', a
|
|
173
|
+
app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
|
|
130
174
|
```
|
|
131
175
|
|
|
132
176
|
| Option | Type | Default | Description |
|
|
@@ -139,26 +183,38 @@ app.use('/', a.router()) // GET /__analytics (dashboard), GET /__analytics
|
|
|
139
183
|
const a = analytics({ pg })
|
|
140
184
|
await a.migrate()
|
|
141
185
|
app.use(a.middleware())
|
|
142
|
-
app.use('/', a
|
|
186
|
+
app.use('/', a) // dashboard routes
|
|
143
187
|
```
|
|
144
188
|
|
|
145
|
-
### auth [
|
|
189
|
+
### auth [α]
|
|
146
190
|
|
|
147
191
|
```ts
|
|
148
192
|
app.use(auth({ token: 'sk-123' })) // static token
|
|
149
193
|
app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
|
|
150
|
-
app.use(auth({ verify: async (token) => ({ sub: 'abc' }) }))
|
|
194
|
+
app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
|
|
151
195
|
app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
|
|
152
196
|
```
|
|
153
197
|
|
|
154
|
-
|
|
198
|
+
| Option | Type | Default | Description |
|
|
199
|
+
|--------|------|---------|-------------|
|
|
200
|
+
| `token` | `string` | — | Static token to match |
|
|
201
|
+
| `header` | `string` | `'Authorization'` | Header name |
|
|
202
|
+
| `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
|
|
203
|
+
| `proxy` | `string` | — | Auth service URL to proxy requests to |
|
|
204
|
+
|
|
205
|
+
### compress [α]
|
|
155
206
|
|
|
156
207
|
```ts
|
|
157
|
-
app.use(compress()) // brotli > gzip > deflate
|
|
158
|
-
app.use(compress({ threshold: 2048 })) //
|
|
208
|
+
app.use(compress()) // brotli > gzip > deflate (min 1KB)
|
|
209
|
+
app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and level
|
|
159
210
|
```
|
|
160
211
|
|
|
161
|
-
|
|
212
|
+
| Option | Type | Default | Description |
|
|
213
|
+
|--------|------|---------|-------------|
|
|
214
|
+
| `threshold` | `number` | `1024` | Minimum byte size to compress |
|
|
215
|
+
| `level` | `number` | `6` | Compression level (zlib) |
|
|
216
|
+
|
|
217
|
+
### cors [α]
|
|
162
218
|
|
|
163
219
|
```ts
|
|
164
220
|
app.use(cors()) // allow all
|
|
@@ -167,18 +223,28 @@ app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
|
|
|
167
223
|
app.use(cors({ credentials: true, maxAge: 3600 }))
|
|
168
224
|
```
|
|
169
225
|
|
|
170
|
-
|
|
226
|
+
| Option | Type | Default | Description |
|
|
227
|
+
|--------|------|---------|-------------|
|
|
228
|
+
| `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
|
|
229
|
+
| `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
|
|
230
|
+
| `allowedHeaders` | `string[]` | — | Custom allowed headers |
|
|
231
|
+
| `exposedHeaders` | `string[]` | — | Response headers exposed to client |
|
|
232
|
+
| `credentials` | `boolean` | `false` | Allow cookies/credentials |
|
|
233
|
+
| `maxAge` | `number` | — | Preflight cache duration (seconds) |
|
|
234
|
+
|
|
235
|
+
### csrf [α]
|
|
171
236
|
|
|
172
237
|
```ts
|
|
173
238
|
app.use(csrf())
|
|
174
|
-
// ctx.csrfToken
|
|
175
|
-
// Auto-validates
|
|
239
|
+
// ctx.csrfToken — set on GET/HEAD/OPTIONS
|
|
240
|
+
// Auto-validates x-csrf-token or x-xsrf-token header on POST/PUT/DELETE/PATCH
|
|
241
|
+
// Falls back to body field matching the key name
|
|
176
242
|
```
|
|
177
243
|
|
|
178
244
|
| Option | Default | Description |
|
|
179
245
|
|--------|---------|-------------|
|
|
180
246
|
| `cookie` | `'_csrf'` | Cookie name |
|
|
181
|
-
| `header` | `'x-csrf-token'` | Header name |
|
|
247
|
+
| `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
|
|
182
248
|
| `key` | `'_csrf'` | Body field fallback |
|
|
183
249
|
| `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
|
|
184
250
|
|
|
@@ -186,31 +252,59 @@ app.use(csrf())
|
|
|
186
252
|
|
|
187
253
|
```ts
|
|
188
254
|
import { deploy, defineConfig } from 'weifuwu'
|
|
189
|
-
const config = defineConfig({
|
|
190
|
-
|
|
255
|
+
const config = defineConfig({
|
|
256
|
+
domain: 'example.com',
|
|
257
|
+
apps: {
|
|
258
|
+
api: { repo: 'git@github.com:user/api.git', entry: 'app.ts', port: 3001, subdomain: 'api' },
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
const server = await deploy(config)
|
|
262
|
+
// server.close(), server.ready, server.url
|
|
263
|
+
// server.apps.list(), server.apps.status(name), server.apps.deploy(name)
|
|
191
264
|
```
|
|
192
265
|
|
|
193
|
-
### health [
|
|
266
|
+
### health [β]
|
|
194
267
|
|
|
195
268
|
```ts
|
|
196
|
-
app.use(health())
|
|
197
|
-
|
|
269
|
+
app.use('/health', health())
|
|
270
|
+
// Returns 200 on success, 503 when check throws
|
|
198
271
|
```
|
|
199
272
|
|
|
200
|
-
|
|
273
|
+
| Option | Type | Default | Description |
|
|
274
|
+
|--------|------|---------|-------------|
|
|
275
|
+
| `path` | `string` | `'/health'` | Health check endpoint |
|
|
276
|
+
| `check` | `() => Promise<void>` | — | Async function; throws → 503 |
|
|
201
277
|
|
|
202
|
-
|
|
278
|
+
### helmet [α]
|
|
279
|
+
|
|
280
|
+
15 security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
|
|
203
281
|
|
|
204
282
|
```ts
|
|
205
283
|
app.use(helmet())
|
|
206
284
|
app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))
|
|
207
285
|
```
|
|
208
286
|
|
|
209
|
-
|
|
287
|
+
| Option | Default | Description |
|
|
288
|
+
|--------|---------|-------------|
|
|
289
|
+
| `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
|
|
290
|
+
| `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
|
|
291
|
+
| `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
|
|
292
|
+
| `referrerPolicy` | `'no-referrer'` | Referrer header |
|
|
293
|
+
| `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
|
|
294
|
+
| `permissionsPolicy` | — | Feature permissions policy |
|
|
295
|
+
| `crossOriginEmbedderPolicy` | — | COEP header |
|
|
296
|
+
| `crossOriginOpenerPolicy` | — | COOP header |
|
|
297
|
+
| `crossOriginResourcePolicy` | — | CORP header |
|
|
298
|
+
|
|
299
|
+
### iii [β] — Worker / Function / Trigger
|
|
300
|
+
|
|
301
|
+
Distributed function execution with WebSocket workers, triggers, and Redis streams.
|
|
210
302
|
|
|
211
303
|
```ts
|
|
304
|
+
import { createWorker } from 'weifuwu'
|
|
212
305
|
const engine = iii({ pg, redis })
|
|
213
|
-
app.use('/iii', engine
|
|
306
|
+
app.use('/iii', engine)
|
|
307
|
+
app.ws('/iii', engine.wsHandler())
|
|
214
308
|
|
|
215
309
|
const w = createWorker('orders')
|
|
216
310
|
w.registerFunction('orders::create', async (payload) => db.query('INSERT INTO orders ...', [payload.items]))
|
|
@@ -218,73 +312,138 @@ engine.addWorker(w)
|
|
|
218
312
|
await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
|
|
219
313
|
```
|
|
220
314
|
|
|
315
|
+
| Option | Type | Default | Description |
|
|
316
|
+
|--------|------|---------|-------------|
|
|
317
|
+
| `pg` | `object` | — | PostgreSQL client for persistent triggers |
|
|
318
|
+
| `redis` | `object` | — | Redis client for streams |
|
|
319
|
+
| `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
|
|
320
|
+
|
|
221
321
|
| Method | Description |
|
|
222
322
|
|--------|-------------|
|
|
223
323
|
| `.addWorker(w)` | Register a worker |
|
|
224
|
-
| `.
|
|
225
|
-
| `.
|
|
324
|
+
| `.removeWorker(w)` | Remove a worker |
|
|
325
|
+
| `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
|
|
326
|
+
| `.listWorkers()` | List registered workers |
|
|
327
|
+
| `.listFunctions()` | List registered functions |
|
|
328
|
+
| `.listTriggers()` | List registered triggers |
|
|
329
|
+
| `.wsHandler()` | WebSocket handler |
|
|
330
|
+
| `.migrate()` | DB setup |
|
|
331
|
+
| `.shutdown()` | Clean shutdown |
|
|
226
332
|
|
|
227
|
-
### logdb [
|
|
333
|
+
### logdb [β]
|
|
228
334
|
|
|
229
335
|
PostgreSQL structured event logging with monthly partitioning.
|
|
230
336
|
|
|
231
337
|
```ts
|
|
232
338
|
const logger = logdb({ pg })
|
|
233
339
|
await logger.migrate()
|
|
234
|
-
app.use('/logs', logger
|
|
340
|
+
app.use('/logs', logger)
|
|
235
341
|
await logger.clean(12) // drop partitions older than 12 months
|
|
236
|
-
await logger.log({ level: 'info', source: 'app', message: 'hello' })
|
|
342
|
+
await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
|
|
237
343
|
```
|
|
238
344
|
|
|
345
|
+
| Option | Type | Default | Description |
|
|
346
|
+
|--------|------|---------|-------------|
|
|
347
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
348
|
+
| `table` | `string` | `'_log_entries'` | Table name |
|
|
349
|
+
|
|
239
350
|
| Method | Path | Description |
|
|
240
351
|
|--------|------|-------------|
|
|
241
352
|
| POST | `/` | Create log entry |
|
|
242
353
|
| GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
|
|
243
354
|
| GET | `/:id` | Get single entry |
|
|
244
355
|
|
|
245
|
-
### logger [
|
|
356
|
+
### logger [α]
|
|
246
357
|
|
|
247
358
|
```ts
|
|
248
359
|
app.use(logger()) // GET /hello 200 5ms
|
|
249
360
|
app.use(logger({ format: 'combined' })) // with query params
|
|
250
361
|
```
|
|
251
362
|
|
|
363
|
+
| Option | Type | Default | Description |
|
|
364
|
+
|--------|------|---------|-------------|
|
|
365
|
+
| `format` | `'short' \| 'combined'` | `'short'` | Log format: path only, or path + query params |
|
|
366
|
+
|
|
252
367
|
### mailer
|
|
253
368
|
|
|
254
369
|
```ts
|
|
255
|
-
const mail = mailer({
|
|
256
|
-
await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p>Body</p>' })
|
|
370
|
+
const mail = mailer({ from: 'noreply@example.com', transport: 'smtp://user:pass@smtp.example.com:587' })
|
|
371
|
+
await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p>Body</p>', cc: 'admin@test.com' })
|
|
257
372
|
```
|
|
258
373
|
|
|
259
|
-
|
|
374
|
+
| Option | Type | Default | Description |
|
|
375
|
+
|--------|------|---------|-------------|
|
|
376
|
+
| `transport` | `string\|object` | — | Nodemailer transport config or connection string |
|
|
377
|
+
| `from` | `string` | — | Default sender address |
|
|
378
|
+
| `send` | `function` | — | Custom send function (alternative to transport) |
|
|
379
|
+
|
|
380
|
+
### messager [β]
|
|
260
381
|
|
|
261
382
|
Real-time chat with channels, WebSocket, agent routing.
|
|
262
383
|
|
|
263
384
|
```ts
|
|
264
385
|
const msg = messager({ pg, agents, redis: redis() })
|
|
265
386
|
await msg.migrate()
|
|
266
|
-
app.
|
|
267
|
-
|
|
387
|
+
app.use('/api', msg)
|
|
388
|
+
app.ws('/ws', msg.wsHandler())
|
|
389
|
+
await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
|
|
268
390
|
```
|
|
269
391
|
|
|
270
|
-
|
|
392
|
+
| Option | Type | Default | Description |
|
|
393
|
+
|--------|------|---------|-------------|
|
|
394
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
395
|
+
| `agents` | `AgentModule` | — | Agent module for routing |
|
|
396
|
+
| `webhookTimeout` | `number` | — | Webhook timeout |
|
|
397
|
+
| `redis` | `object` | — | Redis client |
|
|
398
|
+
|
|
399
|
+
| Method | Description |
|
|
400
|
+
|--------|-------------|
|
|
401
|
+
| `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
|
|
402
|
+
| `.send(channel, content, opts?)` | Send message to channel |
|
|
403
|
+
| `.close()` | Cleanup |
|
|
404
|
+
|
|
405
|
+
### opencode [β]
|
|
271
406
|
|
|
272
407
|
AI programming assistant.
|
|
273
408
|
|
|
274
409
|
```ts
|
|
275
|
-
const oc = await opencode({
|
|
410
|
+
const oc = await opencode({
|
|
411
|
+
pg,
|
|
412
|
+
model: openai('gpt-4o'),
|
|
413
|
+
workspace: '/home/user/project',
|
|
414
|
+
permissions: { bash: { allow: true }, write: { allow: false } },
|
|
415
|
+
})
|
|
276
416
|
await oc.migrate()
|
|
277
|
-
app.use('/opencode',
|
|
417
|
+
app.use('/opencode', oc)
|
|
278
418
|
app.ws('/opencode', oc.wsHandler())
|
|
279
419
|
```
|
|
280
420
|
|
|
281
|
-
|
|
421
|
+
| Option | Type | Default | Description |
|
|
422
|
+
|--------|------|---------|-------------|
|
|
423
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
424
|
+
| `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
|
|
425
|
+
| `baseURL` | `string` | — | OpenAI-compatible API base URL |
|
|
426
|
+
| `apiKey` | `string` | — | API key for the model |
|
|
427
|
+
| `workspace` | `string` | — | Project directory |
|
|
428
|
+
| `systemPrompt` | `string` | — | Custom system prompt |
|
|
429
|
+
| `skills` | `object[]` | — | Custom skill definitions |
|
|
430
|
+
| `permissions` | `object` | — | Tool permission rules |
|
|
431
|
+
|
|
432
|
+
### postgres [α]
|
|
282
433
|
|
|
283
434
|
```ts
|
|
284
435
|
const pg = postgres() // reads DATABASE_URL
|
|
285
436
|
app.use(pg) // injects ctx.sql
|
|
286
437
|
```
|
|
287
438
|
|
|
439
|
+
| Option | Type | Default | Description |
|
|
440
|
+
|--------|------|---------|-------------|
|
|
441
|
+
| `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
|
|
442
|
+
| `max` | `number` | `10` | Max pool connections |
|
|
443
|
+
| `ssl` | `boolean\|object` | — | SSL options |
|
|
444
|
+
| `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
|
|
445
|
+
| `connect_timeout` | `number` | `30` | Connection timeout |
|
|
446
|
+
|
|
288
447
|
```ts
|
|
289
448
|
// Type-safe DDL
|
|
290
449
|
const users = pgTable('_users', { id: serial('id').primaryKey(), name: text('name').notNull(), email: text('email').unique().notNull(), active: boolean('active').default(true), ...timestamps() })
|
|
@@ -301,13 +460,14 @@ await pg.transaction(async (tx) => { const users = pg.table('_users', { ... }).w
|
|
|
301
460
|
|
|
302
461
|
Where helpers: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `isNull`, `isNotNull`, `like`, `contains`, `in_`, `and`, `or`, `not`.
|
|
303
462
|
|
|
304
|
-
### preferences [
|
|
463
|
+
### preferences [α]
|
|
305
464
|
|
|
306
465
|
Locale detection + theme + translations. `/__lang/:locale` and `/__theme/:theme` auto-routed.
|
|
307
466
|
|
|
308
467
|
```ts
|
|
309
468
|
app.use(preferences({ dir: './locales', locale: { default: 'en' }, theme: { default: 'system' } }))
|
|
310
469
|
// ctx.prefs.locale, ctx.prefs.theme, ctx.t('key'), ctx.setPref('locale', 'zh')
|
|
470
|
+
// ctx.setPref() returns a 302 Response with Set-Cookie — return it from your handler
|
|
311
471
|
// GET /__lang/zh → 302 + Set-Cookie (or JSON if Accept: application/json)
|
|
312
472
|
// GET /__theme/dark → same pattern
|
|
313
473
|
```
|
|
@@ -331,48 +491,92 @@ const { theme, resolvedTheme, setTheme } = useTheme()
|
|
|
331
491
|
// resolvedTheme resolves 'system' → 'dark'|'light' based on prefers-color-scheme
|
|
332
492
|
```
|
|
333
493
|
|
|
334
|
-
### queue [
|
|
494
|
+
### queue [α]
|
|
335
495
|
|
|
336
496
|
```ts
|
|
337
497
|
const q = queue({ redis })
|
|
498
|
+
app.use(q) // injects ctx.queue
|
|
338
499
|
await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
|
|
339
500
|
```
|
|
340
501
|
|
|
341
|
-
|
|
502
|
+
| Option | Type | Default | Description |
|
|
503
|
+
|--------|------|---------|-------------|
|
|
504
|
+
| `redis` | `object` | — | Redis client |
|
|
505
|
+
| `url` | `string` | — | Redis URL (alternative to client) |
|
|
506
|
+
| `prefix` | `string` | `'queue:'` | Redis key prefix |
|
|
507
|
+
| `pollInterval` | `number` | `1000` | Poll interval (ms) |
|
|
508
|
+
|
|
509
|
+
| Method | Description |
|
|
510
|
+
|--------|-------------|
|
|
511
|
+
| `.add(name, data, opts?)` | Add job to queue |
|
|
512
|
+
| `.process(handler)` | Register job processor |
|
|
513
|
+
| `.run()` | Start processing |
|
|
514
|
+
| `.stop()` | Stop processing |
|
|
515
|
+
| `.close()` | Cleanup |
|
|
516
|
+
|
|
517
|
+
### rateLimit [α]
|
|
342
518
|
|
|
343
519
|
```ts
|
|
344
520
|
app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
|
|
345
521
|
app.get('/api', rateLimit({ max: 10 }), handler) // per-route
|
|
346
522
|
app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
|
|
523
|
+
// Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After headers
|
|
347
524
|
// m.stop() — clear interval
|
|
348
525
|
```
|
|
349
526
|
|
|
350
|
-
|
|
527
|
+
| Option | Type | Default | Description |
|
|
528
|
+
|--------|------|---------|-------------|
|
|
529
|
+
| `max` | `number` | `100` | Max requests per window |
|
|
530
|
+
| `window` | `number` | `60_000` | Window duration (ms) |
|
|
531
|
+
| `key` | `(req) => string` | IP-based | Key function |
|
|
532
|
+
| `message` | `string` | `'Too Many Requests'` | 429 response body |
|
|
533
|
+
|
|
534
|
+
### redis [α]
|
|
351
535
|
|
|
352
536
|
```ts
|
|
353
|
-
const r = redis()
|
|
354
|
-
app.use(r)
|
|
537
|
+
const r = redis() // reads REDIS_URL
|
|
538
|
+
app.use(r) // injects ctx.redis
|
|
355
539
|
await ctx.redis.set('key', 'value')
|
|
356
540
|
// r.close() — cleanup
|
|
357
541
|
```
|
|
358
542
|
|
|
359
|
-
|
|
543
|
+
| Option | Type | Default | Description |
|
|
544
|
+
|--------|------|---------|-------------|
|
|
545
|
+
| `url` | `string` | `REDIS_URL` env | Redis connection string |
|
|
546
|
+
| (all ioredis options) | — | — | Passed directly to ioredis |
|
|
547
|
+
|
|
548
|
+
### requestId [α]
|
|
360
549
|
|
|
361
550
|
```ts
|
|
362
551
|
app.use(requestId())
|
|
552
|
+
app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID() }))
|
|
363
553
|
// Sets X-Request-ID header on responses, available as ctx.requestId
|
|
364
554
|
```
|
|
365
555
|
|
|
366
|
-
|
|
556
|
+
| Option | Type | Default | Description |
|
|
557
|
+
|--------|------|---------|-------------|
|
|
558
|
+
| `header` | `string` | `'X-Request-ID'` | Header name to read/write |
|
|
559
|
+
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
|
|
560
|
+
|
|
561
|
+
### seo [β] + seoMiddleware [α]
|
|
367
562
|
|
|
368
563
|
```ts
|
|
369
|
-
app.use(seo({ baseUrl: 'https://example.com', robots: [{ userAgent: '*', allow: '/' }], sitemap: { urls: [{ loc: '/' }] } }))
|
|
564
|
+
app.use('/', seo({ baseUrl: 'https://example.com', robots: [{ userAgent: '*', allow: '/' }], sitemap: { urls: [{ loc: '/' }] } }))
|
|
370
565
|
// GET /robots.txt, GET /sitemap.xml
|
|
371
566
|
|
|
372
567
|
app.use(seoMiddleware({ headers: { 'X-Robots-Tag': (path) => path.startsWith('/admin') ? 'noindex' : undefined } }))
|
|
373
568
|
```
|
|
374
569
|
|
|
375
|
-
|
|
570
|
+
Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
|
|
571
|
+
|
|
572
|
+
| Option | Type | Default | Description |
|
|
573
|
+
|--------|------|---------|-------------|
|
|
574
|
+
| `baseUrl` | `string` | — | Base URL for sitemap URLs |
|
|
575
|
+
| `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
|
|
576
|
+
| `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
|
|
577
|
+
| `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
|
|
578
|
+
|
|
579
|
+
### tenant [β]
|
|
376
580
|
|
|
377
581
|
Multi-tenant BaaS with dynamic table API and GraphQL.
|
|
378
582
|
|
|
@@ -380,36 +584,69 @@ Multi-tenant BaaS with dynamic table API and GraphQL.
|
|
|
380
584
|
const t = tenant({ pg, usersTable: '_users' })
|
|
381
585
|
await t.migrate()
|
|
382
586
|
app.use('/api', t.middleware()) // → ctx.tenant
|
|
383
|
-
app.use('/api', t
|
|
587
|
+
app.use('/api', t) // dynamic CRUD
|
|
384
588
|
app.use('/graphql', t.graphql()) // dynamic GraphQL
|
|
385
589
|
```
|
|
386
590
|
|
|
387
|
-
|
|
591
|
+
| Option | Type | Default | Description |
|
|
592
|
+
|--------|------|---------|-------------|
|
|
593
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
594
|
+
| `usersTable` | `string` | — | Users table name for tenant membership lookup |
|
|
595
|
+
|
|
596
|
+
### upload [α]
|
|
388
597
|
|
|
389
598
|
```ts
|
|
390
|
-
app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760 }), (req, ctx) => {
|
|
391
|
-
// ctx.parsed.files.avatar → { name, type, size, path }
|
|
599
|
+
app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }), (req, ctx) => {
|
|
600
|
+
// ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
|
|
601
|
+
// Multiple files with same field name → array
|
|
392
602
|
// ctx.parsed.fields.title → 'hello'
|
|
393
603
|
})
|
|
394
604
|
```
|
|
395
605
|
|
|
396
|
-
|
|
606
|
+
| Option | Type | Default | Description |
|
|
607
|
+
|--------|------|---------|-------------|
|
|
608
|
+
| `dir` | `string` | — | Write files to disk (omit for in-memory) |
|
|
609
|
+
| `maxFileSize` | `number` | — | Max bytes per file |
|
|
610
|
+
| `allowedTypes` | `string[]` | — | Allowed MIME types |
|
|
611
|
+
|
|
612
|
+
### user [β]
|
|
613
|
+
|
|
614
|
+
Authentication: register, login, JWT, OAuth2.
|
|
397
615
|
|
|
398
616
|
```ts
|
|
399
617
|
const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
|
|
400
618
|
await auth.migrate()
|
|
401
|
-
app.use('/auth', auth
|
|
619
|
+
app.use('/auth', auth) // POST /register, POST /login, OAuth2 routes
|
|
402
620
|
app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
|
|
403
621
|
```
|
|
404
622
|
|
|
405
|
-
|
|
623
|
+
| Option | Type | Default | Description |
|
|
624
|
+
|--------|------|---------|-------------|
|
|
625
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
626
|
+
| `jwtSecret` | `string` | — | JWT signing secret |
|
|
627
|
+
| `table` | `string` | `'_users'` | Users table name |
|
|
628
|
+
| `expiresIn` | `string` | `'7d'` | JWT expiration |
|
|
629
|
+
| `oauth2` | `object` | — | OAuth2 client config (PKCE flow) |
|
|
630
|
+
|
|
631
|
+
| Method | Description |
|
|
632
|
+
|--------|-------------|
|
|
633
|
+
| `.register(data)` | Register a new user programmatically |
|
|
634
|
+
| `.login(data)` | Log in programmatically |
|
|
635
|
+
| `.verify(token)` | Verify JWT token |
|
|
636
|
+
| `.middleware()` | JWT verify middleware — sets `ctx.user` |
|
|
637
|
+
|
|
638
|
+
### validate [α]
|
|
406
639
|
|
|
407
640
|
```ts
|
|
408
641
|
import { z } from 'zod'
|
|
409
642
|
const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
|
|
410
|
-
app.post('/users', validate({ body: CreateUser }), (req, ctx) => {
|
|
643
|
+
app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string().optional() }) }), (req, ctx) => {
|
|
411
644
|
// ctx.parsed.body — typed & validated
|
|
645
|
+
// ctx.parsed.query — typed & validated
|
|
646
|
+
// ctx.parsed.params — typed & validated (for dynamic routes)
|
|
647
|
+
// ctx.parsed.headers — typed & validated
|
|
412
648
|
})
|
|
649
|
+
// Validation failure: returns 400 with { error: 'Validation failed', issues: [...] }
|
|
413
650
|
```
|
|
414
651
|
|
|
415
652
|
---
|
|
@@ -481,17 +718,27 @@ const loading = useNavigating() // reactive loading state
|
|
|
481
718
|
|
|
482
719
|
```tsx
|
|
483
720
|
import { useWebsocket, useAction, useFetch, useQueryState, createStore, Head } from 'weifuwu/react'
|
|
484
|
-
import { useLocale, useTheme, applyTheme, addInterceptor, useLoaderData } from 'weifuwu/react'
|
|
721
|
+
import { useLocale, useTheme, applyTheme, addInterceptor, useLoaderData, useFlashMessage } from 'weifuwu/react'
|
|
485
722
|
|
|
486
723
|
// WebSocket — auto-reconnecting
|
|
487
|
-
const { send, lastMessage, readyState } = useWebsocket('/ws/chat', {
|
|
724
|
+
const { send, lastMessage, readyState, close, reconnect } = useWebsocket('/ws/chat', {
|
|
725
|
+
onMessage: (d) => console.log(d),
|
|
726
|
+
reconnect: { maxRetries: 10, delay: 3000 },
|
|
727
|
+
protocols: [], // optional sub-protocols
|
|
728
|
+
enabled: true, // pause/resume connection
|
|
729
|
+
})
|
|
488
730
|
|
|
489
731
|
// Form action
|
|
490
|
-
const { submit, data, error, pending } = useAction('/api/feedback', {
|
|
491
|
-
|
|
732
|
+
const { submit, data, error, pending, reset } = useAction('/api/feedback', {
|
|
733
|
+
method: 'POST',
|
|
734
|
+
headers: { 'X-Custom': 'value' },
|
|
735
|
+
onSuccess: (data) => console.log(data),
|
|
736
|
+
onError: (err) => console.error(err),
|
|
737
|
+
})
|
|
738
|
+
// Auto-reads _csrf cookie, sends as x-csrf-token or x-xsrf-token
|
|
492
739
|
|
|
493
740
|
// Data fetching — cache + dedup + mutate
|
|
494
|
-
const { data, error, loading, mutate } = useFetch('/api/posts', { fallback: loadData })
|
|
741
|
+
const { data, error, loading, mutate } = useFetch('/api/posts', { fallback: loadData, ttl: 30_000 })
|
|
495
742
|
|
|
496
743
|
// URL query state
|
|
497
744
|
const [q, setQ] = useQueryState('q', '')
|
|
@@ -503,9 +750,10 @@ const count = useStore(s => s.count)
|
|
|
503
750
|
|
|
504
751
|
// Per-page meta tags
|
|
505
752
|
<Head><title>Page Title</title><meta name="description" content="..." /></Head>
|
|
506
|
-
|
|
507
753
|
```
|
|
508
754
|
|
|
755
|
+
**`TsxContext`** — React context holding page data (`params`, `query`, `user`, `parsed`, `prefs`, `env`). Used internally by hooks; rarely needed directly.
|
|
756
|
+
|
|
509
757
|
### Locale & Theme
|
|
510
758
|
|
|
511
759
|
```tsx
|
|
@@ -520,7 +768,7 @@ function LangSwitch() {
|
|
|
520
768
|
|--------|-------------|
|
|
521
769
|
| `locale` | Current locale string (from `ctx.prefs.locale`) |
|
|
522
770
|
| `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
|
|
523
|
-
| `t` |
|
|
771
|
+
| `t` | Translate a key using loaded locale messages |
|
|
524
772
|
|
|
525
773
|
```tsx
|
|
526
774
|
import { useTheme } from 'weifuwu/react'
|
|
@@ -571,11 +819,19 @@ addInterceptor(async (url) => {
|
|
|
571
819
|
### Flash messages
|
|
572
820
|
|
|
573
821
|
```ts
|
|
574
|
-
// Server
|
|
822
|
+
// Server — set flash cookie on redirect, auto-cleared after first read
|
|
575
823
|
return ctx.setPref('flash', JSON.stringify({ type: 'success', message: 'Done' })) // 302 + Set-Cookie
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
```tsx
|
|
827
|
+
// Client
|
|
828
|
+
import { useFlashMessage } from 'weifuwu/react'
|
|
576
829
|
|
|
577
|
-
|
|
578
|
-
|
|
830
|
+
function Toast() {
|
|
831
|
+
const flash = useFlashMessage<{ type: string; message: string }>()
|
|
832
|
+
if (!flash) return null
|
|
833
|
+
return <div className={`toast toast-${flash.type}`}>{flash.message}</div>
|
|
834
|
+
}
|
|
579
835
|
```
|
|
580
836
|
|
|
581
837
|
### Dev mode
|
|
@@ -591,21 +847,7 @@ import { openai, streamText, generateText, streamObject, generateObject, tool, e
|
|
|
591
847
|
import { runWorkflow } from 'weifuwu'
|
|
592
848
|
```
|
|
593
849
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
```ts
|
|
597
|
-
const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
|
|
598
|
-
app.use('/chat', chat.router().handler())
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
### Agents
|
|
602
|
-
|
|
603
|
-
```ts
|
|
604
|
-
const agents = agent({ pg })
|
|
605
|
-
await agents.migrate()
|
|
606
|
-
app.use('/api', agents.router())
|
|
607
|
-
await agents.addKnowledge(agentId, 'Title', 'content')
|
|
608
|
-
```
|
|
850
|
+
For AI streaming endpoints see [`aiStream`](#aistream-β). For AI agent APIs see [`agent`](#agent-β).
|
|
609
851
|
|
|
610
852
|
### DAG Workflow
|
|
611
853
|
|