Compare commits

29 Commits
Author SHA1 Message Date
what 898cb72422 feat: Controller 增加 Logger() 方法与标准日志字段 key
Controller.Container() 改用 contracts 自己的 Injector 类型别名(跟 di.go
里其它地方一致),不直接暴露 do.Injector。新增 Logger() *slog.Logger 让
控制器可以拿到当前进程的默认 logger 记结构化日志;LogKeyKind/LogKeyComponent
定义了两个标准字段 key(控制器大类 + 具体实例/类型),供各类 handler 统一
用同一套日志字段命名。
2026-08-21 09:45:00 +08:00
what ff3d65db22 chore: condition 依赖锁定到迁移后的真实版本
condition-v2 的 Condition/Engine 迁移到 db-v2 表达式树的改动已经提交
推送,这里同步锁定,不然独立于 go.work 拉取时 condition 的老代码
(还在用 db.Raw)跟已经锁定的新版 db-v2 对不上。
2026-08-21 08:59:55 +08:00
what 6a5a6e5370 重构: GetAnonymous/GetSystemUser 改为指向 req/userx 的别名
实现搬到了 req/userx(详见 req 仓库对应提交),这里不再自己维护一份,
避免和 req 互相依赖导致 go.mod 只能靠指向本地兄弟目录的 replace 绕开
版本解析。

顺带清理 go.mod:condition/db/reflux/req 都不再需要 replace,锁定成各自
的真实版本号。
2026-08-21 08:53:16 +08:00
what 27b8c31a52 docs: 新增 README,说明当前分支的能力范围
配合 v1-legacy 维护分支的拆分,记录 master 这条线(DI 辅助、插件注册、资源
系统对接、各类服务契约)目前有哪些能力,跟 v1-legacy 分支做区分。
2026-08-20 17:21:12 +08:00
what c3618adb02 重构: 接口层同步 ResFlags 重命名,Model.SetAuthDB 改名 SetPermission
HttpController.AuthDB()、OrmController.AuthDB() 类型从 req.ResScope 同步成 req.ResFlags。Model.SetAuthDB 改名 SetPermission(req.ResFlags),跟 req-v2 新的位标志权限体系对齐;OrmController.AuthDB() 暂不改名,范围只限 orm 这块。
2026-07-22 14:04:49 +08:00
what 8b122816a1 重构: 各类型 Controller 接口精简,配合 framework-v2 internal/handler 收拢公共实现
app.go/condflow.go/cron.go/event_stream.go/executor.go/gobridge.go/grpc.go/job.go/mcp.go/mqtt.go/res_virtual_table.go/ws.go/controller.go/support/service.go 精简掉搬到 framework-v2 internal/handler 里的公共默认实现,只保留接口定义。
2026-07-22 09:14:57 +08:00
what e14aee1498 重构: 根目录 resource.go 精简,res_event.go 替换为 res_watcher.go,新增 di.go
resource.go 只保留跟 base 包无关的部分接口定义。res_event.go(旧资源事件接口)删除,替换成 res_watcher.go(ResWatcher 相关接口)。新增 di.go,提供依赖注入相关的公共辅助函数。
2026-07-22 09:14:22 +08:00
what 9446571363 重构: base 包资源核心类型迁移到 req/resx,新增 res_watcher/res_api_param
base/resource.go、resource_hooks.go、resource_test.go、query_field.go 删除,ResField 等具体实现搬到 req/resx(见 res_field.go 里的类型别名)。res_listener.go 替换成 res_watcher.go,对应资源变更监听概念改名。新增 res_api_param.go 及配套测试(ResApi 参数建模,给 MCP tool 的 JSON Schema 生成用)。
2026-07-22 09:13:05 +08:00
what 53169264da feat: 接口层同步 CallWithUser 与 ResScope 重命名
HTTPService.Invoke 新增 caller req.User 参数,指定时内部转发请求认证成该身份,配合 req-v2 新增的 Router.CallWithUser。

HttpController.AuthDB()、Orm.Model.SetAuthDB()、OrmController.AuthDB() 的返回值/参数类型从 req.ResAuthDB 同步改成 req.ResScope,跟 req-v2 那边的重命名保持一致。
2026-07-21 18:02:16 +08:00
what 9ad5d25836 feat: 升级 samber/do v1 → v2 2026-05-15 09:04:12 +08:00
what 7741f7584d refactor: 服务接口 Stop() 重命名为 Shutdown() 2026-05-15 09:03:14 +08:00
what aed3a5ac22 [feat] git.fsdpf.net/go/req 依赖更新 2024-11-26 17:39:27 +08:00
what 7216c11f0c [feat] git.fsdpf.net/go/req 依赖更新 2024-11-26 17:09:07 +08:00
what abd333d9b6 [feat] Model 新增 IsDuplicated() bool 2024-11-25 21:59:06 +08:00
what 8a4206375f [fix] git.fsdpf.net/go/db 依赖更新 2024-11-25 19:51:34 +08:00
what a911a305e1 [feat] 完善 NewVirtualResource 方法 2024-11-25 15:28:19 +08:00
what 6675dca359 [feat] 移除资源 namespace 字段, 新增 isSystem 字段 2024-11-25 15:05:06 +08:00
what 6f5c443c81 [feat] 完善 PkgVersion GetFilePath GetSourceMapPath 2024-11-23 14:28:22 +08:00
what 4869409bf7 [feat] 完善 PkgVersion GetFilePath GetSourceMapPath 2024-11-22 17:54:37 +08:00
what 911852d14e [feat] 完善 PkgVersion GetFile() GetSourceMap() 2024-11-22 16:47:29 +08:00
what fefaa09793 [feat] PkgVersion 2024-11-22 15:43:05 +08:00
what 4e99e723c9 [feat] PkgVersion 2024-11-22 15:39:14 +08:00
what e434c33afb [feat] FileResponse 支持 header 头定义 2024-11-22 14:54:53 +08:00
what beeb0840d5 [fix] NewRawResponse headers 不生效 2024-11-03 00:54:28 +08:00
what dae5b130bb [feat] 优化 support/response, 并新增 NewHttpStatusResponse 2024-10-24 15:34:47 +08:00
what 58511825d3 [feat] 优化 support/response, 并新增 NewHttpStatusResponse 2024-10-24 15:26:42 +08:00
what 90a3c22545 [feat] 移除 DataListOperation 多余的字段 2024-10-09 14:30:44 +08:00
what 895528e82c [fix] 调整 GridLayoutForm Groups 字段类型 2024-09-24 15:35:34 +08:00
what 964c25efe1 [feat] 新增表单分组标识字段 2024-09-24 15:21:18 +08:00
65 changed files with 3727 additions and 1648 deletions
+38 -30
View File
@@ -1,40 +1,48 @@
# git.fsdpf.net/go/contractsv1-legacy
# git.fsdpf.net/go/contractsmaster
老架构下跨服务共享的契约层:HTTP/gRPC/队列/任务调度/定时任务/MQTT/WebSocket 等各类服务的接口
定义,以及资源系统`resource.go`)、用户审批(`user_approval.go`)等业务相关契约,供
`framework`(服务编排/启动)和各业务项目共同依赖,避免互相直接 import 造成循环依赖。
跨服务共享的契约层:各类服务(HTTP/gRPC/队列/任务调度/定时任务/MQTT/WebSocket/条件流引擎等)
的接口定义,以及资源系统相关的类型别名,供 `framework-v2`(服务编排/启动)和各业务项目共同
依赖,避免互相直接 import 造成循环依赖。配合 "-v2" 系列项目使用。
## 这个分支有什么
## 这个仓库有什么
- `resource.go`/`orm.go`:跟老版 `req`/`orm` 配套的资源、ORM 相关契约。
- `http.go`/`grpc.go`/`grpcall.go`/`server.go`HTTP/gRPC 服务契约。
- `job.go`/`cron.go`/`queue.go`:任务调度、定时任务、消息队列契约。
- `mqtt.go`/`ws.go`MQTT、WebSocket 契约。
- `jwt_auth.go`/`captcha.go`/`user_approval.go`:鉴权、验证码、用户审批相关契约。
- `res_event.go`:资源变更事件契约(新架构里拆成了 `res_watcher.go`/`event_stream.go`,接口
形状不一样)。
- `res_type/`:资源类型相关的独立子包(新架构里已经不存在,能力并到别处了)。
- `base/`/`support/`:基础类型和辅助工具。
### DI 辅助(`di.go`
## 这个分支的定位
`samber/do/v2` 的一层薄封装:`Injector`/`Invoke`/`MustInvoke`/`InvokeNamed`/
`MustInvokeNamed`,全仓库统一用这几个函数取 DI 容器里的服务,不直接 import `samber/do/v2`
给还在维护、但还没迁移到新架构(`master` 分支上新增的 `di.go` DI 辅助函数、`app.go`
`condflow.go``gobridge.go``mcp.go``res_virtual_table.go`/`res_watcher.go` 等)的老项目用,
比如 `zg-estate`(直接依赖)。只接受独立于新架构的 bug 修复。
### 插件与注册(`app.go`
## 依赖方怎么安装/更新
`App` 接口:`LoadGoPlugin(file string) error` 加载 Go plugin`RegControllers`/`RegResWatchers`/
`RegGRpcs`/`RegJobs`/`RegCrons`/`RegMqtts`/`RegFlowDecisions`/`RegGoBridges`/
`RegResVirtualTables` 一批 `Reg*` 方法把插件里定义的各类实现注册进框架,对应的 `GetApp*`/
`Get*` 方法按 code 查回来。
Go modules 不会记住某个版本是从哪个分支解析出来的,`go.mod`/`go.sum` 里存的只是一次性解析出
的 commit 伪版本号,分支上有新提交也不会自动同步。
### 资源系统对接
**首次锁定,或者要拿这个分支上新提交的更新**,都执行同一条命令:
- `resource.go``GetResource`/`MustResource``req.GetResource`/`req.MustResource` 的类型
别名,`GetUser` 按 uuid 查用户。
- `res_watcher.go``ResEventType``resx.ResEventType` 别名)+ `ResWatcher`——资源发生变更时
要通知的外部监听方注册信息。
- `res_virtual_table.go``ResVirtualTableSetup``req.ResVirtualTableSetup` 别名)——`framework-v2`
`sqlite_vtable` build tag 下注册进容器,负责给 SQLite 注册虚拟表模块。
- `event_stream.go`:事件流相关契约。
```
go get git.fsdpf.net/go/contracts@v1-legacy
```
### 各类服务契约
**禁止**跑裸的 `go get -u`(或者 `go get git.fsdpf.net/go/contracts@latest`)——这个仓库没有打
语义化 tagGo 对"最新版本"的解析规则是退回到仓库默认分支(`master`)的 HEAD,会把新架构的
破坏性改动一起拉进来,而不是停留在这个分支上。同时注意这条老线依赖的 `git.fsdpf.net/go/db`/
`git.fsdpf.net/go/req`/`git.fsdpf.net/go/orm` 也要一起锁定在它们各自的 `v1-legacy` 分支,
不能新老混用。
`http.go`/`grpc.go`/`grpcall.go`/`server.go`HTTP/gRPC)、`job.go`/`cron.go`/`queue.go`
(任务调度/定时任务/消息队列)、`mqtt.go`/`ws.go`MQTT/WebSocket)、`jwt_auth.go`/`captcha.go`
(鉴权/验证码)、`condflow.go`(条件流引擎对接)、`gobridge.go`(跨语言 bridge)、`mcp.go`
Model Context Protocol 对接)、`executor.go`(通用执行器契约)。
### 子包
- `base/`:基础类型(用户、角色等业务通用结构)。
- `support/`:辅助工具。
## 分支状态
`master` 是持续开发中的新架构,**跟老仓库 `v1-legacy` 分支不完全兼容**——`di.go`/`app.go`/
`condflow.go`/`gobridge.go`/`mcp.go`/`res_virtual_table.go`/`res_watcher.go` 都是新增的,
`res_event.go`/`res_type/` 这些老概念已经拆分/移除。还在用老版契约的老项目,参见 `v1-legacy`
分支。
+10 -3
View File
@@ -1,17 +1,24 @@
package contracts
type Service interface {
type App interface {
LoadGoPlugin(file string) error
RegControllers(items map[string]Controller) error
RegListens(items map[string]ResListener) error
RegResWatchers(items map[string]ResWatcher) error
RegGRpcs(items map[string]GRpc) error
RegJobs(items map[string]Job) error
RegCrons(items map[string]Cron) error
RegMqtts(items map[string]Mqtt) error
GetResListener(code string) (ResListener, bool)
RegFlowDecisions(items map[string]FlowDecision) error
RegGoBridges(items map[string]GoBridge) error
RegResVirtualTables(items map[string]ResVirtualTable) error
GetResWatcher(code string) (ResWatcher, bool)
GetAppController(code string) (Controller, bool)
GetAppGRpc(code string) (GRpc, bool)
GetAppJob(code string) (Job, bool)
GetAppCron(code string) (Cron, bool)
GetAppMqtt(code string) (Mqtt, bool)
GetAppFlowDecision(code string) (FlowDecision, bool)
GetAppGoBridge(code string) (GoBridge, bool)
GetAppResVirtualTable(code string) (ResVirtualTable, bool)
}
+37
View File
@@ -0,0 +1,37 @@
package base
// DataType 定义参数数据类型
type DataType string
const (
DataTypeString DataType = "string" // 字符串类型
DataTypeInteger DataType = "integer" // 整数类型
DataTypeFloat DataType = "float" // 浮点数类型
DataTypeBoolean DataType = "boolean" // 布尔类型
DataTypeJSON DataType = "json" // JSON类型
)
// SchemaParam 工作流参数
type SchemaParam struct {
Name string `db:"name" json:"name"`
Code string `db:"code" json:"code"`
DataType DataType `db:"data_type" json:"data_type"`
Required bool `db:"required" json:"required"`
Comment string `db:"comment" json:"comment"`
Fields SchemaParams `db:"fields" json:"fields,omitempty"`
}
type SchemaParams []SchemaParam
// Condflow 工作流
type Condflow struct {
Uuid string `db:"uuid" json:"uuid"`
Name string `db:"name" json:"name"`
Code string `db:"code" json:"code"`
Comment string `db:"comment" json:"comment"`
Params SchemaParams `db:"params" json:"params"`
CreatedAt string `db:"created_at" json:"created_at"`
UpdatedAt string `db:"updated_at" json:"updated_at"`
}
type GetCondflow func(code string) (Condflow, bool)
+23
View File
@@ -0,0 +1,23 @@
package base
import "git.fsdpf.net/go/reflux/fieldx"
// CondflowCase 工作流条件分支
type CondflowCase struct {
Uuid string `db:"uuid" json:"uuid"`
CondflowUuid string `db:"condflow_uuid" json:"condflow_uuid"`
Name string `db:"name" json:"name"`
Code string `db:"code" json:"code"`
Priority int `db:"priority" json:"priority"`
Action string `db:"action" json:"action"`
ActionParams fieldx.Schema `db:"action_params" json:"action_params"` // 动作参数值, 对应 params_schema
ActionConfig fieldx.Schema `db:"action_config" json:"action_config"` // 动作配置值, 对应 config_schema
NextCaseOnSuccess bool `db:"next_case_on_success" json:"next_case_on_success"`
NextFlowUuid string `db:"next_flow_uuid" json:"next_flow_uuid"`
NextFlowParams fieldx.Schema `db:"next_flow_params" json:"next_flow_params"` // 流程间参数映射
Comment string `db:"comment" json:"comment"`
CreatedAt string `db:"created_at" json:"created_at"`
UpdatedAt string `db:"updated_at" json:"updated_at"`
}
type GetCondflowCases func(uuid string) []CondflowCase
+25
View File
@@ -0,0 +1,25 @@
package base
// CondflowDecisionAction 执行器动作定义
type CondflowDecisionAction struct {
Name string `db:"name" json:"name"`
Code string `db:"code" json:"code"`
ConfigSchema SchemaParams `db:"config_schema" json:"config_schema"` // 配置项Schema定义
ParamsSchema SchemaParams `db:"params_schema" json:"params_schema"` // 参数Schema定义
ResourceUuid string `db:"resource_uuid" json:"resource_uuid"`
Comment string `db:"comment" json:"comment"`
}
// CondflowDecision 执行器注册表
type CondflowDecision struct {
Uuid string `db:"uuid" json:"uuid"`
ResourceUuid string `db:"resource_uuid" json:"resource_uuid"`
Name string `db:"name" json:"name"`
Code string `db:"code" json:"code"`
Comment string `db:"comment" json:"comment"`
Actions []CondflowDecisionAction `db:"actions" json:"actions"`
CreatedAt string `db:"created_at" json:"created_at"`
UpdatedAt string `db:"updated_at" json:"updated_at"`
}
type GetCondflowDecision func(code string) (CondflowDecision, bool)
+1
View File
@@ -51,6 +51,7 @@ type DataList struct {
ThemeConfig map[string]any `db:"themeConfig"` // 列表布局配置
Roles []string `db:"roles"` // 列表权限
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
SelectionType string `db:"selectionType"` // 行选择类型
Platform string `db:"platform"`
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
+1
View File
@@ -25,6 +25,7 @@ type DataListField struct {
FilterByJsSetting map[string]any `db:"filterByJsSetting"` // 字段筛选组件设置参数
FilterOperator string `db:"filterOperator"` // 字段筛选符号
EditableRoles []string `db:"editableRoles"` // 字段编辑权限
Middlewares [][2]any `db:"middlewares"` // 字段数据转换
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
+1 -11
View File
@@ -5,25 +5,15 @@ type DataListOperation struct {
Puuid string `db:"pUuid"`
DataListUuid string `db:"data_list_uuid"` // 所属列表 UUID
Name string `db:"name"` // 按钮名称
Code string `db:"code"` // 按钮标识
Category string `db:"category"` // 按钮类型
Icon string `db:"icon"` // 按钮图标
Size string `db:"size"` // 按钮大小
Block bool `db:"block"` // 固定宽度
Ghost bool `db:"ghost"` // 背景透明
Shape string `db:"shape"` // 按钮形状
Type string `db:"type"` // 按钮样式
IsPopConfirm bool `db:"isPopConfirm"` // 按钮提示
PopTitle string `db:"popTitle"` // 按钮提示
PopOkText string `db:"popOkText"` // 按钮提示
PopCancelText string `db:"popCancelText"` // 按钮提示
PopOkType string `db:"popOkType"` // 按钮提示
PopPlacement string `db:"popPlacement"` // 按钮提示
Widget string `db:"widget"` // 按钮组件
WidgetType string `db:"widgetType"` // 组件类型
WidgetProps map[string]any `db:"widgetProps"` // 组件PROPS
WidgetSetting map[string]any `db:"widgetSetting"` // 组件设置
WidgetContainerSetting map[string]any `db:"widgetContainerSetting"` // 组件容器设置
Confirm map[string]any `db:"confirm"` // 按钮提示
IsRefresh int `db:"isRefresh"` // 0不刷新, 1刷新所有数据, 2刷新一条数据
NoAuthType string `db:"noAuthType"` // 无按钮权限处理方式
Roles []string `db:"roles"` // 按钮权限
+5 -2
View File
@@ -7,12 +7,15 @@ type GridLayout struct {
Name string `db:"name"` // 布局名称
Type int `db:"type"` // 布局类型 [0=普通布局, 1=辅助布局]
PrimaryKey string `db:"primaryKey"` // 主键
MarginX int `db:"marginX"` // 栅格 x 间距
MarginY int `db:"marginY"` // 栅格 y 间
ItemMargin []any `db:"itemMargin"` // 栅格间距
ContainerPadding []any `db:"containerPadding"` // 容器内边
Cols int `db:"cols"` // 网格列基数
RowHeight float32 `db:"rowHeight"` // 栅格行高
Css map[string]any `db:"css"` // 布局CSS
Props []any `db:"props"` // 布局PROPS
Groups []map[string]any `db:"groups"` // 布局分组
Theme string `db:"theme"` // 布局样式
ThemeConfig map[string]any `db:"themeConfig"` // 布局样式配置
Platform string `db:"platform"`
Roles []string `db:"roles"`
UpdatedAt string `db:"updated_at"`
+1
View File
@@ -3,6 +3,7 @@ package base
type GridLayoutField struct {
Uuid string `db:"uuid"`
GridLayoutUuid string `db:"grid_layout_uuid"` // 所属布局 UUID
Group string `db:"group"`
Type string `db:"type"`
X int `db:"x"`
Y int `db:"y"`
+3
View File
@@ -14,6 +14,9 @@ type GridLayoutForm struct {
MarginY int `db:"marginY"` // 栅格 y 间距
Cols int `db:"cols"` // 网格列基数
FormProps []map[string]string `db:"formProps"` // 表单PROPS
Groups []map[string]any `db:"groups"` // 布局分组
Theme string `db:"theme"` // 布局样式
ThemeConfig map[string]any `db:"themeConfig"` // 布局样式配置
ListenChangeFields []string `db:"listenChangeFields"` // 监听字段
ListenChangeFieldsFunc string `db:"listenChangeFieldsFunc"` // 监听字段回调方法
Platform string `db:"platform"`
+1
View File
@@ -3,6 +3,7 @@ package base
type GridLayoutFormField struct {
Uuid string `db:"uuid"`
GridLayoutFormUuid string `db:"grid_layout_form_uuid"` // 所属布局 UUID
Group string `db:"group"`
Type string `db:"type"`
X int `db:"x"`
Y int `db:"y"`
+107
View File
@@ -0,0 +1,107 @@
package base
import (
"fmt"
"path/filepath"
"strings"
"git.fsdpf.net/go/contracts"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/req"
"github.com/samber/do/v2"
)
type PkgVersion struct {
Id int64 `db:"id"`
Name string `db:"pkg"`
Bundle string `db:"bundle"`
Hash string `db:"hash"`
FileName string `db:"filename"`
SourceMapFileName string `db:"sourcemapFileName"`
Imports []string `db:"imports"`
Platform string `db:"platform"`
ReleaseAt string `db:"release_at"`
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
func (this PkgVersion) GetFilePath(prefix ...string) string {
return filepath.Join(
filepath.Join(prefix...),
this.Platform,
strings.Replace(this.FileName, ".", fmt.Sprintf("-%s.", this.Hash), 1),
)
}
func (this PkgVersion) GetSourceMapPath(prefix ...string) string {
return filepath.Join(
filepath.Join(prefix...),
this.Platform,
strings.Replace(this.SourceMapFileName, ".", fmt.Sprintf("-%s.", this.Hash), 1),
)
}
func (this PkgVersion) GetFile(app do.Injector, u req.User) (file string, err error) {
if this.Id == 0 {
return file, contracts.ErrPkgUnknown
}
res, ok := do.MustInvoke[contracts.GetResource](app)("PkgVersion")
if !ok {
return file, contracts.ErrResNotFound
}
ok, err = res.GetDBTable(u).Where(db.C("id").Eq(this.Id)).Select("file").ScanVal(&file)
if !ok {
return file, contracts.ErrResNotFound
}
return file, err
}
func (this PkgVersion) GetSourceMap(app do.Injector) (file string, err error) {
if this.Id == 0 {
return file, contracts.ErrPkgUnknown
}
// res, ok := do.MustInvoke[contracts.GetResource](app)("PkgVersion")
// if !ok {
// return file, contracts.ErrResNotFound
// }
// _, err = res.GetDBTable().Where("id", this.Id).Value(&file, "sourcemap")
return file, err
}
type GetPkgVersion func(pkg string, opts ...PkgOption) (PkgVersion, bool)
// 筛选选项
type PkgOption func(option *PkgOptions)
type PkgOptions struct {
platform string
hash string
}
func (this PkgOptions) Platform() string {
return this.platform
}
func (this PkgOptions) Hash() string {
return this.hash
}
func PkgPlatform(value string) PkgOption {
return func(option *PkgOptions) {
option.platform = value
}
}
func PkgHash(value string) PkgOption {
return func(option *PkgOptions) {
option.hash = value
}
}
-125
View File
@@ -1,125 +0,0 @@
package base
import (
"reflect"
"strings"
"unicode"
"git.fsdpf.net/go/contracts/res_type"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/req"
)
type QueryField struct {
ResField
typ req.RouteParamType `db:"dataType"`
alias string `db:"alias"`
isExpr bool `db:"isExpr"`
isOmitempty bool
ignored bool
}
func (this QueryField) Alias() string {
return this.alias
}
func (this QueryField) Ignored() bool {
return this.ignored
}
func (this QueryField) IsExpr() bool {
return this.isExpr
}
func (this QueryField) IsOmitempty() bool {
return this.isOmitempty
}
func (this QueryField) Type() req.RouteParamType {
return this.typ
}
func (this *QueryField) SetOptions(options byte) req.QueryField {
if options&req.IsExpr != 0 {
this.isExpr = true
}
if options&req.IsOmitempty != 0 {
this.isOmitempty = true
}
if options&req.Ignored != 0 {
this.ignored = true
}
return this
}
func (this QueryField) GetCodeOrAlias() string {
if this.Alias() != "" {
return this.Alias()
}
return this.Code
}
func (this *QueryField) ToSql() db.Expression {
if this.Alias() != "" {
if this.IsExpr() {
if this.GetCode() == "" {
return db.Raw("null as `" + this.Alias() + "`")
}
return db.Raw("(" + this.GetCode() + ") as `" + this.Alias() + "`")
}
return db.Raw("`" + this.GetCodeResource() + "`.`" + this.GetCode() + "` as `" + this.Alias() + "`")
}
return db.Raw("`" + this.GetCodeResource() + "`.`" + this.GetCode() + "`")
}
func (this *QueryField) ToStructField(tags ...string) reflect.StructField {
var typ reflect.Type
fCode := this.GetCodeOrAlias()
// 字段规则
if !unicode.IsLetter(rune(fCode[0])) {
panic("struct field name invalid. " + fCode)
}
fTag := `db:"` + fCode
if this.ignored {
fTag = fTag + `" json:"-"`
} else if this.isOmitempty {
fTag = fTag + `" json:"` + fCode + `,omitempty"`
} else {
fTag = fTag + `" json:"` + fCode + `"`
}
if len(tags) > 0 {
fTag = strings.Join(tags, " ") + " " + fTag
}
switch this.typ {
case req.ReqString:
typ = reflect.TypeOf(string(""))
case req.ReqNumber:
typ = reflect.TypeOf(res_type.ResFieldByNumber(0))
case req.ReqInteger:
typ = reflect.TypeOf(res_type.ResFieldByInteger(0))
case req.ReqFloat:
typ = reflect.TypeOf(res_type.ResFieldByFloat(0))
case req.ReqBool:
typ = reflect.TypeOf(true)
case req.ReqArray:
typ = reflect.TypeOf([]any{})
case req.ReqJson:
typ = reflect.TypeOf(map[string]any{})
}
return reflect.StructField{
Name: strings.ToUpper(fCode[:1]) + fCode[1:],
Tag: reflect.StructTag(fTag),
Type: typ,
}
}
+6 -9
View File
@@ -1,22 +1,17 @@
package base
type ResApiParam struct {
Code string `db:"code"`
Name string `db:"name"`
Category string `db:"category"`
DataType string `db:"type"`
IsRequired bool `db:"isRequired"`
}
type ResApi struct {
Uuid string `db:"uuid"`
PUuid string `db:"pUuid"`
Code string `db:"code"`
UriPrefix string `db:"uri_prefix"`
Name string `db:"name"`
Desc string `db:"desc"`
PrimaryKey string `db:"primaryKey"`
ResourceUuid string `db:"resource_uuid"`
Method string `db:"method"`
Category string `db:"action"`
Action string `db:"action"`
Version string `db:"version"`
Params []ResApiParam `db:"params"`
Roles []string `db:"roles"`
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
@@ -25,3 +20,5 @@ type ResApi struct {
}
type GetResApi func(code string) (ResApi, bool)
type GetResApiChilds func(code string) []ResApi
+423
View File
@@ -0,0 +1,423 @@
package base
import (
"encoding/json"
"sort"
"strconv"
jschema "github.com/google/jsonschema-go/jsonschema"
"github.com/spf13/cast"
)
type ResApiParam struct {
ID int64 `db:"id" json:"id"`
PID int64 `db:"pid" json:"pid"`
Code string `db:"code" json:"code"`
Name string `db:"name" json:"name"`
Category string `db:"category" json:"category"`
DataType string `db:"type" json:"type"`
Desc string `db:"desc" json:"desc"`
IsRequired bool `db:"isRequired" json:"isRequired"`
Constraints map[string]any `db:"constraints" json:"constraints"`
Rank int `db:"rank" json:"rank"`
RefID int64 `db:"ref_id" json:"ref_id"`
DefaultVal string `db:"df_val" json:"df_val"`
}
type ResApiParams []ResApiParam
// ToJSONSchema 将扁平参数列表还原为 JSON Schema 对象
func (params ResApiParams) ToJSONSchema() *jschema.Schema {
return params.buildObject(0)
}
// ResApiParamsFromJSONSchema 将 JSON Schema 对象解析为扁平参数列表
func ResApiParamsFromJSONSchema(schema *jschema.Schema) ResApiParams {
var params ResApiParams
var seq int64
params.parseObject(schema, 0, &seq)
return params
}
func (params ResApiParams) childrenOf(pid int64) ResApiParams {
var result ResApiParams
for _, p := range params {
if p.PID == pid {
result = append(result, p)
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].Rank < result[j].Rank
})
return result
}
func (params ResApiParams) buildObject(pid int64) *jschema.Schema {
s := &jschema.Schema{Type: "object"}
props := map[string]*jschema.Schema{}
for _, p := range params.childrenOf(pid) {
if p.Code == "items" || p.Code == "oneOf" {
continue
}
props[p.Code] = params.buildProperty(p)
if p.IsRequired {
s.Required = append(s.Required, p.Code)
}
}
if len(props) > 0 {
s.Properties = props
}
sort.Strings(s.Required)
return s
}
func (params ResApiParams) findByID(id int64) (ResApiParam, bool) {
for _, p := range params {
if p.ID == id {
return p, true
}
}
return ResApiParam{}, false
}
// pathOf 计算参数在生成的 JSON Schema 中的路径
func (params ResApiParams) pathOf(id int64) string {
p, ok := params.findByID(id)
if !ok {
return ""
}
if p.PID == 0 {
return "#/properties/" + p.Code
}
parent := params.pathOf(p.PID)
switch p.Code {
case "items":
return parent + "/items"
case "oneOf":
return parent + "/items/oneOf/" + strconv.Itoa(p.Rank)
default:
return parent + "/properties/" + p.Code
}
}
func (params ResApiParams) buildProperty(p ResApiParam) *jschema.Schema {
if p.RefID != 0 {
if _, ok := params.findByID(p.RefID); ok {
return &jschema.Schema{Ref: params.pathOf(p.RefID), Description: p.Desc}
}
}
// any 类型:不设 type,从 oneOf 子节点构建
if p.DataType == "any" {
s := &jschema.Schema{Title: p.Name, Description: p.Desc}
if p.DefaultVal != "" {
var tmp any
if err := json.Unmarshal([]byte(p.DefaultVal), &tmp); err == nil {
s.Default = json.RawMessage(p.DefaultVal)
} else {
s.Default, _ = json.Marshal(p.DefaultVal)
}
}
for _, child := range params.childrenOf(p.ID) {
c := child
if child.Code == "oneOf" {
s.OneOf = append(s.OneOf, params.buildProperty(c))
}
}
params.applyConstraints(s, p.Constraints)
return s
}
// 归一化 DataTypejson → object;空字符串根据子节点推断
dataType := p.DataType
switch dataType {
case "json":
dataType = "object"
case "bool":
dataType = "boolean"
case "":
for _, c := range params.childrenOf(p.ID) {
if c.Code == "oneOf" || c.Code == "items" {
dataType = "array"
break
}
}
if dataType == "" && len(params.childrenOf(p.ID)) > 0 {
dataType = "object"
}
}
s := &jschema.Schema{
Type: dataType,
Title: p.Name,
Description: p.Desc,
}
if p.DefaultVal != "" {
var tmp any
if err := json.Unmarshal([]byte(p.DefaultVal), &tmp); err == nil {
s.Default = json.RawMessage(p.DefaultVal)
} else {
s.Default, _ = json.Marshal(p.DefaultVal)
}
}
params.applyConstraints(s, p.Constraints)
switch dataType {
case "object":
nested := params.buildObject(p.ID)
s.Properties = nested.Properties
s.Required = nested.Required
case "array":
var oneOfSchemas []*jschema.Schema
var itemsChild *ResApiParam
for _, child := range params.childrenOf(p.ID) {
c := child
switch child.Code {
case "oneOf":
oneOfSchemas = append(oneOfSchemas, params.buildProperty(c))
case "items":
itemsChild = &c
}
}
if len(oneOfSchemas) > 0 {
s.Items = &jschema.Schema{OneOf: oneOfSchemas}
} else if itemsChild != nil {
s.Items = params.buildItems(*itemsChild)
}
}
return s
}
func (params ResApiParams) buildItems(p ResApiParam) *jschema.Schema {
if ref, ok := p.Constraints["$ref"].(string); ok {
return &jschema.Schema{Ref: ref}
}
if p.RefID != 0 {
if ref, ok := params.findByID(p.RefID); ok {
path := params.pathOf(p.RefID)
if ref.DataType == "array" {
path += "/items"
}
return &jschema.Schema{Ref: path}
}
}
itemType := p.DataType
if itemType == "json" {
itemType = "object"
} else if itemType == "" && len(params.childrenOf(p.ID)) > 0 {
itemType = "object"
}
s := &jschema.Schema{
Type: itemType,
Title: p.Name,
Description: p.Desc,
}
if s.Description == "" && s.Title != "" {
s.Description = s.Title
}
if itemType == "object" {
nested := params.buildObject(p.ID)
s.Properties = nested.Properties
s.Required = nested.Required
}
params.applyConstraints(s, p.Constraints)
return s
}
func (params ResApiParams) applyConstraints(s *jschema.Schema, c map[string]any) {
for k, v := range c {
switch k {
case "$ref":
if ref, ok := v.(string); ok {
s.Ref = ref
}
case "enum":
if arr, ok := v.([]any); ok {
s.Enum = arr
}
case "format":
if f, ok := v.(string); ok {
s.Format = f
}
case "pattern":
if p, ok := v.(string); ok {
s.Pattern = p
}
case "minLength":
s.MinLength = jschema.Ptr(cast.ToInt(v))
case "maxLength":
s.MaxLength = jschema.Ptr(cast.ToInt(v))
case "minimum":
s.Minimum = jschema.Ptr(cast.ToFloat64(v))
case "maximum":
s.Maximum = jschema.Ptr(cast.ToFloat64(v))
case "multipleOf":
s.MultipleOf = jschema.Ptr(cast.ToFloat64(v))
case "minItems":
s.MinItems = jschema.Ptr(cast.ToInt(v))
case "maxItems":
s.MaxItems = jschema.Ptr(cast.ToInt(v))
case "minProperties":
s.MinProperties = jschema.Ptr(cast.ToInt(v))
case "maxProperties":
s.MaxProperties = jschema.Ptr(cast.ToInt(v))
case "additionalProperties":
switch val := v.(type) {
case bool:
if val {
s.AdditionalProperties = &jschema.Schema{}
} else {
s.AdditionalProperties = &jschema.Schema{Not: &jschema.Schema{}}
}
}
}
}
}
func (params *ResApiParams) parseObject(schema *jschema.Schema, pid int64, seq *int64) {
requiredSet := make(map[string]bool, len(schema.Required))
for _, r := range schema.Required {
requiredSet[r] = true
}
rank := 0
for code, prop := range schema.Properties {
*seq++
id := *seq
p := ResApiParam{
ID: id,
PID: pid,
Code: code,
DataType: prop.Type,
Name: prop.Title,
Desc: prop.Description,
IsRequired: requiredSet[code],
DefaultVal: params.rawToStr(prop.Default),
Rank: rank,
Constraints: params.extractConstraints(prop),
}
*params = append(*params, p)
rank++
switch prop.Type {
case "object":
params.parseObject(prop, id, seq)
case "array":
if prop.Items == nil {
break
}
// items 为纯 oneOf schema:建立 oneOf 子行,不建 items 子行
if len(prop.Items.OneOf) > 0 {
for oneOfRank, sub := range prop.Items.OneOf {
*seq++
oneOfID := *seq
itemType := sub.Type
if itemType == "" {
itemType = "object"
}
*params = append(*params, ResApiParam{
ID: oneOfID,
PID: id,
Code: "oneOf",
DataType: itemType,
Name: sub.Title,
Desc: sub.Description,
Rank: oneOfRank,
Constraints: params.extractConstraints(sub),
})
if itemType == "object" {
params.parseObject(sub, oneOfID, seq)
}
}
break
}
*seq++
itemID := *seq
itemType := prop.Items.Type
if itemType == "" {
itemType = "object"
}
*params = append(*params, ResApiParam{
ID: itemID,
PID: id,
Code: "items",
DataType: itemType,
Name: prop.Items.Title,
Desc: prop.Items.Description,
Constraints: params.extractConstraints(prop.Items),
})
if itemType == "object" {
params.parseObject(prop.Items, itemID, seq)
}
}
}
}
func (params ResApiParams) extractConstraints(s *jschema.Schema) map[string]any {
c := map[string]any{}
if s.Ref != "" {
c["$ref"] = s.Ref
}
if len(s.Enum) > 0 {
c["enum"] = s.Enum
}
if s.Format != "" {
c["format"] = s.Format
}
if s.Pattern != "" {
c["pattern"] = s.Pattern
}
if s.MinLength != nil {
c["minLength"] = *s.MinLength
}
if s.MaxLength != nil {
c["maxLength"] = *s.MaxLength
}
if s.Minimum != nil {
c["minimum"] = *s.Minimum
}
if s.Maximum != nil {
c["maximum"] = *s.Maximum
}
if s.MultipleOf != nil {
c["multipleOf"] = *s.MultipleOf
}
if s.MinItems != nil {
c["minItems"] = *s.MinItems
}
if s.MaxItems != nil {
c["maxItems"] = *s.MaxItems
}
if s.MinProperties != nil {
c["minProperties"] = *s.MinProperties
}
if s.MaxProperties != nil {
c["maxProperties"] = *s.MaxProperties
}
if s.AdditionalProperties != nil {
c["additionalProperties"] = s.AdditionalProperties.Not == nil
}
if len(c) == 0 {
return nil
}
return c
}
func (params ResApiParams) rawToStr(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}
var s string
if err := json.Unmarshal(raw, &s); err == nil {
return s
}
return string(raw)
}
+446
View File
@@ -0,0 +1,446 @@
package base_test
import (
"encoding/json"
"reflect"
"testing"
"git.fsdpf.net/go/contracts/base"
jschema "github.com/google/jsonschema-go/jsonschema"
)
// queryParams 查询接口的扁平参数列表,包含 conditions(过滤条件)和 select(查询字段)
var queryParams = base.ResApiParams{
// resource:目标资源
{
ID: -1, PID: 0, Code: "resource", DataType: "string", Name: "资源", Rank: 0,
Desc: "查询目标资源编码(resource_code",
},
{
ID: 1, PID: 0, Code: "conditions", DataType: "object", Name: "过滤条件", Rank: 1,
Desc: `WHERE 条件树。
exprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;
children 为同结构子节点,递归嵌套实现复杂过滤。`,
},
{
ID: 2, PID: 1, Code: "type", DataType: "string", Name: "逻辑运算符", Rank: 0,
Desc: "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
DefaultVal: "and",
Constraints: map[string]any{"enum": []any{"and", "or"}},
},
{
ID: 3, PID: 1, Code: "exprs", DataType: "array", Name: "条件表达式列表", Rank: 1,
Desc: "当前层的叶子比较列表,每项生成一个 column {operator} value 片段,同层多项以 type 连接",
},
{ID: 4, PID: 3, Code: "items", DataType: "object", Rank: 0},
{
ID: 5, PID: 4, Code: "column", DataType: "string", Name: "字段", Rank: 0,
Desc: "字段编码", IsRequired: true,
},
{
ID: 6, PID: 4, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
},
{
ID: 7, PID: 4, Code: "operator", DataType: "string", Name: "运算符", Rank: 2,
Desc: `标准 SQL 比较运算符:
= / != / > / >= / < / <=
LIKE:模糊匹配(%keyword%
IN:包含,value 逗号分隔多值
IS NULL / IS NOT NULL:空值判断,无需 value
REGEXP:正则匹配`,
IsRequired: true,
DefaultVal: "=",
},
{
ID: 8, PID: 4, Code: "value", DataType: "string", Name: "比较值", Rank: 3,
Desc: "比较目标,内容由 valueType 决定。operator 为 IS NULL/IS NOT NULL 时可省略",
},
{
ID: 9, PID: 4, Code: "valueType", DataType: "string", Name: "值类型", Rank: 4,
Desc: `先选类型再填 value
stringvalue 填字面量如 '2024-01-01'
funcvalue 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS
paramvalue 填请求参数路径如 body.status
sqlvalue 填原始SQL如 CURRENT_DATE
默认 string`,
Constraints: map[string]any{"enum": []any{"string", "func", "sql", "param"}},
},
{
ID: 10, PID: 4, Code: "columnSqlFunc", DataType: "string", Name: "列SQL函数", Rank: 5,
Desc: `对列应用 SQL 函数后再参与比较,拼接为 {func}({columnResource}.{column}) {operator} {value}。
支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。
需要第二参数的函数(如 ROUND、CAST)配合 columnSqlFuncParam 使用。`,
},
{
ID: 13, PID: 4, Code: "columnSqlFuncParam", DataType: "string", Name: "SQL函数参数", Rank: 6,
Desc: `columnSqlFunc 的附加参数,按函数类型用途不同:
通用函数(有 param 时):{func}({columnResource}.{column}, {columnSqlFuncParam}) {operator} {value}
示例:ROUND(col, 2) → columnSqlFuncParam="2"
SUBSTRING(col, 1, 10) → columnSqlFuncParam="1, 10"
JSON 函数(param 为 JSON 路径表达式):
json_member_ofvalue 是否是列 JSON 数组的成员):
无 paramJSON_CONTAINS({value}, JSON_ARRAY({column}))
有 paramJSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))
json_contains(列 JSON 是否包含 value):
无 paramJSON_CONTAINS({column}, JSON_ARRAY({value}))
有 paramJSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({value}))`,
},
{
ID: 14, PID: 4, Code: "ignoreEmptyParam", DataType: "boolean", Name: "忽略空参数", Rank: 7,
Desc: "当 valueType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项,默认 false",
},
{
ID: 11, PID: 1, Code: "children", DataType: "array", Name: "子条件", Rank: 2,
Desc: "子条件节点,递归结构",
},
{ID: 12, PID: 11, Code: "items", DataType: "object", RefID: 1, Rank: 0},
// fields 用 oneOf 子行表示
{
ID: 15, PID: 0, Code: "fields", DataType: "array", Name: "查询字段", Rank: 2,
Desc: "查询字段,不传则返回所有字段",
},
{
ID: 55, PID: 15, Code: "oneOf", DataType: "string", Name: "字段", Rank: 0,
Desc: "直接填字段编码,默认归属于顶层 resource。等价于 {column: \"field\", columnResource: resource}",
},
{ID: 16, PID: 15, Code: "oneOf", DataType: "object", Name: "字段引用", Rank: 1},
{ID: 44, PID: 16, Code: "column", DataType: "string", Name: "字段", Rank: 0, IsRequired: true},
{
ID: 46, PID: 16, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
},
{ID: 45, PID: 16, Code: "alias", DataType: "string", Name: "结果别名", Rank: 2},
{ID: 17, PID: 15, Code: "oneOf", DataType: "object", Name: "表达式字段", Rank: 2},
{ID: 18, PID: 17, Code: "expr", DataType: "string", Name: "SQL 表达式", Rank: 0, IsRequired: true,
Desc: "{column} / {columnResource}.{column} / SQL 表达式"},
{ID: 19, PID: 17, Code: "alias", DataType: "string", Name: "结果别名", Rank: 1},
// orderByitems 用 oneOf 表示三种形式,每个变体含 direction
{
ID: 20, PID: 0, Code: "orderBy", DataType: "array", Name: "排序规则", Rank: 3,
Desc: "排序规则列表,多项按顺序依次应用",
},
{ID: 48, PID: 20, Code: "oneOf", DataType: "object", Name: "资源字段排序", Rank: 1},
{ID: 49, PID: 48, Code: "column", DataType: "string", Name: "字段", Rank: 0, IsRequired: true},
{
ID: 50, PID: 48, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
},
{
ID: 53, PID: 48, Code: "direction", RefID: 56,
},
{ID: 51, PID: 20, Code: "oneOf", DataType: "object", Name: "SQL 表达式排序", Rank: 2},
{ID: 52, PID: 51, Code: "expr", DataType: "string", Name: "字段表达式", Rank: 0, IsRequired: true},
{
ID: 54, PID: 51, Code: "direction", RefID: 56,
},
{
ID: 57, PID: 20, Code: "oneOf", DataType: "string", Name: "SQL 表达式 (ASC)", Rank: 0,
Desc: "直接填字段编码,默认升序(asc)",
},
// groupBy:分组字段
{
ID: 24, PID: 0, Code: "groupBy", DataType: "array", Name: "分组规则", Rank: 4,
Desc: "GROUP BY 字段列表,配合 fields 中的聚合字段使用",
},
{
ID: 58, PID: 24, Code: "oneOf", DataType: "string", Name: "分组表达式", Desc: "{column} / {columnResource}.{column} / SQL 表达式", Rank: 0,
},
{
ID: 59, PID: 24, Code: "oneOf", DataType: "object", Name: "资源字段分组", Rank: 0,
},
{ID: 60, PID: 59, Code: "column", DataType: "string", Name: "字段编码", Rank: 0, IsRequired: true},
{
ID: 61, PID: 59, Code: "columnResource", DataType: "string", Name: "所属资源", Rank: 1,
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
},
// limit / offset:分页
{
ID: 26, PID: 0, Code: "limit", DataType: "integer", Name: "每页条数", Rank: 5,
Desc: "返回的最大记录数,默认 30", DefaultVal: "30", Constraints: map[string]any{"maximum": 50},
},
{
ID: 27, PID: 0, Code: "offset", DataType: "integer", Name: "偏移量", Rank: 6,
Desc: "跳过的记录数,默认 0",
},
// relations:资源关联
{
ID: 28, PID: 0, Code: "relations", DataType: "array", Name: "资源关联", Rank: 7,
Desc: "关联配置列表。\ninner / left / rightSQL JOIN,要求关联资源与主资源在同一数据库连接,将关联字段合并到主查询行。\nhasOne / hasMany:独立子查询,支持跨数据库连接,结果聚合为 JSON 字段(hasOne 为单对象,hasMany 为数组)。\n跨库关联必须使用 hasOne 或 hasMany,不可使用 JOIN 类型。",
},
{ID: 29, PID: 28, Code: "items", DataType: "object", Rank: 0},
{
ID: 30, PID: 29, Code: "code", DataType: "string", Name: "关联标识", Rank: 0,
Desc: "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名",
IsRequired: true,
},
{
ID: 38, PID: 29, Code: "name", DataType: "string", Name: "关联名称", Rank: 1,
Desc: "关联的显示名称",
},
{
ID: 39, PID: 29, Code: "type", DataType: "string", Name: "关联类型", Rank: 2,
Desc: `关联模式:
inner / left / rightSQL JOIN,要求与主资源同一数据库连接,将关联字段合并到主查询行
hasOne:独立子查询,支持跨库,结果聚合为单个对象(result[code] = {}
hasMany:独立子查询,支持跨库,结果聚合为对象数组(result[code] = [...]
跨数据库连接的关联必须使用 hasOne 或 hasMany`,
Constraints: map[string]any{"enum": []any{"inner", "left", "right", "hasOne", "hasMany"}},
},
{ID: 40, PID: 29, Code: "actuallyResource", DataType: "string", Name: "实际资源", Rank: 3,
Desc: "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"},
{ID: 41, PID: 29, Code: "actuallyField", DataType: "string", Name: "实际资源字段", Rank: 4,
Desc: "目标资源(code)参与 ON 条件的字段"},
{ID: 42, PID: 29, Code: "relationResource", DataType: "string", Name: "被关联资源", Rank: 5,
Desc: "ON 条件对端的资源编码(通常为主资源或父关联资源)"},
{ID: 43, PID: 29, Code: "relationField", DataType: "string", Name: "被关联字段", Rank: 6,
Desc: "ON 条件对端资源的字段"},
{ID: 31, PID: 29, Code: "conditions", RefID: 1, Rank: 7, Desc: "关联的过滤条件"},
{ID: 33, PID: 29, Code: "orderBy", RefID: 20, Rank: 8, Desc: "关联排序,仅 hasOne/hasMany 有效"},
{ID: 34, PID: 29, Code: "groupBy", RefID: 24, Rank: 9, Desc: "关联分组,仅 hasOne/hasMany 有效"},
{ID: 35, PID: 29, Code: "limit", RefID: 26, Rank: 10, Desc: "关联条数限制,仅 hasOne/hasMany 有效"},
{ID: 36, PID: 29, Code: "offset", RefID: 27, Rank: 11, Desc: "关联偏移量,仅 hasOne/hasMany 有效"},
}
func TestResApiParams_PrintSchema(t *testing.T) {
b, _ := json.MarshalIndent(queryParams.ToJSONSchema(), "", " ")
t.Log("\n" + string(b))
}
func TestResApiParams_ToJSONSchema(t *testing.T) {
schema := queryParams.ToJSONSchema()
if schema.Type != "object" {
t.Fatalf("expected type=object, got %s", schema.Type)
}
cond, ok := schema.Properties["conditions"]
if !ok {
t.Fatal("missing property: conditions")
}
if cond.Type != "object" {
t.Errorf("conditions: expected type=object, got %s", cond.Type)
}
if cond.Description != "WHERE 条件树。\nexprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;\nchildren 为同结构子节点,递归嵌套实现复杂过滤。" {
t.Errorf("conditions: unexpected description: %s", cond.Description)
}
// conditions.type 枚举和默认值
typeProp, ok := cond.Properties["type"]
if !ok {
t.Fatal("missing property: conditions.type")
}
if len(typeProp.Enum) != 2 {
t.Errorf("conditions.type: expected 2 enum values, got %d", len(typeProp.Enum))
}
var defaultVal string
if err := json.Unmarshal(typeProp.Default, &defaultVal); err != nil || defaultVal != "and" {
t.Errorf("conditions.type: expected default=and, got %s", typeProp.Default)
}
// conditions.exprs 是 arrayitems 是 object
exprsProp, ok := cond.Properties["exprs"]
if !ok {
t.Fatal("missing property: conditions.exprs")
}
if exprsProp.Type != "array" {
t.Errorf("conditions.exprs: expected type=array, got %s", exprsProp.Type)
}
if exprsProp.Items == nil {
t.Fatal("conditions.exprs: missing items")
}
if exprsProp.Items.Type != "object" {
t.Errorf("conditions.exprs.items: expected type=object, got %s", exprsProp.Items.Type)
}
// exprs.items required: column, operator
requiredSet := map[string]bool{}
for _, r := range exprsProp.Items.Required {
requiredSet[r] = true
}
if !requiredSet["column"] || !requiredSet["operator"] {
t.Errorf("conditions.exprs.items: expected required=[column, operator], got %v", exprsProp.Items.Required)
}
// children.items $ref
childrenProp, ok := cond.Properties["children"]
if !ok {
t.Fatal("missing property: conditions.children")
}
if childrenProp.Items == nil {
t.Fatal("conditions.children: missing items")
}
if childrenProp.Items.Ref != "#/properties/conditions" {
t.Errorf("conditions.children.items: expected $ref=#/properties/conditions, got %s", childrenProp.Items.Ref)
}
}
func TestResApiParamsFromJSONSchema(t *testing.T) {
original := queryParams.ToJSONSchema()
params := base.ResApiParamsFromJSONSchema(original)
if len(params) == 0 {
t.Fatal("ResApiParamsFromJSONSchema returned empty params")
}
// 转回 schema 后结构应一致
rebuilt := params.ToJSONSchema()
// 将 original 和 rebuilt 都序列化为 map[string]any 后 DeepEqual 比较
toMap := func(s *jschema.Schema) map[string]any {
b, _ := json.Marshal(s)
var m map[string]any
json.Unmarshal(b, &m)
return m
}
originalMap := toMap(original)
rebuiltMap := toMap(rebuilt)
if !reflect.DeepEqual(originalMap, rebuiltMap) {
ob, _ := json.MarshalIndent(originalMap, "", " ")
rb, _ := json.MarshalIndent(rebuiltMap, "", " ")
t.Errorf("schema mismatch:\noriginal: %s\nrebuilt: %s", ob, rb)
}
}
// createParams res-create 接口参数,data 字段为 any 类型(oneOf object | array<object>
var createParams = base.ResApiParams{
{ID: 315, PID: 0, Code: "resource", DataType: "string", Name: "主资源", Rank: 1,
Desc: "目标资源编码,数据将写入该资源对应的表"},
{ID: 316, PID: 0, Code: "data", DataType: "any", Name: "创建数据", Rank: 2,
Desc: "单条记录传对象,批量创建传对象数组"},
// oneOf 分支1:单条记录(object + additionalProperties: true
{ID: 317, PID: 316, Code: "oneOf", DataType: "json", Name: "单条记录", Rank: 1,
Desc: "创建单条记录,key 为字段编码,value 为字段值",
Constraints: map[string]any{"additionalProperties": true}},
// oneOf 分支2:批量记录(arrayitems 为 object + additionalProperties: true
{ID: 318, PID: 316, Code: "oneOf", DataType: "array", Name: "批量记录", Rank: 2,
Desc: "批量创建,每个元素为一条记录对象"},
{ID: 319, PID: 318, Code: "items", DataType: "json", Name: "记录对象", Rank: 1,
Desc: "批量创建,每个元素为一条记录对象",
Constraints: map[string]any{"additionalProperties": true}},
}
func TestResApiParams_AnyType(t *testing.T) {
schema := createParams.ToJSONSchema()
// data 字段存在
data, ok := schema.Properties["data"]
if !ok {
t.Fatal("missing property: data")
}
// any 类型不应有 type
if data.Type != "" {
t.Errorf("data: expected empty type for any, got %q", data.Type)
}
// oneOf 应有 2 个分支
if len(data.OneOf) != 2 {
t.Fatalf("data.oneOf: expected 2 branches, got %d", len(data.OneOf))
}
// 分支1object
branch1 := data.OneOf[0]
if branch1.Type != "object" {
t.Errorf("data.oneOf[0]: expected type=object, got %q", branch1.Type)
}
if branch1.AdditionalProperties == nil {
t.Error("data.oneOf[0]: expected additionalProperties to be set")
}
// 分支2arrayitems 为 object
branch2 := data.OneOf[1]
if branch2.Type != "array" {
t.Errorf("data.oneOf[1]: expected type=array, got %q", branch2.Type)
}
if branch2.Items == nil {
t.Fatal("data.oneOf[1]: missing items")
}
if branch2.Items.Type != "object" {
t.Errorf("data.oneOf[1].items: expected type=object, got %q", branch2.Items.Type)
}
if branch2.Items.AdditionalProperties == nil {
t.Error("data.oneOf[1].items: expected additionalProperties to be set")
}
}
func TestResApiParams_AdditionalProperties(t *testing.T) {
params := base.ResApiParams{
{ID: 1, PID: 0, Code: "open", DataType: "json", Name: "开放对象",
Constraints: map[string]any{"additionalProperties": true}},
{ID: 2, PID: 0, Code: "strict", DataType: "json", Name: "严格对象",
Constraints: map[string]any{"additionalProperties": false}},
}
schema := params.ToJSONSchema()
open := schema.Properties["open"]
if open.AdditionalProperties == nil {
t.Fatal("open: expected additionalProperties to be set")
}
// true → Not == nil
if open.AdditionalProperties.Not != nil {
t.Error("open: additionalProperties should be true (Not == nil)")
}
strict := schema.Properties["strict"]
if strict.AdditionalProperties == nil {
t.Fatal("strict: expected additionalProperties to be set")
}
// false → Not != nil
if strict.AdditionalProperties.Not == nil {
t.Error("strict: additionalProperties should be false (Not != nil)")
}
}
func TestResApiParams_AnyType_Print(t *testing.T) {
b, _ := json.MarshalIndent(createParams.ToJSONSchema(), "", " ")
t.Log("\n" + string(b))
}
func TestResApiParams_RoundTrip(t *testing.T) {
input := &jschema.Schema{
Type: "object",
Properties: map[string]*jschema.Schema{
"name": {
Type: "string",
Description: "姓名",
MinLength: jschema.Ptr(1),
MaxLength: jschema.Ptr(100),
},
"age": {
Type: "integer",
Minimum: jschema.Ptr(float64(0)),
Maximum: jschema.Ptr(float64(150)),
},
"tags": {
Type: "array",
Items: &jschema.Schema{Type: "string"},
},
},
Required: []string{"name"},
}
params := base.ResApiParamsFromJSONSchema(input)
output := params.ToJSONSchema()
if _, ok := output.Properties["name"]; !ok {
t.Error("round-trip: missing property name")
}
if _, ok := output.Properties["age"]; !ok {
t.Error("round-trip: missing property age")
}
tags, ok := output.Properties["tags"]
if !ok {
t.Error("round-trip: missing property tags")
} else if tags.Items == nil || tags.Items.Type != "string" {
t.Errorf("round-trip: tags.items expected type=string, got %+v", tags.Items)
}
}
+344
View File
@@ -0,0 +1,344 @@
{
"tools": [
{
"name": "res-query",
"description": "Query records with filters, joins, aggregates, ordering, and pagination.\rThe main resource and each hasOne/hasMany relation are independent query contexts; inner/left/right are JOINs within their parent context.\rTop-level conditions / groupBy / orderBy / limit / offset apply to the main resource SQL. Each relation has its own conditions / groupBy / orderBy / limit that apply to that relation's SQL.\rIMPORTANT:\r(1) All field codes MUST come from res-schema results already in this conversation — never guess or infer field names.\r(2) Fields belonging to a hasOne/hasMany context must use object form with codeResource set to that relation's code.\r(3) Call res-schema first if the resource schema has not been fetched yet.",
"inputSchema": {
"type": "object",
"properties": {
"conditions": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/properties/conditions"
},
"title": "子条件",
"description": "子条件节点,递归结构"
},
"exprs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string",
"title": "字段编码",
"description": "必须来自 res-schema 返回的字段列表,禁止猜测字段名"
},
"fieldResource": {
"type": "string",
"title": "所属资源",
"description": "字段所属资源标识,取值为主资源编码或当前查询上下文(conditions 所在层级)对应的 relations[].code"
},
"fieldSqlFunc": {
"type": "string",
"title": "列SQL函数",
"description": "对列应用 SQL 函数后再参与比较,拼接为 {func}({fieldResource}.{field}) {operator} {token}。\r\n支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。\r\n需要第二参数的函数(如 ROUND、CAST)配合 fieldSqlFuncParam 使用。"
},
"fieldSqlFuncParam": {
"type": "string",
"title": "列SQL函数参数",
"description": "fieldSqlFunc 的附加参数,按函数类型用途不同:\r\r通用函数(有 param 时):{func}({fieldResource}.{field}, {fieldSqlFuncParam}) {operator} {token}\r 示例:ROUND(col, 2) → fieldSqlFuncParam=\\\"2\\\"\r SUBSTRING(col, 1, 10) → fieldSqlFuncParam=\\\"1, 10\\\""
},
"ignoreEmptyParam": {
"type": "boolean",
"title": "忽略空参数",
"description": "当 tokenType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项",
"default": false
},
"operator": {
"type": "string",
"title": "运算符",
"description": "标准 SQL 比较运算符:\r\n= / != / > / >= / < / <=\r\nLIKE:模糊匹配(%keyword%\r\nIN:包含,value 逗号分隔多值\r\nIS NULL / IS NOT NULL:空值判断,无需 value\r\nREGEXP:正则匹配",
"default": "="
},
"token": {
"type": "string",
"title": "比较值",
"description": "比较目标,内容由 tokenType 决定。operator 为 IS NULL/IS NOT NULL 时可省略"
},
"tokenType": {
"type": "string",
"title": "比较值类型",
"description": "先选类型再填 token\r\nstringtoken 填字面量如 '2024-01-01'\r\nfunctoken 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparamtoken 填请求参数路径如 body.status\r\nsqltoken 填原始SQL如 CURRENT_DATE",
"default": "string",
"enum": [
"string",
"func"
]
}
},
"title": "条件项",
"description": "单条比较表达式,生成 [{fieldSqlFunc}(]{fieldResource}.{field}[)] {operator} {token} 片段",
"required": [
"field",
"operator"
]
},
"title": "条件表达式列表",
"description": "当前层的叶子比较列表,每项生成一个 {fieldResource}.{field} {operator} {token} 片段,同层多项以 type 连接"
},
"type": {
"type": "string",
"title": "逻辑连接符",
"description": "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
"default": "and",
"enum": [
"and",
"or"
]
}
},
"title": "树形查询条件",
"description": "WHERE 条件树。\rexprs 每项生成一段比较:[{fieldSqlFunc}(]{fieldResource}.{field}[)] {operator} {token},同层多项以 type(and/or) 连接;\rchildren 为同结构子节点,递归嵌套实现复杂过滤。",
"required": [
"exprs",
"type"
]
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"alias": {
"type": "string",
"title": "别名",
"description": "输出字段别名,对应 SQL AS。isExpr=true 时必填,否则结果列无法被引用;普通字段不填则默认使用 code 值"
},
"code": {
"type": "string",
"title": "字段编码",
"description": "两种互斥用法,每个对象只能选其一:\r1. 字段编码,格式为 {Resource}.{field}Resource 取值为主资源编码或 relations[].code,如 User.amount / Order.priceisExpr 保持默认 false\r2. 原始 SQL 表达式(如 COUNT(*) / SUM(amount)),必须同时设置 isExpr = true,且 alias 必填\r注意:SELECT 中混入聚合表达式与非聚合字段时,需配合 groupBy 使用,否则结果不符合预期\r重要:字段编码必须来自 res-schema 返回的字段列表,禁止猜测或推断字段名"
},
"codeResource": {
"type": "string",
"title": "所属资源",
"description": "字段所属的表标识,取值为主资源编码或 relations[].code"
},
"dataType": {
"type": "string",
"title": "数据类型",
"description": "指定字段的返回数据类型,影响序列化方式;不填则由资源字段定义决定",
"default": "string",
"enum": [
"string",
"bool",
"number",
"array",
"json",
"integer",
"float"
]
},
"isExpr": {
"type": "boolean",
"title": "是否表达式",
"description": "设为 true 时,code 内容作为原始 SQL 表达式拼入 SELECT(如聚合函数 COUNT(*) / SUM(amount)),而非普通字段编码。isExpr=true 时 alias 为必填项",
"default": false
}
},
"title": "资源字段",
"description": "结构化字段,支持指定所属资源、输出别名、聚合表达式及数据类型",
"required": [
"code",
"codeResource"
]
},
"title": "查询字段",
"description": "SELECT 字段列表,涵盖所有查询上下文(主资源 + 所有 hasOne/hasMany),通过 codeResource 路由到对应的独立 SQL。不传则返回所有字段。\r主资源和每个 hasOne/hasMany 各是一个独立查询上下文,inner/left/right 只是其所在上下文内的 JOIN。\r仅查询聚合结果(如 COUNT(*) / SUM(amount))时,fields 只传聚合字段,不要混入普通字段,除非同时提供 groupBy。\r重要:所有字段编码必须先通过 res-schema 确认,禁止凭名称语义猜测"
},
"groupBy": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"title": "字段编码",
"description": "格式必须为 {Resource}.{field},如 User.status。\r{Resource} 取值为主资源编码或 relations[].code"
},
{
"type": "object",
"properties": {
"expr": {
"type": "string",
"title": "表达式",
"description": "type=field 时填 {Resource}.{field}(如 User.status\rtype=sql 时填原始 SQL 表达式(如 DATE(created_at)"
},
"type": {
"type": "string",
"title": "表达式类型",
"description": "field:按资源字段分组,expr 填 {Resource}.{field},如 User.status\rsql:按原始 SQL 表达式分组,expr 填任意合法 SQL,如 DATE(created_at)",
"default": "sql",
"enum": [
"field",
"sql"
]
}
},
"title": "分组表达式",
"description": "结构化分组项,type=field 用于资源字段,type=sql 用于原始 SQL 表达式",
"required": [
"expr",
"type"
]
}
]
},
"title": "分组规则",
"description": "GROUP BY 列表,配合 fields 中的聚合表达式使用。\r字段必须带资源前缀({Resource}.{field});聚合函数(COUNT / SUM 等)放在 fields 中,不属于此处"
},
"limit": {
"type": "integer",
"title": "每页条数",
"description": "当前查询上下文返回的最大记录数(顶层时限制主资源结果,relations 内时限制该关联结果)",
"default": 30
},
"offset": {
"type": "integer",
"title": "偏移量",
"description": "主资源查询跳过的记录数,仅顶层有效,relations 内不支持",
"default": 0
},
"orderBy": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"title": "字段编码",
"description": "格式必须为 {Resource}.{field},如 User.created_at,默认升序(ASC)。\r需要降序或使用 SQL 表达式时使用对象形式"
},
{
"type": "object",
"properties": {
"direction": {
"type": "string",
"title": "排序方向",
"default": "asc",
"enum": [
"asc",
"desc"
]
},
"expr": {
"type": "string",
"title": "表达式",
"description": "type=field 时填 {Resource}.{field}(如 User.created_at);type=sql 时填原始 SQL 表达式(如 COUNT(*) / DATE(created_at)"
},
"type": {
"type": "string",
"title": "表达式类型",
"description": "field:按资源字段排序,expr 填 {Resource}.{field},如 User.created_at\rsql:按原始 SQL 表达式排序,expr 填任意合法 SQL,如 COUNT(*) / DATE(created_at)",
"default": "field",
"enum": [
"field",
"sql"
]
}
},
"title": "排序表达式",
"description": "结构化排序项,type=field 用于资源字段,type=sql 用于原始 SQL 表达式",
"required": [
"type",
"expr"
]
}
]
},
"title": "排序规则",
"description": "ORDER BY 列表,多项按顺序依次应用。\r字段必须带资源前缀({Resource}.{field});需要降序或 SQL 表达式排序时使用对象形式"
},
"relations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"actuallyField": {
"type": "string",
"title": "实际资源字段",
"description": "目标资源(code)参与 ON 条件的字段"
},
"actuallyResource": {
"type": "string",
"title": "实际资源",
"description": "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"
},
"code": {
"type": "string",
"title": "关联标识",
"description": "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名。\r通常与 actuallyResource 保持一致;仅当多个关联引用同一资源导致冲突时,才需要重命名加以区分"
},
"conditions": {
"$ref": "#/properties/conditions",
"description": "该关联的过滤条件,结构与顶层 conditions 相同。hasOne/hasMany 作用于子查询 WHEREinner/left/right 作用于 JOIN ON 或主查询 WHERE"
},
"groupBy": {
"$ref": "#/properties/groupBy",
"description": "该关联的 GROUP BY,仅 hasOne/hasMany 子查询生效,结构与顶层 groupBy 相同"
},
"limit": {
"$ref": "#/properties/limit",
"description": "该关联的最大返回条数,仅 hasOne/hasMany 子查询生效,结构与顶层 limit 相同"
},
"name": {
"type": "string",
"title": "关联名称",
"description": "关联的显示名称"
},
"orderBy": {
"$ref": "#/properties/orderBy",
"description": "该关联的 ORDER BY,仅 hasOne/hasMany 子查询生效,结构与顶层 orderBy 相同"
},
"relationField": {
"type": "string",
"title": "被关联资源字段",
"description": "ON 条件对端资源的字段"
},
"relationResource": {
"type": "string",
"title": "被关联资源",
"description": "ON 条件对端的资源编码(通常为主资源或父关联资源)"
},
"type": {
"type": "string",
"title": "关联类型",
"description": "关联模式:\r\ninner / left / rightSQL JOIN,要求与主资源同一数据库连接,将关联字段合并到主查询行\r\nhasOne:独立子查询,支持跨库,结果聚合为单个对象(result[code] = {}\r\nhasMany:独立子查询,支持跨库,结果聚合为对象数组(result[code] = [...]\r\n跨数据库连接的关联必须使用 hasOne 或 hasMany",
"enum": [
"inner",
"left",
"right",
"hasOne",
"hasMany"
]
}
},
"required": [
"actuallyField",
"actuallyResource",
"code",
"relationField",
"relationResource",
"type"
]
},
"title": "资源关联",
"description": "关联配置列表。\rinner / left / rightSQL JOIN,要求关联资源与主资源在同一数据库连接,将关联字段合并到主查询行。\rhasOne / hasMany:独立子查询,支持跨数据库连接,结果聚合为 JSON 字段(hasOne 为单对象,hasMany 为数组)。\r跨库关联必须使用 hasOne 或 hasMany,不可使用 JOIN 类型。"
},
"resource": {
"type": "string",
"title": "主资源",
"description": "主资源编码(resource_code),作为本次查询的根上下文,relations 中的关联资源均以此为基础展开"
}
},
"required": [
"fields",
"resource"
]
},
"annotations": {}
}
]
}
+475
View File
@@ -0,0 +1,475 @@
{
"tools": [
{
"name": "res-query",
"description": "Query records with filters, joins, aggregates, ordering, pagination, and recursive tree traversal.\rAll relations are SQL JOINs within a single query; the main resource is the primary table.\rPrefer JOIN over multiple queries: when the needed data spans several resources, combine same-connection resources into one query using relations, then make separate queries only for resources on different connections.\rWRONG: 3 queries — User → UserRole → Role, even though User and UserRole share the same connection.\rRIGHT: 1 query joining User + UserRole (same connection), then 1 separate query for Role (different connection).\rIMPORTANT:\r(1) Never guess field names — only use codes from res-schema results in this conversation.\r(2) Call res-schema first if the resource schema has not been fetched yet.\r(3) Use recursive to traverse all descendants of hierarchical data (e.g. categories, menus, org charts); returns a flat list, not a nested tree.",
"inputSchema": {
"type": "object",
"properties": {
"conditions": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/properties/conditions"
},
"title": "子条件",
"description": "子条件节点,递归结构"
},
"exprs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string",
"title": "字段编码",
"description": "必须来自 res-schema 返回的字段列表,禁止猜测字段名"
},
"fieldResource": {
"type": "string",
"title": "所属资源",
"description": "字段所属的表标识,取值为主资源编码或 relations[].code"
},
"fieldSqlFunc": {
"type": "string",
"title": "列SQL函数",
"description": "对列应用 SQL 函数后再参与比较,拼接为 {func}({fieldResource}.{field}) {operator} {token}。\r\n支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。\r\n需要第二参数的函数(如 ROUND、CAST)配合 fieldSqlFuncParam 使用。"
},
"fieldSqlFuncParam": {
"type": "string",
"title": "列SQL函数参数",
"description": "fieldSqlFunc 的附加参数,按函数类型用途不同:\r\r通用函数(有 param 时):{func}({fieldResource}.{field}, {fieldSqlFuncParam}) {operator} {token}\r 示例:ROUND(col, 2) → fieldSqlFuncParam=\"2\"\r SUBSTRING(col, 1, 10) → fieldSqlFuncParam=\"1, 10\"\r\rJSON 函数(param 为 JSON 路径表达式):\r json_member_oftoken 是否是列 JSON 数组的成员):\r 无 paramJSON_CONTAINS({token}, JSON_ARRAY({field}))\r 有 paramJSON_CONTAINS({fieldSqlFuncParam}, JSON_ARRAY({field}))\r json_contains(列 JSON 是否包含 token):\r 无 paramJSON_CONTAINS({field}, JSON_ARRAY({token}))\r 有 paramJSON_CONTAINS({fieldSqlFuncParam}, JSON_ARRAY({token}))"
},
"ignoreEmptyParam": {
"type": "boolean",
"title": "忽略空参数",
"description": "当 tokenType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项",
"default": false
},
"operator": {
"type": "string",
"title": "运算符",
"description": "标准 SQL 比较运算符:\r\n= / != / > / >= / < / <=\r\nLIKE:模糊匹配(%keyword%\r\nIN:包含,token 逗号分隔多值\r\nIS NULL / IS NOT NULL:空值判断,无需 token\r\nREGEXP:正则匹配",
"default": "="
},
"token": {
"type": "string",
"title": "比较值",
"description": "比较目标,内容由 tokenType 决定。operator 为 IS NULL/IS NOT NULL 时可省略"
},
"tokenType": {
"type": "string",
"title": "比较值类型",
"description": "先选类型再填 token\r\nstringtoken 填字面量如 '2024-01-01'\r\nfunctoken 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparamtoken 填请求参数路径如 body.status\r\nsqltoken 填原始SQL如 CURRENT_DATE",
"default": "string",
"enum": [
"string",
"func"
]
}
},
"title": "条件项",
"description": "单条比较表达式,生成 [{fieldSqlFunc}(]{fieldResource}.{field}[)] {operator} {token} 片段",
"required": [
"field",
"operator"
]
},
"title": "条件表达式列表",
"description": "当前层的叶子比较列表,每项生成一个 {fieldResource}.{field} {operator} {token} 片段,同层多项以 type 连接"
},
"type": {
"type": "string",
"title": "逻辑连接符",
"description": "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
"default": "and",
"enum": [
"and",
"or"
]
}
},
"title": "树形查询条件",
"description": "WHERE 条件树。\rexprs 每项生成一段比较:[{fieldSqlFunc}(]{fieldResource}.{field}[)] {operator} {token},同层多项以 type(and/or) 连接;\rchildren 为同结构子节点,递归嵌套实现复杂过滤。",
"required": [
"exprs",
"type"
]
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"alias": {
"type": "string",
"title": "别名",
"description": "输出字段别名,对应 SQL AS。isExpr=true 时必填;普通字段在多表 JOIN 存在同名冲突时填写,其余情况默认使用 code 值"
},
"code": {
"type": "string",
"title": "字段编码",
"description": "两种互斥用法,每个对象只能选其一:\r1. 普通字段编码,只填字段名本身如 amount,不含资源前缀,所属表由 codeResource 单独指定,isExpr 保持默认 false\r2. 原始 SQL 表达式(如 COUNT(*) / SUM(User.amount)),可含 {Resource}.{field} 引用,必须同时设置 isExpr = true 且 alias 必填\r注意:SELECT 中混入聚合表达式与非聚合字段时,需配合 groupBy 使用,否则结果不符合预期\r重要:字段编码必须来自 res-schema 返回的字段列表,禁止猜测或推断字段名"
},
"codeResource": {
"type": "string",
"title": "所属资源",
"description": "字段所属的表标识,取值为主资源编码或 relations[].code"
},
"dataType": {
"type": "string",
"title": "数据类型",
"description": "指定字段的返回数据类型,影响序列化方式;不填则由资源字段定义决定",
"default": "string",
"enum": [
"string",
"bool",
"number",
"array",
"json",
"integer",
"float"
]
},
"isExpr": {
"type": "boolean",
"title": "是否表达式",
"description": "设为 true 时,code 内容作为原始 SQL 表达式拼入 SELECT(如聚合函数 COUNT(*) / SUM(amount)),而非普通字段编码。isExpr=true 时 alias 为必填项",
"default": false
}
},
"title": "资源字段",
"description": "结构化字段,支持指定所属表、输出别名、聚合表达式及数据类型",
"required": [
"code"
]
},
"title": "查询字段",
"description": "SELECT 字段列表,不传则返回所有字段(仅主资源字段,不含 JOIN 表)。\r使用 relations 时,JOIN 表的字段不会自动出现在结果中,必须在 fields 里显式列出需要的 JOIN 表字段,否则 JOIN 无意义。\r普通字段:code 填字段名,codeResource 指定所属表;聚合/表达式:isExpr=truecode 填 SQL 表达式,alias 必填。\r仅查询聚合结果时,fields 只传聚合字段,不要混入普通字段,除非同时提供 groupBy。\r重要:所有字段编码必须先通过 res-schema 确认,禁止凭名称语义猜测"
},
"groupBy": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"title": "字段编码",
"description": "格式必须为 {Resource}.{field},如 User.status。\r{Resource} 取值为主资源编码或 relations[].code"
},
{
"type": "object",
"properties": {
"expr": {
"type": "string",
"title": "表达式",
"description": "type=field 时填 {Resource}.{field}(如 User.status\rtype=sql 时填原始 SQL 表达式(如 DATE(created_at)"
},
"type": {
"type": "string",
"title": "表达式类型",
"description": "field:按资源字段分组,expr 填 {Resource}.{field},如 User.status\rsql:按原始 SQL 表达式分组,expr 填任意合法 SQL,如 DATE(created_at)",
"default": "field",
"enum": [
"field",
"sql"
]
}
},
"title": "分组表达式",
"description": "结构化分组项,type=field 用于资源字段,type=sql 用于原始 SQL 表达式",
"required": [
"type",
"expr"
]
}
]
},
"title": "分组规则",
"description": "GROUP BY 列表,配合 fields 中的聚合表达式使用。\r字段必须带资源前缀({Resource}.{field});聚合函数(COUNT / SUM 等)放在 fields 中,不属于此处"
},
"limit": {
"type": "integer",
"title": "每页条数",
"description": "返回的最大记录数",
"default": 30
},
"offset": {
"type": "integer",
"title": "偏移量",
"description": "跳过的记录数",
"default": 0
},
"orderBy": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"title": "字段编码",
"description": "格式必须为 {Resource}.{field},如 User.created_at,默认升序(ASC)。\r需要降序或使用 SQL 表达式时使用对象形式"
},
{
"type": "object",
"properties": {
"direction": {
"type": "string",
"title": "排序方向",
"default": "asc",
"enum": [
"asc",
"desc"
]
},
"expr": {
"type": "string",
"title": "表达式",
"description": "type=field 时填 {Resource}.{field}(如 User.created_at);type=sql 时填原始 SQL 表达式(如 COUNT(*) / DATE(created_at)"
},
"type": {
"type": "string",
"title": "表达式类型",
"description": "field:按资源字段排序,expr 填 {Resource}.{field},如 User.created_at\rsql:按原始 SQL 表达式排序,expr 填任意合法 SQL,如 COUNT(*) / DATE(created_at)",
"default": "field",
"enum": [
"field",
"sql"
]
}
},
"title": "排序表达式",
"description": "结构化排序项,type=field 用于资源字段,type=sql 用于原始 SQL 表达式",
"required": [
"type",
"expr"
]
}
]
},
"title": "排序规则",
"description": "ORDER BY 列表,多项按顺序依次应用。\r字段必须带资源前缀({Resource}.{field});需要降序或 SQL 表达式排序时使用对象形式"
},
"relations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"actuallyField": {
"type": "string",
"title": "实际资源字段",
"description": "目标资源(code)参与 ON 条件的字段"
},
"actuallyResource": {
"type": "string",
"title": "实际资源",
"description": "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"
},
"code": {
"type": "string",
"title": "关联标识",
"description": "JOIN 表别名(AS code),同时作为 fields/conditions/groupBy/orderBy 中 {Resource} 的取值。\r通常与 actuallyResource 保持一致;仅当多个关联引用同一资源导致冲突时,才需要重命名"
},
"conditions": {
"$ref": "#/properties/conditions",
"description": "该 JOIN 的附加过滤条件,作用于 JOIN ON 或主查询 WHERE,结构与顶层 conditions 相同"
},
"name": {
"type": "string",
"title": "关联名称",
"description": "关联的显示名称"
},
"relationField": {
"type": "string",
"title": "被关联资源字段",
"description": "ON 条件对端资源的字段"
},
"relationResource": {
"type": "string",
"title": "被关联资源",
"description": "ON 条件对端的资源编码(通常为主资源或父关联资源)"
},
"type": {
"type": "string",
"title": "关联类型",
"description": "SQL JOIN 类型(仅支持同库连接,不可跨库):\rinner:内连接,只返回两表均匹配的行\rleft:左连接,保留主表所有行\rright:右连接,保留关联表所有行",
"enum": [
"inner",
"left",
"right"
]
}
},
"required": [
"actuallyField",
"actuallyResource",
"code",
"relationField",
"relationResource",
"type"
]
},
"title": "资源关联",
"description": "SQL JOIN 列表,所有关联在同一条 SQL 中执行。\r关联资源的字段通过 code(表别名)在 fields / conditions / groupBy / orderBy 中引用。\r重要:所有关联资源必须与主资源在同一数据库连接。res-schema 返回的资源名称后面标有连接标识(如 · default / · service-support),建立 JOIN 前必须确认主资源与所有关联资源的连接标识完全一致,不一致则不可使用 JOIN,需分次查询后在应用层合并。"
},
"resource": {
"type": "string",
"title": "主资源",
"description": "主资源编码(resource),作为本次查询的主表,relations 中的关联资源均以此为基础 JOIN"
},
"recursive": {
"type": "object",
"title": "递归查询",
"description": "树形/层级数据的递归查询配置。配置后会以 pField=root 的记录为根节点,向下递归遍历所有子孙节点,返回平铺的记录列表(非嵌套树)。",
"properties": {
"pField": {
"type": "string",
"title": "父节点字段",
"description": "存储父节点 ID 的字段编码,格式为 {Resource}.{field}。{Resource} 取值为主资源编码或 relations[].code,如 Category.pid"
},
"cField": {
"type": "string",
"title": "当前节点字段",
"description": "存储自身 ID 的字段编码,格式为 {Resource}.{field}。{Resource} 取值为主资源编码或 relations[].code,如 Category.id"
},
"root": {
"type": "string",
"title": "根节点值",
"description": "递归起点,pField 等于此值的记录作为根节点,通常为 0 或空字符串"
},
"depth": {
"type": "integer",
"title": "递归深度",
"description": "最大递归层数,防止循环引用导致无限递归,不传则不限制"
}
},
"required": ["pField", "cField", "root"]
}
},
"required": [
"fields",
"resource"
]
},
"annotations": {}
},
{
"name": "res-create",
"description": "Create one or multiple new records for the specified resource. Returns all fields of the created record(s).\rCall res-schema first to get the field list before building the data object.\rIMPORTANT: Never guess field names — only use codes from res-schema results.",
"inputSchema": {
"type": "object",
"properties": {
"resource": {
"type": "string",
"title": "主资源",
"description": "目标资源编码,数据将写入该资源对应的表"
},
"data": {
"title": "创建数据",
"description": "单条记录传对象,批量创建传对象数组。\rkey 为字段编码(必须来自 res-schema 返回的字段列表,禁止猜测),value 为写入值。\r有默认值的字段可以不传,系统会自动填充。\r批量创建(array)时,所有记录必须包含完全相同的字段集合,不可部分记录有某字段而其他记录没有。\r系统保留字段禁止传入,包括:id、created_at、updated_at、created_user、owned_user、enabled 及其他由系统自动维护的字段",
"oneOf": [
{
"type": "object",
"title": "单条记录",
"description": "创建单条记录,key 为字段编码,value 为字段值",
"additionalProperties": true
},
{
"type": "array",
"title": "批量记录",
"description": "批量创建,每个元素为一条记录对象",
"items": {
"type": "object",
"additionalProperties": true
}
}
]
}
},
"required": ["resource", "data"]
},
"annotations": {}
},
{
"name": "res-update",
"description": "Update records by their IDs. Returns only the updated fields of the modified records.\rBefore calling this tool, ALWAYS use res-query to retrieve and show the user the records that will be affected. Only call res-update after the user has reviewed and confirmed.\rPass __confirm__: true when the user has approved the update.\rIMPORTANT: Never guess field names — only use codes from res-schema results.",
"inputSchema": {
"type": "object",
"properties": {
"resource": {
"type": "string",
"title": "主资源",
"description": "目标资源编码"
},
"ids": {
"type": "array",
"title": "记录ID列表",
"description": "必填,指定要更新的记录 ID 列表。必须通过 res-query 预先查询获得,禁止手动构造。",
"items": {
"type": "integer"
}
},
"data": {
"type": "object",
"title": "更新数据",
"description": "只传需要修改的字段,未传字段保持不变。\rkey 为字段编码(必须来自 res-schema 返回的字段列表,禁止猜测),value 为新值。\r系统保留字段禁止传入,包括:id、created_at、updated_at、created_user、owned_user、enabled 及其他由系统自动维护的字段",
"additionalProperties": true
},
"__confirm__": {
"type": "boolean",
"title": "确认执行",
"description": "首次调用不传。仅当服务端返回需要确认的提示时,重新调用并传入 true 以执行更新。不可主动传入,否则将跳过安全确认步骤。",
"default": false
}
},
"required": ["resource", "ids", "data"]
},
"annotations": {}
},
{
"name": "res-delete",
"description": "Hard-delete records by ID (physical removal, unrecoverable).\rWARNING: Prefer soft delete — use res-update to set deleted_at=CURRENT_TIMESTAMP and enabled=0 instead.\rOnly call res-delete when the user explicitly requests permanent/physical deletion.\rBefore calling, ALWAYS use res-query to show the user the affected records and obtain confirmation.\rPass __confirm__: true after the user has confirmed.",
"inputSchema": {
"type": "object",
"properties": {
"resource": {
"type": "string",
"title": "主资源",
"description": "目标资源编码"
},
"ids": {
"type": "array",
"title": "记录ID列表",
"description": "必填,指定要删除的记录 ID 列表。必须通过 res-query 预先查询获得,禁止手动构造。",
"items": {
"type": "integer"
}
},
"__confirm__": {
"type": "boolean",
"title": "确认执行",
"description": "首次调用不传。仅当服务端返回需要确认的提示时,重新调用并传入 true 以执行删除。不可主动传入,否则将跳过安全确认步骤。",
"default": false
}
},
"required": ["resource", "ids"]
},
"annotations": {}
},
{
"name": "res-schema",
"description": "Look up available resources and their field definitions. Without keywords: lists all resources. With keywords: returns matched resources with their field list, types, and constraints. Keywords match against resource code, name, or table name; supports Chinese fuzzy matching; use | to separate multiple terms (e.g. \"用户|权限\"). Call this before using res-query / res-update / res-delete if the resource schema is not already in the current conversation.",
"inputSchema": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"title": "查找关键字",
"description": "搜索关键词,用于筛选资源列表。支持多个关键词,用 | 分隔。\r对每条资源的编码(code)、名称(name)、数据表名(table)进行匹配,\r支持中文分词模糊匹配,结果按匹配相关度从高到低排序。\r示例:\"用户\" 或 \"用户|权限\""
}
}
},
"annotations": {}
}
]
}
+758
View File
@@ -0,0 +1,758 @@
[
{
"id": 271,
"pid": 0,
"code": "fields",
"name": "查询字段",
"category": "param",
"type": "array",
"desc": "查询字段,不传则返回所有字段",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 282,
"pid": 0,
"code": "groupBy",
"name": "分组规则",
"category": "param",
"type": "array",
"desc": "GROUP BY 字段列表,配合 fields 中的聚合字段使用",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 287,
"pid": 0,
"code": "orderBy",
"name": "排序规则",
"category": "param",
"type": "array",
"desc": "排序规则列表,多项按顺序依次应用",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 299,
"pid": 0,
"code": "relations",
"name": "资源关联",
"category": "param",
"type": "array",
"desc": "关联配置列表,支持 JOINinner / left / right)将关联字段合并到主查询,或子查询(hasOne / hasMany)将关联数据聚合为独立 JSON 字段",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 258,
"pid": 0,
"code": "conditions",
"name": "树形查询条件",
"category": "param",
"type": "json",
"desc": "WHERE 条件树。\rexprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;\rchildren 为同结构子节点,递归嵌套实现复杂过滤。",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 297,
"pid": 0,
"code": "limit",
"name": "每页条数",
"category": "param",
"type": "integer",
"desc": "返回的最大记录数",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": "30"
},
{
"id": 298,
"pid": 0,
"code": "offset",
"name": "偏移量",
"category": "param",
"type": "integer",
"desc": "跳过的记录数",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": "0"
},
{
"id": 259,
"pid": 258,
"code": "type",
"name": "逻辑连接符",
"category": "param",
"type": "string",
"desc": "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
"isRequired": true,
"constraints": {
"enum": [
"and",
"or"
]
},
"rank": 0,
"ref_id": 0,
"df_val": "and"
},
{
"id": 261,
"pid": 258,
"code": "children",
"name": "子条件",
"category": "param",
"type": "array",
"desc": "子条件节点,递归结构",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 260,
"pid": 258,
"code": "exprs",
"name": "条件表达式列表",
"category": "param",
"type": "array",
"desc": "当前层的叶子比较列表,每项生成一个 column {operator} value 片段,同层多项以 type 连接",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 262,
"pid": 260,
"code": "items",
"name": "_items",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 312,
"pid": 261,
"code": "items",
"name": "_items",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 258,
"df_val": ""
},
{
"id": 263,
"pid": 262,
"code": "column",
"name": "字段编码",
"category": "param",
"type": "string",
"desc": "",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 264,
"pid": 262,
"code": "columnResource",
"name": "所属资源",
"category": "param",
"type": "string",
"desc": "字段所属资源标识,取值为顶层 resource 或 relations[].code",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 268,
"pid": 262,
"code": "columnSqlFunc",
"name": "列SQL函数",
"category": "param",
"type": "string",
"desc": "对列应用 SQL 函数后再参与比较,拼接为 {func}({columnResource}.{column}) {operator} {value}。\r\n支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。\r\n需要第二参数的函数(如 ROUND、CAST)配合 columnSqlFuncParam 使用。",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 269,
"pid": 262,
"code": "columnSqlFuncParam",
"name": "列SQL函数参数",
"category": "param",
"type": "string",
"desc": "columnSqlFunc 的附加参数,按函数类型用途不同:\r\r通用函数(有 param 时):{func}({columnResource}.{column}, {columnSqlFuncParam}) {operator} {value}\r 示例:ROUND(col, 2) → columnSqlFuncParam=\"2\"\r SUBSTRING(col, 1, 10) → columnSqlFuncParam=\"1, 10\"\r\rJSON 函数(param 为 JSON 路径表达式):\r json_member_ofvalue 是否是列 JSON 数组的成员):\r 无 paramJSON_CONTAINS({value}, JSON_ARRAY({column}))\r 有 paramJSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))\r json_contains(列 JSON 是否包含 value):\r 无 paramJSON_CONTAINS({column}, JSON_ARRAY({value}))\r 有 paramJSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({value}))",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 265,
"pid": 262,
"code": "operator",
"name": "运算符",
"category": "param",
"type": "string",
"desc": "标准 SQL 比较运算符:\r\n= / != / \u003e / \u003e= / \u003c / \u003c=\r\nLIKE:模糊匹配(%keyword%\r\nIN:包含,value 逗号分隔多值\r\nIS NULL / IS NOT NULL:空值判断,无需 value\r\nREGEXP:正则匹配",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": "="
},
{
"id": 266,
"pid": 262,
"code": "value",
"name": "比较值",
"category": "param",
"type": "string",
"desc": "比较目标,内容由 valueType 决定。operator 为 IS NULL/IS NOT NULL 时可省略",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 267,
"pid": 262,
"code": "valueType",
"name": "比较值类型",
"category": "param",
"type": "string",
"desc": "先选类型再填 value\r\nstringvalue 填字面量如 '2024-01-01'\r\nfuncvalue 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparamvalue 填请求参数路径如 body.status\r\nsqlvalue 填原始SQL如 CURRENT_DATE",
"isRequired": false,
"constraints": {
"enum": [
"string",
"func"
]
},
"rank": 0,
"ref_id": 0,
"df_val": "string"
},
{
"id": 270,
"pid": 262,
"code": "ignoreEmptyParam",
"name": "忽略空参数",
"category": "param",
"type": "bool",
"desc": "当 valueType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": "false"
},
{
"id": 272,
"pid": 271,
"code": "oneOf",
"name": "字段编码",
"category": "param",
"type": "string",
"desc": "字段引用,格式:{columnResource}.{column} 或 {column}",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 279,
"pid": 271,
"code": "oneOf",
"name": "表达式字段",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 275,
"pid": 271,
"code": "oneOf",
"name": "资源字段",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 278,
"pid": 275,
"code": "alias",
"name": "别名",
"category": "param",
"type": "string",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 276,
"pid": 275,
"code": "column",
"name": "字段编码",
"category": "param",
"type": "string",
"desc": "",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 277,
"pid": 275,
"code": "columnResource",
"name": "所属资源",
"category": "param",
"type": "string",
"desc": "字段所属资源标识,取值为顶层 resource 或 relations[].code",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 281,
"pid": 279,
"code": "alias",
"name": "别名",
"category": "param",
"type": "string",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 280,
"pid": 279,
"code": "expr",
"name": "SQL 表达式",
"category": "param",
"type": "string",
"desc": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 283,
"pid": 282,
"code": "oneOf",
"name": "分组表达式",
"category": "param",
"type": "string",
"desc": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 284,
"pid": 282,
"code": "oneOf",
"name": "资源字段",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 285,
"pid": 284,
"code": "column",
"name": "字段编码",
"category": "param",
"type": "string",
"desc": "",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 286,
"pid": 284,
"code": "columnResource",
"name": "所属资源",
"category": "param",
"type": "string",
"desc": "字段所属资源标识,取值为顶层 resource 或 relations[].code",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 288,
"pid": 287,
"code": "oneOf",
"name": "SQL 表达式 (ASC)",
"category": "param",
"type": "string",
"desc": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用\r默认升序(asc)",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 289,
"pid": 287,
"code": "oneOf",
"name": "SQL 表达式排序",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 292,
"pid": 287,
"code": "oneOf",
"name": "资源字段排序",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 291,
"pid": 289,
"code": "direction",
"name": "排序方向",
"category": "param",
"type": "string",
"desc": "",
"isRequired": false,
"constraints": {
"enum": [
"asc",
"desc"
]
},
"rank": 0,
"ref_id": 0,
"df_val": "asc"
},
{
"id": 290,
"pid": 289,
"code": "expr",
"name": "SQL 表达式",
"category": "param",
"type": "string",
"desc": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 293,
"pid": 292,
"code": "column",
"name": "字段编码",
"category": "param",
"type": "string",
"desc": "",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 295,
"pid": 292,
"code": "columnResource",
"name": "所属资源",
"category": "param",
"type": "string",
"desc": "",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 296,
"pid": 292,
"code": "direction",
"name": "排序方向",
"category": "param",
"type": "string",
"desc": "",
"isRequired": false,
"constraints": {
"enum": [
"asc",
"desc"
]
},
"rank": 0,
"ref_id": 0,
"df_val": "asc"
},
{
"id": 300,
"pid": 299,
"code": "items",
"name": "",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 305,
"pid": 300,
"code": "actuallyField",
"name": "实际资源字段",
"category": "param",
"type": "string",
"desc": "目标资源(code)参与 ON 条件的字段",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 304,
"pid": 300,
"code": "actuallyResource",
"name": "实际资源",
"category": "param",
"type": "string",
"desc": "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 303,
"pid": 300,
"code": "code",
"name": "关联标识",
"category": "param",
"type": "string",
"desc": "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 302,
"pid": 300,
"code": "name",
"name": "关联名称",
"category": "param",
"type": "string",
"desc": "关联的显示名称",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 307,
"pid": 300,
"code": "relationField",
"name": "被关联资源字段",
"category": "param",
"type": "string",
"desc": "ON 条件对端资源的字段",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 306,
"pid": 300,
"code": "relationResource",
"name": "被关联资源",
"category": "param",
"type": "string",
"desc": "ON 条件对端的资源编码(通常为主资源或父关联资源)",
"isRequired": true,
"constraints": {},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 301,
"pid": 300,
"code": "type",
"name": "关联类型",
"category": "param",
"type": "string",
"desc": "关联模式:\r\ninner / left / rightSQL JOIN,将关联资源字段合并到主查询行\r\nhasOne:子查询,关联结果聚合为单个对象(result[code] = {}\r\nhasMany:子查询,关联结果聚合为对象数组(result[code] = [...]",
"isRequired": true,
"constraints": {
"enum": [
"inner",
"left",
"right",
"hasOne",
"hasMany"
]
},
"rank": 0,
"ref_id": 0,
"df_val": ""
},
{
"id": 309,
"pid": 300,
"code": "groupBy",
"name": "关联分组,仅 hasOne/hasMany 有效",
"category": "param",
"type": "array",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 282,
"df_val": ""
},
{
"id": 310,
"pid": 300,
"code": "orderBy",
"name": "关联排序,仅 hasOne/hasMany 有效",
"category": "param",
"type": "array",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 287,
"df_val": ""
},
{
"id": 308,
"pid": 300,
"code": "conditions",
"name": "关联的过滤条件",
"category": "param",
"type": "json",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 258,
"df_val": ""
},
{
"id": 311,
"pid": 300,
"code": "limit",
"name": "关联条数限制,仅 hasOne/hasMany 有效",
"category": "param",
"type": "integer",
"desc": "",
"isRequired": false,
"constraints": {},
"rank": 0,
"ref_id": 297,
"df_val": ""
}
]
+324
View File
@@ -0,0 +1,324 @@
{
"type": "object",
"properties": {
"conditions": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/properties/conditions"
},
"title": "子条件",
"description": "子条件节点,递归结构"
},
"exprs": {
"type": "array",
"items": {
"type": "object",
"properties": {
"column": {
"type": "string",
"title": "字段编码"
},
"columnResource": {
"type": "string",
"title": "所属资源",
"description": "字段所属资源标识,取值为顶层 resource 或 relations[].code"
},
"columnSqlFunc": {
"type": "string",
"title": "列SQL函数",
"description": "对列应用 SQL 函数后再参与比较,拼接为 {func}({columnResource}.{column}) {operator} {value}。\r\n支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。\r\n需要第二参数的函数(如 ROUND、CAST)配合 columnSqlFuncParam 使用。"
},
"columnSqlFuncParam": {
"type": "string",
"title": "列SQL函数参数",
"description": "columnSqlFunc 的附加参数,按函数类型用途不同:\r\r通用函数(有 param 时):{func}({columnResource}.{column}, {columnSqlFuncParam}) {operator} {value}\r 示例:ROUND(col, 2) → columnSqlFuncParam=\"2\"\r SUBSTRING(col, 1, 10) → columnSqlFuncParam=\"1, 10\"\r\rJSON 函数(param 为 JSON 路径表达式):\r json_member_ofvalue 是否是列 JSON 数组的成员):\r 无 paramJSON_CONTAINS({value}, JSON_ARRAY({column}))\r 有 paramJSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))\r json_contains(列 JSON 是否包含 value):\r 无 paramJSON_CONTAINS({column}, JSON_ARRAY({value}))\r 有 paramJSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({value}))"
},
"ignoreEmptyParam": {
"type": "boolean",
"title": "忽略空参数",
"description": "当 valueType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项",
"default": false
},
"operator": {
"type": "string",
"title": "运算符",
"description": "标准 SQL 比较运算符:\r\n= / != / \u003e / \u003e= / \u003c / \u003c=\r\nLIKE:模糊匹配(%keyword%\r\nIN:包含,value 逗号分隔多值\r\nIS NULL / IS NOT NULL:空值判断,无需 value\r\nREGEXP:正则匹配",
"default": "="
},
"value": {
"type": "string",
"title": "比较值",
"description": "比较目标,内容由 valueType 决定。operator 为 IS NULL/IS NOT NULL 时可省略"
},
"valueType": {
"type": "string",
"title": "比较值类型",
"description": "先选类型再填 value\r\nstringvalue 填字面量如 '2024-01-01'\r\nfuncvalue 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparamvalue 填请求参数路径如 body.status\r\nsqlvalue 填原始SQL如 CURRENT_DATE",
"default": "string",
"enum": [
"string",
"func"
]
}
},
"title": "_items",
"description": "_items",
"required": [
"column",
"operator"
]
},
"title": "条件表达式列表",
"description": "当前层的叶子比较列表,每项生成一个 column {operator} value 片段,同层多项以 type 连接"
},
"type": {
"type": "string",
"title": "逻辑连接符",
"description": "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
"default": "and",
"enum": [
"and",
"or"
]
}
},
"title": "树形查询条件",
"description": "WHERE 条件树。\rexprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;\rchildren 为同结构子节点,递归嵌套实现复杂过滤。",
"required": [
"exprs",
"type"
]
},
"fields": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"title": "字段编码",
"description": "字段引用,格式:{columnResource}.{column} 或 {column}"
},
{
"type": "object",
"properties": {
"alias": {
"type": "string",
"title": "别名"
},
"expr": {
"type": "string",
"title": "SQL 表达式",
"description": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用"
}
},
"title": "表达式字段"
},
{
"type": "object",
"properties": {
"alias": {
"type": "string",
"title": "别名"
},
"column": {
"type": "string",
"title": "字段编码"
},
"columnResource": {
"type": "string",
"title": "所属资源",
"description": "字段所属资源标识,取值为顶层 resource 或 relations[].code"
}
},
"title": "资源字段",
"required": [
"column"
]
}
]
},
"title": "查询字段",
"description": "查询字段,不传则返回所有字段"
},
"groupBy": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"title": "分组表达式",
"description": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用"
},
{
"type": "object",
"properties": {
"column": {
"type": "string",
"title": "字段编码"
},
"columnResource": {
"type": "string",
"title": "所属资源",
"description": "字段所属资源标识,取值为顶层 resource 或 relations[].code"
}
},
"title": "资源字段",
"required": [
"column"
]
}
]
},
"title": "分组规则",
"description": "GROUP BY 字段列表,配合 fields 中的聚合字段使用"
},
"limit": {
"type": "integer",
"title": "每页条数",
"description": "返回的最大记录数",
"default": 30
},
"offset": {
"type": "integer",
"title": "偏移量",
"description": "跳过的记录数",
"default": 0
},
"orderBy": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"title": "SQL 表达式 (ASC)",
"description": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用\r默认升序(asc)"
},
{
"type": "object",
"properties": {
"direction": {
"type": "string",
"title": "排序方向",
"default": "asc",
"enum": [
"asc",
"desc"
]
},
"expr": {
"type": "string",
"title": "SQL 表达式",
"description": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用"
}
},
"title": "SQL 表达式排序"
},
{
"type": "object",
"properties": {
"column": {
"type": "string",
"title": "字段编码"
},
"columnResource": {
"type": "string",
"title": "所属资源"
},
"direction": {
"type": "string",
"title": "排序方向",
"default": "asc",
"enum": [
"asc",
"desc"
]
}
},
"title": "资源字段排序",
"required": [
"column",
"columnResource"
]
}
]
},
"title": "排序规则",
"description": "排序规则列表,多项按顺序依次应用"
},
"relations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"actuallyField": {
"type": "string",
"title": "实际资源字段",
"description": "目标资源(code)参与 ON 条件的字段"
},
"actuallyResource": {
"type": "string",
"title": "实际资源",
"description": "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"
},
"code": {
"type": "string",
"title": "关联标识",
"description": "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名"
},
"conditions": {
"$ref": "#/properties/conditions"
},
"groupBy": {
"$ref": "#/properties/groupBy"
},
"limit": {
"$ref": "#/properties/limit"
},
"name": {
"type": "string",
"title": "关联名称",
"description": "关联的显示名称"
},
"orderBy": {
"$ref": "#/properties/orderBy"
},
"relationField": {
"type": "string",
"title": "被关联资源字段",
"description": "ON 条件对端资源的字段"
},
"relationResource": {
"type": "string",
"title": "被关联资源",
"description": "ON 条件对端的资源编码(通常为主资源或父关联资源)"
},
"type": {
"type": "string",
"title": "关联类型",
"description": "关联模式:\r\ninner / left / rightSQL JOIN,将关联资源字段合并到主查询行\r\nhasOne:子查询,关联结果聚合为单个对象(result[code] = {}\r\nhasMany:子查询,关联结果聚合为对象数组(result[code] = [...]",
"enum": [
"inner",
"left",
"right",
"hasOne",
"hasMany"
]
}
},
"required": [
"actuallyField",
"actuallyResource",
"code",
"relationField",
"relationResource",
"type"
]
},
"title": "资源关联",
"description": "关联配置列表,支持 JOINinner / left / right)将关联字段合并到主查询,或子查询(hasOne / hasMany)将关联数据聚合为独立 JSON 字段"
}
}
}
+4 -2
View File
@@ -1,6 +1,8 @@
package base
import "git.fsdpf.net/go/condition"
import (
"git.fsdpf.net/go/condition"
)
type ResCondition struct {
Id int `db:"id"`
@@ -53,7 +55,7 @@ func NewConditionByRes(items []ResCondition, opts ...condition.Option) (root *co
conditions[item.Id] = condition.NewExpr(
item.ColumnResource, item.Column,
condition.Token(item.Value, condition.TokenType(item.ValueType)),
condition.Operator(condition.ConditionOperator(item.Operator)),
condition.Operator(condition.ConditionOperator(condition.GetDBOperator(item.Operator))),
condition.FieldSqlFn(item.ColumnSqlFunc, item.ColumnSqlFuncParam),
condition.IgnoreEmptyParma(item.IgnoreEmptyParma),
)
+51 -1
View File
@@ -1,5 +1,15 @@
package base
import (
"encoding/json"
"git.fsdpf.net/go/reflux"
"git.fsdpf.net/go/reflux/valuex"
)
// role_uuid => value
type ResRoleValue map[string]string
type ResConfigure struct {
Uuid string `db:"uuid"`
ResourceUuid string `db:"resource_uuid"`
@@ -9,8 +19,48 @@ type ResConfigure struct {
Type string `db:"type"`
IsPrivate bool `db:"isPrivate"`
Desc string `db:"desc"`
RoleValues []ResRoleValue `db:"role_values"`
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
type GetResConfigures func(uuid string) []ResConfigure
// GetValueByRoles 根据角色优先级获取配置值
//
// - roles: 角色UUIDs
//
// 返回: valuex.Accessor 对象和是否找到角色特定值的标志
func (rc *ResConfigure) GetValueByRoles(roles ...string) (valuex.Accessor, bool) {
// 如果没有角色值配置或没有传入角色,返回默认值
if len(rc.RoleValues) == 0 || len(roles) == 0 {
return reflux.New(rc.toTyped(rc.Value)), true
}
for _, rv := range rc.RoleValues {
for _, role := range roles {
if value, exists := rv[role]; exists {
return reflux.New(rc.toTyped(value)), true
}
}
}
// 没有找到匹配的角色值,返回默认值
return reflux.New(rc.toTyped(rc.Value)), false
}
func (rc *ResConfigure) toTyped(s string) any {
switch rc.Type {
case "json":
var v map[string]any
if json.Unmarshal([]byte(s), &v) == nil {
return v
}
case "array":
var v []any
if json.Unmarshal([]byte(s), &v) == nil {
return v
}
}
return s
}
type GetResConfigure func(key string, roles ...string) (valuex.Accessor, bool)
+4 -261
View File
@@ -1,266 +1,9 @@
package base
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"git.fsdpf.net/go/contracts/res_type"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/schema"
"git.fsdpf.net/go/req"
"github.com/google/uuid"
"github.com/spf13/cast"
"git.fsdpf.net/go/req/resx"
)
// 资源字段
type ResField struct {
Uuid string `db:"uuid"`
Name string `db:"name"`
Code string `db:"code"`
CodeResource string `db:"codeResource"`
DataType req.ResDataType `db:"table_type"`
Length string `db:"length"`
Comment string `db:"comment"`
Default string `db:"default"`
}
func (this ResField) ToStructField(tags ...string) reflect.StructField {
var typ reflect.Type
fCode := this.Code
fTag := `db:"` + fCode + `" json:"` + fCode + `"`
if len(tags) > 0 {
fTag = strings.Join(tags, " ") + " " + fTag
}
switch this.DataType {
case req.ResString, req.ResText, req.ResEnum,
req.ResTimestamp, req.ResDate, req.ResDatetime:
typ = reflect.TypeOf(string(""))
case req.ResInteger, req.ResSmallInteger:
typ = reflect.TypeOf(res_type.ResFieldByInteger(0))
case req.ResDecimal:
typ = reflect.TypeOf(float64(0))
case req.ResBoolean:
typ = reflect.TypeOf(true)
case req.ResJson:
if this.Default != "" && this.Default[0:1] == "[" {
typ = reflect.TypeOf([]any{})
} else {
typ = reflect.TypeOf(map[string]any{})
}
}
return reflect.StructField{
Name: strings.ToUpper(fCode[:1]) + fCode[1:],
Tag: reflect.StructTag(fTag),
Type: typ,
}
}
func (this ResField) GetCode() string {
return this.Code
}
func (this ResField) GetCodeResource() string {
return this.CodeResource
}
func (this ResField) GetName() string {
return this.Name
}
func (this ResField) GetDataType() req.ResDataType {
return this.DataType
}
func (this ResField) GetQueryDataType() req.RouteParamType {
switch this.GetDataType() {
case req.ResEnum,
req.ResTimestamp, req.ResDate,
req.ResDatetime, req.ResString,
req.ResText:
return req.ReqString
case req.ResInteger, req.ResSmallInteger:
return req.ReqInteger
case req.ResDecimal:
return req.ReqFloat
case req.ResBoolean:
return req.ReqBool
case req.ResJson:
if this.Default != "" && this.Default[0:1] == "[" {
return req.ReqArray
}
return req.ReqJson
}
return req.ReqString
}
func (this ResField) ToValue(v any) any {
switch this.DataType {
case req.ResString, req.ResText, req.ResEnum,
req.ResTimestamp, req.ResDate, req.ResDatetime:
return strings.Trim(cast.ToString(v), " ")
case req.ResInteger, req.ResSmallInteger:
return cast.ToInt(v)
case req.ResDecimal:
return strings.Trim(cast.ToString(v), " ")
case req.ResBoolean:
if v, _ := strconv.ParseBool(fmt.Sprintf("%v", v)); v {
return 1
}
return 0
case req.ResJson:
if v == nil {
if this.Default != "" && this.Default[0:1] == "[" {
return db.Raw("'[]'")
} else if this.Default != "" && this.Default[0:1] == "{" {
return db.Raw("'{}'")
} else if this.Default == "" {
return db.Raw("'{}'")
}
return this.Default
}
if str, ok := v.(string); ok {
return str
} else if raw, ok := v.(db.Expression); ok {
return raw
} else if b, err := json.Marshal(v); err == nil {
return string(b)
} else {
panic(fmt.Sprintf("%s, 类型转换错误, %s", this.Code, err))
}
}
return strings.Trim(cast.ToString(v), " ")
}
func (this ResField) GetRawDefault(driver string) db.Expression {
if this.DataType == req.ResJson {
if this.Default != "" && this.Default[0:1] == "[" {
return db.Raw("'[]'")
} else if this.Default != "" && this.Default[0:1] == "{" {
return db.Raw("'{}'")
} else if this.Default == "" {
return db.Raw("'{}'")
}
} else if this.DataType == req.ResBoolean {
if v, _ := strconv.ParseBool(this.Default); v {
return db.Raw("'1'")
}
return db.Raw("'0'")
}
if len(this.Default) > 4 && strings.ToLower(this.Default[0:4]) == "sql:" {
sql := strings.ToLower(this.Default[4:])
if sql == "uuid()" {
if driver == "sqlite" {
return db.Raw("'" + uuid.NewString() + "'")
}
return db.Raw("uuid()")
}
return db.Raw(sql)
}
if this.Default == "" {
if this.GetDataType() == req.ResDate || this.GetDataType() == req.ResDatetime {
return db.Raw("NULL")
}
return db.Raw("''")
}
if strings.ToUpper(this.Default) == "CURRENT_TIMESTAMP" {
return db.Raw(this.Default)
}
return db.Raw("'" + this.Default + "'")
}
func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDefinition) {
isNull := false
comment := this.Name
def := any(this.Default)
if this.Comment != "" {
comment += " [ " + strings.Trim(this.Comment, `' "`) + " ]"
}
switch this.DataType {
case "string":
len := 255
if v, err := strconv.Atoi(strings.Trim(this.Length, `' "`)); err == nil {
len = v
}
temp = table.String(this.Code, len)
case "smallInteger":
// integer 默认长度 4
temp = table.SmallInteger(this.Code)
case "boolean":
// integer 默认长度 1
temp = table.Boolean(this.Code)
case "integer":
// integer 默认长度 11
temp = table.Integer(this.Code)
case "date", "dateTime":
if this.DataType == "date" {
temp = table.Date(this.Code)
} else {
temp = table.DateTime(this.Code)
}
if strings.ToUpper(this.Default) == "CURRENT_TIMESTAMP" {
def = db.Raw(this.Default)
} else if def == "" {
isNull = true
}
case "decimal":
allowed := strings.SplitN(this.Length, ",", 2)
total := 8
places := 2
if v, err := strconv.Atoi(strings.Trim(allowed[0], `' "`)); err == nil {
total = v
}
if v, err := strconv.Atoi(strings.Trim(allowed[1], `' "`)); err == nil {
places = v
}
temp = table.Decimal(this.Code, total, places)
case "enum":
allowed := []string{}
for _, v := range strings.Split(this.Length, ",") {
allowed = append(allowed, strings.Trim(v, `' "`))
}
temp = table.Enum(this.Code, allowed)
case "json":
temp = table.Json(this.Code)
isNull = true
case "text":
temp = table.Text(this.Code)
isNull = true
}
if isNull {
temp.Nullable()
} else {
temp.Default(def)
}
temp.Comment(comment)
return temp
}
func (this ResField) ToQueryField(t req.RouteParamType, alias string, options byte) req.QueryField {
o := &QueryField{
ResField: this,
typ: t,
alias: alias,
}
return o.SetOptions(options)
}
// ResField 的实现搬到了 git.fsdpf.net/go/req/resxresx 自己需要用它做字段级脱敏,
// 不能反过来 import contracts/base,见 resx.ResField 的注释),这里保留类型别名兼容原有引用。
type ResField = resx.ResField
+22
View File
@@ -0,0 +1,22 @@
package base
import (
"testing"
"git.fsdpf.net/go/db/schema"
"git.fsdpf.net/go/req"
"git.fsdpf.net/go/req/resx"
_ "git.fsdpf.net/go/db/schema/dialect/mysql"
_ "git.fsdpf.net/go/db/schema/dialect/sqlite3"
)
func TestResFieldToBlueprint(t *testing.T) {
t.Run("id", func(t *testing.T) {
table := schema.NewBlueprint("users")
id := resx.NewResField("id", "User", resx.FieldWithUuid("00000000-0000-0000-0000-000000000000"), resx.FieldWithName("ID"), resx.FieldWithLength("20"), resx.FieldWithDefault("sql:AUTO_INCREMENT"), resx.FieldWithDataType(req.ResInteger))
id.ToBlueprint(table)
})
}
-26
View File
@@ -1,26 +0,0 @@
package base
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
type ResFields []ResField
func (c *ResFields) Scan(value any) error {
if value == nil {
return nil
}
if err := json.Unmarshal(value.([]byte), c); err != nil {
return fmt.Errorf("ResFields json.Unmarshal Error, %s", err)
}
return nil
}
func (c ResFields) Value() (driver.Value, error) {
b, err := json.Marshal(c)
return string(b), err
}
+36 -2
View File
@@ -1,14 +1,48 @@
package base
import "time"
type ResJob struct {
Uuid string `db:"uuid"`
Code string `db:"code"`
Name string `db:"name"`
ResourceUuid string `db:"resource_uuid"`
Replay int64 `db:"replay"`
IsRecord bool `db:"isRecord"`
MaxAttempts int `db:"maxAttempts"` // 最大重试次数
Concurrent int `db:"concurrent"` // 同类 job 最大并发数,0 表示不限制
Timeout int `db:"timeout"` // 单次执行超时秒数,0 表示不限制
Retry int `db:"retry"` // 重试间隔秒数,0 表示不重试,>0 表示固定间隔秒数
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
type GetResJob func(code string) (ResJob, bool)
const (
FailedJobStatusResolved = 0 // 已解决(成功完成)
FailedJobStatusPending = 1 // 待重试
FailedJobStatusExceeded = 2 // 已超出最大重试次数
FailedJobStatusInterrupted = 3 // shutdown 中断
)
type ResFailedJob struct {
ID int64 `db:"id"`
Queue string `db:"queue"` // 投递队列(ResJob.Code
Payload string `db:"payload"` // JSON
Platform string `db:"platform"`
Saas string `db:"saas"`
TraceId string `db:"trace_id"`
Exception string `db:"exception"`
Attempts int `db:"attempts"`
MaxAttempts int `db:"maxAttempts"`
Status int `db:"status"`
RetryAt *time.Time `db:"retry_at"`
RetriedAt *time.Time `db:"retried_at"`
OwnedUser string `db:"owned_user"` // 触发任务的用户 uuid
CreatedAt string `db:"created_at"`
UpdatedAt string `db:"updated_at"`
}
// CanRetry 是否还可以重试
func (j ResFailedJob) CanRetry() bool {
return j.Status == FailedJobStatusPending && j.Attempts < j.MaxAttempts
}
-13
View File
@@ -1,13 +0,0 @@
package base
type ResListener struct {
Uuid string `db:"uuid"`
Code string `db:"code"`
Name string `db:"name"`
ResourceUuid string `db:"resource_uuid"`
Event []any `db:"event"`
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
type GetResListens func(categoryUuid string, event ...string) []ResListener
+20
View File
@@ -0,0 +1,20 @@
package base
// ResWatcher 描述一条"资源写操作监听"注册记录:Code 对应一个实现了 contracts.ResWatcher 的处理器,
// ResourceUuids 是它关心的资源(支持 1-n 个)
type ResWatcher struct {
Uuid string `db:"uuid"`
Code string `db:"code"`
Name string `db:"name"`
ResourceUuids []string `db:"resource_uuid"`
OwnedUser string `db:"owned_user"` // Handle 执行时使用的用户身份(同 Cron/Mqtt 约定)
MaxDepth int `db:"max_depth"` // 允许级联触发的最大深度,用于防止 watcher 之间循环触发
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
// GetResWatcher 按 code 查询单条 ResWatcher 注册记录
type GetResWatcher func(code string) (ResWatcher, bool)
// GetResWatchers 按资源 uuid 反查关心该资源的 ResWatcher 注册记录
type GetResWatchers func(resourceUuid string) []ResWatcher
-458
View File
@@ -1,458 +0,0 @@
package base
import (
"database/sql"
"fmt"
"log"
"reflect"
"unicode"
"github.com/samber/do"
"github.com/samber/lo"
"git.fsdpf.net/go/condition"
"git.fsdpf.net/go/contracts"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/req"
)
// 资源变更事件
const ResChangeEventTopic = "res-change-event-topic"
// 资源变更数据
const ResChangeRecordTopic = "res-change-record-topic"
type ResChangeEventTopicPayload struct {
Type string // insert | delete | update
Res Resource // 变更资源
Result sql.Result // 执行结果
}
type ResChangeRecordTopicPayload struct {
Type string // insert | delete | update
User req.User // 操作用户
Res Resource // 变更资源
Result sql.Result // 执行结果
Old []map[string]any // 旧数据
New []map[string]any // 新数据
}
// 资源
type Resource struct {
container *do.Injector
Uuid string `db:"uuid"`
PUuid string `db:"pUuid"`
Code string `db:"code"`
Name string `db:"name"`
IsResVirtual bool `db:"isVirtual"`
Table string `db:"table"`
Namespace string `db:"namespace"`
Workspace string `db:"workspace"`
Primarykey string `db:"primaryKey"`
IsHistoryRecord bool `db:"isHistoryRecord"`
HistoryCacheMax int `db:"historyCacheMax"`
Fields ResFields `db:"fields"`
Roles map[string]any `db:"roles"`
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
func (this *Resource) InitContainer(container *do.Injector) {
this.container = container
}
// 资源UUID
func (this Resource) GetUuid() string {
return this.Uuid
}
// 资源CODE
func (this Resource) GetCode() string {
return this.Code
}
// 资源名
func (this Resource) GetName() string {
return this.Name
}
// 主键
func (this Resource) GetPrimarykey() string {
return this.Primarykey
}
// 是否虚拟资源
func (this Resource) IsVirtual() bool {
return this.IsResVirtual
}
// 是否系统资源
func (this Resource) IsSystem() bool {
return this.Namespace == "Framework\\Service"
}
// 资源字段
func (this Resource) GetFields() (result []req.ResField) {
for _, item := range this.Fields {
result = append(result, item)
}
return result
}
// 资源字段
func (this Resource) GetField(code string) (req.ResField, bool) {
return lo.Find(this.GetFields(), func(v req.ResField) bool {
return v.GetCode() == code
})
}
// 判断资源字段
func (this Resource) HasField(code string) bool {
return lo.SomeBy(this.GetFields(), func(v req.ResField) bool {
return v.GetCode() == code
})
}
// 开启事物
func (this Resource) BeginTransaction() (*db.Transaction, error) {
return this.GetDBConn().BeginTransaction()
}
// 获取资源链接
func (this Resource) GetDBConn() *db.Connection {
db := do.MustInvoke[db.DB](this.container)
if this.IsSystem() {
return db.Connection("service-support")
}
return db.Connection("default")
}
// 获取资源对应的数据库连接
func (this Resource) GetDBBuilder() *db.Builder {
return this.GetDBConn().Query()
}
// 获取资源对应的数据表
func (this Resource) GetTable() db.Expression {
if this.IsVirtual() {
return db.Raw("(" + this.Table + ")")
}
return db.Raw(this.Table)
}
func (this Resource) GetDBDriver() string {
return this.GetDBConn().GetConfig().Driver
}
func (this Resource) GetAuthDBTable(u req.User, params ...any) *db.Builder {
builder := this.GetDBTable(append(params, u)...)
// 数据权限过滤
builder.Before(func(b *db.Builder, t string, data ...map[string]any) {
if t == db.TYPE_SELECT || t == db.TYPE_UPDATE || t == db.TYPE_DELETE {
this.WithRolesCondition(b, t, u)
}
})
return builder
}
// GetDBTable("Test", contracts.User)
func (this Resource) GetDBTable(params ...any) *db.Builder {
builder := this.GetDBBuilder()
var user req.User
alias := this.Code
for _, param := range params {
switch v := param.(type) {
case *db.Transaction:
builder.Tx = v
case string:
alias = v
case req.User:
user = v
}
}
// 格式化数据库存储数据
builder.Before(func(b *db.Builder, t string, data ...map[string]any) {
if t == db.TYPE_UPDATE {
// 格式化保存数据
this.formatSaveValue(data[0])
}
if t == db.TYPE_INSERT {
// 移除 table alias
b.Table(string(this.GetTable()))
for i := 0; i < len(data); i++ {
// 格式化保存数据
this.formatSaveValue(data[i])
// 填充保存数据
this.fillSaveValue(data[i], user, db.TYPE_INSERT)
}
}
})
// 资源事件
this.onResEvent(builder)
// 用户事件
if this.IsHistoryRecord {
this.onUserEvent(builder, user)
}
// 虚拟资源暂时不考虑鉴权
if !this.IsVirtual() {
// 返回鉴权后的 DB Builder
// return
}
return builder.Table(string(this.GetTable()), alias)
}
func (this Resource) WithRolesCondition(b *db.Builder, t string, u req.User) error {
isFullRight := false
isFullNot := false
NewOrm := do.MustInvoke[contracts.NewOrm](this.container)
NewOrmModel := do.MustInvoke[contracts.NewOrmModel](this.container)
NewOrmJoin := do.MustInvoke[contracts.NewOrmJoin](this.container)
GetResRelationResource := do.MustInvoke[GetResRelationResource](this.container)
GetResRelations := do.MustInvoke[GetResRelations](this.container)
GetResource := do.MustInvoke[contracts.GetResource](this.container)
GetOrmConditions := do.MustInvoke[contracts.GetOrmConditions](this.container)
items := do.MustInvoke[GetResRoles](this.container)(this.GetUuid(), u.Roles()...)
subTables := lo.Reduce(items, func(carry string, item ResRole, _ int) string {
db := this.GetDBBuilder().Table(string(this.GetTable()), this.GetCode()).Select(db.Raw("distinct `" + this.GetCode() + "`.*"))
joins := lo.Filter(GetResRelations(item.Uuid), func(item ResRelation, _ int) bool {
return item.Type == "inner" || item.Type == "left" || item.Type == "right"
})
for i := 0; i < len(joins); i++ {
oResource, ok := GetResource(joins[i].ResourceCode)
if !ok {
continue
}
rResource, ok := GetResRelationResource(joins[i])
if !ok {
continue
}
join := NewOrmJoin(contracts.RelationType(joins[i].Type), oResource, joins[i].Code, joins[i].RelationResource, joins[i].RelationField, joins[i].RelationForeignKey)
// 关联扩展条件
join.SetCondition(GetOrmConditions(joins[i].Uuid, condition.Describe("关联扩展条件")))
join.Inject(db, NewOrmModel(rResource, rResource.GetCode(), rResource.GetName()))
}
conditions := GetOrmConditions(item.Uuid, condition.Describe("关联扩展条件"))
if len(joins) == 0 && conditions.IsEmpty() {
// 无权限, 直接跳过这个 unoin 语句
if carry != "" {
return carry
}
// 第一个无权限除外, 避免所有用户所属角色都是无权限
db.WhereRaw("false")
isFullNot = true
} else if len(joins) == 0 && conditions.IsNotEmpty() && conditions.IsAlwaysRight() /* 1=1 的这种条件*/ {
// 只要有1个满权限, 直接返回单条语句
isFullRight = true
return db.ToSql()
} else if conditions.IsNotEmpty() {
oOrm := NewOrm(this, nil)
oOrm.SetGlobalParams(req.NewGlobalParam("{}", u))
db.Where(conditions.ToSql(oOrm.GetModel()))
// 如果前面是无权限的sql查看, 这直接返回本次查询
if isFullNot {
isFullNot = false
return db.ToSql()
}
}
if carry != "" {
carry += " UNION "
}
return fmt.Sprintf("%s(%s)", carry, db.ToSql())
}, "")
if isFullRight {
return nil
}
if isFullNot {
b.WhereRaw("false")
} else if subTables != "" {
if t == db.TYPE_SELECT {
b.FromSub(subTables, b.TableAlias)
} else {
b.WhereRaw(fmt.Sprintf(
"`%s`.`id` in (SELECT `temp`.`id` FROM (%s) as `temp`)",
lo.Ternary(b.TableAlias != "", b.TableAlias, this.GetCode()),
subTables,
))
}
}
return nil
}
// 格式化保存数据
func (this Resource) formatSaveValue(data map[string]any) {
//
for k, v := range data {
if k == "id" || k == "created_user" || k == "created_at" || k == "deleted_at" || k == "updated_at" {
delete(data, k)
} else if val, ok := v.(db.Expression); ok {
data[k] = val
} else if field, ok := this.GetField(k); ok {
data[k] = field.ToValue(v)
}
}
}
// 填充保存数据
func (this Resource) fillSaveValue(data map[string]any, u req.User, t string) {
for _, field := range this.GetFields() {
fCode := field.GetCode()
if fCode == "id" || fCode == "created_user" || fCode == "created_at" || fCode == "deleted_at" || fCode == "updated_at" || fCode == "owned_user" {
continue
}
// 只有新增默认字段
if _, ok := data[fCode]; !ok {
data[fCode] = field.GetRawDefault(this.GetDBDriver())
}
}
// 拥有者
if _, ok := data["owned_user"]; !ok {
data["owned_user"] = u.Uuid()
}
// 创建者
data["created_user"] = u.Uuid()
if this.GetDBDriver() == "sqlite" {
// 更新时间
// sqlite 不能自动更新时间, "DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
data["updated_at"] = db.Raw("CURRENT_TIMESTAMP")
} else {
}
}
func (this Resource) GetStruct(extends ...reflect.StructField) any {
fields := []reflect.StructField{}
for _, field := range this.Fields {
if unicode.IsLetter(rune(field.Code[0])) {
fields = append(fields, field.ToStructField())
} else {
log.Printf("资源字段错误, 必须以字母开头 <- %s", field.Code)
}
}
fields = lo.UniqBy(append(fields, extends...), func(v reflect.StructField) string {
return v.Name
})
t := reflect.StructOf(fields)
return reflect.New(t).Interface()
}
func (this Resource) GetSliceStruct(extends ...reflect.StructField) any {
t := reflect.TypeOf(this.GetStruct(extends...))
st := reflect.SliceOf(t.Elem())
return reflect.New(st).Interface()
}
// 资源事件
func (this Resource) onResEvent(builder *db.Builder) {
builder.After(func(b *db.Builder, t string, result sql.Result, err error, data ...map[string]any) {
if err != nil || t == db.TYPE_SELECT {
return
} else if num, err := result.RowsAffected(); num == 0 || err != nil {
return
}
// 全局触发器
// 1. 清除系统缓存
if err := do.MustInvoke[contracts.Queue](this.container).Publish(ResChangeEventTopic, ResChangeEventTopicPayload{
Type: t,
Res: this,
Result: result,
}); err != nil {
log.Println("Queue Publish Err:", ResChangeEventTopic, err)
}
})
}
// 用户事件
func (this Resource) onUserEvent(builder *db.Builder, user req.User) {
old := []map[string]any{}
builder.Before(func(b *db.Builder, t string, data ...map[string]any) {
if t != db.TYPE_UPDATE && t != db.TYPE_DELETE {
return
}
// 查询保存之前的数据
if _, err := b.Get(&old); err != nil {
panic(err)
}
})
builder.After(func(b *db.Builder, t string, result sql.Result, err error, data ...map[string]any) {
if err != nil || t == db.TYPE_SELECT {
return
} else if num, err := result.RowsAffected(); num == 0 || err != nil {
return
}
if user == nil {
user = GetAnonymous()
}
// 触发消息队列
if err := do.MustInvoke[contracts.Queue](this.container).Publish(ResChangeRecordTopic, ResChangeRecordTopicPayload{
Type: t,
User: user,
Res: this,
Old: old,
New: data,
Result: result,
}); err != nil {
log.Println("Queue Publish Err:", ResChangeRecordTopic, err)
}
})
}
func NewVirtualResource(pRes Resource, code, name, sql string, fields []ResField) req.Resource {
return &Resource{
Uuid: code,
PUuid: pRes.Uuid,
Code: code,
Name: name,
IsResVirtual: true,
Table: sql,
Namespace: pRes.Namespace,
Workspace: pRes.Workspace,
IsHistoryRecord: false,
HistoryCacheMax: 0,
Fields: fields,
Roles: nil,
}
}
+1
View File
@@ -15,6 +15,7 @@ type Route struct {
Exact bool `db:"exact"`
Platform string `db:"platform"`
Extra map[string]any `db:"extra"`
Roles []string `db:"roles"`
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
+7 -82
View File
@@ -1,88 +1,13 @@
package base
import (
"github.com/samber/lo"
"git.fsdpf.net/go/req"
"git.fsdpf.net/go/req/userx"
)
type user struct {
id int64
uuid string
username string
nickname string
roles []string
runtime req.UserRuntime
}
// GetAnonymous 获取匿名用户。实现搬到了 req/userx(req 自己的子包,不反过来依赖 contracts),
// 这里保留只是兼容原有调用方,函数本身没法用 type = 起别名(type 只能别名类型,不能别名函数),
// 用 var 绑同一个函数值效果等价。
var GetAnonymous = userx.GetAnonymous
func (this user) ID() int64 {
return this.id
}
func (this user) Uuid() string {
return this.uuid
}
func (this user) Username() string {
return this.username
}
func (this user) Nickname() string {
return this.nickname
}
func (this user) GetUserInfo() map[string]any {
return nil
}
func (this user) Roles() (roles []string) {
return this.roles
}
func (this user) HasUserRoles(roles ...string) bool {
return lo.Contains(this.Roles(), "ffffffff-ffff-ffff-ffff-ffffffffffff") || len(lo.Intersect(this.Roles(), roles)) > 0
}
func (this user) IsAnonymous() bool {
return this.id == 0
}
func (this user) Runtime() req.UserRuntime {
return this.runtime
}
// 获取匿名用户
func GetAnonymous(opts ...req.UserRuntimeOption) req.User {
u := user{
id: 0,
uuid: "00000000-0000-0000-0000-000000000000",
username: "anonymous",
nickname: "匿名者",
roles: []string{"00000000-0000-0000-0000-000000000000"},
runtime: req.UserRuntime{},
}
for _, opt := range opts {
opt(&u.runtime)
}
return u
}
// 系统用户
func GetSystemUser(opts ...req.UserRuntimeOption) req.User {
u := user{
id: -1,
uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff",
username: "system",
nickname: "系统",
roles: []string{"ffffffff-ffff-ffff-ffff-ffffffffffff"},
runtime: req.UserRuntime{},
}
for _, opt := range opts {
opt(&u.runtime)
}
return u
}
// GetSystemUser 系统用户,同上。
var GetSystemUser = userx.GetSystemUser
+24
View File
@@ -0,0 +1,24 @@
package base
import (
"git.fsdpf.net/go/req"
)
type UserAsset struct {
ID int64 `db:"id"`
Name string `db:"name"`
FileName string `db:"filename"`
FileUrl string `db:"file_url"`
File string `db:"file"`
Mime string `db:"mime"`
Size int64 `db:"size"`
Ext string `db:"ext"`
Hit int64 `db:"hit"`
IsPrivate bool `db:"isPrivate"`
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
type GetUserAsset func(user req.User, code string) (UserAsset, bool)
type UploadUserAsset func(user req.User, file string, name string) (UserAsset, error)
+15
View File
@@ -0,0 +1,15 @@
package contracts
import (
"git.fsdpf.net/go/reflux"
"git.fsdpf.net/go/req"
)
type CondflowService interface {
Run(name string, input reflux.R, u req.User) error
}
type FlowDecision interface {
Controller
Res() req.Resource
}
+12 -22
View File
@@ -1,35 +1,25 @@
package contracts
import (
"log/slog"
"git.fsdpf.net/go/req"
"github.com/samber/do"
)
// 日志结构化字段的标准 key:LogKeyKind 标识控制器大类(Http/Cron/Job 等),
// LogKeyComponent 标识具体是哪一个实例/类型。
const (
LogKeyKind = "kind"
LogKeyComponent = "component"
)
type Controller interface {
// 初始化
Init() error
// 获取 Container
Container() *do.Injector
Container() Injector
// 派遣一个任务
Dispatch(job string, payload any, u req.User) error
}
type BaseController struct {
container *do.Injector
}
func (this BaseController) Container() *do.Injector {
return this.container
}
func (BaseController) Init() error {
return nil
}
func (this BaseController) Dispatch(job string, payload any, u req.User) error {
return do.MustInvoke[JobService](this.Container()).Dispatch(job, payload, u)
}
func NewBaseController(container *do.Injector) Controller {
return &BaseController{container}
// Logger 返回当前进程的默认 slog logger
Logger() *slog.Logger
}
+1 -8
View File
@@ -4,7 +4,7 @@ import "git.fsdpf.net/go/req"
type CronService interface {
Start() error
Stop() error
Shutdown() error
Restart() error
}
@@ -14,10 +14,3 @@ type Cron interface {
Run(req.User) error
}
type CronBase struct {
Controller
}
func (CronBase) Run(req.User) error {
return nil
}
+21
View File
@@ -0,0 +1,21 @@
package contracts
import "github.com/samber/do/v2"
type Injector = do.Injector
func Invoke[T any](i Injector) (T, error) {
return do.Invoke[T](i)
}
func MustInvoke[T any](i Injector) T {
return do.MustInvoke[T](i)
}
func InvokeNamed[T any](i Injector, name string) (T, error) {
return do.InvokeNamed[T](i, name)
}
func MustInvokeNamed[T any](i Injector, name string) T {
return do.MustInvokeNamed[T](i, name)
}
+10 -1
View File
@@ -101,6 +101,7 @@ var (
// 请求第三个接口
ErrApiRequest = &Errno{Code: 20501, Msg: "Api Request Error"}
ErrApiResponsed = &Errno{Code: 20502, Msg: "Api Responsed Error"}
ErrApiAuth = &Errno{Code: 20503, Msg: "Api Auth Error"}
// 验证码错误,206
ErrCaptcha = &Errno{Code: 20601, Msg: "验证码生成器不存在"}
@@ -110,7 +111,15 @@ var (
ErrCaptchaSend = &Errno{Code: 20605, Msg: "验证码发送错误"}
ErrCaptchaInterval = &Errno{Code: 20606, Msg: "验证码获取失败, 请稍后再试"}
//
// Pkg 错误
ErrPkgUnknown = &Errno{Code: 20701, Msg: "pkg unknown"}
ErrPkgInvalid = &Errno{Code: 20702, Msg: "pkg invalid"}
ErrPkgPlatformInvalid = &Errno{Code: 20703, Msg: "pkg platform invalid"}
ErrPkgUpgrade = &Errno{Code: 20704, Msg: "pkg upgrade fail"}
ErrPkgRollback = &Errno{Code: 20705, Msg: "pkg version rollback fail"}
ErrPkgDownload = &Errno{Code: 20706, Msg: "pkg download fail"}
ErrPkgUpload = &Errno{Code: 20707, Msg: "pkg upload fail"}
ErrPkgBuild = &Errno{Code: 20708, Msg: "pkg build fail"}
)
// 使用 错误码 和 error 创建新的 错误
+21
View File
@@ -0,0 +1,21 @@
package contracts
import (
"net/http"
"git.fsdpf.net/go/req"
)
type EventStream interface {
Controller
// 获取请求信息
Request() *http.Request
// 请求处理
Execute(req.GlobalParams) error
// 获取用户信息
User() req.User
// 路由信息
Route() req.Route
// 发送数据
Sent(data, event string) error
}
+10
View File
@@ -0,0 +1,10 @@
package contracts
import (
"git.fsdpf.net/go/req"
)
type Executor interface {
Controller
Res() req.Resource
}
+35 -26
View File
@@ -1,56 +1,65 @@
module git.fsdpf.net/go/contracts
go 1.21
go 1.25.5
require (
git.fsdpf.net/go/condition v0.0.0-20240509034445-afc243ea916e
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b
github.com/go-chi/chi/v5 v5.0.12
github.com/golang/protobuf v1.5.2
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/jhump/protoreflect v1.15.1
github.com/lestrrat-go/jwx v1.2.25
github.com/samber/do v1.6.0
github.com/samber/lo v1.39.0
github.com/spf13/cast v1.6.0
github.com/spf13/viper v1.15.0
github.com/tidwall/gjson v1.17.1
github.com/tidwall/sjson v1.2.5
google.golang.org/grpc v1.52.0
git.fsdpf.net/go/condition v0.0.0-20260821005829-d6c354769032
git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508
git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1
git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff
github.com/google/jsonschema-go v0.4.3
github.com/mark3labs/mcp-go v0.54.1
github.com/samber/do/v2 v2.0.0
github.com/sashabaranov/go-openai v1.41.2
)
require (
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-sqlite3 v1.14.42 // indirect
github.com/samber/go-type-to-string v1.8.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
)
require (
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758 // indirect
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff // indirect
github.com/bufbuild/protocompile v0.4.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/protobuf v1.5.2
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jhump/protoreflect v1.15.1
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.1 // indirect
github.com/lestrrat-go/jwx v1.2.25
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/samber/lo v1.49.1
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.10.0
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.15.0
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tidwall/gjson v1.17.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect
google.golang.org/grpc v1.52.0
google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+65 -64
View File
@@ -36,36 +36,20 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.fsdpf.net/go/condition v0.0.0-20240509022115-37018837d1b0 h1:MHjkN+zrSCqb7Qswn9fFrHT4XaoDnGbXz0ssSKnWnQk=
git.fsdpf.net/go/condition v0.0.0-20240509022115-37018837d1b0/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q=
git.fsdpf.net/go/condition v0.0.0-20240509023514-a871ccfa75ac h1:pJQ0sFjPtv/RCy8k0mC/Kpn/j6As5bSqXyG0MWaylhc=
git.fsdpf.net/go/condition v0.0.0-20240509023514-a871ccfa75ac/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q=
git.fsdpf.net/go/condition v0.0.0-20240509024751-9694d9033266 h1:s5On5PKF1ZiIGgq6R2Nk/2GcURuZxDAVK1Sru54Ybn0=
git.fsdpf.net/go/condition v0.0.0-20240509024751-9694d9033266/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q=
git.fsdpf.net/go/condition v0.0.0-20240509030058-b0e337133e44 h1:XHJxWJuofEL7TLMDBsywi48nciaon8bEFOJLMYkOrNk=
git.fsdpf.net/go/condition v0.0.0-20240509030058-b0e337133e44/go.mod h1:xgX0fc3LEgrPk9tihGQqoQND4EWlNgYbLCi70TuCvvM=
git.fsdpf.net/go/condition v0.0.0-20240509034445-afc243ea916e h1:ZfwjqZS5URF4Y0JfQeytSdmXimJ4l74ooWRppza+3Tk=
git.fsdpf.net/go/condition v0.0.0-20240509034445-afc243ea916e/go.mod h1:K5ocyKVxgC5mI1k/crgrEozK0r51qAavWg0hweXo6PU=
git.fsdpf.net/go/db v0.0.0-20230621051209-5740d112407f h1:H+/H6j+hyFMgSy+tUNmymRy7/PWQRjjp8qY/DrhwrsY=
git.fsdpf.net/go/db v0.0.0-20230621051209-5740d112407f/go.mod h1:397Sdx1cJS0OlHtTX1bVl//9k3Xn0Klnc6jC4MAkb6w=
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b h1:fRgWNOQ9dAYuUZHQ24oi1XqRbJIcJvZGbnQDaAKI7IY=
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b/go.mod h1:397Sdx1cJS0OlHtTX1bVl//9k3Xn0Klnc6jC4MAkb6w=
git.fsdpf.net/go/req v0.0.0-20240508133526-672fc634ef20 h1:PhjRtac2r0Vn1WBBzhRhz4//bJhHgkf/SHZe3mQYPto=
git.fsdpf.net/go/req v0.0.0-20240508133526-672fc634ef20/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
git.fsdpf.net/go/req v0.0.0-20240509031332-f00b993f9c7d h1:TKNhQWDKyL50eVC/ptur1YuRkjRnKOlMsE1Y9tFCVbk=
git.fsdpf.net/go/req v0.0.0-20240509031332-f00b993f9c7d/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
git.fsdpf.net/go/req v0.0.0-20240509031626-6501b5ed0f2b h1:KlVBAuWnFt1lcRTCcBU165MIldJqleO/LahWrd3FvUA=
git.fsdpf.net/go/req v0.0.0-20240509031626-6501b5ed0f2b/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
git.fsdpf.net/go/req v0.0.0-20240509033523-c8cda2d8db56 h1:7ElgXUzt75brzOr44mItipTQTk++kZNoGOyTWHLxkTU=
git.fsdpf.net/go/req v0.0.0-20240509033523-c8cda2d8db56/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
git.fsdpf.net/go/req v0.0.0-20240509033936-f8a11598db60 h1:hHrEcUSbtajC2LAo2fvEo7yrSSDeM/s5FAuQluP7yZQ=
git.fsdpf.net/go/req v0.0.0-20240509033936-f8a11598db60/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758 h1:nHys8Bwb29b/FvnE100KVH62aD2j5CEPxTQ/RRHo27k=
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
git.fsdpf.net/go/condition v0.0.0-20260821005829-d6c354769032 h1:6sLpt4Ste4Kfr0GLJRHYTQInodMOUOK5dm4iVC1CNQU=
git.fsdpf.net/go/condition v0.0.0-20260821005829-d6c354769032/go.mod h1:fgyIm04l+hAGv6Z8oKQqX61CATpzw7D2qaxD1x2ffmA=
git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508 h1:xv2SV/Qb0TKJFnzfBtSKSeXwa6NZPRBVoGfWDahsezo=
git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508/go.mod h1:oDPmvCdZn/vTpbuPYPYkjdrhpis/XtMIi3eni4sYRvM=
git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1 h1:TRn++7oWwtANQM5JWTQGdW6fAkWMIbIsZX6tzmnsrLQ=
git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1/go.mod h1:8OLCLMUwdsZ8u2sRI9y/qaE4fTf5d0SrCJ6Zn+lttFY=
git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0 h1:HWM1soZb13IIQA69M8DgyLYu9goz7PArtWI2mQay918=
git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0/go.mod h1:Lm8WoA8EYs2696rmlxpySoCQY26Qh97cDxVF6NLBfZ8=
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff h1:1hokQGKGIstoI7WDBir2N24OGgnussZ3GmucK4TQRuI=
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff/go.mod h1:NUoyQtFr905YT+pi850mvSf4YX0WghQIcMQkTvize5o=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -82,26 +66,28 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -142,7 +128,10 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -158,8 +147,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@@ -178,10 +167,12 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
@@ -194,10 +185,14 @@ github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0ot
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mark3labs/mcp-go v0.54.1 h1:Ap/ptEB9FtWzFKM8NDsTA7QDxerQOC06eZigrTldVj0=
github.com/mark3labs/mcp-go v0.54.1/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
@@ -209,19 +204,22 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/samber/do v1.6.0 h1:Jy/N++BXINDB6lAx5wBlbpHlUdl0FKpLWgGEV9YWqaU=
github.com/samber/do v1.6.0/go.mod h1:DWqBvumy8dyb2vEnYZE7D7zaVEB64J45B0NjTlY/M4k=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/do/v2 v2.0.0 h1:tnunwWaoqSfJ9hxVIaJawIo7JXHQlqT9d9YBXlE9Keg=
github.com/samber/do/v2 v2.0.0/go.mod h1:ZSBCE7Xr6nTNIOVo4DBrkl2+ydUbIOzJjjdV8En5XO4=
github.com/samber/go-type-to-string v1.8.0 h1:5z6tDTjtXxkIAoAuHAZYMYR8mkBZjVgeSH7jcSLqc8w=
github.com/samber/go-type-to-string v1.8.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -231,6 +229,8 @@ github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jH
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@@ -238,13 +238,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -253,6 +252,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -271,8 +272,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -283,8 +284,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -340,8 +339,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -361,7 +360,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -398,8 +398,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -408,8 +408,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -561,8 +561,9 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743 h1:yqElulDvOF26oZ2O+2/aoX7mQ8DY/6+p39neytrycd8=
google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+13
View File
@@ -0,0 +1,13 @@
package contracts
type GoBridgeService interface {
Start() error
Shutdown() error
Restart() error
}
// GoBridge 继承 Controller,可通过 gobridge.WithHandlers 绑定到 Pool
// 所有公开方法自动暴露给 Python 通过 call_go() 调用。
type GoBridge interface {
Controller
}
-7
View File
@@ -16,10 +16,3 @@ type GRpc interface {
GetGRpcServiceDesc() *grpc.ServiceDesc
}
type GRpcBase struct {
Controller
}
func (GRpcBase) GetGRpcServiceDesc() *grpc.ServiceDesc {
return nil
}
+41 -52
View File
@@ -1,77 +1,66 @@
package contracts
import (
"io"
"net/http"
"git.fsdpf.net/go/req"
"github.com/samber/do"
)
var defaultWsClientGroup WsClientGroup = "__DEFAULT__"
var wsClientID WsClientID = 0
type Http interface {
type HTTPService interface {
// Start 启动 HTTP 服务。
Start() error
Stop() error
// Shutdown 停止 HTTP 服务,等待当前请求处理完成后退出。
Shutdown() error
// Restart 重启 HTTP 服务,先停止再重新加载路由并启动。
Restart() error
// Invoke 在进程内直接调用 HTTP 路由处理器,不通过网络。
//
// 参数:
// - method: HTTP 方法,如 "GET"、"POST" 等。
// - path: 请求路径,如 "/api/user/list"。
// - body: 请求体,无请求体时传 nil。
// - headers: 附加请求头键值对,无需额外请求头时传 nil。
// - caller: 指定认证中间件应认证成的调用者身份,为 nil 时按 headers 里的
// Authorization/token 正常解析(等同于外部请求)。只有进程内部
// 发起调用时才可能传非 nil 值,不受外部请求影响。
//
// 返回:
// - *http.Response: 包含状态码、响应头及响应体的结果。
// - error: 构造请求失败时返回错误。
Invoke(method, path string, body io.Reader, header http.Header, caller req.User) (*http.Response, error)
}
type HttpController interface {
Controller
// 获取用户信息
// User 返回当前请求的认证用户信息
User() req.User
// 获取请求信息
// Request 返回当前 HTTP 请求对象。
Request() *http.Request
// 请求处理
// Execute 处理请求并返回响应数据。
Execute(req.GlobalParams) any
// 路由信息
// Route 返回当前请求匹配到的路由信息
Route() req.Route
// 内部调用
Call(code string, params map[string]any, category ...req.RouteCategory) (req.HttpResponse, error)
// 内部调用
// Invoke()
// Call 在进程内以路由 code 发起内部调用,等同于向自身发送 HTTP 请求。
//
// 参数:
// - code: 路由标识码。
// - params: 附加请求参数。
// - category: 可选的路由匹配选项。
//
// 返回:
// - req.HttpResponse: 内部调用的响应结果。
// - error: 调用失败时返回错误。
Call(code string, params map[string]any, category ...req.RouteMatchOption) (req.HttpResponse, error)
// 数据库权限
AuthDB() req.ResAuthDB
}
type HttpHandleController struct {
Controller
request *http.Request
}
func (this HttpHandleController) Request() *http.Request {
return this.request
}
func (this HttpHandleController) Route() req.Route {
return this.Request().Context().Value(req.RouteCtx{Name: "Route"}).(req.Route)
}
func (this HttpHandleController) User() req.User {
return this.Request().Context().Value(req.RouteCtx{Name: "User"}).(req.User)
}
func (this HttpHandleController) Call(code string, params map[string]any, category ...req.RouteCategory) (req.HttpResponse, error) {
return do.MustInvoke[req.Router](this.Container()).Call(this.Request(), code, params, category...)
}
func (HttpHandleController) Execute(params req.GlobalParams) any {
return nil
}
func (HttpHandleController) AuthDB() req.ResAuthDB {
return req.ResAuthOn
}
func NewHttpController(container *do.Injector, request *http.Request) HttpController {
return &HttpHandleController{
Controller: &BaseController{container},
request: request,
}
// AuthDB 返回当前控制器的数据库权限模式。
AuthDB() req.ResFlags
}
+12 -6
View File
@@ -4,7 +4,7 @@ import "git.fsdpf.net/go/req"
type JobService interface {
Start() error
Stop() error
Shutdown() error
Restart() error
Dispatch(job string, payload any, u req.User) error
}
@@ -13,12 +13,18 @@ type Job interface {
Controller
// 任务处理
Handle(any, req.User) error
// OnExceeded 重试次数耗尽时由框架回调,用于清理状态。默认实现为空操作。
OnExceeded(any, req.User) error
}
type JobBase struct {
Controller
// NoRetryError 表示不可重试的错误,job 返回此类型后框架立即标记为 exceeded,不再调度重试。
type NoRetryError struct {
Err error
}
func (JobBase) Handle(any, req.User) error {
return nil
}
func (e *NoRetryError) Error() string { return e.Err.Error() }
func (e *NoRetryError) Unwrap() error { return e.Err }
// ErrNoRetry 将 err 包装为不可重试错误,job handler 使用此函数终止重试。
func ErrNoRetry(err error) error { return &NoRetryError{Err: err} }
+2 -2
View File
@@ -4,11 +4,11 @@ import (
"net/http"
"github.com/lestrrat-go/jwx/jwt"
"github.com/samber/do"
"github.com/samber/do/v2"
)
type JWTAuth interface {
HttpMiddleware(container *do.Injector) func(next http.Handler) http.Handler
HttpMiddleware(container do.Injector) func(next http.Handler) http.Handler
Encode(claims map[string]interface{}) (token string, err error)
Decode(token string) (jwt.Token, error)
}
+76
View File
@@ -0,0 +1,76 @@
package contracts
import (
"context"
"git.fsdpf.net/go/req"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
openai "github.com/sashabaranov/go-openai"
)
// InjectOption 是 InjectTools 的可选配置项。
type InjectOption func(*InjectOptions)
type InjectOptions struct {
Codes []string
OnCall func(openai.ToolCall) // 工具执行前回调
OnResult func(openai.ToolCall, string) // 工具执行后回调
}
// WithFilter 只注入指定 apiCode 的 MCP server 工具;不传则注入全部已注册的。
func WithFilter(codes ...string) InjectOption {
return func(o *InjectOptions) { o.Codes = codes }
}
// WithOnCall 设置工具执行前回调,tc 为完整的 ToolCall 对象。
func WithOnCall(fn func(tc openai.ToolCall)) InjectOption {
return func(o *InjectOptions) { o.OnCall = fn }
}
// WithOnResult 设置工具执行后回调,tc 为 ToolCall 对象,result 为返回文本。
func WithOnResult(fn func(tc openai.ToolCall, result string)) InjectOption {
return func(o *InjectOptions) { o.OnResult = fn }
}
type MCPService interface {
Serve(req.Route) MCPServer
Shutdown() error
// InjectTools 将工具列表注入 ChatCompletionRequest.Tools,同时返回 Instructions 和工具调用执行器。
// ctx 须携带 req.UserWithToolFilter 据此过滤工具)。
// 通过 WithOnCall / WithOnResult 设置实时进度回调,WithFilter 限定注入范围。
InjectTools(ctx context.Context, impl mcp.Implementation, req *openai.ChatCompletionRequest, opts ...InjectOption) (
instructions string,
handleToolCalls func(toolCalls []openai.ToolCall) []openai.ChatCompletionMessage,
err error,
)
}
type MCPServer interface {
Server
Shutdown() error
}
type MCPTool interface {
Controller
// 获取 MCPServer
MCP() *server.MCPServer
// Sent 在工具执行期间通过 notifications/progress 向客户端推送进度消息。
// 客户端请求须携带 _meta.progressToken,否则本方法为空操作。
Sent(ctx context.Context, data string) error
// Execute 处理请求并返回响应数据,返回值支持以下类型:
// - mcp.Content / []mcp.Content 直接作为工具内容返回
// - *mcp.CallToolResult 完整结果,直接透传
// - contracts.Errno / *contracts.Errno 业务错误码,转为工具错误
// - contracts.Err / *contracts.Err 同上
// - string 文本结果
// - []byte 文本结果(原始字节)
// - error 工具错误
// - nil 空结果
// - 其他任意类型 JSON 序列化后返回,同时填充 StructuredContent
Execute(context.Context, req.GlobalParams) any
}
type MCPResource interface {
Controller
}
+3 -33
View File
@@ -2,12 +2,11 @@ package contracts
import (
"git.fsdpf.net/go/req"
"github.com/samber/do"
)
type MqttService interface {
Start() error
Stop() error
Shutdown() error
Restart() error
// 消息推送
@@ -33,9 +32,9 @@ type MqttService interface {
type Mqtt interface {
Controller
// 链接
// OnConnect(g GlobalParams, topic string) error
OnConnect(g req.GlobalParams) error
// 断开链接
// OnDisconnect(g GlobalParams, topic string) error
OnDisconnect(g req.GlobalParams) error
// 订阅事件
OnSubscribed(g req.GlobalParams, topic string) error
// 取消订阅事件
@@ -56,32 +55,3 @@ type Mqtt interface {
// OnRetainedExpired(g GlobalParams, topic string) error
}
type MqttHandle struct {
Controller
}
func (this *MqttHandle) OnSubscribed(g req.GlobalParams, topic string) error {
return ErrFuncNotImplemented
}
func (this MqttHandle) OnUnsubscribed(g req.GlobalParams, topic string) error {
return ErrFuncNotImplemented
}
func (this MqttHandle) OnMessage(g req.GlobalParams, topic string, retain bool, qos byte) error {
return ErrFuncNotImplemented
}
func (this MqttHandle) OnMessageDropped(g req.GlobalParams, topic string, retain bool, qos byte) error {
return ErrFuncNotImplemented
}
func (this MqttHandle) OnPublish(g req.GlobalParams, topic string, retain bool, qos byte, raw []byte) ([]byte, error) {
return nil, ErrFuncNotImplemented
}
func NewMqttController(container *do.Injector) Mqtt {
return &MqttHandle{
Controller: &BaseController{container},
}
}
+52 -43
View File
@@ -1,10 +1,13 @@
package contracts
import (
"context"
"reflect"
"git.fsdpf.net/go/condition"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/reflux"
"git.fsdpf.net/go/req"
)
@@ -12,39 +15,35 @@ type OrmExecute int
type RelationType string
type OrderByDirection string
const (
OrmExecute_Query OrmExecute = iota
OrmExecute_Show
OrmExecute_Store
OrmExecute_Destroy
OrmQuery OrmExecute = iota
OrmShow
OrmStore
OrmDestroy
)
const (
RelationType_HasOne RelationType = "hasOne"
RelationType_HasMany RelationType = "hasMany"
RelationType_Inner RelationType = "inner"
RelationType_Left RelationType = "left"
RelationType_Right RelationType = "right"
OrmHasOne RelationType = "hasOne"
OrmHasMany RelationType = "hasMany"
OrmInnerJoin RelationType = "inner"
OrmLeftJoin RelationType = "left"
OrmRightJoin RelationType = "right"
)
const (
OrderByDirection_ASC OrderByDirection = db.ORDER_ASC
OrderByDirection_DESC OrderByDirection = db.ORDER_DESC
)
type ModelDecorator = func(Model) Model
type Orm interface {
SetGlobalParams(g req.GlobalParams) Orm
SetGlobalParams(g req.UserAccessor) Orm
GetModel(params ...string) Model
SetRelationModel(m Model) Model
SetQueryField(qf req.QueryField) error
SetOrderBy(params []OrderBy) error
SetCondition(cond *condition.Condition) error
SetController(ctr OrmController) error
Execute(OrmExecute) (any, error)
Clone(tx *db.Transaction) Orm
Count() int
Controller() reflect.Value
Execute(OrmExecute) (reflux.R, error)
Clone(tx *db.TxDatabase) Orm
Count() int64
// Exists() bool
}
@@ -54,7 +53,6 @@ type Relation interface {
Type() RelationType
SetType(RelationType)
SetCondition(*condition.Condition) *condition.Condition
SetPrependCondition(*condition.Condition) *condition.Condition
GetCondition() *condition.Condition
GetRelationResource() string
GetRelationField() string
@@ -66,57 +64,63 @@ type Join interface {
GetCode() string
GetResource() req.Resource
// GetDependencies(items []ResRelation, dependencies ...string) []string
Inject(dbBuilder *db.Builder, m Model)
Inject(dbBuilder *db.SelectDataset, m Model)
}
type Model interface {
Relation
condition.TokenValue
IsDuplicated() bool
GetCode() string
GetPrimaryKey() string
GetQueryFieldsStruct(extends ...reflect.StructField) any
GetQueryFieldsSliceStruct(fields ...reflect.StructField) any
GetAttribute() map[string]any
GetResult() reflect.Value
GetAttribute() reflux.R
SetRelationAttribute(string, reflux.R) error
GetResult() reflux.R
GetResource() req.Resource
QueryBuilder() *db.Builder
GetTransaction() *db.Transaction
SetQueryField(req.QueryField)
SetRelationResult(code string, vItems reflect.Value)
GetTransaction() *db.TxDatabase
SetQueryField(...req.QueryField)
// SetRelationResult(code string, items []reflux.R)
GetJoinsCode() []string
SetAuthDB(req.ResAuthDB)
SetPermission(req.ResFlags)
SetOrderBy(params ...OrderBy) Model
SetGroupBy(params ...GroupBy) Model
SetJoin(Join) Model
SetLimit(int) Model
SetOffset(int) Model
SetAttribute(data any) Model
SetLimit(uint) Model
SetOffset(uint) Model
// SetAttribute(data any) Model
SetPrimaryKey(string) Model
SetWithRecursive(QueryWithRecursive) Model
DelQueryField(string)
SelectDataset() *db.SelectDataset
SelectDatasetWithUser(u req.User) *db.SelectDataset
Query() error
Store() error
Destroy() error
Count() (int, error)
Count() (int64, error)
// Exists() (bool, error)
}
type OrmController interface {
AuthDB() req.ResAuthDB
AuthDB() req.ResFlags
}
type OrderBy interface {
ToSql() db.Expression
Inject(dbBuilder *db.Builder, m Model)
ToSql() exp.OrderedExpression
Inject(sd *db.SelectDataset, m Model)
}
type GroupBy interface {
ToSql() db.Expression
Inject(dbBuilder *db.Builder, m Model)
ToSql() exp.ColumnListExpression
Inject(sd *db.SelectDataset, m Model)
}
type QueryWithRecursive interface {
ToQueryBuilder(*db.Builder, Model) (*db.Builder, any)
ToTreeData(any) any
ToQueryBuilder(*db.SelectDataset, Model) (*db.SelectDataset, any)
ToTreeData([]reflux.R) reflux.R
// RootCondition 返回锚点条件(pField = root),供外部如 Count() 在不展开递归查询的情况下复用。
RootCondition() exp.Expression
}
// @title 创建关联模型
@@ -129,18 +133,23 @@ type NewOrmRelation func(t RelationType, m Model, rResource, rField, rForeignKey
type NewOrmJoin func(t RelationType, res req.Resource, alias, rResource, rField, rForeignKey string) Join
type NewOrmQueryWithRecursive func(pField, cField string, root any, isWithoutCondition bool, depth int) QueryWithRecursive
type NewOrmQueryWithRecursive func(pField, cField exp.IdentifierExpression, root any, isWithoutCondition bool, depth uint) QueryWithRecursive
type NewOrmOrderBy func(sql string, direction OrderByDirection) OrderBy
type NewOrmOrderBy func(sql db.Expression, direction exp.SortDirection) OrderBy
type NewOrmGroupBy func(sql string) GroupBy
type NewOrmGroupBy func(db.Expression) GroupBy
type NewOrmModel func(res req.Resource, code, name string) Model
type NewOrm func(res req.Resource, tx *db.Transaction) Orm
type NewOrm func(res req.Resource, opts ...OrmOption) Orm
type GetOrmConditions func(categoryUuid string, opts ...condition.Option) *condition.Condition
type GetOrmOrderBy func(categoryUuid string) []OrderBy
type GetOrmGroupBy func(categoryUuid string) []GroupBy
type OrmOption func(Orm)
type WithTx func(tx *db.TxDatabase) OrmOption
type WithContext func(ctx context.Context) OrmOption
type WithUserAccessor func(ua req.UserAccessor) OrmOption
-56
View File
@@ -1,56 +0,0 @@
package contracts
import (
"git.fsdpf.net/go/req"
"github.com/samber/do"
"github.com/samber/lo"
)
type ResEventService interface {
Start() error
Stop() error
Restart() error
}
type ResListener interface {
Insert(new map[string]any, u req.User, id int64) error
Update(new map[string]any, u req.User, old map[string]any) error
Delete(old map[string]any, u req.User) error
GetResource() req.Resource
GetCode() string
}
type BaseListener struct {
Container *do.Injector
res req.Resource // 监听资源
code string // code
events []string // 监听动作
}
func (this BaseListener) GetCode() string {
return this.code
}
func (this BaseListener) GetResource() req.Resource {
return this.res
}
func (this BaseListener) HasEvent(event string) bool {
return lo.Contains(this.events, event)
}
func (BaseListener) Insert(new map[string]any, u req.User, id int64) error {
return nil
}
func (BaseListener) Update(new map[string]any, u req.User, old map[string]any) error {
return nil
}
func (BaseListener) Delete(old map[string]any, u req.User) error {
return nil
}
func NewBaseListener(code string, res req.Resource, container *do.Injector) *BaseListener {
return &BaseListener{code: code, res: res, Container: container}
}
-27
View File
@@ -1,27 +0,0 @@
package res_type
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
// 解析map ----------------------------------
type ResFieldByMap map[string]any
func (c *ResFieldByMap) Scan(value any) error {
if value == nil {
return nil
}
if err := json.Unmarshal(value.([]byte), c); err != nil {
return fmt.Errorf("ResFieldByMap json.Unmarshal Error, %s", err)
}
return nil
}
func (c ResFieldByMap) Value() (driver.Value, error) {
b, err := json.Marshal(c)
return string(b), err
}
-3
View File
@@ -1,3 +0,0 @@
package res_type
type ResFieldByFloat float64
-3
View File
@@ -1,3 +0,0 @@
package res_type
type ResFieldByInteger int64
-38
View File
@@ -1,38 +0,0 @@
package res_type
import (
"database/sql/driver"
"encoding/json"
"fmt"
)
// 切片 ------------------------------------
type ResFieldByAnys []any
func (c *ResFieldByAnys) Scan(value any) error {
if value == nil {
return nil
}
switch v := value.(type) {
case []byte:
if len(v) == 0 {
return nil
}
if rune('[') != rune(v[0]) {
return nil
}
default:
}
if err := json.Unmarshal(value.([]byte), c); err != nil {
return fmt.Errorf("ResFieldByAnys json.Unmarshal Error, %s, %s", value, err)
}
return nil
}
func (c ResFieldByAnys) Value() (driver.Value, error) {
b, err := json.Marshal(c)
return string(b), err
}
-29
View File
@@ -1,29 +0,0 @@
package res_type
import (
"database/sql/driver"
"github.com/spf13/cast"
)
// Number 类型 --------------------------------
type ResFieldByNumber float64
func (this *ResFieldByNumber) Scan(value any) error {
if value == nil {
return nil
}
switch s := value.(type) {
case []byte:
*this = ResFieldByNumber(cast.ToFloat64(string(s)))
default:
*this = ResFieldByNumber(cast.ToFloat64(s))
}
return nil
}
func (this ResFieldByNumber) Value() (driver.Value, error) {
return this, nil
}
-28
View File
@@ -1,28 +0,0 @@
package res_type
import (
"database/sql/driver"
"fmt"
)
// 字符串 -------------------------------------
type ResFieldByString string
func (this *ResFieldByString) Scan(value any) error {
if value == nil {
return nil
}
switch s := value.(type) {
case []byte:
*this = ResFieldByString(s)
default:
*this = ResFieldByString(fmt.Sprintf("%v", s))
}
return nil
}
func (c ResFieldByString) Value() (driver.Value, error) {
return c, nil
}
+16
View File
@@ -0,0 +1,16 @@
package contracts
import (
"git.fsdpf.net/go/req"
)
// ResVirtualTableSetup 由 framework-v2 在 sqlite_vtable build tag 下注册到容器,负责向 SQLite
// 注册虚拟表模块,具体说明见 req.ResVirtualTableSetup。
type ResVirtualTableSetup = req.ResVirtualTableSetup
// ResVirtualTable 在 req.ResVirtualTable 基础上合并 Controller,使实现方可以访问 DI 容器等框架能力;
// 具体方法说明见 req.ResVirtualTable。
type ResVirtualTable interface {
Controller
req.ResVirtualTable
}
+28
View File
@@ -0,0 +1,28 @@
package contracts
import (
"git.fsdpf.net/go/req/resx"
)
// ResEventType 资源操作类型:读(SELECT+ 写(INSERT/UPDATE/DELETE
type ResEventType = resx.ResEventType
const (
ResEventSelect = resx.ResEventSelect
ResEventInsert = resx.ResEventInsert
ResEventUpdate = resx.ResEventUpdate
ResEventDelete = resx.ResEventDelete
)
// ResChangeRow 描述一次资源写操作中某一行的变更内容
type ResChangeRow = resx.ResChangeRow
// ResWatcher 关注一个或多个资源写操作的处理器,通过 _res_listener 注册表按资源绑定,
// 每次事件触发都会重新构造实例(见 framework-v2 的 handler.ResWatcherBase / internal/resource 的
// watcherDispatcher.getAppWatcher
type ResWatcher interface {
Controller
// Handle 处理一次资源变更,row 自带 User()
Handle(row ResChangeRow) error
GetCode() string
}
+10 -4
View File
@@ -1,9 +1,15 @@
package contracts
import "git.fsdpf.net/go/req"
import (
"git.fsdpf.net/go/req"
)
// 资源
type GetResource func(code string) (req.Resource, bool)
// GetResource 资源;类型定义搬到了 req.GetResource(req 的子包需要用它查资源,不能反过来
// import contracts),这里保留别名兼容原有引用。
type GetResource = req.GetResource
// 获取用户
// MustResource 资源, 没有则抛出错误;类型定义搬到了 req.MustResource,这里保留别名兼容原有引用。
type MustResource = req.MustResource
// GetUser 获取用户
type GetUser func(uuid string, options ...req.UserRuntimeOption) (req.User, error)
+6 -10
View File
@@ -8,8 +8,9 @@ import (
"git.fsdpf.net/go/condition"
"git.fsdpf.net/go/contracts"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/utils"
"github.com/samber/do"
"github.com/samber/do/v2"
"github.com/samber/lo"
"github.com/spf13/cast"
)
@@ -17,14 +18,9 @@ import (
type Operation struct {
Uuid string `json:"uuid"`
Name string `json:"name"`
Code string `json:"code"`
Icon string `json:"icon"`
Size string `json:"size"`
Block bool `json:"block"`
Ghost bool `json:"ghost"`
Shape string `json:"shape"`
Type string `json:"type"`
PopConfirm any `json:"popConfirm,omitempty"`
Confirm map[string]any `json:"confirm"`
Widget string `json:"widget"`
WidgetType string `json:"widgetType"`
WidgetProps any `json:"widgetProps"`
@@ -42,7 +38,7 @@ type OperationAccess struct {
data map[string][]any
}
func (o Operation) ToDBColumn(model contracts.Model, container *do.Injector) (column db.Expression, field reflect.StructField) {
func (o Operation) ToDBColumn(model contracts.Model, container do.Injector) (column db.Expression, field reflect.StructField) {
GetOrmConditions := do.MustInvoke[contracts.GetOrmConditions](container)
uuid := strings.ReplaceAll(o.Uuid, "-", "_")
@@ -57,9 +53,9 @@ func (o Operation) ToDBColumn(model contracts.Model, container *do.Injector) (co
}
if cond.IsEmpty() {
column = db.Raw(pk + " as `u_" + uuid + "`")
column = db.I(pk).As("u_" + uuid)
} else {
column = db.Raw("if(" + string(cond.ToSql(model)) + ", " + pk + ", 0) as `u_" + uuid + "`")
column = exp.NewSQLFunctionExpression("IF", cond.ToSql(model), db.I(pk), 0).As("u_" + uuid)
}
// 主键类型
+49 -82
View File
@@ -13,24 +13,18 @@ import (
"github.com/samber/lo"
)
type HttpStatusResponse struct {
status int
body req.HttpResponse
}
type RawResponse struct {
headers [][2]string
raw []byte
}
type JsonResponse struct {
raw []byte
}
type MsgResponse struct {
code int
msg string
}
type ErrResponse struct {
*MsgResponse
stack []byte
}
type FileResponse struct {
headers [][2]string
disposition string
name string
}
@@ -41,11 +35,29 @@ var wsUpgrader = websocket.Upgrader{
},
}
func (this HttpStatusResponse) Get(path ...string) req.GlobalParams {
if this.body == nil {
return req.NewGlobalParams("", nil)
}
return this.body.Get(path...)
}
func (this HttpStatusResponse) Send(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(this.status)
if this.body != nil {
this.body.Send(w, r)
}
}
func (this RawResponse) Get(path ...string) req.GlobalParams {
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(this.raw), nil), req.NewGlobalParam(string(this.raw), nil).Get(strings.Join(path, ".")))
return lo.Ternary(len(path) == 0, req.NewGlobalParams(string(this.raw), nil), req.NewGlobalParams(string(this.raw), nil).Get(strings.Join(path, ".")))
}
func (this RawResponse) Send(w http.ResponseWriter, r *http.Request) {
for i := 0; i < len(this.headers); i++ {
w.Header().Set(this.headers[i][0], this.headers[i][1])
}
if r.Header.Get("Upgrade") != "" && strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
c, _ := wsUpgrader.Upgrade(w, r, nil)
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, string(this.raw)))
@@ -55,96 +67,51 @@ func (this RawResponse) Send(w http.ResponseWriter, r *http.Request) {
}
}
func (this JsonResponse) Get(path ...string) req.GlobalParams {
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(this.raw), nil), req.NewGlobalParam(string(this.raw), nil).Get(strings.Join(path, ".")))
}
func (this JsonResponse) Send(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") != "" && strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
c, _ := wsUpgrader.Upgrade(w, r, nil)
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, string(this.raw)))
c.Close()
} else {
w.Header().Set("Content-Type", "application/json")
w.Write(this.raw)
}
}
func (this MsgResponse) Get(path ...string) req.GlobalParams {
if resp, err := json.Marshal(map[string]any{
"code": this.code,
"msg": this.msg,
}); err == nil {
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(resp), nil), req.NewGlobalParam(string(resp), nil).Get(strings.Join(path, ".")))
}
return nil
}
func (this MsgResponse) Send(w http.ResponseWriter, r *http.Request) {
if resp, err := json.Marshal(this.Get().Value()); err == nil {
(&JsonResponse{raw: resp}).Send(w, r)
} else {
HttpResponse(err).Send(w, r)
}
}
func (this ErrResponse) Get(path ...string) req.GlobalParams {
if resp, err := json.Marshal(map[string]any{
"code": this.code,
"msg": this.msg,
"error": string(this.stack),
}); err == nil {
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(resp), nil), req.NewGlobalParam(string(resp), nil).Get(strings.Join(path, ".")))
}
return nil
}
func (this ErrResponse) Send(w http.ResponseWriter, r *http.Request) {
if resp, err := json.Marshal(this.Get().Value()); err == nil {
(&JsonResponse{raw: resp}).Send(w, r)
} else {
HttpResponse(err).Send(w, r)
}
}
func (this FileResponse) Get(path ...string) req.GlobalParams {
if resp, err := json.Marshal(map[string]any{
"name": this.name,
"disposition": this.disposition,
}); err == nil {
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(resp), nil), req.NewGlobalParam(string(resp), nil).Get(strings.Join(path, ".")))
return lo.Ternary(len(path) == 0, req.NewGlobalParams(string(resp), nil), req.NewGlobalParams(string(resp), nil).Get(strings.Join(path, ".")))
}
return nil
}
func (this FileResponse) Send(w http.ResponseWriter, r *http.Request) {
if this.Get("disposition").String() != "" {
w.Header().Set("Content-Disposition", `attachment; filename="`+this.Get("disposition").String()+`"`)
}
if _, err := os.Stat(this.Get("name").String()); err != nil && os.IsNotExist(err) {
w.WriteHeader(http.StatusNotFound)
} else {
if this.Get("disposition").String() != "" {
w.Header().Set("Content-Disposition", `attachment; filename="`+this.Get("disposition").String()+`"`)
}
for i := 0; i < len(this.headers); i++ {
w.Header().Set(this.headers[i][0], this.headers[i][1])
}
http.ServeFile(w, r, this.Get("name").String())
}
}
func NewRawResponse(b []byte) req.HttpResponse {
return &RawResponse{raw: b}
func NewHttpStatusResponse(status int, body req.HttpResponse) req.HttpResponse {
return &HttpStatusResponse{status: status, body: body}
}
func NewRawResponse(b []byte, headers ...[2]string) req.HttpResponse {
return &RawResponse{raw: b, headers: headers}
}
func NewFileResponse(name string, disposition string, headers ...[2]string) req.HttpResponse {
return &FileResponse{name: name, disposition: disposition, headers: headers}
}
func NewJsonResponse(b []byte) req.HttpResponse {
return &JsonResponse{raw: b}
}
func NewFileResponse(name string, disposition string) req.HttpResponse {
return &FileResponse{name: name, disposition: disposition}
return NewRawResponse(b, [2]string{"Content-Type", "application/json"})
}
func NewMsgResponse(msg string, code int) req.HttpResponse {
return &MsgResponse{msg: msg, code: code}
return NewJsonResponse([]byte(fmt.Sprintf(`{"code": %d, "msg": %q}`, code, msg)))
}
func NewErrResponse(err *contracts.Err) req.HttpResponse {
@@ -173,7 +140,7 @@ func HttpResponse(data any) req.HttpResponse {
"msg": contracts.OK.Msg,
"data": json.RawMessage(v),
}); e == nil {
return &JsonResponse{raw: b}
return NewJsonResponse(b)
} else {
err = e
}
@@ -183,7 +150,7 @@ func HttpResponse(data any) req.HttpResponse {
"msg": contracts.OK.Msg,
"data": v,
}); e == nil {
return &JsonResponse{raw: b}
return NewJsonResponse(b)
} else {
err = e
}
+3 -3
View File
@@ -21,10 +21,10 @@ func GoPluginCore(core string, services ...any) {
} else if fn, ok := f.(func(map[string]contracts.Controller) error); ok {
fn(s)
}
case map[string]contracts.ResListener:
if f, err := p.Lookup("RegListens"); err != nil {
case map[string]contracts.ResWatcher:
if f, err := p.Lookup("RegResWatchers"); err != nil {
panic(err)
} else if fn, ok := f.(func(map[string]contracts.ResListener) error); ok {
} else if fn, ok := f.(func(map[string]contracts.ResWatcher) error); ok {
fn(s)
}
case map[string]contracts.GRpc:
+1 -1
View File
@@ -56,7 +56,7 @@ type UserApproval interface {
// 判断发起审批请求权限
HasUserRoles(req.User) bool
// 获取审批列表标记字段值
Execute(id int64, status UserApprovalExecute, r *http.Request, tx *db.Transaction) (current, next UserApprovalFlow, err error)
Execute(id int64, status UserApprovalExecute, r *http.Request, tx *db.TxDatabase) (current, next UserApprovalFlow, err error)
}
type UserApprovalFlow interface {
+2 -50
View File
@@ -5,17 +5,12 @@ import (
"git.fsdpf.net/go/req"
"github.com/gorilla/websocket"
"github.com/samber/do"
)
type WsClientGroup string
type WsClientID uint
// type WsClient struct {
// User
// Group WsClientGroup
// Socket *websocket.Conn
// }
const DefaultWsClientGroup WsClientGroup = "__DEFAULT__"
type WsClient interface {
req.User
@@ -50,50 +45,7 @@ type WsController interface {
// 获取 Ws 客户端
Client() WsClient
// 获取 Ws 标识
ClientId(req.GlobalParams) WsClientID
ClientID(req.GlobalParams) WsClientID
// 获取 Ws 分组
ClientGroup(req.GlobalParams) WsClientGroup
}
type WsHandleController struct {
Controller
ws WsClient
request *http.Request
}
func (this WsHandleController) Request() *http.Request {
return this.request
}
func (this WsHandleController) Route() req.Route {
return this.Request().Context().Value(req.RouteCtx{Name: "Route"}).(req.Route)
}
func (this WsHandleController) User() req.User {
return this.Request().Context().Value(req.RouteCtx{Name: "User"}).(req.User)
}
func (this WsHandleController) Execute(req.GlobalParams) error {
return ErrFuncNotImplemented
}
func (this WsHandleController) Client() WsClient {
return this.ws
}
func (WsHandleController) ClientId(req.GlobalParams) WsClientID {
wsClientID++
return wsClientID
}
func (WsHandleController) ClientGroup(req.GlobalParams) WsClientGroup {
return defaultWsClientGroup
}
func NewWsController(container *do.Injector, request *http.Request, ws WsClient) WsController {
return &WsHandleController{
Controller: &BaseController{container},
request: request,
ws: ws,
}
}