@ailnaf/zoddb 0.0.0-stage → 0.2.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +15 -0
  3. package/README.md +440 -2
  4. package/dist/aggregate.d.ts +42 -0
  5. package/dist/aggregate.d.ts.map +1 -0
  6. package/dist/aggregate.js +77 -0
  7. package/dist/aggregate.js.map +1 -0
  8. package/dist/auth-schema.d.ts +26 -0
  9. package/dist/auth-schema.d.ts.map +1 -0
  10. package/dist/auth-schema.js +139 -0
  11. package/dist/auth-schema.js.map +1 -0
  12. package/dist/better-auth-adapter.d.ts +21 -0
  13. package/dist/better-auth-adapter.d.ts.map +1 -0
  14. package/dist/better-auth-adapter.js +175 -0
  15. package/dist/better-auth-adapter.js.map +1 -0
  16. package/dist/cli.d.ts +3 -0
  17. package/dist/cli.d.ts.map +1 -0
  18. package/dist/cli.js +341 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/config.d.ts +22 -0
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +52 -0
  23. package/dist/config.js.map +1 -0
  24. package/dist/db.d.ts +183 -0
  25. package/dist/db.d.ts.map +1 -0
  26. package/dist/db.js +1063 -0
  27. package/dist/db.js.map +1 -0
  28. package/dist/ddl.d.ts +21 -0
  29. package/dist/ddl.d.ts.map +1 -0
  30. package/dist/ddl.js +127 -0
  31. package/dist/ddl.js.map +1 -0
  32. package/dist/dev.d.ts +9 -0
  33. package/dist/dev.d.ts.map +1 -0
  34. package/dist/dev.js +21 -0
  35. package/dist/dev.js.map +1 -0
  36. package/dist/errors.d.ts +63 -0
  37. package/dist/errors.d.ts.map +1 -0
  38. package/dist/errors.js +122 -0
  39. package/dist/errors.js.map +1 -0
  40. package/dist/filter.d.ts +67 -0
  41. package/dist/filter.d.ts.map +1 -0
  42. package/dist/filter.js +230 -0
  43. package/dist/filter.js.map +1 -0
  44. package/dist/graph.d.ts +54 -0
  45. package/dist/graph.d.ts.map +1 -0
  46. package/dist/graph.js +52 -0
  47. package/dist/graph.js.map +1 -0
  48. package/dist/index.d.ts +8 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +6 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/init.d.ts +12 -0
  53. package/dist/init.d.ts.map +1 -0
  54. package/dist/init.js +62 -0
  55. package/dist/init.js.map +1 -0
  56. package/dist/migrate.d.ts +58 -0
  57. package/dist/migrate.d.ts.map +1 -0
  58. package/dist/migrate.js +447 -0
  59. package/dist/migrate.js.map +1 -0
  60. package/dist/migrations.d.ts +80 -0
  61. package/dist/migrations.d.ts.map +1 -0
  62. package/dist/migrations.js +281 -0
  63. package/dist/migrations.js.map +1 -0
  64. package/dist/relation-graph.d.ts +50 -0
  65. package/dist/relation-graph.d.ts.map +1 -0
  66. package/dist/relation-graph.js +93 -0
  67. package/dist/relation-graph.js.map +1 -0
  68. package/dist/relations.d.ts +128 -0
  69. package/dist/relations.d.ts.map +1 -0
  70. package/dist/relations.js +21 -0
  71. package/dist/relations.js.map +1 -0
  72. package/dist/schema.d.ts +247 -0
  73. package/dist/schema.d.ts.map +1 -0
  74. package/dist/schema.js +584 -0
  75. package/dist/schema.js.map +1 -0
  76. package/dist/studio/page.d.ts +3 -0
  77. package/dist/studio/page.d.ts.map +1 -0
  78. package/dist/studio/page.js +708 -0
  79. package/dist/studio/page.js.map +1 -0
  80. package/dist/studio/server.d.ts +26 -0
  81. package/dist/studio/server.d.ts.map +1 -0
  82. package/dist/studio/server.js +415 -0
  83. package/dist/studio/server.js.map +1 -0
  84. package/dist/table.d.ts +156 -0
  85. package/dist/table.d.ts.map +1 -0
  86. package/dist/table.js +165 -0
  87. package/dist/table.js.map +1 -0
  88. package/dist/where.d.ts +18 -0
  89. package/dist/where.d.ts.map +1 -0
  90. package/dist/where.js +76 -0
  91. package/dist/where.js.map +1 -0
  92. package/docs/api.md +476 -0
  93. package/package.json +70 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - 2026-10-18
4
+
5
+ graph query 2.0:补关系型后台/内容应用的高频查询表达。
6
+
7
+ ### Added
8
+
9
+ - `every` 关系过滤:空关系为 true,存在不满足(含 NULL 谓词)为 false;支持 forward / reverse / M2M,与 `some` / `none` 组合
10
+ - `with.aggregate`:关系级 `count` / `sum` / `max`
11
+ - 空关系 `count=0`、`sum/max=null`
12
+ - `sum` 仅数值列;`max` 保留列类型
13
+ - 支持一对多、反向、M2M through
14
+ - 聚合复用关系 `where`,不受 `limit/offset` 影响
15
+ - 结果挂在 `<relation>Aggregate`
16
+ - 三层 `with` 加载:每层支持 `where` / `orderBy` / `limit` / `offset`;第 4 层在运行时和类型层显式拒绝
17
+ - 查询数回归:20 父行下 `every`=1、aggregate=3、三层 `with`=3,锁定无 N+1
18
+ - Studio / 外部 dogfood 覆盖新查询能力
19
+
20
+ ### Changed
21
+
22
+ - `with` 关系加载上限从文档承诺的 2 层提升到 3 层;关系过滤仍为深度 2
23
+ - 文档明确批量查询数边界和新能力限制
24
+
25
+ ### Known limitations
26
+
27
+ - 仅支持 PostgreSQL
28
+ - 关系过滤最大深度 2
29
+ - 关系聚合仅 `count` / `sum` / `max`;不做 `avg` / `min` / 关系 groupBy / having / 嵌套聚合
30
+ - M2M 行级 `where` / `orderBy` / `limit` 仍不支持
31
+ - forward-only migration;不支持 down migration、shadow DB、零停机在线 DDL
32
+
33
+ [0.2.0]: https://www.npmjs.com/package/@ailnaf/zoddb/v/0.2.0
34
+
35
+ ## [0.1.0] - 2026-10-18
36
+
37
+ 首个可发布预览版。
38
+
39
+ ### Added
40
+
41
+ - zod-first graph 建模:`defineSchema`、`node`、`edge`、`manyToMany`
42
+ - graph 运行时 `zoddb`:类型化 CRUD、关系加载、关系过滤、嵌套写入
43
+ - 多对多 through 写入:`connect` / `set` / `disconnect`
44
+ - `relationGraph(schema)` 纯 JSON 关系图
45
+ - better-auth 子路径:`authSchema` / `zodDbAdapter`
46
+ - 生产迁移 CLI:advisory lock、checksum、历史完整性、baseline、dry-run、JSON status
47
+ - CLI 自动加载项目根 `.env`
48
+ - `zoddb studio`:本地 schema explorer + 数据浏览(表/列/PK/唯一约束/关系;分页/总数/排序/过滤/投影、`with` 关系预览、`some/none`、FK 跳转、M2M through;仅使用公开 graph IR 与 `zoddb` runtime)
49
+ - `zoddb init` 生成 `.mts` scaffold,避免 CommonJS 项目的首次 `dev` 加载失败
50
+ - `zoddb studio --write`:受控写模式(create/update/delete/upsert、嵌套 create、M2M 写入、transaction;默认只读、同源 token、删除确认)
51
+ - Studio 只读迁移状态面板:调用公开 `zoddb status --json` 展示 applied/pending/missing/out-of-order/empty/tampered/drift
52
+ - `verify:pack` 覆盖 tarball 安装后的 Studio schema / rows / migrations 请求
53
+
54
+ ### Changed
55
+
56
+ - npm 包名为 `@ailnaf/zoddb`(npm 拒绝无 scope 的 `zoddb`);CLI 命令仍为 `zoddb`
57
+ - 公开运行时面收口为 **15 个 graph/error exports**
58
+ - `table()`、低层 DDL、`defineDb`、`relations`、`pg`、迁移函数不再从主入口导出
59
+ - 迁移改为 CLI 工作流;低层能力保留在内部模块
60
+
61
+ ### Breaking
62
+
63
+ - 旧的低层 `table()` / DDL / `defineDb` / `relations` 导入会失败
64
+ - 需要低层建表或迁移脚本的用户应改用 `zoddb init/dev/generate/migrate`
65
+
66
+ ### Known limitations
67
+
68
+ - 仅支持 PostgreSQL
69
+ - 关系过滤最大深度 2;不支持 `every`、关系排序/聚合全套
70
+ - forward-only migration;不支持 down migration、shadow DB、零停机在线 DDL
71
+ - better-auth 承诺 core + admin + organization,其他插件需自行验证
72
+
73
+ [0.1.0]: https://www.npmjs.com/package/@ailnaf/zoddb/v/0.1.0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 zoddb contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,441 @@
1
- # Temporary Holding Version
1
+ # zoddb
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Postgres 的 zod-first ORM:**只写 graph 建模,不写 SQL**。表、类型、关系、DDL 和迁移都从同一个 schema 推导;没有 codegen,没有低层表构造器。
4
+
5
+ ```ts
6
+ const adults = await db.users.findMany({
7
+ where: { age: { gte: 18 } },
8
+ orderBy: { createdAt: 'desc' },
9
+ limit: 20,
10
+ })
11
+ // adults: { id: string; email: string; name: string | null; ... }[]
12
+ ```
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ npm i @ailnaf/zoddb postgres zod
18
+ ```
19
+
20
+ CLI 命令仍然是 `zoddb`。也可以从 tarball 安装:
21
+
22
+ ```bash
23
+ npm pack
24
+ npm i ./ailnaf-zoddb-0.2.0.tgz postgres zod
25
+ ```
26
+
27
+ 要求 Node.js **>= 22.18**。
28
+
29
+ ## 快速开始
30
+
31
+ ```bash
32
+ npx zoddb init
33
+
34
+ # init 生成 .env.example;复制为 .env 并填入 DATABASE_URL
35
+ cp .env.example .env
36
+ # 编辑 .env:DATABASE_URL=postgres://postgres:postgres@localhost:5432/zoddb
37
+
38
+ npx zoddb dev --once
39
+ ```
40
+
41
+ `init` 生成 `zoddb.config.mts`、`schema.mts`、`.env.example`;`.mts` 在 CommonJS 和 ESM 项目里都能被 Node 直接加载。已有文件默认不覆盖,`--force` 才覆盖。
42
+
43
+ `dev --once` 会读取 graph schema,生成 additive 迁移并应用;删列/改类型默认拒绝。
44
+
45
+ ## 建模:一个 graph
46
+
47
+ ```ts
48
+ // src/schema.ts
49
+ import { z } from 'zod'
50
+ import { defineSchema, edge, manyToMany, node } from '@ailnaf/zoddb'
51
+
52
+ export const schema = defineSchema({
53
+ nodes: {
54
+ users: node(z.object({
55
+ id: z.uuid(),
56
+ email: z.email(),
57
+ name: z.string().nullable(),
58
+ createdAt: z.date(),
59
+ }), { unique: [['email']] }),
60
+
61
+ posts: node(z.object({
62
+ id: z.uuid(),
63
+ authorId: z.uuid(),
64
+ title: z.string(),
65
+ published: z.boolean().default(false),
66
+ createdAt: z.date(),
67
+ })),
68
+
69
+ tags: node(z.object({
70
+ id: z.uuid(),
71
+ slug: z.string(),
72
+ }), { unique: [['slug']] }),
73
+
74
+ postTags: node(z.object({
75
+ postId: z.uuid(),
76
+ tagId: z.uuid(),
77
+ }), { primaryKey: ['postId', 'tagId'] }),
78
+ },
79
+
80
+ edges: {
81
+ users: {
82
+ posts: edge('posts', { from: 'id', to: 'authorId', kind: 'many' }),
83
+ },
84
+ posts: {
85
+ author: edge('users', { from: 'authorId', to: 'id', onDelete: 'cascade' }),
86
+ tags: manyToMany('tags', { through: 'postTags', from: 'postId', to: 'tagId' }),
87
+ },
88
+ },
89
+ })
90
+ ```
91
+
92
+ 约定:
93
+
94
+ - `users` → 表 `users`,`createdAt` → 列 `created_at`
95
+ - `id: z.uuid()` → 主键 + `gen_random_uuid()`
96
+ - `createdAt` / `updatedAt` → `timestamptz default now()`
97
+ - `z.email()` → text,`z.int()` → int8,`z.date()` → timestamptz,`z.enum()` → text
98
+ - 关系只写在 `edges[源表]`;不要在 node 里写 relations
99
+ - `reverse` 可自动生成反向关系;反向关系只读
100
+ - M2M 必须显式声明 through 表
101
+
102
+ ## 连接
103
+
104
+ ```ts
105
+ import { zoddb } from '@ailnaf/zoddb'
106
+ import { schema } from './schema.js'
107
+
108
+ export const db = zoddb({
109
+ schema,
110
+ url: process.env.DATABASE_URL!,
111
+ })
112
+ ```
113
+
114
+ > 应用进程不会自动读 `.env`;Node 22.18+ 可用 `node --env-file=.env app.mts` 或 `process.loadEnvFile()`。CLI 会自动读取项目根 `.env`。
115
+
116
+ `db` 按表名提供命名空间:
117
+
118
+ ```ts
119
+ db.users
120
+ db.posts
121
+ db.tags
122
+ ```
123
+
124
+ 每个命名空间支持:
125
+
126
+ ```text
127
+ create / insert / insertMany / seed / update / delete / upsert
128
+ count / aggregate / groupBy / findMany / findOne
129
+ ```
130
+
131
+ ## 写入
132
+
133
+ ```ts
134
+ const ada = await db.users.create({
135
+ email: 'ada@acme.dev',
136
+ name: 'Ada',
137
+ })
138
+
139
+ const [updated] = await db.users.update(
140
+ { id: ada.id },
141
+ { name: 'Ada Lovelace' },
142
+ )
143
+
144
+ await db.users.upsert(
145
+ { email: 'ada@acme.dev', name: 'Ada' },
146
+ { conflict: ['email'], update: ['name'] },
147
+ )
148
+
149
+ await db.users.delete({ id: ada.id })
150
+ ```
151
+
152
+ - 写入先过 zod;`id` / `createdAt` / `updatedAt` 自动生成
153
+ - 读出的行默认也校验;失败抛 `RowValidationError`
154
+ - `update` / `delete` 条件不能为空;存在 `updatedAt` 时自动刷新
155
+
156
+ 嵌套 create:
157
+
158
+ ```ts
159
+ const grace = await db.users.create({
160
+ email: 'grace@acme.dev',
161
+ name: 'Grace',
162
+ posts: {
163
+ create: [
164
+ { title: 'Type-safe SQL', published: true },
165
+ { title: 'Draft' },
166
+ ],
167
+ },
168
+ })
169
+ // grace.posts: Post[]
170
+ ```
171
+
172
+ 父行和子行同一事务;many 支持 `create` / `connect`,one 只支持 `connect`。
173
+
174
+ ## 查询
175
+
176
+ ```ts
177
+ const rows = await db.users.findMany({
178
+ where: {
179
+ email: { contains: '@acme', mode: 'insensitive' },
180
+ name: { isNull: false },
181
+ },
182
+ orderBy: [{ createdAt: 'desc' }, { id: 'asc' }],
183
+ limit: 20,
184
+ offset: 0,
185
+ })
186
+
187
+ const one = await db.users.findOne({
188
+ where: { email: 'ada@acme.dev' },
189
+ })
190
+ ```
191
+
192
+ 支持 `eq` / `ne` / `lt` / `lte` / `gt` / `gte` / `in` / `notIn` / `contains` / `startsWith` / `endsWith` / `isNull`,以及 `AND` / `OR` / `NOT`。
193
+
194
+ `select` 只取需要的列;`cursor` 做 keyset 分页,必须覆盖 `orderBy`。
195
+
196
+ ## 关系读取
197
+
198
+ ```ts
199
+ const users = await db.users.findMany({
200
+ orderBy: { createdAt: 'desc' },
201
+ with: {
202
+ posts: {
203
+ where: { published: true },
204
+ orderBy: { createdAt: 'desc' },
205
+ limit: 2,
206
+ with: { author: true },
207
+ },
208
+ },
209
+ })
210
+ ```
211
+
212
+ 关系聚合:
213
+
214
+ ```ts
215
+ const users = await db.users.findMany({
216
+ with: {
217
+ posts: {
218
+ aggregate: {
219
+ count: true,
220
+ sum: { views: true },
221
+ max: { createdAt: true },
222
+ },
223
+ },
224
+ },
225
+ })
226
+
227
+ users[0].postsAggregate
228
+ // { count: number, sum: { views: number | null }, max: { createdAt: Date | null } }
229
+ ```
230
+
231
+ - `count` 空关系为 0;`sum` / `max` 空关系为 null
232
+ - 普通关系聚合复用 `where`,但不受 `limit/offset` 影响;M2M 行级 `where` 仍不支持
233
+
234
+ - 每个父行独立分页(窗口函数),不是全局 LIMIT
235
+ - 1 条主查询 + 每条关系 1 条批量查询,不做 N+1
236
+ - 关系过滤内联为主查询的 `EXISTS`;每个 aggregate 关系额外 1 条聚合查询;三层 `with` 每层 1 条
237
+ - 支持最多 3 层 `with`;每层 `where/orderBy/limit` 独立生效,第 4 层运行时和类型层都拒绝
238
+ - 关系名和返回类型从 graph 推导;拼错会在编译期报错
239
+
240
+ 关系过滤:
241
+
242
+ ```ts
243
+ const authors = await db.users.findMany({
244
+ where: {
245
+ posts: {
246
+ some: {
247
+ published: true,
248
+ tags: { some: { slug: 'orm' } },
249
+ },
250
+ },
251
+ },
252
+ })
253
+
254
+ const quiet = await db.users.findMany({
255
+ where: { posts: { none: { published: true } } },
256
+ })
257
+
258
+ const allPublished = await db.users.findMany({
259
+ where: { posts: { every: { published: true } } },
260
+ })
261
+ ```
262
+
263
+ 编译为 `EXISTS` / `NOT EXISTS`,内联进主查询,不按父行查询;`every` 空关系为 `true`,存在一个不满足为 `false`;支持 forward / reverse / M2M through,最大深度 2。
264
+
265
+ ## 多对多
266
+
267
+ ```ts
268
+ const post = await db.posts.create({
269
+ authorId: ada.id,
270
+ title: 'Why zoddb',
271
+ tags: {
272
+ connect: [{ id: orm.id }, { id: pg.id }],
273
+ },
274
+ })
275
+
276
+ await db.posts.update(
277
+ { id: post.id },
278
+ { tags: { set: [{ id: pg.id }] } },
279
+ )
280
+
281
+ await db.posts.update(
282
+ { id: post.id },
283
+ { tags: { disconnect: [{ id: pg.id }] } },
284
+ )
285
+ ```
286
+
287
+ - create 只支持 `connect`
288
+ - update 支持 `set` / `connect` / `disconnect`
289
+ - 只管理 through link,不创建 target 行
290
+ - update 的 where 必须命中恰好一个父行
291
+
292
+ ## 事务
293
+
294
+ ```ts
295
+ await db.transaction(async tx => {
296
+ const user = await tx.users.create({
297
+ email: 'tx@acme.dev',
298
+ name: 'TX',
299
+ })
300
+
301
+ await tx.posts.create({
302
+ authorId: user.id,
303
+ title: 'created in tx',
304
+ })
305
+ })
306
+ ```
307
+
308
+ 回调抛错自动回滚;`tx` 有同样的 graph 命名空间。
309
+
310
+ ## Studio(本地只读 / 可选写模式)
311
+
312
+ ```bash
313
+ npx zoddb studio
314
+ ```
315
+
316
+ CLI 会读取 `zoddb.config.ts`/`zoddb.config.mts` 的 schema,在 `127.0.0.1` 启动本地 Studio,并输出带 token 的 URL。
317
+
318
+ - 不配置数据库 URL 时:只展示表、列、PK、唯一约束和关系
319
+ - 配置 `DATABASE_URL` / `--url` 时:增加只读数据浏览(分页、总数、排序、操作符过滤、`with` 关系预览、`some/none` 关系过滤、FK 跳转、M2M through)
320
+ - 配置迁移目录时:侧栏只读展示 `zoddb status --json` 的 applied / pending / missing / out-of-order / empty / tampered / drift 快照
321
+ - 不在 Studio 中执行迁移、DDL 或 SQL 控制台;迁移状态通过公开 CLI 子进程读取
322
+
323
+ Studio 的数据层只使用公开的 `relationGraph(schema)` 和 `zoddb` runtime,因此它也是公开接口的验证器。
324
+
325
+ ### 写模式
326
+
327
+ ```bash
328
+ npx zoddb studio --write
329
+ ```
330
+
331
+ - 默认只读;只有 `--write` 才显示新建/编辑/删除/事务入口
332
+ - 写请求走 `POST /api/write`,要求本地 token 和同源请求
333
+ - 支持 create / update / delete / upsert / 嵌套 create / M2M 写入 / transaction
334
+ - 删除需要二次确认;不提供 SQL、DDL 或迁移 apply
335
+ - `npm run verify:pack` 会从 tarball 安装后启动 Studio,验证 schema / rows / migrations 三类请求
336
+
337
+
338
+ ## 迁移
339
+
340
+ ```bash
341
+ zoddb generate --schema ./src/schema.ts --dir ./migrations --name init
342
+ zoddb migrate --dir ./migrations
343
+ zoddb status --schema ./src/schema.ts --dir ./migrations
344
+ ```
345
+
346
+ - `generate` 只写文件,不执行;破坏性变更默认拒绝,`--allow-destructive` 显式放行
347
+ - `migrate` 用 advisory lock 串行化,每个文件一个事务
348
+ - 历史记录 `_zoddb_migrations`;缺失、乱序、空文件、checksum 变化默认拒绝
349
+ - 破坏性变更默认拒绝
350
+
351
+ CI / 存量库:
352
+
353
+ ```bash
354
+ zoddb migrate --dir ./migrations --dry-run
355
+ zoddb status --schema ./src/schema.ts --dir ./migrations --json
356
+ zoddb baseline --schema ./src/schema.ts --dir ./migrations --through <file>
357
+ ```
358
+
359
+ - `--dry-run` 只读,不写历史、不建业务表
360
+ - `--json` 输出稳定字段;待应用/异常 exit 1
361
+ - `baseline` 只标记 through 及之前的迁移,不执行文件 SQL;默认要求 schema 与库一致
362
+
363
+ ## 关系图 IR
364
+
365
+ ```ts
366
+ import { relationGraph } from '@ailnaf/zoddb'
367
+
368
+ const graph = relationGraph(schema)
369
+ // { nodes, edges };包含反向关系和 M2M through
370
+ ```
371
+
372
+ 不连接数据库、不执行 SQL,适合可视化与审计。
373
+
374
+ ## better-auth
375
+
376
+ ```ts
377
+ import { betterAuth } from 'better-auth'
378
+ import { authSchema, zodDbAdapter } from '@ailnaf/zoddb/better-auth'
379
+ import { defineSchema, zoddb } from '@ailnaf/zoddb'
380
+
381
+ const auth = authSchema({ plugins: [] })
382
+ const schema = defineSchema({
383
+ nodes: auth.nodes,
384
+ edges: auth.edges,
385
+ })
386
+ const db = zoddb({ schema, url: process.env.DATABASE_URL! })
387
+
388
+ const handler = betterAuth({
389
+ database: zodDbAdapter(db, { schema }),
390
+ emailAndPassword: { enabled: true },
391
+ })
392
+ ```
393
+
394
+ - `authSchema` 生成 core 四表;插件表由 plugins 推导
395
+ - `auth.nodes` / `auth.edges` 可直接展开
396
+ - adapter 按 schema 模型名自动映射
397
+
398
+ ## 错误
399
+
400
+ | 错误 | 场景 |
401
+ |---|---|
402
+ | `ZodError` | 写入值不符合 zod |
403
+ | `RowValidationError` | 读出的行校验失败 |
404
+ | `UniqueViolationError` | 23505,带 table / columns |
405
+ | `NotNullViolationError` / `ForeignKeyViolationError` | 23502 / 23503 |
406
+ | `NoRowsError` | `.one()` 无结果 |
407
+ | `UndefinedParamError` | SQL 参数是 undefined |
408
+ | `PgError` | 其它 PG 错误 |
409
+
410
+ ```ts
411
+ import { UniqueViolationError } from '@ailnaf/zoddb'
412
+
413
+ try {
414
+ await db.users.create({ email: 'ada@acme.dev', name: 'Ada' })
415
+ } catch (e) {
416
+ if (e instanceof UniqueViolationError) {
417
+ console.log(e.table, e.columns)
418
+ }
419
+ }
420
+ ```
421
+
422
+ ## 边界
423
+
424
+ - 公开运行时只有 graph 建模、`zoddb`、`relationGraph` 和错误类型
425
+ - 低层 `table` / DDL / `defineDb` / `relations` / `pg` 是内部实现,不对外
426
+ - 不支持 query builder、懒加载、任意深度递归、关系排序/聚合全套
427
+ - 不支持 down migration、shadow DB、零停机在线 DDL
428
+ - 仅 Postgres
429
+
430
+ 公开契约见 [`docs/api.md`](./docs/api.md);发布准备见 [`RELEASING.md`](./RELEASING.md)、变更见 [`CHANGELOG.md`](./CHANGELOG.md)。
431
+
432
+ ## 开发
433
+
434
+ ```bash
435
+ npm run check # typecheck + 390 个测试
436
+ npm run build # tsc → dist
437
+ npm run verify:pack # 打包安装 smoke + 类型消费
438
+ npm run verify:dogfood # 独立 app 干净安装 + 全链路 dogfood
439
+ ```
440
+
441
+ 需要本地 PG(默认 `postgres://postgres@127.0.0.1:55432/zoddb_test`,或设 `DATABASE_URL`)。
@@ -0,0 +1,42 @@
1
+ import type postgres from 'postgres';
2
+ import { type WhereFilter } from './filter.js';
3
+ import { type RowOf, type SqlFragment, type Table } from './table.js';
4
+ export type AggKey<T extends Table> = keyof RowOf<T> & string;
5
+ type SumFields<S extends readonly string[]> = S extends readonly [] ? {} : {
6
+ [K in S[number] as `sum_${K}`]: string | null;
7
+ };
8
+ type AvgFields<A extends readonly string[]> = A extends readonly [] ? {} : {
9
+ [K in A[number] as `avg_${K}`]: string | null;
10
+ };
11
+ type MinFields<T extends Table, N extends readonly string[]> = N extends readonly [] ? {} : {
12
+ [K in N[number] as `min_${K}`]: RowOf<T>[Extract<K, keyof RowOf<T>>] | null;
13
+ };
14
+ type MaxFields<T extends Table, X extends readonly string[]> = X extends readonly [] ? {} : {
15
+ [K in X[number] as `max_${K}`]: RowOf<T>[Extract<K, keyof RowOf<T>>] | null;
16
+ };
17
+ /** 聚合结果:分组键 + count + 只包含请求了的聚合字段 */
18
+ export type AggRow<T extends Table, G extends readonly string[] = [], S extends readonly string[] = [], A extends readonly string[] = [], N extends readonly string[] = [], X extends readonly string[] = []> = Pick<RowOf<T>, Extract<G[number], keyof RowOf<T>>> & {
19
+ count: number;
20
+ } & SumFields<S> & AvgFields<A> & MinFields<T, N> & MaxFields<T, X>;
21
+ export interface AggregateOptions<T extends Table> {
22
+ where?: WhereFilter<T>;
23
+ sum?: readonly AggKey<T>[];
24
+ avg?: readonly AggKey<T>[];
25
+ min?: readonly AggKey<T>[];
26
+ max?: readonly AggKey<T>[];
27
+ /** 高级逃生口:如 sql`count(*) > 1` */
28
+ having?: SqlFragment;
29
+ /** 高级逃生口:如 sql`count(*) desc` */
30
+ orderBy?: SqlFragment;
31
+ limit?: number;
32
+ offset?: number;
33
+ }
34
+ export type AggregateRunner = (sql: string, params: readonly unknown[], meta: {
35
+ operation: string;
36
+ table?: string;
37
+ }) => Promise<unknown[]>;
38
+ export declare function aggregateImpl(raw: postgres.Sql, table: Table, opts: AggregateOptions<Table> & {
39
+ groupBy?: readonly string[];
40
+ }, requireGroup: boolean, run?: AggregateRunner): Promise<Array<Record<string, unknown>>>;
41
+ export {};
42
+ //# sourceMappingURL=aggregate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import { compileWhere } from './filter.js';
2
+ import { isSqlFragment } from './table.js';
3
+ const quoteIdent = (s) => `"${s.replace(/"/g, '""')}"`;
4
+ export async function aggregateImpl(raw, table, opts, requireGroup, run) {
5
+ const sum = opts.sum ?? [];
6
+ const avg = opts.avg ?? [];
7
+ const min = opts.min ?? [];
8
+ const max = opts.max ?? [];
9
+ const groupBy = opts.groupBy ?? [];
10
+ if (requireGroup && groupBy.length === 0)
11
+ throw new Error('groupBy: groupBy 不能为空');
12
+ const planOf = (k) => {
13
+ const p = table.__plan.find(p => p.outKey === k);
14
+ if (!p)
15
+ throw new Error(`aggregate: 未知列 ${table.name}.${k}`);
16
+ return p;
17
+ };
18
+ for (const k of [...groupBy, ...sum, ...avg, ...min, ...max])
19
+ planOf(k);
20
+ const where = opts.where ? compileWhere(table, opts.where) : { text: '', params: [] };
21
+ const params = [...where.params];
22
+ const fragmentSQL = (frag) => {
23
+ if (!frag)
24
+ return '';
25
+ if (!isSqlFragment(frag))
26
+ throw new Error('aggregate: having/orderBy 必须是 sql 片段');
27
+ let out = frag.strings[0] ?? '';
28
+ frag.params.forEach((p, i) => {
29
+ params.push(p);
30
+ out += `$${params.length}${frag.strings[i + 1] ?? ''}`;
31
+ });
32
+ return out;
33
+ };
34
+ const selects = [];
35
+ const groupCols = [];
36
+ for (const k of groupBy) {
37
+ const col = quoteIdent(planOf(k).dbKey);
38
+ groupCols.push(col);
39
+ selects.push(`${col} as ${quoteIdent(k)}`);
40
+ }
41
+ selects.push('count(*)::int as "count"');
42
+ for (const k of sum)
43
+ selects.push(`sum(${quoteIdent(planOf(k).dbKey)})::text as ${quoteIdent(`sum_${k}`)}`);
44
+ for (const k of avg)
45
+ selects.push(`avg(${quoteIdent(planOf(k).dbKey)})::text as ${quoteIdent(`avg_${k}`)}`);
46
+ for (const k of min)
47
+ selects.push(`min(${quoteIdent(planOf(k).dbKey)}) as ${quoteIdent(`min_${k}`)}`);
48
+ for (const k of max)
49
+ selects.push(`max(${quoteIdent(planOf(k).dbKey)}) as ${quoteIdent(`max_${k}`)}`);
50
+ let text = `select ${selects.join(', ')} from ${quoteIdent(table.name)}`;
51
+ if (where.text)
52
+ text += ` where ${where.text}`;
53
+ if (groupCols.length > 0)
54
+ text += ` group by ${groupCols.join(', ')}`;
55
+ const having = fragmentSQL(opts.having);
56
+ if (having)
57
+ text += ` having ${having}`;
58
+ const order = fragmentSQL(opts.orderBy);
59
+ if (order)
60
+ text += ` order by ${order}`;
61
+ if (opts.limit !== undefined) {
62
+ if (!Number.isInteger(opts.limit) || opts.limit < 0)
63
+ throw new Error('limit 必须是非负整数');
64
+ params.push(opts.limit);
65
+ text += ` limit $${params.length}`;
66
+ }
67
+ if (opts.offset !== undefined) {
68
+ if (!Number.isInteger(opts.offset) || opts.offset < 0)
69
+ throw new Error('offset 必须是非负整数');
70
+ params.push(opts.offset);
71
+ text += ` offset $${params.length}`;
72
+ }
73
+ if (run)
74
+ return await run(text, params, { operation: 'aggregate', table: table.name });
75
+ return await raw.unsafe(text, params);
76
+ }
77
+ //# sourceMappingURL=aggregate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.js","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"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"}