weifuwu 0.54.1 → 0.55.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 +182 -42
- package/dist/db/errors.d.ts +44 -0
- package/dist/db/postgres/connection.d.ts +64 -0
- package/dist/db/postgres/pool.d.ts +76 -0
- package/dist/db/postgres/protocol.d.ts +71 -0
- package/dist/db/postgres/schema.d.ts +19 -0
- package/dist/db/redis/client.d.ts +44 -0
- package/dist/db/redis/connection.d.ts +60 -0
- package/dist/db/redis/pool.d.ts +54 -0
- package/dist/db/redis/resp.d.ts +42 -0
- package/dist/db/redis/subscriber.d.ts +17 -0
- package/dist/index.js +1648 -115
- package/dist/make-executable-schema.d.ts +20 -0
- package/dist/make-executable-schema.test.d.ts +1 -0
- package/dist/postgres/client.d.ts +10 -1
- package/dist/postgres/types.d.ts +35 -14
- package/dist/redis/client.d.ts +6 -0
- package/dist/redis/types.d.ts +13 -6
- package/dist/types.d.ts +3 -5
- package/package.json +25 -10
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install weifuwu
|
|
|
17
17
|
|
|
18
18
|
## 设计理念
|
|
19
19
|
|
|
20
|
-
**零运行时依赖** — 前端无 npm
|
|
20
|
+
**零运行时依赖** — 前端无 npm 运行时依赖(自研 VDOM,不引入 Virtual DOM 库、rxjs、immer 等)。后端仅依赖 `graphql` + `ws`(语言/协议本身)——**数据库客户端(PostgreSQL/Redis 协议)、GraphQL schema 工具全部自研**。esbuild 编译 TSX 的结果即可直接运行。
|
|
21
21
|
|
|
22
22
|
**两阶段组件模型** — 组件 = `(initProps, ctx) => (props) => VNode`。外层函数只执行一次(mount),内层函数每次状态/props 变化时执行(render)。无 class、无 `this`、无 Hook。
|
|
23
23
|
|
|
@@ -25,6 +25,8 @@ npm install weifuwu
|
|
|
25
25
|
|
|
26
26
|
**中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` 等),Handler/组件从 `ctx` 读取。
|
|
27
27
|
|
|
28
|
+
**自研数据层** — `ctx.sql`(PG v3 协议)与 `ctx.redis`(RESP2 协议)为**自研客户端**:确定性输出、行为可预测、统一错误模型。jsonb 自动解码、TTL 安全 API、schema 写前校验——高频痛点(双重编码/parseRow 样板/`'EX'` 参数顺序)从根上消除。
|
|
29
|
+
|
|
28
30
|
**SSR + 动态编译** — 后端 `ctx.ui.js()` 用 esbuild 实时编译 TSX,开发时改代码即刷即用,零构建步骤。
|
|
29
31
|
|
|
30
32
|
---
|
|
@@ -148,7 +150,7 @@ createApp()
|
|
|
148
150
|
|------|---------|------|
|
|
149
151
|
| `weifuwu/client` | `https://unpkg.com/weifuwu@latest/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
|
|
150
152
|
| `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 41 个 UI 组件(Button, Card, Table, Modal 等) |
|
|
151
|
-
| 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS +
|
|
153
|
+
| 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 82 个主题 Token + 35 个布局原语 |
|
|
152
154
|
| 独立布局系统 | `https://unpkg.com/weifuwu@latest/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
|
|
153
155
|
|
|
154
156
|
|
|
@@ -162,8 +164,8 @@ createApp()
|
|
|
162
164
|
| `weifuwu` | **serve** | HTTP 服务器 | Router |
|
|
163
165
|
| `weifuwu` | **cors** | CORS 跨域中间件 | Router |
|
|
164
166
|
| `weifuwu` | **serveStatic** | 静态文件服务(ETag/304/目录索引) | Router |
|
|
165
|
-
| `weifuwu` | **postgres** | PostgreSQL
|
|
166
|
-
| `weifuwu` | **redis** | Redis
|
|
167
|
+
| `weifuwu` | **postgres** | PostgreSQL 客户端(自研 PG v3 协议)→ `ctx.sql` | Router, DATABASE_URL |
|
|
168
|
+
| `weifuwu` | **redis** | Redis 客户端(自研 RESP2 协议)→ `ctx.redis` | Router, REDIS_URL |
|
|
167
169
|
| `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 → `ctx.ui` | Router |
|
|
168
170
|
| `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
|
|
169
171
|
| `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
|
|
@@ -177,7 +179,7 @@ createApp()
|
|
|
177
179
|
| `weifuwu/client` | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
|
|
178
180
|
| `weifuwu/client` | **popup** | 弹层 fixed 定位工具(`computeFixedPos` / `computeFixedPosRect`) | — |
|
|
179
181
|
| `weifuwu/components` | **42 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
|
|
180
|
-
| `weifuwu/layout` | **CSS 布局** | 35 个布局原语 +
|
|
182
|
+
| `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 82 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
|
|
181
183
|
|
|
182
184
|
---
|
|
183
185
|
|
|
@@ -461,82 +463,204 @@ app.get('/assets/*', serveStatic('./assets', {
|
|
|
461
463
|
|
|
462
464
|
---
|
|
463
465
|
|
|
464
|
-
## postgres — PostgreSQL
|
|
466
|
+
## postgres — PostgreSQL 客户端(自研)
|
|
467
|
+
|
|
468
|
+
> **自研 PG v3 协议**(零第三方依赖)——支持 SCRAM-SHA-256 认证、扩展查询(参数化)、类型映射(int8 超范围自动 string 防丢精度)、事务、连接池(acquire 超时防饿死)、schema 写前校验、statement_timeout 慢查询保护。
|
|
465
469
|
|
|
466
470
|
```ts
|
|
467
|
-
import { postgres
|
|
471
|
+
import { postgres } from 'weifuwu'
|
|
468
472
|
|
|
469
|
-
// 注入 ctx.sql
|
|
473
|
+
// 注入 ctx.sql(懒连接池)
|
|
470
474
|
app.use(postgres())
|
|
471
475
|
|
|
472
|
-
//
|
|
476
|
+
// ① tagged template —— 插值自动参数化(防注入)
|
|
473
477
|
app.get('/users', async (req, ctx) => {
|
|
474
|
-
const users = await ctx.sql`SELECT * FROM users WHERE
|
|
478
|
+
const users = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
|
|
475
479
|
return Response.json(users)
|
|
476
480
|
})
|
|
477
481
|
|
|
478
|
-
//
|
|
482
|
+
// ② jsonb 对象直传——自动序列化,不再有双重编码/parseRow 样板
|
|
483
|
+
app.post('/decks', async (req, ctx) => {
|
|
484
|
+
const deck = await req.json()
|
|
485
|
+
await ctx.sql`INSERT INTO decks (title, deck_json) VALUES (${deck.title}, ${deck})`
|
|
486
|
+
// 读回来自动是对象:rows[0].deck_json === { slides: [...] }(不是字符串)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// ③ 事务(postgres.js 兼容 begin)
|
|
479
490
|
app.post('/transfer', async (req, ctx) => {
|
|
480
|
-
|
|
491
|
+
await ctx.sql.begin(async sql => {
|
|
481
492
|
await sql`UPDATE accounts SET balance = balance - 100 WHERE id = 1`
|
|
482
493
|
await sql`UPDATE accounts SET balance = balance + 100 WHERE id = 2`
|
|
483
494
|
})
|
|
484
|
-
return Response.json({ ok: true })
|
|
485
495
|
})
|
|
486
496
|
```
|
|
487
497
|
|
|
498
|
+
### 类型映射(自动)
|
|
499
|
+
|
|
500
|
+
| 数据库类型 | 返回 JS 类型 |
|
|
501
|
+
|-----------|-------------|
|
|
502
|
+
| json / jsonb | `object`(自动 JSON.parse) |
|
|
503
|
+
| int2 / int4 / int8(安全范围内) | `number` |
|
|
504
|
+
| **int8(超出安全范围)** | **`string`**(防静默丢精度,金额/ID 关键) |
|
|
505
|
+
| float / numeric | `number` |
|
|
506
|
+
| boolean | `boolean` |
|
|
507
|
+
| text / varchar / uuid / date | `string` |
|
|
508
|
+
| NULL | `null` |
|
|
509
|
+
|
|
510
|
+
### 类型层(查询泛型 + schema 写前校验)
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
// ① 查询结果泛型(编译期类型,无需手写 interface + 断言)
|
|
514
|
+
interface Deck { id: number; title: string; deck_json: { slides: unknown[] } }
|
|
515
|
+
const decks = await ctx.sql.query<Deck>('SELECT id, title, deck_json FROM decks')
|
|
516
|
+
|
|
517
|
+
// ② schema 注册 → insert 写前校验(脏数据源头拦截)
|
|
518
|
+
ctx.sql.register('decks', {
|
|
519
|
+
title: { type: 'text', required: true },
|
|
520
|
+
status: { type: 'enum', values: ['outline', 'ready'] },
|
|
521
|
+
deck_json: { type: 'jsonb' },
|
|
522
|
+
})
|
|
523
|
+
await ctx.sql.insert('decks', { title: 'x', status: 'INVALID' }) // → ValidationError
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 方法面
|
|
527
|
+
|
|
528
|
+
| 方法 | 说明 |
|
|
529
|
+
|------|------|
|
|
530
|
+
| `ctx.sql\`...\`` | tagged template → 参数化查询(插值=参数,表名需硬编码) |
|
|
531
|
+
| `ctx.sql.query<T>(sql, params?)` | 参数化查询 + 泛型 |
|
|
532
|
+
| `ctx.sql.unsafe(sql, params?)` | 原生 SQL(DDL / 动态表名) |
|
|
533
|
+
| `ctx.sql.begin(fn)` | 事务(回调收到 tagged template sql) |
|
|
534
|
+
| `ctx.sql.transaction(fn)` | 事务(回调收到 `{ query }`) |
|
|
535
|
+
| `ctx.sql.register(table, schema)` | 注册表结构(写前校验) |
|
|
536
|
+
| `ctx.sql.insert(table, row)` | schema 校验 + 参数化插入 |
|
|
537
|
+
| `ctx.sql\`...\` 内嵌片段` | 条件 SQL 片段(嵌套过滤,参数自动重编号) |
|
|
538
|
+
| `ctx.sql.close()` | 关闭连接池 |
|
|
539
|
+
|
|
540
|
+
### 条件片段(嵌套过滤)
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
const status = req.query.status // 可能为空
|
|
544
|
+
const rows = await ctx.sql`
|
|
545
|
+
SELECT * FROM orders WHERE amount > ${100}
|
|
546
|
+
${status ? ctx.sql`AND status = ${status}` : ctx.sql``}
|
|
547
|
+
`
|
|
548
|
+
// 空片段内联为空,参数自动重编号——同一 SQL 无论条件多少都安全参数化
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 选项
|
|
552
|
+
|
|
488
553
|
| 选项 | 类型 | 默认值 | 说明 |
|
|
489
554
|
|------|------|--------|------|
|
|
490
|
-
| `
|
|
491
|
-
| `max` | `number` | `10` | 连接池大小 |
|
|
492
|
-
| `
|
|
493
|
-
| `
|
|
555
|
+
| `connection` | `string` | `DATABASE_URL` | 连接字符串 |
|
|
556
|
+
| `max`(或 `poolSize`) | `number` | `10` | 连接池大小 |
|
|
557
|
+
| `acquireTimeoutMs` | `number` | `30000` | 池全忙时 acquire 超时(防饿死,0=无限) |
|
|
558
|
+
| `statementTimeoutMs`(或 `statementTimeout`) | `number` | `0` | 语句超时(慢查询保护,0=禁用) |
|
|
559
|
+
| `onQuery` | `(sql, durationMs, rowCount) => void` | — | 查询观测钩子(慢查询日志/审计) |
|
|
494
560
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
| `ctx.sql.close()` | `() => Promise<void>` | 关闭连接池 |
|
|
561
|
+
### 幂等迁移(内置)
|
|
562
|
+
|
|
563
|
+
`postgres()` 返回的中间件自带迁移跟踪(`_weifuwu_migrations` 表),模块启动时检查-执行-记录三步幂等:
|
|
499
564
|
|
|
500
565
|
```ts
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
566
|
+
const db = postgres()
|
|
567
|
+
await db.migrate() // ① 建迁移跟踪表(幂等)
|
|
568
|
+
|
|
569
|
+
if (!(await db.isMigrated('users'))) { // ② 检查是否已迁移
|
|
570
|
+
await db.sql.unsafe(`CREATE TABLE users (...)`)
|
|
571
|
+
await db.markMigrated('users') // ③ 记录(幂等,重复调用无害)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
app.use(db)
|
|
505
575
|
```
|
|
506
576
|
|
|
577
|
+
> 多副本部署时天然安全:`markMigrated` 用 `ON CONFLICT DO NOTHING`,两个实例同时迁移也不会重复执行。
|
|
578
|
+
|
|
579
|
+
### 错误映射(自动)
|
|
580
|
+
|
|
581
|
+
`ctx.sql` 查询错误自动映射为 `HttpError`,业务无需手写 catch:
|
|
582
|
+
|
|
583
|
+
| 错误码 | 含义 | HTTP |
|
|
584
|
+
|--------|------|------|
|
|
585
|
+
| `23505` | 唯一约束冲突 | **409** |
|
|
586
|
+
| `23503` / `23502` / `23514` | 外键 / 非空 / 检查约束 | **400** |
|
|
587
|
+
| `22P02` / `22003` | 类型 / 数值错误 | **400** |
|
|
588
|
+
|
|
589
|
+
> 未映射的错误码原样抛出(带 `code` 属性,如 `42P01` 表不存在)。
|
|
590
|
+
|
|
591
|
+
> **裁剪声明**:逻辑复制 / 大对象 / 显式游标 / 二进制 COPY 不支持(明确抛 `ProtocolError('unsupported')`,而非静默出错)。
|
|
592
|
+
|
|
507
593
|
---
|
|
508
594
|
|
|
509
|
-
## redis — Redis
|
|
595
|
+
## redis — Redis 客户端(自研)
|
|
596
|
+
|
|
597
|
+
> **自研 RESP2 协议**(零第三方依赖)——连接/重连(断线 pending 拒绝、指数退避)/离线队列/管道/Pub-Sub(订阅断线自动重放)+ 消除 ioredis 高频痛点(TTL 参数顺序、JSON 手动序列化、缓存样板)。
|
|
510
598
|
|
|
511
599
|
```ts
|
|
512
600
|
import { redis } from 'weifuwu'
|
|
513
601
|
|
|
514
602
|
app.use(redis())
|
|
515
603
|
|
|
604
|
+
// ① TTL 安全 —— 直接传秒,不会写错
|
|
605
|
+
app.post('/cache/:key', async (req, ctx) => {
|
|
606
|
+
const { value } = await req.json()
|
|
607
|
+
await ctx.redis.set(ctx.params.key, value, 3600) // ioredis 要 set(k, v, 'EX', 3600)
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
// ② JSON 零样板 —— 自动序列化(AI 缓存场景)
|
|
516
611
|
app.get('/cache/:key', async (req, ctx) => {
|
|
517
|
-
const val = await ctx.redis.
|
|
518
|
-
|
|
519
|
-
return Response.json({ value: val })
|
|
612
|
+
const val = await ctx.redis.jsonGet(ctx.params.key) // 自动 JSON.parse
|
|
613
|
+
return Response.json(val ?? { miss: true })
|
|
520
614
|
})
|
|
521
615
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
await ctx.redis.
|
|
525
|
-
|
|
616
|
+
// ③ 缓存便捷 —— 读-算-写一体,null 不缓存(防穿透)
|
|
617
|
+
app.get('/llm/:id', async (req, ctx) => {
|
|
618
|
+
const result = await ctx.redis.cache(`llm:${ctx.params.id}`, async () => {
|
|
619
|
+
return await generateLLM(ctx.params.id) // miss 才执行
|
|
620
|
+
}, 3600)
|
|
621
|
+
return Response.json(result)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
// ④ Pub/Sub —— 发布用 ctx.redis,订阅用独立连接(回调式,断线自动重连恢复订阅)
|
|
625
|
+
app.post('/events', async (req, ctx) => {
|
|
626
|
+
await ctx.redis.publish('events', JSON.stringify({ type: 'deck.created' }))
|
|
526
627
|
})
|
|
628
|
+
|
|
629
|
+
const sub = ctx.redis.createSubscriber()
|
|
630
|
+
await sub.connect()
|
|
631
|
+
await sub.subscribe('events', (channel, message) => {
|
|
632
|
+
// 收到实时消息
|
|
633
|
+
})
|
|
634
|
+
await sub.psubscribe('jobs:*', (channel, message) => {
|
|
635
|
+
// 模式匹配订阅
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
// ⑤ 任意命令透传 + keyPrefix 隔离
|
|
639
|
+
await ctx.redis.command('LRANGE', 'list', '0', '-1')
|
|
640
|
+
|
|
641
|
+
app.use(redis({ keyPrefix: 'api:' })) // 之后所有 key 自动加前缀
|
|
642
|
+
await ctx.redis.set('user', 1) // 实际写入 'api:user'
|
|
527
643
|
```
|
|
528
644
|
|
|
645
|
+
### 方法面
|
|
646
|
+
|
|
647
|
+
| 方法 | 说明 |
|
|
648
|
+
|------|------|
|
|
649
|
+
| `get / set(key, val, ttl?) / del / incr / expire / ttl` | 基础命令(set 直接传秒) |
|
|
650
|
+
| `jsonGet / jsonSet(key, val, ttl?)` | JSON 自动序列化 |
|
|
651
|
+
| `cache(key, fn, ttl)` | 缓存读-算-写(null 不缓存防穿透) |
|
|
652
|
+
| `publish(channel, msg)` | Pub-Sub 发布 |
|
|
653
|
+
| `createSubscriber()` | 独立订阅连接(`subscribe`/`psubscribe` 回调式) |
|
|
654
|
+
| `command(name, ...args)` | 底层命令透传 |
|
|
655
|
+
| `close()` | 关闭连接池 |
|
|
656
|
+
|
|
529
657
|
| 选项 | 类型 | 默认值 | 说明 |
|
|
530
658
|
|------|------|--------|------|
|
|
531
659
|
| `url` | `string` | `REDIS_URL` 环境变量 | 连接字符串 |
|
|
532
|
-
| `
|
|
533
|
-
|
|
534
|
-
| ctx 注入 | 类型 | 说明 |
|
|
535
|
-
|----------|------|------|
|
|
536
|
-
| `ctx.redis` | `ioredis.Redis` | ioredis 实例 |
|
|
537
|
-
| `ctx.redis.close()` | `() => Promise<void>` | 关闭连接 |
|
|
660
|
+
| `poolSize` | `number` | `5` | 连接池大小 |
|
|
661
|
+
| `keyPrefix` | `string` | `''` | 所有 key 自动加前缀(多应用隔离) |
|
|
538
662
|
|
|
539
|
-
|
|
663
|
+
> **裁剪声明**:集群(MOVED 路由)/ 哨兵 / 自动管道不支持(standalone 优先)。
|
|
540
664
|
|
|
541
665
|
---
|
|
542
666
|
|
|
@@ -594,6 +718,8 @@ app.get('/style.css', (req, ctx) => ctx.ui.css('weifuwu/components/style.css'))
|
|
|
594
718
|
|
|
595
719
|
## graphql — GraphQL 端点
|
|
596
720
|
|
|
721
|
+
> **SDL + resolvers 绑定为自研实现**(`makeExecutableSchema`,56 行替代 @graphql-tools/schema)——支持根类型与嵌套类型字段 resolver、默认属性查找。
|
|
722
|
+
|
|
597
723
|
```ts
|
|
598
724
|
import type { GraphQLHandler } from 'weifuwu'
|
|
599
725
|
|
|
@@ -893,6 +1019,20 @@ h('div', { class: 'x' }, child1, child2)
|
|
|
893
1019
|
| `h(type, props, ...children)` | hyperscript |
|
|
894
1020
|
| `jsx` / `jsxs` / `jsxDEV` | JSX 编译目标 |
|
|
895
1021
|
| `Fragment` | 片段 |
|
|
1022
|
+
| `Portal` / `createPortal(children, portalKey?)` | 渲染到 `document.body#__wf_portal` 独立容器(弹层/对话框,脱离父级 overflow 裁剪) |
|
|
1023
|
+
|
|
1024
|
+
```tsx
|
|
1025
|
+
import { createPortal } from 'weifuwu/client'
|
|
1026
|
+
|
|
1027
|
+
// 内容渲染到 body 下的独立容器(不在父组件的 DOM 树内)
|
|
1028
|
+
const Tooltip = (_init, ctx) =>
|
|
1029
|
+
(props) => createPortal(
|
|
1030
|
+
<div class="tooltip">{props.text}</div>
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
// 配合 ctx.ui.selfId('name') 可从任何地方精准刷新 portal 内容
|
|
1034
|
+
ctx.ui.render(['name'])
|
|
1035
|
+
```
|
|
896
1036
|
|
|
897
1037
|
---
|
|
898
1038
|
|
|
@@ -1987,7 +2127,7 @@ props 变化 ──────────────────────
|
|
|
1987
2127
|
|
|
1988
2128
|
# 布局系统 (`weifuwu/layout`)
|
|
1989
2129
|
|
|
1990
|
-
纯 CSS 布局原语 +
|
|
2130
|
+
纯 CSS 布局原语 + 82 个主题 Token。不绑定任何 JS 框架。
|
|
1991
2131
|
|
|
1992
2132
|
> **全栈 weifuwu 项目**:`weifuwu/components/style.css` 已包含布局系统,一条 import 就够了,无需单独引用本页。
|
|
1993
2133
|
> 本页仅适用于**非 weifuwu 项目**或**只需 CSS 布局**的场景。
|
|
@@ -2047,7 +2187,7 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
|
|
|
2047
2187
|
| | `wf-inline-block` | display: inline-block |
|
|
2048
2188
|
| | `wf-contents` | display: contents |
|
|
2049
2189
|
|
|
2050
|
-
##
|
|
2190
|
+
## 82 个主题 Token
|
|
2051
2191
|
|
|
2052
2192
|
```css
|
|
2053
2193
|
/* 品牌色 */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db — 统一错误模型
|
|
3
|
+
*
|
|
4
|
+
* 自研 postgres/redis 客户端共享的错误类型体系。
|
|
5
|
+
* 目标:错误语义统一 → 业务层 catch 可编程(按 kind/code 决策)。
|
|
6
|
+
*
|
|
7
|
+
* kind: connection | protocol | timeout | validation | retryable
|
|
8
|
+
* code: PG 错误码(如 23505 唯一冲突)或协议码
|
|
9
|
+
*/
|
|
10
|
+
export type DbErrorKind = 'connection' | 'protocol' | 'timeout' | 'validation' | 'retryable';
|
|
11
|
+
export declare class DbError extends Error {
|
|
12
|
+
readonly kind: DbErrorKind;
|
|
13
|
+
readonly code?: string;
|
|
14
|
+
readonly cause?: unknown;
|
|
15
|
+
constructor(kind: DbErrorKind, message: string, options?: {
|
|
16
|
+
code?: string;
|
|
17
|
+
cause?: unknown;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** 明确不支持的协议能力(诚实裁剪):COPY 二进制、逻辑复制、集群、哨兵等 */
|
|
21
|
+
export declare class ProtocolError extends DbError {
|
|
22
|
+
constructor(feature: string, message?: string);
|
|
23
|
+
}
|
|
24
|
+
/** 连接失败(含重连尝试次数) */
|
|
25
|
+
export declare class ConnectionError extends DbError {
|
|
26
|
+
readonly attempts: number;
|
|
27
|
+
constructor(message: string, attempts?: number, cause?: unknown);
|
|
28
|
+
}
|
|
29
|
+
/** 可重试错误:序列化失败/死锁(PG 40P01/40001)等 */
|
|
30
|
+
export declare class RetryableError extends DbError {
|
|
31
|
+
constructor(message: string, code?: string, cause?: unknown);
|
|
32
|
+
}
|
|
33
|
+
/** 超时:statement_timeout / connect_timeout / idle 等 */
|
|
34
|
+
export declare class TimeoutError extends DbError {
|
|
35
|
+
readonly operation: string;
|
|
36
|
+
readonly ms: number;
|
|
37
|
+
constructor(operation: string, ms: number);
|
|
38
|
+
}
|
|
39
|
+
/** 写前校验失败(schema 注册 → 脏数据拦截) */
|
|
40
|
+
export declare class ValidationError extends DbError {
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
|
43
|
+
/** 判断错误是否可安全重试(事务层用) */
|
|
44
|
+
export declare function isRetryable(err: unknown): boolean;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/postgres — PostgreSQL 连接(v3 协议)
|
|
3
|
+
*
|
|
4
|
+
* 连接流程: startup → 认证(SCRAM-SHA-256 / md5 / cleartext)→ 参数/就绪
|
|
5
|
+
* 查询流程: Query(Q) → RowDescription/DataRow/CommandComplete → ReadyForQuery
|
|
6
|
+
*
|
|
7
|
+
* 认证支持:
|
|
8
|
+
* R=0 OK
|
|
9
|
+
* R=3 cleartext
|
|
10
|
+
* R=5 md5
|
|
11
|
+
* R=10/11/12 SCRAM-SHA-256(PG15+ 默认)
|
|
12
|
+
*/
|
|
13
|
+
export interface PgConnectionOptions {
|
|
14
|
+
host?: string;
|
|
15
|
+
port?: number;
|
|
16
|
+
user?: string;
|
|
17
|
+
password?: string;
|
|
18
|
+
database?: string;
|
|
19
|
+
/** 连接超时 ms。默认 10_000。 */
|
|
20
|
+
connectTimeoutMs?: number;
|
|
21
|
+
/** 语句超时 ms(慢查询保护,会话级 SET statement_timeout)。默认 0 = 禁用。 */
|
|
22
|
+
statementTimeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface Row {
|
|
25
|
+
[col: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
export declare class PgConnection {
|
|
28
|
+
private opts;
|
|
29
|
+
private timeoutSet;
|
|
30
|
+
private awaitingReady;
|
|
31
|
+
private pendingErrorZ;
|
|
32
|
+
private socket;
|
|
33
|
+
private stream;
|
|
34
|
+
private status;
|
|
35
|
+
private waiters;
|
|
36
|
+
constructor(options?: PgConnectionOptions);
|
|
37
|
+
get connected(): boolean;
|
|
38
|
+
connect(): Promise<void>;
|
|
39
|
+
private onReady;
|
|
40
|
+
private onAuthFail;
|
|
41
|
+
private expectingAuth;
|
|
42
|
+
private authStage;
|
|
43
|
+
private authCtx;
|
|
44
|
+
private prepared;
|
|
45
|
+
private stmtSeq;
|
|
46
|
+
private currentQuery;
|
|
47
|
+
private onData;
|
|
48
|
+
private handle;
|
|
49
|
+
/** SCRAM client-final 消息 */
|
|
50
|
+
private scramFinal;
|
|
51
|
+
private scramServerSignature;
|
|
52
|
+
/** 事务:BEGIN → fn(tx) → COMMIT;fn 抛错 → ROLLBACK(回滚失败吞掉,保留原始错误) */
|
|
53
|
+
transaction<T>(fn: (tx: {
|
|
54
|
+
query: (sql: string, params?: (string | number | boolean | object | null)[]) => Promise<Row[]>;
|
|
55
|
+
}) => Promise<T>): Promise<T>;
|
|
56
|
+
/** 查询:无参数走简单协议(Q),有参数走扩展查询(Parse/Bind/Execute/Sync) */
|
|
57
|
+
query(sql: string, params?: (string | number | boolean | object | null)[]): Promise<Row[]>;
|
|
58
|
+
private send;
|
|
59
|
+
private notifyIdle;
|
|
60
|
+
/** 等待连接空闲(事务/多语句流程用) */
|
|
61
|
+
waitIdle(): Promise<void>;
|
|
62
|
+
/** 终止连接 */
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/postgres — PostgreSQL 连接池(借贷模型)
|
|
3
|
+
*
|
|
4
|
+
* 连接空闲队列 + 等待者队列:
|
|
5
|
+
* query → acquire(空闲连接 or 等待)→ 执行 → release
|
|
6
|
+
* 事务 → acquire 单个连接执行整个 BEGIN→fn→COMMIT/ROLLBACK
|
|
7
|
+
*
|
|
8
|
+
* 解决 PgConnection 单连接串行(一个 currentQuery)的限制:
|
|
9
|
+
* 并发查询路由到不同连接,全忙时排队等待而非 reject。
|
|
10
|
+
*/
|
|
11
|
+
import { type PgConnectionOptions, type Row } from './connection.ts';
|
|
12
|
+
import { type Schema } from './schema.ts';
|
|
13
|
+
export interface PgPoolOptions extends PgConnectionOptions {
|
|
14
|
+
/** 池大小(连接数)。默认 5。 */
|
|
15
|
+
poolSize?: number;
|
|
16
|
+
/** acquire 超时 ms(池全忙时等待上限,防饿死)。默认 30_000。0 = 无限。 */
|
|
17
|
+
acquireTimeoutMs?: number;
|
|
18
|
+
/** 查询观测钩子(慢查询日志/审计) */
|
|
19
|
+
onQuery?: (sql: string, durationMs: number, rowCount: number) => void;
|
|
20
|
+
}
|
|
21
|
+
type QueryParams = (string | number | boolean | object | null)[];
|
|
22
|
+
export declare class PgPool {
|
|
23
|
+
private all;
|
|
24
|
+
private available;
|
|
25
|
+
private waiters;
|
|
26
|
+
private closed;
|
|
27
|
+
private opts;
|
|
28
|
+
private initPromise;
|
|
29
|
+
private schemas;
|
|
30
|
+
/** 懒连接:构造不连接,ensure() 首次初始化(中间件注入场景) */
|
|
31
|
+
constructor(options?: PgPoolOptions);
|
|
32
|
+
static create(options?: PgPoolOptions): Promise<PgPool>;
|
|
33
|
+
private readyPromise;
|
|
34
|
+
private ensure;
|
|
35
|
+
private init;
|
|
36
|
+
/** 获取一个空闲连接(全忙则排队等待) */
|
|
37
|
+
private acquire;
|
|
38
|
+
private release;
|
|
39
|
+
/** 连接可用事件统一入口:优先唤醒等待者,否则回空闲池(release 与 replenish 共用) */
|
|
40
|
+
private dispatchAvailable;
|
|
41
|
+
/** 坏连接剔除后异步重建(池容量保持)——就绪后走统一分发(唤醒 waiter) */
|
|
42
|
+
private replenish;
|
|
43
|
+
query<T = Row>(sql: string, params?: QueryParams): Promise<T[]>;
|
|
44
|
+
/** 注册表结构(元数据闭环:校验/类型推断的起点) */
|
|
45
|
+
register(table: string, schema: Schema): void;
|
|
46
|
+
/** 写前校验 + 参数化插入(schema 驱动,脏数据源头拦截) */
|
|
47
|
+
insert<T = Row>(table: string, row: Record<string, unknown>): Promise<T[]>;
|
|
48
|
+
/**
|
|
49
|
+
* tagged template: sql\`SELECT * FROM t WHERE id = \${id}\`
|
|
50
|
+
* 插值 = 参数(postgres.js 语义,防注入);表名必须硬编码(插值会被当参数)。
|
|
51
|
+
* 对象插值自动 JSON.stringify → jsonb。
|
|
52
|
+
*/
|
|
53
|
+
tag(strings: TemplateStringsArray, ...values: unknown[]): Promise<Row[]>;
|
|
54
|
+
/** postgres.js 兼容事务 API: begin(fn)——fn 收到 tagged template 事务 sql */
|
|
55
|
+
begin<T>(fn: (txSql: TaggedSql) => Promise<T>): Promise<T>;
|
|
56
|
+
/** 片段:可嵌套的 SQL 片段(postgres.js fragment 语义,条件过滤模式) */
|
|
57
|
+
frag(strings: TemplateStringsArray, ...values: unknown[]): SqlFragment;
|
|
58
|
+
/** 原生 SQL(DDL / 动态表名场景);$1 占位符 + 参数数组 */
|
|
59
|
+
unsafe(sql: string, params?: QueryParams): Promise<Row[]>;
|
|
60
|
+
/** 事务:固定在单个连接上执行整个 BEGIN→fn→COMMIT/ROLLBACK */
|
|
61
|
+
transaction<T>(fn: (tx: {
|
|
62
|
+
query: (sql: string, params?: QueryParams) => Promise<Row[]>;
|
|
63
|
+
}) => Promise<T>): Promise<T>;
|
|
64
|
+
close(): Promise<void>;
|
|
65
|
+
get size(): number;
|
|
66
|
+
}
|
|
67
|
+
/** 片段对象:嵌套 SQL 片段(内部含已解析的 sql + params) */
|
|
68
|
+
export interface SqlFragment {
|
|
69
|
+
__fragment: {
|
|
70
|
+
sql: string;
|
|
71
|
+
params: QueryParams;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** tagged template 事务 sql(begin 回调参数) */
|
|
75
|
+
export type TaggedSql = (strings: TemplateStringsArray, ...values: unknown[]) => Promise<Row[]>;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/postgres — PostgreSQL v3 协议消息编解码
|
|
3
|
+
*
|
|
4
|
+
* 消息帧: type(1) + length(4, 含自身) + payload
|
|
5
|
+
* StartupMessage 特殊: length(4) + version(4) + 参数(键\0值\0...\0)
|
|
6
|
+
*
|
|
7
|
+
* 响应解析辅助: authCode / parseRowDescription / parseDataRow / readyStatus / parseErrorFields
|
|
8
|
+
*/
|
|
9
|
+
/** 客户端消息类型 */
|
|
10
|
+
export type ClientMessageType = 'Q' | 'P' | 'B' | 'E' | 'S' | 'X' | 'p' | 'd' | 'H' | 'C' | 'D' | 'c' | 'f';
|
|
11
|
+
/** 解析后的消息 */
|
|
12
|
+
export interface Message {
|
|
13
|
+
type: string;
|
|
14
|
+
payload: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
/** 编码消息帧: type + length(4) + payload */
|
|
17
|
+
export declare function encodeMessage(type: string, payload: Uint8Array): Uint8Array;
|
|
18
|
+
/** StartupMessage: length(4) + version(4=196608) + 参数键值对 + \0 */
|
|
19
|
+
export declare function startupMessage(params: Record<string, string>): Uint8Array;
|
|
20
|
+
/** 简单查询消息: Q + SQL + \0 终止符 */
|
|
21
|
+
export declare function queryMessage(sql: string): Uint8Array;
|
|
22
|
+
/** Parse: P + statementName\0 + query\0 + paramTypeCount(2) + OIDs */
|
|
23
|
+
export declare function parseMessage(name: string, sql: string, paramTypes?: number[]): Uint8Array;
|
|
24
|
+
/** Bind: B + portal\0 + statement\0 + fmtCount + formats + paramCount + params + resultFmtCount */
|
|
25
|
+
export declare function bindMessage(statement: string, params: (string | Uint8Array | null)[], paramFormats?: number[]): Uint8Array;
|
|
26
|
+
/** Execute: E + portal\0 + maxRows(4) */
|
|
27
|
+
export declare function executeMessage(portal?: string, maxRows?: number): Uint8Array;
|
|
28
|
+
/** Sync / Terminate / PasswordMessage */
|
|
29
|
+
export declare function syncMessage(): Uint8Array;
|
|
30
|
+
/** Flush: 强制服务器处理已缓冲的扩展查询消息(Parse/Bind/Execute 需 Flush 或 Sync 才执行) */
|
|
31
|
+
export declare function flushMessage(): Uint8Array;
|
|
32
|
+
/** Describe: D + 目标类型(S/P) + name\0——服务器返回 ParameterDescription(t) / RowDescription(T) */
|
|
33
|
+
export declare function describeMessage(kind: 'S' | 'P', name?: string): Uint8Array;
|
|
34
|
+
export declare function terminateMessage(): Uint8Array;
|
|
35
|
+
export declare function passwordMessage(password: string): Uint8Array;
|
|
36
|
+
/** 增量消息流解析:零拷贝(buffer + offset 指针),喂入任意分片 */
|
|
37
|
+
export declare class MessageStream {
|
|
38
|
+
private buf;
|
|
39
|
+
private off;
|
|
40
|
+
push(chunk: Uint8Array): Message[];
|
|
41
|
+
/** 追加分片:已消费部分先行压缩(一次拷贝),避免 O(n²) 累积 */
|
|
42
|
+
private append;
|
|
43
|
+
private compact;
|
|
44
|
+
/** 尝试读一条完整消息;不完整返回 null(不消费) */
|
|
45
|
+
private tryRead;
|
|
46
|
+
}
|
|
47
|
+
/** 便捷:解析 buffer 中所有完整消息 */
|
|
48
|
+
export declare function parseMessageStream(data: Uint8Array): Message[];
|
|
49
|
+
/** Authentication (R) 的认证码 */
|
|
50
|
+
export declare function authCode(msg: Message): number;
|
|
51
|
+
export interface ColumnInfo {
|
|
52
|
+
name: string;
|
|
53
|
+
typeOid: number;
|
|
54
|
+
typeLen: number;
|
|
55
|
+
}
|
|
56
|
+
/** RowDescription (T): 列信息 */
|
|
57
|
+
export declare function parseRowDescription(payload: Uint8Array): ColumnInfo[];
|
|
58
|
+
/** DataRow (D): 值列表(null 为 null,其余为文本字节) */
|
|
59
|
+
export declare function parseDataRow(payload: Uint8Array): (string | null)[];
|
|
60
|
+
/** ReadyForQuery (Z) 状态 */
|
|
61
|
+
export declare function readyStatus(payload: Uint8Array): 'idle' | 'tx' | 'error';
|
|
62
|
+
export interface ErrorFields {
|
|
63
|
+
severity?: string;
|
|
64
|
+
code?: string;
|
|
65
|
+
message?: string;
|
|
66
|
+
detail?: string;
|
|
67
|
+
hint?: string;
|
|
68
|
+
position?: string;
|
|
69
|
+
}
|
|
70
|
+
/** ErrorResponse (E) 字段 */
|
|
71
|
+
export declare function parseErrorFields(payload: Uint8Array): ErrorFields;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/postgres — Schema 注册与写前校验
|
|
3
|
+
*
|
|
4
|
+
* 注册表结构后,insert 在写入前校验字段类型/必填/枚举——脏数据在源头拦截。
|
|
5
|
+
* 这是元数据闭环的第一环(迁移/类型推断/缓存失效共享 schema 的起点)。
|
|
6
|
+
*/
|
|
7
|
+
export type ColumnType = 'text' | 'int' | 'jsonb' | 'enum';
|
|
8
|
+
export interface ColumnDef {
|
|
9
|
+
type: ColumnType;
|
|
10
|
+
/** 必填(INSERT 时缺失报错) */
|
|
11
|
+
required?: boolean;
|
|
12
|
+
/** enum 类型允许的值 */
|
|
13
|
+
values?: string[];
|
|
14
|
+
}
|
|
15
|
+
export type Schema = Record<string, ColumnDef>;
|
|
16
|
+
/** 校验单值;失败抛 ValidationError */
|
|
17
|
+
export declare function validateValue(def: ColumnDef, value: unknown, column: string): void;
|
|
18
|
+
/** 校验整个 row(必填 + 各列类型) */
|
|
19
|
+
export declare function validateRow(schema: Schema, row: Record<string, unknown>): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/db/redis — Redis 客户端高层 API
|
|
3
|
+
*
|
|
4
|
+
* 在 RedisConnection 之上提供:
|
|
5
|
+
* - TTL 安全: set(key, val, ttl) 直接生效(无需记 'EX' 前缀顺序)
|
|
6
|
+
* - JSON 存取: jsonGet/jsonSet 自动序列化(AI 缓存场景零样板)
|
|
7
|
+
* - 缓存便捷: cache(key, fn, ttl) 读缓存 → miss 执行 fn → 回填
|
|
8
|
+
*/
|
|
9
|
+
import { type RedisConnectionOptions } from './connection.ts';
|
|
10
|
+
import type { RespValue } from './resp.ts';
|
|
11
|
+
export interface RedisClientOptions extends RedisConnectionOptions {
|
|
12
|
+
}
|
|
13
|
+
export declare class RedisClient {
|
|
14
|
+
private conn;
|
|
15
|
+
private constructor();
|
|
16
|
+
/** 建立连接并返回就绪的客户端 */
|
|
17
|
+
static connect(options?: RedisClientOptions): Promise<RedisClient>;
|
|
18
|
+
/** 底层命令透传(RESP 值) */
|
|
19
|
+
command(name: string, ...args: (string | number)[]): Promise<RespValue>;
|
|
20
|
+
get(key: string): Promise<string | null>;
|
|
21
|
+
/**
|
|
22
|
+
* SET。ttl 秒可省略;传入即安全生效(内部转 SET key val EX ttl)。
|
|
23
|
+
*/
|
|
24
|
+
set(key: string, value: string | number, ttl?: number): Promise<'OK'>;
|
|
25
|
+
/** DEL 多 key,返回删除数量 */
|
|
26
|
+
del(...keys: string[]): Promise<number>;
|
|
27
|
+
/** INCR,返回自增后的值 */
|
|
28
|
+
incr(key: string): Promise<number>;
|
|
29
|
+
/** EXPIRE,返回 1=设置成功 0=key 不存在 */
|
|
30
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
31
|
+
/** TTL 剩余秒数;-1=无 TTL -2=key 不存在 */
|
|
32
|
+
ttl(key: string): Promise<number>;
|
|
33
|
+
/** 读取并 JSON.parse;key 不存在返回 null */
|
|
34
|
+
jsonGet(key: string): Promise<unknown | null>;
|
|
35
|
+
/** JSON.stringify 后写入,可选 TTL */
|
|
36
|
+
jsonSet(key: string, value: unknown, ttl?: number): Promise<'OK'>;
|
|
37
|
+
/**
|
|
38
|
+
* 缓存读-算-写。命中返回缓存值;miss 执行 fn 并回填。
|
|
39
|
+
* fn 返回 null 时不缓存(防穿透)。
|
|
40
|
+
*/
|
|
41
|
+
cache<T>(key: string, fn: () => Promise<T | null>, ttl: number): Promise<T | null>;
|
|
42
|
+
/** 主动关闭连接 */
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
}
|