draftgo-cli 3.0.1 → 3.0.29
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 +67 -17
- package/package.json +13 -8
- package/resources/skill/SKILL.md +118 -22
- package/resources/skill/core/architecture.md +4 -24
- package/resources/skill/core/modules.md +14 -4
- package/resources/skill/init/SKILL.md +3 -4
- package/resources/skill/practices/anti-patterns.md +14 -4
- package/resources/skill/practices/best-practices.md +25 -6
- package/resources/skill/practices/dev-declaration.md +23 -3
- package/resources/skill/pull/SKILL.md +9 -1
- package/resources/skill/push/SKILL.md +103 -68
- package/resources/skill/quickref/api-endpoints.md +63 -41
- package/resources/skill/quickref/api.json +5084 -4975
- package/resources/skill/quickref/app-api.md +4 -14
- package/resources/skill/rules/dev-workflow.md +154 -57
- package/resources/skill/rules/frontend.md +569 -21
- package/resources/skill/rules/parallel.md +10 -10
- package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
- package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
- package/resources/skill/scripts/draftgo_delete.py +0 -2
- package/resources/skill/scripts/draftgo_init.py +15 -3
- package/resources/skill/scripts/draftgo_pull.py +154 -87
- package/resources/skill/scripts/draftgo_push.py +363 -174
- package/resources/skill/specs/custom-services.md +199 -0
- package/resources/skill/specs/data.md +195 -5
- package/resources/skill/specs/db-relations.md +227 -0
- package/resources/skill/specs/runtime.md +30 -0
- package/resources/skill/specs/security.md +3 -3
- package/resources/skill/specs/ui-protocol.md +79 -48
- package/resources/skill/story/SKILL.md +2 -7
- package/src/cli.js +9 -0
- package/src/commands/api.js +59 -0
- package/src/commands/autoPush.js +41 -0
- package/src/commands/check.js +27 -17
- package/src/commands/delete.js +6 -4
- package/src/commands/deploy.js +31 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/help.js +27 -9
- package/src/commands/init.js +17 -2
- package/src/commands/map.js +18 -7
- package/src/commands/new.js +20 -17
- package/src/commands/sync.js +10 -3
- package/src/commands/update.js +15 -56
- package/src/commands/upgrade.js +52 -0
- package/src/commands/verifyUi.js +199 -0
- package/src/index.js +12 -1
- package/src/localdev/compose.js +8 -1
- package/src/platforms.js +3 -3
- package/src/projectConfig.js +11 -1
- package/src/projectMap.js +274 -39
- package/src/skill.js +113 -29
- package/src/updateCheck.js +37 -5
- package/resources/skill/quickref/dg-components.md +0 -198
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
read_when: 编写、修改、调试或评审自定义服务时;使用服务 SDK、路由、事件、定时任务或服务依赖时
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Go 自定义服务契约
|
|
6
|
+
|
|
7
|
+
DraftGo 的新自定义服务使用 Go。服务代码是完整的 `package main`,可以使用标准库和 `go.mod` 中声明的第三方库。平台在保存或发布时编译服务、运行 `Register` 并保存触发器清单;执行时在独立子进程中调用选定 handler。
|
|
8
|
+
|
|
9
|
+
平台 SDK 的固定导入路径是 `draftgo/sdk`。它是 DraftGo 构建器注入的本地 module,不从 GitHub 或其他网络仓库下载,也不要在服务的 `go.mod` 中自行添加或 `replace` 此依赖。
|
|
10
|
+
|
|
11
|
+
## 最小服务
|
|
12
|
+
|
|
13
|
+
```go
|
|
14
|
+
package main
|
|
15
|
+
|
|
16
|
+
import "draftgo/sdk"
|
|
17
|
+
|
|
18
|
+
func Register(app *sdk.App) {
|
|
19
|
+
app.Route("GET", "/health", health)
|
|
20
|
+
app.On("order.paid", afterPaid)
|
|
21
|
+
app.Schedule("0 9 * * 1-5", weekdayReport)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func health(ctx *sdk.Context) (any, error) {
|
|
25
|
+
return ctx.Respond(map[string]any{"ok": true}, 200, nil), nil
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func afterPaid(ctx *sdk.Context) (any, error) {
|
|
29
|
+
ctx.Log.Info("order paid event received")
|
|
30
|
+
return nil, nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func weekdayReport(ctx *sdk.Context) (any, error) { return nil, nil }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`Register` 必须没有业务副作用。它只注册 handler;网络请求、写数据库、发通知等操作放在 handler 内。
|
|
37
|
+
|
|
38
|
+
## 本地资源
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
.draftgo/custom_scripts/
|
|
42
|
+
├── index.json
|
|
43
|
+
└── script_<id>_<slug>.go
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`index.json` 的 Go 服务字段:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"name": "order-service",
|
|
51
|
+
"slug": "order-service",
|
|
52
|
+
"mode": "mixed",
|
|
53
|
+
"code_file": ".draftgo/custom_scripts/script_new_order-service.go",
|
|
54
|
+
"go_mod": "module example.com/order-service\n\ngo 1.26.0\n\nrequire github.com/google/uuid v1.6.0\n",
|
|
55
|
+
"go_sum": ""
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- 新服务使用 `mode=mixed`,允许同一个 `Register` 同时注册 route、event 和 scheduled。旧服务可继续使用单一 `route`、`event` 或 `scheduled` mode。
|
|
60
|
+
- `go_mod` 和可选的 `go_sum` 随服务版本保存并由 `draftgo push custom_scripts` 推送。
|
|
61
|
+
- 新依赖应锁定明确版本。构建错误会在保存/发布时返回,不会替换当前有效清单。
|
|
62
|
+
|
|
63
|
+
## 触发器
|
|
64
|
+
|
|
65
|
+
### Route
|
|
66
|
+
|
|
67
|
+
```go
|
|
68
|
+
func Register(app *sdk.App) {
|
|
69
|
+
app.Route("POST", "/orders", createOrder)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func createOrder(ctx *sdk.Context) (any, error) {
|
|
73
|
+
body, _ := ctx.Input["body"].(map[string]any)
|
|
74
|
+
return ctx.Respond(body, 201, nil), nil
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`slug=commerce` 时地址为 `POST /api/x/commerce/orders`。Route 精确匹配,不支持 `/orders/{id}` 模板;ID 使用 query 或 body。
|
|
79
|
+
|
|
80
|
+
`ctx.Input` 的 Route 字段:`method`、`headers`、`body`、`query_params`、`path_params`。当前身份通过 `ctx.Auth.CurrentUser()` 获取,入站请求头通过 `ctx.Headers.Get("Authorization")` 等读取。
|
|
81
|
+
|
|
82
|
+
Route 默认以请求调用者身份访问 `draftgo.DB`、`draftgo.Users` 和其他平台能力。服务由管理员创建、拥有 `scripts:*` 管理权限,或在请求中收到 SAT,都不会让普通 SDK 调用自动提升;`draftgo.Auth.RequireAdmin()` 也只检查当前调用者。
|
|
83
|
+
|
|
84
|
+
可信服务需要管理员权限时,逐次显式使用 `draftgo.Admin.*`。这不是服务配置项,也不需要 `admin_access` 开关:调用 `Admin` 就是管理员调用声明。运行时为**这一次**平台 SDK 调用注入管理员身份,并把服务、版本、真实调用者、操作、资源和结果写进该次执行的审计日志;SAT、数据库连接和管理员凭据不会暴露给服务代码。
|
|
85
|
+
|
|
86
|
+
```go
|
|
87
|
+
func catalog(draftgo *sdk.Context) (any, error) {
|
|
88
|
+
// 继承调用者权限
|
|
89
|
+
owned, err := draftgo.DB.Query("order", sdk.QueryOptions{})
|
|
90
|
+
if err != nil { return nil, err }
|
|
91
|
+
|
|
92
|
+
// 显式管理员权限;仅此调用提升
|
|
93
|
+
internal, err := draftgo.Admin.DB.Query("internal_catalog", sdk.QueryOptions{})
|
|
94
|
+
if err != nil { return nil, err }
|
|
95
|
+
|
|
96
|
+
return draftgo.Respond(map[string]any{
|
|
97
|
+
"orders": owned.Items,
|
|
98
|
+
"catalog": internal.Items, // 生产代码应再按客户端可见字段组装
|
|
99
|
+
}, 200, nil)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`draftgo.Admin` 提供与普通 SDK 对齐的 `DB`、`Users`、`Auth`、`Notify`、`HTTP`、`Cache`、`Config`、`AIHub` 能力。它等价于管理员在平台拥有的权限,不做资源级白名单;因此只能授予可信服务编辑者,并且 Route 返回值仍必须由代码负责脱敏。
|
|
104
|
+
|
|
105
|
+
### Event
|
|
106
|
+
|
|
107
|
+
```go
|
|
108
|
+
app.On("user.registered", welcome)
|
|
109
|
+
|
|
110
|
+
func welcome(ctx *sdk.Context) (any, error) {
|
|
111
|
+
payload, _ := ctx.Input["payload"].(map[string]any)
|
|
112
|
+
ctx.Log.Info("registered user: " + fmt.Sprint(payload["user_id"]))
|
|
113
|
+
return nil, nil
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
事件异步且不阻塞原请求。事件输入含 `event`、`timestamp`、`payload`。
|
|
118
|
+
|
|
119
|
+
事件 `payload` 含 `user_id` 或 `actor_user_id` 且该用户仍存在时,handler 继承该用户身份;无法解析用户时才以系统身份执行。事件服务应把事件数据视为业务输入,而不是把它当成绕过资源权限的通道。
|
|
120
|
+
|
|
121
|
+
### Scheduled
|
|
122
|
+
|
|
123
|
+
```go
|
|
124
|
+
app.Schedule("0 2 * * *", cleanup)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
cron 使用五字段表达式,也支持 `interval:5m`。定时 handler 的 `ctx.Input` 为空对象。
|
|
128
|
+
|
|
129
|
+
定时任务没有调用者,会以系统身份执行。因此它只能由可信编辑者维护,写入范围应限制在明确的数据类型,并在 handler 中记录可审计的业务日志。
|
|
130
|
+
|
|
131
|
+
## 平台 SDK
|
|
132
|
+
|
|
133
|
+
所有资源访问经受控 RPC 返回 Go 主服务。不要自行读取数据库连接、服务 token 或宿主机环境变量。
|
|
134
|
+
|
|
135
|
+
```go
|
|
136
|
+
record, err := ctx.DB.Create("order", map[string]any{"title": "DraftGo"})
|
|
137
|
+
records, err := ctx.DB.Query("order", sdk.QueryOptions{
|
|
138
|
+
Filters: map[string]any{"status": "paid"},
|
|
139
|
+
Page: 1, PageSize: 20, OrderBy: "id", Order: "desc",
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
user, err := ctx.Users.Get(12)
|
|
143
|
+
err = ctx.Auth.RequireLogin()
|
|
144
|
+
err = ctx.Notify.Send(12, "完成", "订单已创建", "info")
|
|
145
|
+
|
|
146
|
+
cached, err := ctx.Cache.Get("daily-report")
|
|
147
|
+
err = ctx.Cache.Set("daily-report", map[string]any{"ok": true}, time.Hour)
|
|
148
|
+
|
|
149
|
+
value, err := ctx.Config.Get("feature_flag", false)
|
|
150
|
+
response, err := ctx.HTTP.Get(ctx.Context(), "https://api.example.com/health", nil, 10*time.Second)
|
|
151
|
+
|
|
152
|
+
reply, err := ctx.AIHub.Chat(ctx.Context(), sdk.AIChatRequest{AgentID: 12, Message: "总结订单"})
|
|
153
|
+
ctx.Log.Info("service completed")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`ctx.DB` 支持 `Create`、`CreateMany`、`Get`、`Update`、`UpdateMany`、`Delete`、`Query`。`Query` 返回 `sdk.QueryResult{Items, Total, Page, PageSize}`。
|
|
157
|
+
|
|
158
|
+
`ctx.Users` 支持 `Get`、`List`、`Update`。`ctx.Auth` 支持 `RequireLogin`、`RequireAdmin`、`RequireRole`、`CurrentUser`。
|
|
159
|
+
|
|
160
|
+
`ctx.HTTP` 支持 `Get`、`Post`、`Put`、`Patch`、`Delete`;响应为 `sdk.HTTPResponse{StatusCode, Headers, Data}`。服务可使用 `config.http_allowed_hosts` 限制出站目标;HTTP timeout 最终限制为 1-30 秒,响应体最大 5 MB。
|
|
161
|
+
|
|
162
|
+
`ctx.AIHub.Chat` 与 `ctx.AIHub.GenerateImage` 使用已配置的 Agent。AIHub 权限仍受服务配置与调用身份控制。
|
|
163
|
+
|
|
164
|
+
日志每次执行最多 500 条、单条最多 4096 字符。不要记录 token、Cookie、密码或完整个人信息。
|
|
165
|
+
|
|
166
|
+
## 权限与运行限制
|
|
167
|
+
|
|
168
|
+
- `scripts:read/create/update/delete/execute` 控制可信人员管理服务。
|
|
169
|
+
- Route 调用者仍由服务 `permission` 与 `config.route_security` 控制;管理权限不绕过 Route 调用权限。
|
|
170
|
+
- Route 的普通 SDK 数据访问继承调用者权限;不要把“管理员创建服务”误写成自动提升。只有显式 `draftgo.Admin.*` 调用才以管理员执行并记录审计;定时任务和无可解析用户的事件是系统身份例外。
|
|
171
|
+
- `config.timeout`、`max_concurrency`、`queue_timeout_ms` 适用于服务执行。Route 饱和时返回 HTTP 429。
|
|
172
|
+
- Go 服务以独立进程运行,超时会终止该进程;它不是为不可信多租户代码准备的安全沙箱。只向可信编辑者授予服务编辑权限。
|
|
173
|
+
- 每个保存版本按源码、依赖、SDK 和 Runner 协议生成不可变构建键。代码或依赖变更会生成新构建产物;旧版本可通过现有版本恢复接口重新激活。
|
|
174
|
+
|
|
175
|
+
## 管理 API
|
|
176
|
+
|
|
177
|
+
管理 API 使用 `{code, data, message}` 信封并受 `scripts:*` 权限控制:
|
|
178
|
+
|
|
179
|
+
| 方法 | 路径 |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `POST` / `GET` | `/api/scripts` |
|
|
182
|
+
| `GET` | `/api/scripts/options/roles` |
|
|
183
|
+
| `GET` / `PUT` / `DELETE` | `/api/scripts/{id}` |
|
|
184
|
+
| `GET` | `/api/scripts/{id}/routes` |
|
|
185
|
+
| `POST` | `/api/scripts/{id}/enable`、`/api/scripts/{id}/disable`、`/api/scripts/{id}/execute` |
|
|
186
|
+
| `GET` | `/api/scripts/{id}/versions`、`/api/scripts/{id}/executions` |
|
|
187
|
+
| `POST` | `/api/scripts/{id}/versions/{version_id}/restore` |
|
|
188
|
+
| `GET` | `/api/scripts/{id}/executions/{execution_id}` |
|
|
189
|
+
|
|
190
|
+
运行时 route 为 `ANY /api/x/{slug}/{path}`,不使用管理 API 信封。
|
|
191
|
+
|
|
192
|
+
## 验收清单
|
|
193
|
+
|
|
194
|
+
- [ ] `package main` 且实现 `Register(app *sdk.App)`。
|
|
195
|
+
- [ ] 服务使用 `mode=mixed`,第三方库写入 `go_mod`。
|
|
196
|
+
- [ ] 路由使用 `app.Route`,事件使用 `app.On`,定时任务使用 `app.Schedule`。
|
|
197
|
+
- [ ] handler 返回 `(any, error)`,需要状态码时使用 `ctx.Respond`。
|
|
198
|
+
- [ ] Route 显式设置 `permission` 与 `route_security`。
|
|
199
|
+
- [ ] 推送后请求无副作用 GET Route,必要时运行 `draftgo push custom_scripts --probe-routes`。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
read_when: 操作动态 DB 之前 · 设计数据结构时 · 使用 filters 检索时
|
|
2
|
+
read_when: 操作动态 DB 之前 · 设计数据结构时 · 使用 filters 检索时 · 定义关联关系时
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# 动态 DB & 数据层
|
|
@@ -16,6 +16,7 @@ read_when: 操作动态 DB 之前 · 设计数据结构时 · 使用 filters 检
|
|
|
16
16
|
"name": { "type": "string", "title": "姓名", "required": true, "searchable": "fuzzy" },
|
|
17
17
|
"status": { "type": "string", "title": "状态", "required": false, "searchable": "exact" },
|
|
18
18
|
"amount": { "type": "number", "title": "金额", "required": false, "searchable": "range" },
|
|
19
|
+
"paid_at": { "type": "datetime", "title": "支付时间", "required": false, "searchable": "range" },
|
|
19
20
|
"tags": { "type": "array", "title": "标签", "required": false, "searchable": "contains" },
|
|
20
21
|
"note": { "type": "string", "title": "备注", "required": false, "searchable": false }
|
|
21
22
|
}
|
|
@@ -28,7 +29,180 @@ read_when: 操作动态 DB 之前 · 设计数据结构时 · 使用 filters 检
|
|
|
28
29
|
}
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
**searchable 模式**:`false`(不可检索)/ `"exact"`(精确)/ `"fuzzy"`(模糊)/ `"range"
|
|
32
|
+
**searchable 模式**:`false`(不可检索)/ `"exact"`(精确)/ `"fuzzy"`(模糊)/ `"range"`(数值/时间范围)/ `"contains"`(数组包含)
|
|
33
|
+
|
|
34
|
+
`date` 字段保存为 `YYYY-MM-DD`;`datetime` 字段保存为 ISO 8601 字符串,带时区的输入会规范化为 UTC。`searchable: true` 对 `number` / `date` / `datetime` 会自动推断为 `range`。
|
|
35
|
+
|
|
36
|
+
**系统字段**:以下系统字段**无需在 schema 中定义**,可直接用于检索和排序:
|
|
37
|
+
|
|
38
|
+
| 字段 | 类型 | 检索模式 | 说明 |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| `id` | number | range | 记录 ID,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
|
|
41
|
+
| `userid` | number | range | 所属用户 ID,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
|
|
42
|
+
| `created_at` | datetime | range | 创建时间,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
|
|
43
|
+
| `updated_at` | datetime | range | 更新时间,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
|
|
44
|
+
|
|
45
|
+
⚠️ **注意**:虽然 DB 表有 `status` 系统列(1=正常 0=禁用 -1=删除),但由于业务常用此字段名,**需在 schema 中显式声明 `status` 的 searchable 才可检索**。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 自定义服务内的 ctx.DB.Query
|
|
50
|
+
|
|
51
|
+
Go 自定义服务使用 `ctx.DB.Query(type, sdk.QueryOptions{...})` 操作动态 DB,返回 `sdk.QueryResult`:
|
|
52
|
+
|
|
53
|
+
```go
|
|
54
|
+
result, err := ctx.DB.Query("order", sdk.QueryOptions{
|
|
55
|
+
Filters: map[string]any{"status": "paid"},
|
|
56
|
+
Page: 1, PageSize: 20, OrderBy: "id", Order: "desc",
|
|
57
|
+
})
|
|
58
|
+
items := result.Items
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| 参数 | 说明 |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `filters` | 字典形式结构化过滤;默认 `{field: value}` 是 `eq` 精确匹配 |
|
|
64
|
+
| `Page` / `PageSize` | 分页参数;零值交由平台使用默认列表行为 |
|
|
65
|
+
| `order_by` / `order` | 按 searchable 字段排序,`order` 为 `asc` / `desc` |
|
|
66
|
+
|
|
67
|
+
⚠️ 返回结构为 `sdk.QueryResult{Items, Total, Page, PageSize}`。需要完整数据时必须按 `Total` 分页读取,不能用超大 `PageSize` 假装全量。
|
|
68
|
+
|
|
69
|
+
普通用户调用时只读 `status=1` 数据;系统身份/管理员脚本可读全部状态数据,但仍受 db_meta permission 约束。不要把“拿不到禁用数据”和分页截断混在一起排查。
|
|
70
|
+
|
|
71
|
+
`filters` 操作符示例:
|
|
72
|
+
|
|
73
|
+
```go
|
|
74
|
+
ctx.DB.Query("order", sdk.QueryOptions{Filters: map[string]any{
|
|
75
|
+
"status": "paid",
|
|
76
|
+
"customer_name": map[string]any{"op": "like", "value": "张"},
|
|
77
|
+
"amount": map[string]any{"op": "gte", "value": 100},
|
|
78
|
+
"id": map[string]any{"op": "in", "value": []int{1, 2, 3}},
|
|
79
|
+
}})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
字段必须在 db_meta schema 中标记 `searchable`,系统字段 `id/userid/created_at/updated_at` 可直接检索和排序。`status` 若作为业务字段检索,仍需在 schema 中显式声明。
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 关联关系(ref)
|
|
87
|
+
|
|
88
|
+
**完整示例**:`specs/db-relations.md`(随 skill 分发,不依赖项目根目录文档)
|
|
89
|
+
|
|
90
|
+
### 基本配置
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"type": "order",
|
|
95
|
+
"schema": {
|
|
96
|
+
"properties": {
|
|
97
|
+
"user_id": {
|
|
98
|
+
"type": "number",
|
|
99
|
+
"label": "用户",
|
|
100
|
+
"ref": {
|
|
101
|
+
"type": "user", // 关联的数据类型(必填)
|
|
102
|
+
"relation": "many-to-one", // 关联类型(可选,默认 many-to-one)
|
|
103
|
+
"onDelete": "cascade" // 删除策略(可选,默认 no_action)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 关联类型(relation)
|
|
112
|
+
|
|
113
|
+
| 类型 | 字段类型 | 说明 | 示例 |
|
|
114
|
+
|-----|---------|------|------|
|
|
115
|
+
| `many-to-one` | number | 多个记录指向一个(默认) | 多个订单 → 一个用户 |
|
|
116
|
+
| `one-to-many` | 虚拟字段 | 反向关系,需指定 `inverse_field` | 一个用户 → 多个订单 |
|
|
117
|
+
| `many-to-many` | array | 字段存储 ID 数组 | 一篇文章 ↔ 多个标签 |
|
|
118
|
+
|
|
119
|
+
### 删除策略(onDelete)
|
|
120
|
+
|
|
121
|
+
| 策略 | 行为 | 适用场景 |
|
|
122
|
+
|-----|------|----------|
|
|
123
|
+
| `cascade` | 级联删除所有引用记录 | 订单依赖用户 |
|
|
124
|
+
| `set_null` | 将引用字段置为 null | 删除分类后商品仍保留 |
|
|
125
|
+
| `set_default` | 设为 `ref.default` 指定的值 | 删除自定义分类后归入"默认分类" |
|
|
126
|
+
| `restrict` | 有引用时禁止删除,返回 400 | 防止误删有依赖的关键数据 |
|
|
127
|
+
| `detach` | 仅多对多,从数组中移除 ID | 删除标签时从文章中移除 |
|
|
128
|
+
| `no_action` | 不处理(默认) | 兼容旧数据 |
|
|
129
|
+
|
|
130
|
+
**删除链路规则:**
|
|
131
|
+
- `many-to-one` 和 `many-to-many` 是真实持有引用 ID 的字段,会参与 `onDelete` 处理。
|
|
132
|
+
- `one-to-many` 是查询用虚拟反向关系,只用于 `populate`,删除链路会跳过它。
|
|
133
|
+
- 需要删除 A 时自动处理引用 A 的 B,必须在 B 的真实引用字段上配置 `ref.onDelete`。
|
|
134
|
+
|
|
135
|
+
### 关联查询(populate)
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
// 多对一:查询订单,自动填充用户信息
|
|
139
|
+
const res = await App.get(`db/order`, { populate: ['user_id'] });
|
|
140
|
+
// 返回:data.user_id_obj = {id, data: {name, email}}
|
|
141
|
+
|
|
142
|
+
// 多对多:查询文章,自动填充标签列表
|
|
143
|
+
const res = await App.get(`db/article`, { populate: ['tag_ids'] });
|
|
144
|
+
// 返回:data.tag_ids_objs = [{id, data}, {id, data}, ...]
|
|
145
|
+
|
|
146
|
+
// 一对多:查询用户,自动填充订单列表
|
|
147
|
+
const res = await App.get(`db/user/123`, { populate: ['orders'] });
|
|
148
|
+
// 返回:data.orders_objs = [{id, data}, {id, data}, ...]
|
|
149
|
+
|
|
150
|
+
// 多字段同时填充
|
|
151
|
+
const res = await App.get(`db/order`, { populate: ['user_id', 'product_id'] });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 完整示例
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"type": "order",
|
|
159
|
+
"label": "订单",
|
|
160
|
+
"schema": {
|
|
161
|
+
"properties": {
|
|
162
|
+
"user_id": {
|
|
163
|
+
"type": "number",
|
|
164
|
+
"label": "用户",
|
|
165
|
+
"required": true,
|
|
166
|
+
"ref": {
|
|
167
|
+
"type": "user",
|
|
168
|
+
"relation": "many-to-one",
|
|
169
|
+
"onDelete": "cascade"
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"product_ids": {
|
|
173
|
+
"type": "array",
|
|
174
|
+
"label": "商品列表",
|
|
175
|
+
"ref": {
|
|
176
|
+
"type": "product",
|
|
177
|
+
"relation": "many-to-many",
|
|
178
|
+
"onDelete": "detach"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"category_id": {
|
|
182
|
+
"type": "number",
|
|
183
|
+
"label": "分类",
|
|
184
|
+
"ref": {
|
|
185
|
+
"type": "category",
|
|
186
|
+
"relation": "many-to-one",
|
|
187
|
+
"onDelete": "set_default",
|
|
188
|
+
"default": 0
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**行为演示:**
|
|
197
|
+
- 删除用户 → 自动删除其所有订单(cascade)
|
|
198
|
+
- 删除商品 → 从订单的 product_ids 数组中移除(detach)
|
|
199
|
+
- 删除分类 → 订单的 category_id 设为 0(set_default)
|
|
200
|
+
|
|
201
|
+
**⚠️ 注意事项:**
|
|
202
|
+
- 级联删除会递归处理,深层级联可能影响性能
|
|
203
|
+
- 级联操作继承当前用户权限,无权删除子记录会导致整个操作失败
|
|
204
|
+
- 所有级联操作在同一事务中执行,失败会自动回滚
|
|
205
|
+
- 不要在前端用多次 DELETE 手工模拟级联;优先用 DBMeta 的 `ref.onDelete` 让基座统一保证一致性
|
|
32
206
|
|
|
33
207
|
---
|
|
34
208
|
|
|
@@ -43,6 +217,20 @@ const res = await App.get(`db/order`, {
|
|
|
43
217
|
});
|
|
44
218
|
const { items, total } = res.data;
|
|
45
219
|
|
|
220
|
+
// 非后台管理页面读取业务列表时,如果当前用户含 admin 角色,带 scope=mine
|
|
221
|
+
// 这只影响 GET /api/db/{type} 列表,让管理员业务视角只看自己的 owner 数据
|
|
222
|
+
const ownOrders = await App.get(`db/order`, {
|
|
223
|
+
page: 1,
|
|
224
|
+
page_size: 20,
|
|
225
|
+
scope: 'mine',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// 系统字段检索和排序(无需在 schema 中定义)
|
|
229
|
+
const res = await App.get(`db/order`, {
|
|
230
|
+
filters: ['created_at:gte:2024-01-01', 'userid:eq:123'],
|
|
231
|
+
order_by: 'created_at', order: 'desc',
|
|
232
|
+
});
|
|
233
|
+
|
|
46
234
|
// 创建(业务字段必须放在 data 包裹里)
|
|
47
235
|
await App.post(`db/order`, { data: { name: '张三', amount: 200 }, status: 1 });
|
|
48
236
|
|
|
@@ -67,13 +255,15 @@ await App.patch(`db/order/batch`, [
|
|
|
67
255
|
|---|---|---|
|
|
68
256
|
| `eq` | 精确等于 | exact / fuzzy / range |
|
|
69
257
|
| `like` | 模糊包含 | fuzzy |
|
|
70
|
-
| `gte` / `lte` / `gt` / `lt` |
|
|
71
|
-
| `in` | 枚举命中(值逗号分隔:`status:in:paid,pending`) | exact / fuzzy |
|
|
258
|
+
| `gte` / `lte` / `gt` / `lt` | 数值/时间范围 | range |
|
|
259
|
+
| `in` | 枚举命中(值逗号分隔:`status:in:paid,pending`) | exact / fuzzy / range |
|
|
72
260
|
| `contains` | 数组字段包含某值 | contains |
|
|
73
261
|
|
|
74
262
|
- 省略操作符(`filters: ['name:张三']`)默认 `like`
|
|
75
263
|
- 多个 filters 为 AND
|
|
76
264
|
- 字段未标 searchable 或操作符不匹配 → 后端返回 400
|
|
265
|
+
- **系统字段**(`id` / `userid` / `created_at` / `updated_at`)无需在 schema 中声明,可直接使用
|
|
266
|
+
- `scope=mine` 只对拥有 admin 角色的用户在 `GET /api/db/{type}` 列表请求中生效;非后台管理页面若当前用户是管理员,读取动态 DB 业务列表时应带该参数;后台管理页不要带,详情和写操作也不要带
|
|
77
267
|
|
|
78
268
|
---
|
|
79
269
|
|
|
@@ -102,7 +292,7 @@ await App.patch(`db/order/batch`, [
|
|
|
102
292
|
|
|
103
293
|
| 参数 | 说明 |
|
|
104
294
|
|---|---|
|
|
105
|
-
| `page` / `page_size` |
|
|
295
|
+
| `page` / `page_size` | 分页(不传返回全量且无上限;任一传入则分页,缺失项按 `page=1` / `page_size=20` 兜底) |
|
|
106
296
|
| `search` | 全文搜索(动态 DB 不用此参数) |
|
|
107
297
|
| `status` | 状态过滤 |
|
|
108
298
|
| `type` / `tag` | 类型/标签过滤 |
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
read_when: 需要 DB 关联关系完整示例时 · 设计 populate / onDelete 时
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# DB 关联关系与级联操作
|
|
6
|
+
|
|
7
|
+
DraftGo 动态 DB 通过 DBMeta.schema.properties.<field>.ref 定义关联关系,并支持删除一致性处理和 populate 关联查询。
|
|
8
|
+
|
|
9
|
+
## 关联类型
|
|
10
|
+
|
|
11
|
+
### many-to-one
|
|
12
|
+
|
|
13
|
+
多个记录指向一个目标记录,字段真实保存目标记录 ID。
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"type": "order",
|
|
18
|
+
"schema": {
|
|
19
|
+
"properties": {
|
|
20
|
+
"user_id": {
|
|
21
|
+
"type": "number",
|
|
22
|
+
"label": "用户ID",
|
|
23
|
+
"ref": {
|
|
24
|
+
"type": "user",
|
|
25
|
+
"relation": "many-to-one",
|
|
26
|
+
"onDelete": "cascade"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
数据示例:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"data": {
|
|
39
|
+
"user_id": 123,
|
|
40
|
+
"amount": 99.99
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### one-to-many
|
|
46
|
+
|
|
47
|
+
一个目标记录反向查询多个引用它的记录。该字段是查询用虚拟字段,本身不保存 ID。
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"type": "user",
|
|
52
|
+
"schema": {
|
|
53
|
+
"properties": {
|
|
54
|
+
"orders": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"label": "订单列表",
|
|
57
|
+
"ref": {
|
|
58
|
+
"type": "order",
|
|
59
|
+
"relation": "one-to-many",
|
|
60
|
+
"inverse_field": "user_id"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
规则:
|
|
69
|
+
|
|
70
|
+
- `inverse_field` 是对方类型中指向当前记录 ID 的字段。
|
|
71
|
+
- one-to-many 只用于 populate,不参与删除链路。
|
|
72
|
+
- 删除一致性要配置在真实持有引用 ID 的 many-to-one 或 many-to-many 字段上。
|
|
73
|
+
|
|
74
|
+
### many-to-many
|
|
75
|
+
|
|
76
|
+
字段保存目标记录 ID 数组。
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"type": "article",
|
|
81
|
+
"schema": {
|
|
82
|
+
"properties": {
|
|
83
|
+
"tag_ids": {
|
|
84
|
+
"type": "array",
|
|
85
|
+
"label": "标签列表",
|
|
86
|
+
"ref": {
|
|
87
|
+
"type": "tag",
|
|
88
|
+
"relation": "many-to-many",
|
|
89
|
+
"onDelete": "detach"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
数据示例:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"data": {
|
|
102
|
+
"title": "Hello World",
|
|
103
|
+
"tag_ids": [1, 2, 3]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## onDelete 策略
|
|
109
|
+
|
|
110
|
+
| 策略 | 行为 | 适用场景 |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `cascade` | 删除所有引用记录 | 子记录依赖父记录 |
|
|
113
|
+
| `set_null` | 将引用字段置为 null | 删除分类后保留商品 |
|
|
114
|
+
| `set_default` | 设为 `ref.default` 指定值 | 删除分类后归入默认分类 |
|
|
115
|
+
| `restrict` | 有引用时禁止删除,返回 400 | 防止误删关键数据 |
|
|
116
|
+
| `detach` | 仅多对多,从数组中移除 ID | 删除标签后从文章标签列表移除 |
|
|
117
|
+
| `no_action` | 不处理,默认值 | 兼容旧数据 |
|
|
118
|
+
|
|
119
|
+
删除链路规则:
|
|
120
|
+
|
|
121
|
+
- many-to-one 和 many-to-many 是真实持有引用 ID 的字段,会参与 onDelete。
|
|
122
|
+
- one-to-many 是虚拟反向关系,只用于 populate,删除链路跳过。
|
|
123
|
+
- 需要删除 A 时自动处理引用 A 的 B,必须在 B 的真实引用字段上配置 `ref.onDelete`。
|
|
124
|
+
|
|
125
|
+
## populate 查询
|
|
126
|
+
|
|
127
|
+
### many-to-one
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
GET /api/db/order?populate=user_id
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
返回时保留原字段,并在 data 中追加 `<field>_obj`:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"id": 1,
|
|
138
|
+
"data": {
|
|
139
|
+
"user_id": 123,
|
|
140
|
+
"amount": 99.99,
|
|
141
|
+
"user_id_obj": {
|
|
142
|
+
"id": 123,
|
|
143
|
+
"data": {
|
|
144
|
+
"name": "张三",
|
|
145
|
+
"email": "zhang@example.com"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### many-to-many
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
GET /api/db/article?populate=tag_ids
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
返回时追加 `<field>_objs`:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"id": 1,
|
|
163
|
+
"data": {
|
|
164
|
+
"title": "Hello World",
|
|
165
|
+
"tag_ids": [1, 2, 3],
|
|
166
|
+
"tag_ids_objs": [
|
|
167
|
+
{ "id": 1, "data": { "name": "技术" } },
|
|
168
|
+
{ "id": 2, "data": { "name": "编程" } }
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### one-to-many
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
GET /api/db/user/123?populate=orders
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
返回时追加 `<field>_objs`:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"id": 123,
|
|
185
|
+
"data": {
|
|
186
|
+
"name": "张三",
|
|
187
|
+
"orders_objs": [
|
|
188
|
+
{ "id": 1, "data": { "amount": 99.99 } },
|
|
189
|
+
{ "id": 2, "data": { "amount": 199.99 } }
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## 字段配置速查
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"field_name": {
|
|
200
|
+
"type": "number",
|
|
201
|
+
"label": "显示名称",
|
|
202
|
+
"required": false,
|
|
203
|
+
"searchable": "range",
|
|
204
|
+
"ref": {
|
|
205
|
+
"type": "user",
|
|
206
|
+
"relation": "many-to-one",
|
|
207
|
+
"onDelete": "cascade",
|
|
208
|
+
"default": 0,
|
|
209
|
+
"inverse_field": "user_id"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
配置规则:
|
|
216
|
+
|
|
217
|
+
- many-to-one:`type: "number"` + `relation: "many-to-one"`。
|
|
218
|
+
- many-to-many:`type: "array"` + `relation: "many-to-many"`。
|
|
219
|
+
- one-to-many:虚拟字段,必须配置 `inverse_field`。
|
|
220
|
+
- `ref.type` 指向动态 DB 类型,例如 `user`、`doctor`、`order`,必须与 DBMeta.type 一致。
|
|
221
|
+
|
|
222
|
+
## 注意事项
|
|
223
|
+
|
|
224
|
+
- populate 只会填充 schema 中声明了 `ref` 的字段;普通的 `doctorid: 1` 不会自动展开。
|
|
225
|
+
- 字段名不要求必须是 `*_id`,但要与 schema 和 populate 参数完全一致,例如字段叫 `doctorid` 就传 `populate=doctorid`。
|
|
226
|
+
- 关联类型自身的 read 权限仍会生效;无权读取的关联记录不会作为可用数据返回。
|
|
227
|
+
- 级联删除在同一事务中执行,失败会回滚。
|