weifuwu 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,9 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
19
19
  - **GraphQL** — `graphql(handler)` sub-Router with GraphiQL IDE
20
20
  - **AI streaming** — `ai(handler)` sub-Router via Vercel AI SDK
21
21
  - **AI workflows** — `workflow(handler)` sub-Router — intent-to-execution pipelines with `tool()` + SSE
22
- - **PostgreSQL** — `postgres()` — zod-to-DDL, auto-migration, 6 CRUD methods, `ctx.sql` escape hatch
22
+ - **AI Agent** — `agent()` — server-side AI agents with chat/workflow/knowledge types, OpenAI-compatible, Ollama-ready
23
+ - **Messaging** — `messager()` — real-time chat with channels, WebSocket, agent routing, webhook support
24
+ - **Tenant BaaS** — `tenant()` — multi-tenant dynamic tables, auto REST + GraphQL, row-level isolation, pgvector/HNSW
23
25
  - **Redis** — `redis()` — ioredis client, `ctx.redis`, middleware
24
26
  - **Queue** — `queue()` — Redis-backed job queue with immediate, delayed, and cron scheduling
25
27
  - **Auth** — `user()` — register/login/JWT + OAuth2 Server (authorization code + PKCE + client_credentials)
@@ -363,6 +365,263 @@ app.get('/api', auth({ verify: (token) => auth.verify(token) }), handler)
363
365
 
364
366
  For `client_credentials` tokens (machine-to-machine), `verify()` returns `null` since no user is associated.
365
367
 
368
+ ## Tenant BaaS
369
+
370
+ Built-in multi-tenant backend-as-a-service — define tables at runtime via API, get RESTful CRUD + GraphQL automatically, with row-level tenant isolation.
371
+
372
+ ```ts
373
+ import { serve, Router, postgres, user, tenant } from 'weifuwu'
374
+
375
+ const pg = postgres()
376
+ const u = user({ pg, jwtSecret: process.env.JWT_SECRET! })
377
+ const t = tenant({ pg, usersTable: '_users' })
378
+
379
+ await pg.migrate()
380
+ await u.migrate()
381
+ await t.migrate() // creates _tenants, _tenant_members, _user_tables
382
+
383
+ const app = new Router()
384
+ app.use('/auth', u.router())
385
+ app.use('/api', u.middleware()) // → ctx.user
386
+ app.use('/api', t.middleware()) // → ctx.tenant
387
+ app.use('/api', t.router()) // → management + data CRUD
388
+ app.use('/graphql', t.graphql()) // → dynamic GraphQL
389
+ ```
390
+
391
+ ### System tables
392
+
393
+ | Table | Purpose |
394
+ |-------|---------|
395
+ | `_tenants` | Tenant records (`id TEXT PK DEFAULT gen_random_uuid()`, `name`, `created_at`) |
396
+ | `_tenant_members` | User-tenant membership (`tenant_id`, `user_id`, `role`) |
397
+ | `_user_tables` | Dynamic table definitions (`tenant_id`, `slug`, `fields JSONB`) |
398
+
399
+ ### Dynamic table API
400
+
401
+ Create a table at runtime:
402
+
403
+ ```json
404
+ POST /api/tables
405
+ {
406
+ "slug": "articles",
407
+ "fields": [
408
+ { "name": "title", "type": "string", "required": true },
409
+ { "name": "content", "type": "text" },
410
+ { "name": "status", "type": "enum", "options": ["draft", "published"], "default": "draft" },
411
+ { "name": "views", "type": "integer", "default": 0 },
412
+ { "name": "embedding", "type": "vector", "dimensions": 1536, "index": "hnsw" }
413
+ ]
414
+ }
415
+ ```
416
+
417
+ → Creates a PostgreSQL table with `id SERIAL PK`, `tenant_id TEXT NOT NULL`, and the specified columns, plus indexes. The table name is internally scoped to the tenant.
418
+
419
+ ### Field types
420
+
421
+ | type | PostgreSQL | Index support |
422
+ |------|-----------|---------------|
423
+ | `string` | `TEXT` | `true`, `unique` |
424
+ | `integer` | `INTEGER` | `true`, `desc`, `unique` |
425
+ | `float` | `DOUBLE PRECISION` | `true`, `desc` |
426
+ | `boolean` | `BOOLEAN` | `true` |
427
+ | `text` | `TEXT` | `true` |
428
+ | `datetime` | `TIMESTAMPTZ` | `true`, `desc` |
429
+ | `date` | `DATE` | `true`, `desc` |
430
+ | `enum` | `TEXT` (with validation) | `true` |
431
+ | `json` | `JSONB` | `gin` |
432
+ | `vector` | `vector(n)` (pgvector) | `hnsw` (HNSW, vector_cosine_ops) |
433
+
434
+ ### Relationships
435
+
436
+ Declare a foreign key via the `relation` field:
437
+
438
+ ```json
439
+ { "name": "article_id", "type": "integer", "relation": { "table": "articles", "onDelete": "cascade" } }
440
+ ```
441
+
442
+ Supported relationship patterns:
443
+
444
+ | Pattern | Detection | REST | GraphQL |
445
+ |---------|-----------|------|---------|
446
+ | **belongs_to** | Field with `relation` | — | `comment.article` resolver |
447
+ | **has_many** | Another table has a relation pointing here | `GET /api/articles/:id/comments` | `article.comments` resolver |
448
+ | **M2M** | Junction table with exactly two relation fields | `GET /api/articles/:id/tags` (bypasses junction) | `article.tags` / `tag.articles` resolver |
449
+ | **Self-ref** | Relation field pointing to same table | — | With depth control |
450
+
451
+ ### RESTful API
452
+
453
+ All routes require `ctx.tenant` (set by `t.middleware()`). All queries automatically filter by `tenant_id`.
454
+
455
+ | Route | Method | Description |
456
+ |-------|--------|-------------|
457
+ | `/sys/tenants` | POST | Create tenant, caller becomes admin |
458
+ | `/sys/tenants` | GET | List user's tenants |
459
+ | `/sys/tenants/invite` | POST | Invite user by email (admin) |
460
+ | `/sys/tenants/members/:userId` | DELETE | Remove member (admin) |
461
+ | `/sys/tables` | POST/GET | Create / list dynamic tables |
462
+ | `/sys/tables/:slug` | GET/PATCH/DELETE | Get schema / add fields / drop table |
463
+ | `/:slug` | GET | List rows (limit, offset, sort) |
464
+ | `/:slug` | POST | Create row |
465
+ | `/:slug/:id` | GET/PATCH/DELETE | Get / update / delete row |
466
+ | `/:slug/:id/:_nested` | GET | List related rows (has_many / M2M) |
467
+ | `/:slug/:id/:_nested` | POST | Create related row (auto-fills relation field) |
468
+
469
+ ### Vector search
470
+
471
+ ```http
472
+ GET /api/articles?search_vector=[0.1,0.2,...]&search_field=embedding&search_limit=10
473
+ ```
474
+
475
+ Returns rows ordered by cosine distance (`<=>`), includes `_distance` field. Supports `l2` (`<->`) and `ip` (`<#>`):
476
+
477
+ ```http
478
+ GET /api/articles?search_vector=[...]&search_field=embedding&search_distance=l2
479
+ ```
480
+
481
+ ### GraphQL
482
+
483
+ Dynamic GraphQL schema generated per-request based on the authenticated tenant's tables:
484
+
485
+ ```graphql
486
+ type Article {
487
+ id: ID!
488
+ title: String!
489
+ content: String
490
+ status: String
491
+ comments(limit: Int, offset: Int): [Comment!]!
492
+ }
493
+
494
+ type Query {
495
+ articles(limit: Int, offset: Int): [Article!]!
496
+ getArticle(id: ID!): Article
497
+ }
498
+
499
+ type Mutation {
500
+ createArticle(data: CreateArticleInput!): Article!
501
+ updateArticle(id: ID!, data: PatchArticleInput!): Article!
502
+ deleteArticle(id: ID!): Boolean!
503
+ }
504
+ ```
505
+
506
+ Built with `graphql-js` native constructors (`GraphQLObjectType`), no SDL generation, no `makeExecutableSchema`.
507
+
508
+ ### Middleware
509
+
510
+ `t.middleware()` extracts the tenant context:
511
+
512
+ 1. Requires `ctx.user` (from `u.middleware()`)
513
+ 2. Looks up user's tenant memberships
514
+ 3. Single tenant → automatically set `ctx.tenant`
515
+ 4. Multiple tenants → require `X-Tenant-ID` header, return 300 with tenant list if missing
516
+ 5. No tenants → 403
517
+
518
+ ### Tenant lifecycle
519
+
520
+ ```ts
521
+ const t = tenant({ pg, usersTable: '_users' })
522
+
523
+ // Create a tenant — the caller becomes admin
524
+ const tenant = await (await fetch('http://localhost/api/sys/tenants', {
525
+ method: 'POST',
526
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer <jwt>' },
527
+ body: JSON.stringify({ name: 'Acme Corp' }),
528
+ })).json()
529
+ // → { id: "uuid", name: "Acme Corp", created_at: "..." }
530
+
531
+ // Invite a member
532
+ await fetch('http://localhost/api/sys/tenants/invite', {
533
+ method: 'POST',
534
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer <jwt>' },
535
+ body: JSON.stringify({ email: 'colleague@acme.com', role: 'member' }),
536
+ })
537
+ ```
538
+
539
+ ## AI Agent
540
+
541
+ Server-side AI agents with OpenAI-compatible API. Built-in chat, workflow (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
542
+
543
+ ```ts
544
+ import { agent } from 'weifuwu'
545
+
546
+ const agents = agent({ pg })
547
+
548
+ await agents.migrate()
549
+ app.use('/api', agents.router())
550
+ ```
551
+
552
+ | Type | Description | Execution |
553
+ |------|-------------|-----------|
554
+ | `chat` | Pure conversation | `streamText()` / `generateText()` |
555
+ | `workflow` | Tool-calling agent | `streamText({ tools })` |
556
+
557
+ ### Knowledge (RAG)
558
+
559
+ Add documents to any agent — `searchKnowledge` tool auto-injected:
560
+
561
+ ```ts
562
+ await agents.addKnowledge(agentId, 'Title', 'Document content...')
563
+ // The agent automatically calls searchKnowledge when answering
564
+ ```
565
+
566
+ ### Streaming
567
+
568
+ ```http
569
+ POST /agents/:id/run { input: "hello", stream: true }
570
+ → event-stream (fullStream SSE: text-delta, tool-call, tool-result, finish)
571
+ ```
572
+
573
+ ### Programmatic API
574
+
575
+ ```ts
576
+ const result = await agents.run(agentId, { input: 'hello', stream: false })
577
+ // { output: "Hello!", elapsed: 1234 }
578
+ ```
579
+
580
+ ## Messager
581
+
582
+ Real-time chat with channels, WebSocket, and agent routing.
583
+
584
+ ```ts
585
+ import { messager, agent } from 'weifuwu'
586
+
587
+ const agents = agent({ pg })
588
+ const msg = messager({ pg, agents })
589
+
590
+ await msg.migrate()
591
+ app.use('/api', msg.router())
592
+ app.ws('/ws', u.middleware(), msg.wsHandler())
593
+ ```
594
+
595
+ ### Channels
596
+
597
+ ```http
598
+ POST /channels name, type (channel|dm), members
599
+ GET /channels
600
+ GET /channels/:id
601
+ ```
602
+
603
+ ### Messages
604
+
605
+ ```http
606
+ GET /channels/:id/messages ?limit=50&before={id}
607
+ POST /channels/:id/messages content, sender_type, type
608
+ POST /channels/:id/read last_message_id
609
+ ```
610
+
611
+ ### WebSocket
612
+
613
+ ```json
614
+ { "type": "message", "channel_id": 1, "content": "Hi" }
615
+ { "type": "typing", "channel_id": 1, "is_typing": true }
616
+ { "type": "read", "channel_id": 1, "last_message_id": 42 }
617
+ ```
618
+
619
+ ### Programmatic send
620
+
621
+ ```ts
622
+ await msg.send(channelId, 'System message', { sender_type: 'system' })
623
+ ```
624
+
366
625
  ## WebSocket
367
626
  message(ws, ctx, data) {
368
627
  ws.send(`echo: ${data}`)
@@ -764,6 +1023,36 @@ Returns `{ stop, port, hostname, ready }`.
764
1023
 
765
1024
  Returns `UserModule` — `{ router, middleware, migrate, register, login, verify, registerClient, getClient, revokeClient, close }`.
766
1025
 
1026
+ ### `tenant(options)`
1027
+
1028
+ | Option | Default | Description |
1029
+ |--------|---------|-------------|
1030
+ | `pg` | — | PostgreSQL client from `postgres()` |
1031
+ | `usersTable` | — | Users table name (matching the `table` option passed to `user()`) |
1032
+
1033
+ Returns `TenantModule` — `{ migrate, middleware, router, graphql, close }`.
1034
+
1035
+ ### `agent(options)`
1036
+
1037
+ | Option | Default | Description |
1038
+ |--------|---------|-------------|
1039
+ | `pg` | — | PostgreSQL client from `postgres()` |
1040
+ | `model` | env `OPENAI_MODEL` → Ollama | `LanguageModel` from ai SDK |
1041
+ | `embeddingModel` | env `OPENAI_EMBEDDING_MODEL` → Ollama | `EmbeddingModel` for knowledge RAG |
1042
+ | `embeddingDimension` | `1024` | Vector dimension for pgvector |
1043
+ | `tools` | — | Tools for workflow-type agents (ai SDK `Tool` objects) |
1044
+
1045
+ Returns `AgentModule` — `{ migrate, router, run, addKnowledge, close }`.
1046
+
1047
+ ### `messager(options)`
1048
+
1049
+ | Option | Default | Description |
1050
+ |--------|---------|-------------|
1051
+ | `pg` | — | PostgreSQL client from `postgres()` |
1052
+ | `agents` | — | `AgentModule` instance (enables agent message routing) |
1053
+
1054
+ Returns `MessagerModule` — `{ migrate, router, wsHandler, send, close }`.
1055
+
767
1056
  ### `tsx(options)`
768
1057
 
769
1058
  | Option | Default | Description |
@@ -803,6 +1092,9 @@ Returns `Promise<Router>`.
803
1092
  | `redis(options?)` | Redis client (ioredis) — injects `ctx.redis` |
804
1093
  | `queue(options?)` | Redis-backed job queue — immediate, delayed, cron scheduling |
805
1094
  | `user(options)` | Built-in authentication (password + OAuth2 Server + JWT, middleware) |
1095
+ | `tenant(options)` | Multi-tenant BaaS — dynamic tables, REST + GraphQL auto-generation, row-level isolation |
1096
+ | `agent(options)` | AI Agent — chat/workflow/knowledge, Ollama-ready, programmatic API |
1097
+ | `messager(options)` | Real-time messaging — channels, WebSocket, agent routing, webhooks |
806
1098
  | `graphql(handler)` | GraphQL endpoint (GET/POST + GraphiQL) |
807
1099
  | `ai(handler)` | AI streaming endpoint (POST) |
808
1100
  | `workflow(handler)` | Workflow engine (POST + SSE) |
@@ -0,0 +1,2 @@
1
+ import type { AgentOptions, AgentModule } from './types.ts';
2
+ export declare function agent(options: AgentOptions): AgentModule;
@@ -0,0 +1,2 @@
1
+ export { agent } from './client.ts';
2
+ export type { AgentOptions, AgentModule, AgentConfig, RunParams, RunResult } from './types.ts';
@@ -0,0 +1,6 @@
1
+ import type { Sql } from 'postgres';
2
+ export interface MigrateOptions {
3
+ sql: Sql<{}>;
4
+ embeddingDimension: number;
5
+ }
6
+ export declare function migrate(opts: MigrateOptions): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import type { Sql } from 'postgres';
2
+ import { Router } from '../router.ts';
3
+ import type { RunParams } from './types.ts';
4
+ interface RestDeps {
5
+ sql: Sql<{}>;
6
+ runner: {
7
+ run: (agentId: number, params: RunParams) => Promise<any>;
8
+ addKnowledge: (agentId: number, title: string, content: string) => Promise<any>;
9
+ };
10
+ }
11
+ export declare function buildRouter(deps: RestDeps): Router;
12
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { LanguageModel, EmbeddingModel, Tool } from 'ai';
2
+ import type { Sql } from 'postgres';
3
+ import type { RunParams, RunResult, KnowledgeDoc } from './types.ts';
4
+ interface RunnerDeps {
5
+ sql: Sql<{}>;
6
+ getModel: () => LanguageModel;
7
+ getEmbeddingModel: () => EmbeddingModel;
8
+ userTools?: Record<string, Tool>;
9
+ }
10
+ export declare function createRunner(deps: RunnerDeps): {
11
+ run: (agentId: number, params: RunParams) => Promise<RunResult>;
12
+ addKnowledge: (agentId: number, title: string, content: string) => Promise<KnowledgeDoc>;
13
+ };
14
+ export {};
@@ -0,0 +1,51 @@
1
+ import type { LanguageModel, EmbeddingModel, Tool } from 'ai';
2
+ export interface AgentConfig {
3
+ id: number;
4
+ tenant_id: string | null;
5
+ name: string;
6
+ description: string;
7
+ type: 'chat' | 'workflow';
8
+ model: string;
9
+ system_prompt: string;
10
+ owner_id: number;
11
+ active: boolean;
12
+ created_at: string;
13
+ updated_at: string;
14
+ }
15
+ export interface KnowledgeDoc {
16
+ id: number;
17
+ agent_id: number;
18
+ title: string;
19
+ content: string;
20
+ embedding?: number[];
21
+ metadata: Record<string, unknown>;
22
+ created_at: string;
23
+ }
24
+ export interface RunParams {
25
+ input: string;
26
+ stream?: boolean;
27
+ messages?: Array<{
28
+ role: string;
29
+ content: string;
30
+ }>;
31
+ }
32
+ export type RunResult = {
33
+ output: string;
34
+ elapsed: number;
35
+ } | {
36
+ stream: ReadableStream<Uint8Array>;
37
+ };
38
+ export interface AgentOptions {
39
+ pg: any;
40
+ model?: LanguageModel;
41
+ embeddingModel?: EmbeddingModel;
42
+ embeddingDimension?: number;
43
+ tools?: Record<string, Tool>;
44
+ }
45
+ export interface AgentModule {
46
+ migrate: () => Promise<void>;
47
+ router: () => any;
48
+ run: (agentId: number, params: RunParams) => Promise<RunResult>;
49
+ addKnowledge: (agentId: number, title: string, content: string) => Promise<KnowledgeDoc>;
50
+ close: () => Promise<void>;
51
+ }
package/dist/index.d.ts CHANGED
@@ -33,3 +33,9 @@ export { redis } from './redis/index.ts';
33
33
  export type { RedisOptions, RedisClient } from './redis/types.ts';
34
34
  export { queue } from './queue/index.ts';
35
35
  export type { QueueOptions, QueueJob, Queue } from './queue/types.ts';
36
+ export { tenant } from './tenant/index.ts';
37
+ export type { TenantOptions, TenantModule, TenantContext, FieldDef, FieldType, RelationDef, UserTableRow } from './tenant/types.ts';
38
+ export { agent } from './agent/index.ts';
39
+ export type { AgentOptions, AgentModule, AgentConfig, RunParams, RunResult, KnowledgeDoc } from './agent/types.ts';
40
+ export { messager } from './messager/index.ts';
41
+ export type { MessagerOptions, MessagerModule, Channel, ChannelMember, Message } from './messager/types.ts';