重构: 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 生成用)。
This commit is contained in:
+19
-19
@@ -1,25 +1,25 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
type GridLayout struct {
|
type GridLayout struct {
|
||||||
Uuid string `db:"uuid"`
|
Uuid string `db:"uuid"`
|
||||||
ResourceUuid string `db:"resource_uuid"` // 所属资源UUID
|
ResourceUuid string `db:"resource_uuid"` // 所属资源UUID
|
||||||
Code string `db:"code"` // 布局标识
|
Code string `db:"code"` // 布局标识
|
||||||
Name string `db:"name"` // 布局名称
|
Name string `db:"name"` // 布局名称
|
||||||
Type int `db:"type"` // 布局类型 [0=普通布局, 1=辅助布局]
|
Type int `db:"type"` // 布局类型 [0=普通布局, 1=辅助布局]
|
||||||
PrimaryKey string `db:"primaryKey"` // 主键
|
PrimaryKey string `db:"primaryKey"` // 主键
|
||||||
MarginX int `db:"marginX"` // 栅格 x 间距
|
ItemMargin []any `db:"itemMargin"` // 栅格间距
|
||||||
MarginY int `db:"marginY"` // 栅格 y 间距
|
ContainerPadding []any `db:"containerPadding"` // 容器内边距
|
||||||
Cols int `db:"cols"` // 网格列基数
|
Cols int `db:"cols"` // 网格列基数
|
||||||
RowHeight float32 `db:"rowHeight"` // 栅格行高
|
RowHeight float32 `db:"rowHeight"` // 栅格行高
|
||||||
Css map[string]any `db:"css"` // 布局CSS
|
Css map[string]any `db:"css"` // 布局CSS
|
||||||
Props []any `db:"props"` // 布局PROPS
|
Props []any `db:"props"` // 布局PROPS
|
||||||
Groups []map[string]any `db:"groups"` // 布局分组
|
Groups []map[string]any `db:"groups"` // 布局分组
|
||||||
Theme string `db:"theme"` // 布局样式
|
Theme string `db:"theme"` // 布局样式
|
||||||
ThemeConfig map[string]any `db:"themeConfig"` // 布局样式配置
|
ThemeConfig map[string]any `db:"themeConfig"` // 布局样式配置
|
||||||
Platform string `db:"platform"`
|
Platform string `db:"platform"`
|
||||||
Roles []string `db:"roles"`
|
Roles []string `db:"roles"`
|
||||||
UpdatedAt string `db:"updated_at"`
|
UpdatedAt string `db:"updated_at"`
|
||||||
CreatedAt string `db:"created_at"`
|
CreatedAt string `db:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetGridLayout func(code string) (GridLayout, bool)
|
type GetGridLayout func(code string) (GridLayout, bool)
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"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.V(nil).As(this.Alias())
|
|
||||||
}
|
|
||||||
return db.L(this.GetCode()).As(this.Alias())
|
|
||||||
}
|
|
||||||
return db.T(this.GetCodeResource()).Col(this.GetCode()).As(this.Alias())
|
|
||||||
}
|
|
||||||
return db.T(this.GetCodeResource()).Col(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(float64(0))
|
|
||||||
case req.ReqInteger:
|
|
||||||
typ = reflect.TypeOf(int64(0))
|
|
||||||
case req.ReqFloat:
|
|
||||||
typ = reflect.TypeOf(float64(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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-8
@@ -1,22 +1,17 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
type ResApiParam struct {
|
|
||||||
Code string `db:"code" json:"code"`
|
|
||||||
Name string `db:"name" json:"name"`
|
|
||||||
Category string `db:"category" json:"category"`
|
|
||||||
DataType string `db:"type" json:"type"`
|
|
||||||
IsRequired bool `db:"isRequired" json:"isRequired"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResApi struct {
|
type ResApi struct {
|
||||||
Uuid string `db:"uuid"`
|
Uuid string `db:"uuid"`
|
||||||
|
PUuid string `db:"pUuid"`
|
||||||
Code string `db:"code"`
|
Code string `db:"code"`
|
||||||
UriPrefix string `db:"uri_prefix"`
|
UriPrefix string `db:"uri_prefix"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
|
Desc string `db:"desc"`
|
||||||
PrimaryKey string `db:"primaryKey"`
|
PrimaryKey string `db:"primaryKey"`
|
||||||
ResourceUuid string `db:"resource_uuid"`
|
ResourceUuid string `db:"resource_uuid"`
|
||||||
Method string `db:"method"`
|
Method string `db:"method"`
|
||||||
Action string `db:"action"`
|
Action string `db:"action"`
|
||||||
|
Version string `db:"version"`
|
||||||
Params []ResApiParam `db:"params"`
|
Params []ResApiParam `db:"params"`
|
||||||
Roles []string `db:"roles"`
|
Roles []string `db:"roles"`
|
||||||
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
|
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
|
||||||
@@ -25,3 +20,5 @@ type ResApi struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetResApi func(code string) (ResApi, bool)
|
type GetResApi func(code string) (ResApi, bool)
|
||||||
|
|
||||||
|
type GetResApiChilds func(code string) []ResApi
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归一化 DataType:json → 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)
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
string:value 填字面量如 '2024-01-01'
|
||||||
|
func:value 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS
|
||||||
|
param:value 填请求参数路径如 body.status
|
||||||
|
sql:value 填原始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_of(value 是否是列 JSON 数组的成员):
|
||||||
|
无 param:JSON_CONTAINS({value}, JSON_ARRAY({column}))
|
||||||
|
有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))
|
||||||
|
json_contains(列 JSON 是否包含 value):
|
||||||
|
无 param:JSON_CONTAINS({column}, JSON_ARRAY({value}))
|
||||||
|
有 param:JSON_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},
|
||||||
|
|
||||||
|
// orderBy:items 用 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 / right:SQL 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 / right:SQL 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 是 array,items 是 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:批量记录(array,items 为 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分支1:object
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分支2:array,items 为 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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\nstring:token 填字面量如 '2024-01-01'\r\nfunc:token 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparam:token 填请求参数路径如 body.status\r\nsql:token 填原始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.price,isExpr 保持默认 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 作用于子查询 WHERE;inner/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 / right:SQL 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 / right:SQL JOIN,要求关联资源与主资源在同一数据库连接,将关联字段合并到主查询行。\rhasOne / hasMany:独立子查询,支持跨数据库连接,结果聚合为 JSON 字段(hasOne 为单对象,hasMany 为数组)。\r跨库关联必须使用 hasOne 或 hasMany,不可使用 JOIN 类型。"
|
||||||
|
},
|
||||||
|
"resource": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "主资源",
|
||||||
|
"description": "主资源编码(resource_code),作为本次查询的根上下文,relations 中的关联资源均以此为基础展开"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"fields",
|
||||||
|
"resource"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"annotations": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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_of(token 是否是列 JSON 数组的成员):\r 无 param:JSON_CONTAINS({token}, JSON_ARRAY({field}))\r 有 param:JSON_CONTAINS({fieldSqlFuncParam}, JSON_ARRAY({field}))\r json_contains(列 JSON 是否包含 token):\r 无 param:JSON_CONTAINS({field}, JSON_ARRAY({token}))\r 有 param:JSON_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\nstring:token 填字面量如 '2024-01-01'\r\nfunc:token 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparam:token 填请求参数路径如 body.status\r\nsql:token 填原始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=true,code 填 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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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": "关联配置列表,支持 JOIN(inner / 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_of(value 是否是列 JSON 数组的成员):\r 无 param:JSON_CONTAINS({value}, JSON_ARRAY({column}))\r 有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))\r json_contains(列 JSON 是否包含 value):\r 无 param:JSON_CONTAINS({column}, JSON_ARRAY({value}))\r 有 param:JSON_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\nstring:value 填字面量如 '2024-01-01'\r\nfunc:value 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparam:value 填请求参数路径如 body.status\r\nsql:value 填原始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 / right:SQL 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": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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_of(value 是否是列 JSON 数组的成员):\r 无 param:JSON_CONTAINS({value}, JSON_ARRAY({column}))\r 有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))\r json_contains(列 JSON 是否包含 value):\r 无 param:JSON_CONTAINS({column}, JSON_ARRAY({value}))\r 有 param:JSON_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\nstring:value 填字面量如 '2024-01-01'\r\nfunc:value 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparam:value 填请求参数路径如 body.status\r\nsql:value 填原始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 / right:SQL JOIN,将关联资源字段合并到主查询行\r\nhasOne:子查询,关联结果聚合为单个对象(result[code] = {})\r\nhasMany:子查询,关联结果聚合为对象数组(result[code] = [...])",
|
||||||
|
"enum": [
|
||||||
|
"inner",
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
"hasOne",
|
||||||
|
"hasMany"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"actuallyField",
|
||||||
|
"actuallyResource",
|
||||||
|
"code",
|
||||||
|
"relationField",
|
||||||
|
"relationResource",
|
||||||
|
"type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"title": "资源关联",
|
||||||
|
"description": "关联配置列表,支持 JOIN(inner / left / right)将关联字段合并到主查询,或子查询(hasOne / hasMany)将关联数据聚合为独立 JSON 字段"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-3
@@ -1,6 +1,8 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"git.fsdpf.net/go/reflux"
|
"git.fsdpf.net/go/reflux"
|
||||||
"git.fsdpf.net/go/reflux/valuex"
|
"git.fsdpf.net/go/reflux/valuex"
|
||||||
)
|
)
|
||||||
@@ -30,19 +32,35 @@ type ResConfigure struct {
|
|||||||
func (rc *ResConfigure) GetValueByRoles(roles ...string) (valuex.Accessor, bool) {
|
func (rc *ResConfigure) GetValueByRoles(roles ...string) (valuex.Accessor, bool) {
|
||||||
// 如果没有角色值配置或没有传入角色,返回默认值
|
// 如果没有角色值配置或没有传入角色,返回默认值
|
||||||
if len(rc.RoleValues) == 0 || len(roles) == 0 {
|
if len(rc.RoleValues) == 0 || len(roles) == 0 {
|
||||||
return reflux.New(rc.Value), false
|
return reflux.New(rc.toTyped(rc.Value)), true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rv := range rc.RoleValues {
|
for _, rv := range rc.RoleValues {
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
if value, exists := rv[role]; exists {
|
if value, exists := rv[role]; exists {
|
||||||
return reflux.New(value), true
|
return reflux.New(rc.toTyped(value)), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有找到匹配的角色值,返回默认值
|
// 没有找到匹配的角色值,返回默认值
|
||||||
return reflux.New(rc.Value), false
|
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)
|
type GetResConfigure func(key string, roles ...string) (valuex.Accessor, bool)
|
||||||
|
|||||||
+4
-269
@@ -1,274 +1,9 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"git.fsdpf.net/go/req/resx"
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.fsdpf.net/go/db"
|
|
||||||
"git.fsdpf.net/go/db/schema"
|
|
||||||
"git.fsdpf.net/go/req"
|
|
||||||
"github.com/spf13/cast"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResField 资源字段
|
// ResField 的实现搬到了 git.fsdpf.net/go/req/resx(resx 自己需要用它做字段级脱敏,
|
||||||
type ResField struct {
|
// 不能反过来 import contracts/base,见 resx.ResField 的注释),这里保留类型别名兼容原有引用。
|
||||||
Uuid string `db:"uuid" json:"uuid"`
|
type ResField = resx.ResField
|
||||||
Name string `db:"name" json:"name"`
|
|
||||||
Code string `db:"code" json:"code"`
|
|
||||||
CodeResource string `db:"codeResource" json:"codeResource"`
|
|
||||||
DataType req.ResDataType `db:"data_type" json:"data_type"`
|
|
||||||
Length string `db:"length" json:"length"`
|
|
||||||
Comment string `db:"comment" json:"comment"`
|
|
||||||
Default string `db:"default" json:"default"`
|
|
||||||
Virtual bool `db:"virtual" json:"virtual"`
|
|
||||||
VirtualExpr string `db:"virtual_expr" json:"virtual_expr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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(int64(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) IsVirtual() bool {
|
|
||||||
return this.Virtual
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if this.DataType == req.ResJson {
|
|
||||||
if v == nil {
|
|
||||||
if this.Default != "" && this.Default[0:1] == "[" {
|
|
||||||
return db.V("[]")
|
|
||||||
} else if this.Default != "" && this.Default[0:1] == "{" {
|
|
||||||
return db.V("{}")
|
|
||||||
} else if this.Default == "" {
|
|
||||||
return db.V("{}")
|
|
||||||
}
|
|
||||||
return this.GetRawDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this ResField) GetRawDefault() db.Expression {
|
|
||||||
if this.DataType == req.ResJson {
|
|
||||||
if this.Default != "" && this.Default[0:1] == "[" {
|
|
||||||
return db.V("[]")
|
|
||||||
} else if this.Default != "" && this.Default[0:1] == "{" {
|
|
||||||
return db.V("{}")
|
|
||||||
} else if this.Default == "" {
|
|
||||||
return db.V("{}")
|
|
||||||
}
|
|
||||||
} else if this.DataType == req.ResBoolean {
|
|
||||||
if v, _ := strconv.ParseBool(this.Default); v {
|
|
||||||
return db.V(true)
|
|
||||||
}
|
|
||||||
return db.V(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(this.Default) > 4 && strings.ToLower(this.Default[0:4]) == "sql:" {
|
|
||||||
return db.L(this.Default[4:])
|
|
||||||
}
|
|
||||||
|
|
||||||
if this.Default == "" {
|
|
||||||
if this.GetDataType() == req.ResDate || this.GetDataType() == req.ResDatetime {
|
|
||||||
return db.V(nil)
|
|
||||||
}
|
|
||||||
return db.V("")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.ToUpper(this.Default) == "CURRENT_TIMESTAMP" {
|
|
||||||
return db.L(this.Default)
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.V(this.Default)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDefinition) {
|
|
||||||
switch this.Code {
|
|
||||||
case "id":
|
|
||||||
return table.BigIncrements("id").AutoIncrement().Comment("ID")
|
|
||||||
case "enabled":
|
|
||||||
return table.Boolean("enabled").Default("1").Comment("是否有效")
|
|
||||||
case "created_user":
|
|
||||||
return table.Char("created_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("创建者")
|
|
||||||
case "owned_user":
|
|
||||||
return table.Char("owned_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("拥有者")
|
|
||||||
case "created_at":
|
|
||||||
return table.Timestamp("created_at").UseCurrent().Comment("创建时间")
|
|
||||||
case "updated_at":
|
|
||||||
return table.Timestamp("updated_at").UseCurrent().Default(db.L("ON UPDATE CURRENT_TIMESTAMP")).Comment("更新时间")
|
|
||||||
case "deleted_at":
|
|
||||||
return table.DateTime("deleted_at").Nullable().Comment("删除时间")
|
|
||||||
}
|
|
||||||
|
|
||||||
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", "timestamp":
|
|
||||||
if this.DataType == "date" {
|
|
||||||
temp = table.Date(this.Code)
|
|
||||||
} else {
|
|
||||||
temp = table.DateTime(this.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.ToUpper(this.Default) == "SQL:CURRENT_TIMESTAMP" {
|
|
||||||
def = db.L("CURRENT_TIMESTAMP")
|
|
||||||
} 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
|
|
||||||
case "vector":
|
|
||||||
temp = table.Vector(this.Code, cast.ToInt(this.Length))
|
|
||||||
isNull = false
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("Unknown type: %s", this.DataType))
|
|
||||||
}
|
|
||||||
|
|
||||||
if this.IsVirtual() {
|
|
||||||
temp.VirtualAs(this.VirtualExpr)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-10
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"git.fsdpf.net/go/db/schema"
|
"git.fsdpf.net/go/db/schema"
|
||||||
"git.fsdpf.net/go/req"
|
"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/mysql"
|
||||||
_ "git.fsdpf.net/go/db/schema/dialect/sqlite3"
|
_ "git.fsdpf.net/go/db/schema/dialect/sqlite3"
|
||||||
@@ -14,16 +15,7 @@ func TestResFieldToBlueprint(t *testing.T) {
|
|||||||
t.Run("id", func(t *testing.T) {
|
t.Run("id", func(t *testing.T) {
|
||||||
table := schema.NewBlueprint("users")
|
table := schema.NewBlueprint("users")
|
||||||
|
|
||||||
id := ResField{
|
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))
|
||||||
Uuid: "00000000-0000-0000-0000-000000000000",
|
|
||||||
Name: "ID",
|
|
||||||
Code: "id",
|
|
||||||
CodeResource: "User",
|
|
||||||
DataType: req.ResInteger,
|
|
||||||
Length: "20",
|
|
||||||
Comment: "",
|
|
||||||
Default: "sql:AUTO_INCREMENT",
|
|
||||||
}
|
|
||||||
|
|
||||||
id.ToBlueprint(table)
|
id.ToBlueprint(table)
|
||||||
})
|
})
|
||||||
|
|||||||
+36
-2
@@ -1,14 +1,48 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type ResJob struct {
|
type ResJob struct {
|
||||||
Uuid string `db:"uuid"`
|
Uuid string `db:"uuid"`
|
||||||
Code string `db:"code"`
|
Code string `db:"code"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
ResourceUuid string `db:"resource_uuid"`
|
ResourceUuid string `db:"resource_uuid"`
|
||||||
Replay int64 `db:"replay"`
|
MaxAttempts int `db:"maxAttempts"` // 最大重试次数
|
||||||
IsRecord bool `db:"isRecord"`
|
Concurrent int `db:"concurrent"` // 同类 job 最大并发数,0 表示不限制
|
||||||
|
Timeout int `db:"timeout"` // 单次执行超时秒数,0 表示不限制
|
||||||
|
Retry int `db:"retry"` // 重试间隔秒数,0 表示不重试,>0 表示固定间隔秒数
|
||||||
UpdatedAt string `db:"updated_at"`
|
UpdatedAt string `db:"updated_at"`
|
||||||
CreatedAt string `db:"created_at"`
|
CreatedAt string `db:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetResJob func(code string) (ResJob, bool)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -1,437 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/samber/do/v2"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
|
|
||||||
"git.fsdpf.net/go/condition"
|
|
||||||
"git.fsdpf.net/go/contracts"
|
|
||||||
"git.fsdpf.net/go/db"
|
|
||||||
"git.fsdpf.net/go/db/engine"
|
|
||||||
"git.fsdpf.net/go/db/exec"
|
|
||||||
"git.fsdpf.net/go/db/exp"
|
|
||||||
"git.fsdpf.net/go/db/schema"
|
|
||||||
"git.fsdpf.net/go/req"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResChangeEventTopic 资源变更事件
|
|
||||||
const ResChangeEventTopic = "res-change-event-topic"
|
|
||||||
|
|
||||||
// ResChangeRecordTopic 资源变更数据
|
|
||||||
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 // 新数据
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResVirtualTableSetup 由 framework-v2 在 sqlite_vtable build tag 下注册到容器。
|
|
||||||
// resource.DB() 通过 do.Invoke 取出后调用,完成 vtab 模块注册和 CREATE VIRTUAL TABLE。
|
|
||||||
// res 提供 Code / Table / Fields 信息,vt 是 ResVirtualTable 的具体实现。
|
|
||||||
type ResVirtualTableSetup func(res Resource) error
|
|
||||||
|
|
||||||
// Resource 资源
|
|
||||||
type Resource struct {
|
|
||||||
container do.Injector
|
|
||||||
initOnce *sync.Once
|
|
||||||
|
|
||||||
Uuid string `db:"uuid"`
|
|
||||||
PUuid string `db:"pUuid"`
|
|
||||||
Code string `db:"code"`
|
|
||||||
Name string `db:"name"`
|
|
||||||
Conn string `db:"conn"`
|
|
||||||
IsResVirtual bool `db:"isVirtual"`
|
|
||||||
Table string `db:"table"`
|
|
||||||
Primarykey string `db:"primaryKey"`
|
|
||||||
HistoryRoles []string `db:"historyRoles"`
|
|
||||||
Fields []ResField `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
|
|
||||||
this.initOnce = &sync.Once{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUuid 资源UUID
|
|
||||||
func (this Resource) GetUuid() string {
|
|
||||||
return this.Uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCode 资源CODE
|
|
||||||
func (this Resource) GetCode() string {
|
|
||||||
return this.Code
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetName 资源名
|
|
||||||
func (this Resource) GetName() string {
|
|
||||||
return this.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConn 数据库连接
|
|
||||||
func (this Resource) GetConn() string {
|
|
||||||
return this.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrimarykey 主键
|
|
||||||
func (this Resource) GetPrimarykey() string {
|
|
||||||
return this.Primarykey
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsVirtual 是否虚拟资源
|
|
||||||
func (this Resource) IsVirtual() bool {
|
|
||||||
return this.IsResVirtual
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSystem 是否系统资源
|
|
||||||
func (this Resource) IsSystem() bool {
|
|
||||||
return this.Conn == "service-support"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFields 资源字段
|
|
||||||
func (this Resource) GetFields() (result []req.ResField) {
|
|
||||||
for _, item := range this.Fields {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetField 资源字段
|
|
||||||
func (this Resource) GetField(code string) (req.ResField, bool) {
|
|
||||||
return lo.Find(this.GetFields(), func(v req.ResField) bool {
|
|
||||||
return v.GetCode() == code
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasField 判断资源字段
|
|
||||||
func (this Resource) HasField(code string) bool {
|
|
||||||
return lo.SomeBy(this.GetFields(), func(v req.ResField) bool {
|
|
||||||
return v.GetCode() == code
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeginTransaction 开启事物
|
|
||||||
func (this Resource) BeginTransaction() (*db.TxDatabase, error) {
|
|
||||||
return this.DB().Begin()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB 获取资源链接
|
|
||||||
func (this Resource) DB() *db.Database {
|
|
||||||
dbEngine := do.MustInvoke[engine.Engine](this.container)
|
|
||||||
conn := dbEngine.Connection(this.Conn)
|
|
||||||
|
|
||||||
if isLocalDB(conn.Dialect()) && !this.IsResVirtual && this.Table != "" && this.initOnce != nil {
|
|
||||||
this.initOnce.Do(func() {
|
|
||||||
if err := this.autoCreateTable(conn); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// vtable 资源:查询走 engine 中的 :memory: 连接("__"+Conn),
|
|
||||||
// 原文件连接仅用于 _vtab_cache 持久化,两者互不阻塞。
|
|
||||||
if conn.Dialect() == "vtable" && this.Table != "" && this.initOnce != nil {
|
|
||||||
memConn := dbEngine.Connection("__" + this.Conn)
|
|
||||||
this.initOnce.Do(func() {
|
|
||||||
if err := this.autoSetupVtab(memConn); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return memConn
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// autoSetupVtab 通过容器获取 ResVirtualTableSetup(由 framework-v2 在 sqlite_vtable tag 下注册),
|
|
||||||
// 再从 App 取出当前资源对应的 ResVirtualTable 实现,完成 vtab 注册和 CREATE VIRTUAL TABLE。
|
|
||||||
func (this Resource) autoSetupVtab(conn *db.Database) error {
|
|
||||||
_, err := conn.Exec(fmt.Sprintf(`CREATE VIRTUAL TABLE IF NOT EXISTS %s USING %s_mod()`, this.Table, this.Table))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// autoCreateTable 自动创建数据表
|
|
||||||
func (this Resource) autoCreateTable(conn *db.Database) error {
|
|
||||||
sb := schema.New(conn)
|
|
||||||
return sb.Create(this.Table, func(bp *schema.Blueprint) {
|
|
||||||
bp.Comment = this.Name
|
|
||||||
for _, field := range this.Fields {
|
|
||||||
field.ToBlueprint(bp)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTable 获取资源对应的数据表
|
|
||||||
func (this Resource) GetTable() exp.LiteralExpression {
|
|
||||||
if this.IsResVirtual {
|
|
||||||
return db.L("(" + this.Table + ")")
|
|
||||||
}
|
|
||||||
return db.V(db.T(this.Table))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDBTable 获取资源对应的数据库连接
|
|
||||||
func (this Resource) GetDBTable(u req.User, opts ...req.ResOption) (sd *db.SelectDataset) {
|
|
||||||
if u == nil {
|
|
||||||
panic("GetDBTable: user cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
op := &req.ResOptions{}
|
|
||||||
|
|
||||||
for _, cb := range opts {
|
|
||||||
cb(op)
|
|
||||||
}
|
|
||||||
|
|
||||||
alias := this.GetCode()
|
|
||||||
|
|
||||||
if op.Alias != "" {
|
|
||||||
alias = op.Alias
|
|
||||||
}
|
|
||||||
|
|
||||||
sd = this.DB().From(this.GetTable().As(alias))
|
|
||||||
|
|
||||||
if op.Tx != nil {
|
|
||||||
sd = op.Tx.From(this.GetTable().As(alias))
|
|
||||||
}
|
|
||||||
|
|
||||||
sd.WithHook(&ResourceHooks{res: this, u: u, tx: op.Tx})
|
|
||||||
|
|
||||||
return sd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (this Resource) GetRolesCondition(u req.User) (*db.SelectDataset, db.Expression) {
|
|
||||||
isFullRight := false
|
|
||||||
isFullNot := false
|
|
||||||
|
|
||||||
if u == nil {
|
|
||||||
u = GetAnonymous()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是系统用户, 则不做校验
|
|
||||||
if u.Uuid() == "ffffffff-ffff-ffff-ffff-ffffffffffff" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
// 这里不能放在 u.Uuid() == "ffffffff-ffff-ffff-ffff-ffffffffffff" 前面
|
|
||||||
// 会导致初始化数据时循环依赖
|
|
||||||
GetResRoles, err := do.Invoke[GetResRoles](this.container)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
items := GetResRoles(this.GetUuid(), u.Roles()...)
|
|
||||||
|
|
||||||
if len(items) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
GetResource := do.MustInvoke[contracts.GetResource](this.container)
|
|
||||||
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)
|
|
||||||
GetOrmConditions := do.MustInvoke[contracts.GetOrmConditions](this.container)
|
|
||||||
|
|
||||||
sub := lo.Reduce(items, func(carry *db.SelectDataset, item ResRole, _ int) *db.SelectDataset {
|
|
||||||
sd := this.DB().From(this.GetTable().As(this.GetCode())).Select(db.T(this.GetCode()).All())
|
|
||||||
|
|
||||||
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(sd, NewOrmModel(rResource, rResource.GetCode(), rResource.GetName()))
|
|
||||||
}
|
|
||||||
|
|
||||||
conditions := GetOrmConditions(item.Uuid, condition.Describe("关联扩展条件"))
|
|
||||||
|
|
||||||
if len(joins) == 0 && conditions.IsEmpty() {
|
|
||||||
// 无权限, 直接跳过这个 unoin 语句
|
|
||||||
if carry != nil {
|
|
||||||
return carry
|
|
||||||
}
|
|
||||||
// 第一个无权限除外, 避免所有用户所属角色都是无权限
|
|
||||||
sd = sd.Where(db.V(false))
|
|
||||||
isFullNot = true
|
|
||||||
} else if len(joins) == 0 && conditions.IsNotEmpty() && conditions.IsAlwaysRight() /* 1=1 的这种条件*/ {
|
|
||||||
// 只要有1个满权限, 直接返回单条语句
|
|
||||||
isFullRight = true
|
|
||||||
return sd
|
|
||||||
} else if conditions.IsNotEmpty() {
|
|
||||||
oOrm := NewOrm(this, nil)
|
|
||||||
oOrm.SetGlobalParams(req.NewGlobalParams("{}", u))
|
|
||||||
|
|
||||||
sd = sd.Where(conditions.ToSql(oOrm.GetModel()))
|
|
||||||
|
|
||||||
// 如果前面是无权限的sql查看, 这直接返回本次查询
|
|
||||||
if isFullNot {
|
|
||||||
isFullNot = false
|
|
||||||
return sd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if carry != nil {
|
|
||||||
return carry.Union(sd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sd
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
if isFullRight {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if isFullNot {
|
|
||||||
return nil, db.V(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// onResEvent 资源事件
|
|
||||||
func (this Resource) onResEvent(t string, result sql.Result) {
|
|
||||||
if num, err := result.RowsAffected(); num == 0 || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 有些地方可能没有注册 contracts.Queue
|
|
||||||
if qu, err := do.Invoke[contracts.Queue](this.container); err == nil {
|
|
||||||
// 全局触发器
|
|
||||||
// 1. 清除系统缓存
|
|
||||||
if err := qu.Publish(ResChangeEventTopic, ResChangeEventTopicPayload{
|
|
||||||
Type: t,
|
|
||||||
Res: this,
|
|
||||||
Result: result,
|
|
||||||
}); err != nil {
|
|
||||||
log.Println("Queue Publish Err:", ResChangeEventTopic, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// onUserEvent 用户事件
|
|
||||||
func (this Resource) onUserEvent(builder exec.QueryExecutor, 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 req.Resource, code, name string, sd *db.SelectDataset, fields []ResField) req.Resource {
|
|
||||||
fieldsCopy := make([]ResField, len(fields))
|
|
||||||
|
|
||||||
copy(fieldsCopy, fields)
|
|
||||||
|
|
||||||
for i := 0; i < len(fieldsCopy); i++ {
|
|
||||||
fieldsCopy[i].CodeResource = code
|
|
||||||
}
|
|
||||||
|
|
||||||
sql, _, err := sd.WithDialect(pRes.DB().Dialect()).ToSQL()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Resource{
|
|
||||||
Uuid: code,
|
|
||||||
PUuid: pRes.GetUuid(),
|
|
||||||
Code: code,
|
|
||||||
Name: name,
|
|
||||||
IsResVirtual: true,
|
|
||||||
Conn: pRes.GetConn(),
|
|
||||||
Table: sql,
|
|
||||||
Fields: fieldsCopy,
|
|
||||||
Roles: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.fsdpf.net/go/db"
|
|
||||||
"git.fsdpf.net/go/db/exp"
|
|
||||||
"git.fsdpf.net/go/db/schema"
|
|
||||||
"git.fsdpf.net/go/req"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var changeLogOnce sync.Map // key: Conn,每个数据库连接只建一次 _change_log 表
|
|
||||||
|
|
||||||
// isLocalDB 判断是否为本地文件型数据库(LastInsertId 返回最后一条而非第一条)
|
|
||||||
func isLocalDB(dialect string) bool {
|
|
||||||
return dialect == "sqlite3" || dialect == "duckdb"
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceHooks struct {
|
|
||||||
res Resource
|
|
||||||
u req.User
|
|
||||||
tx *db.TxDatabase
|
|
||||||
snapshots []map[string]any // UPDATE/DELETE 前预查的行数据
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) isTracked() bool {
|
|
||||||
if len(rh.res.HistoryRoles) == 0 || rh.res.IsVirtual() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lo.Contains(rh.res.HistoryRoles, "00000000-0000-0000-0000-000000000000") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return lo.Some(rh.res.HistoryRoles, rh.u.Roles())
|
|
||||||
}
|
|
||||||
|
|
||||||
// autoCreateChangeLog 在当前连接上建 _change_log 表,每个连接只执行一次
|
|
||||||
func (rh *ResourceHooks) autoCreateChangeLog() error {
|
|
||||||
conn := rh.res.DB()
|
|
||||||
v, _ := changeLogOnce.LoadOrStore(rh.res.Conn, &sync.Once{})
|
|
||||||
var err error
|
|
||||||
v.(*sync.Once).Do(func() {
|
|
||||||
sb := schema.New(conn)
|
|
||||||
err = sb.Create("_change_log", func(bp *schema.Blueprint) {
|
|
||||||
bp.Comment = "资源数据变更记录"
|
|
||||||
bp.BigIncrements("id").AutoIncrement().Comment("ID")
|
|
||||||
bp.Boolean("enabled").Default("1").Comment("是否有效")
|
|
||||||
bp.Char("created_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("操作用户")
|
|
||||||
bp.Char("owned_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("拥有者")
|
|
||||||
bp.Timestamp("created_at").UseCurrent().Comment("操作时间")
|
|
||||||
bp.Timestamp("updated_at").UseCurrent().Default(db.L("ON UPDATE CURRENT_TIMESTAMP")).Comment("更新时间")
|
|
||||||
bp.DateTime("deleted_at").Nullable().Comment("删除时间")
|
|
||||||
bp.Char("resource_uuid", 36).Default("").Comment("资源UUID")
|
|
||||||
bp.String("category", 10).Default("").Comment("操作类型 INSERT/UPDATE/DELETE")
|
|
||||||
bp.Char("trace_id", 36).Default("").Comment("请求追踪ID")
|
|
||||||
bp.Integer("row_id").Default("0").Comment("受影响行ID")
|
|
||||||
bp.Json("snapshot").Nullable().Comment("操作前数据快照")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// captureSnapshot 在写操作执行前,查出受影响的行存入 snapshots
|
|
||||||
func (rh *ResourceHooks) captureSnapshot(where exp.ExpressionList) {
|
|
||||||
sd := rh.res.DB().From(rh.res.GetTable())
|
|
||||||
if where != nil && len(where.Expressions()) > 0 {
|
|
||||||
sd = sd.Where(where.Expressions()...)
|
|
||||||
}
|
|
||||||
rows, err := sd.Executor().GetRecords()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("_change_log captureSnapshot err: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rh.snapshots = rows
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeChangeLogs 将变更写入 _change_log
|
|
||||||
func (rh *ResourceHooks) writeChangeLogs(category string, result sql.Result) {
|
|
||||||
if err := rh.autoCreateChangeLog(); err != nil {
|
|
||||||
log.Printf("_change_log init err: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
traceId := rh.u.Runtime().TraceId()
|
|
||||||
opUser := rh.u.Uuid()
|
|
||||||
|
|
||||||
var rows []any
|
|
||||||
|
|
||||||
switch category {
|
|
||||||
case "INSERT":
|
|
||||||
lastId, _ := result.LastInsertId()
|
|
||||||
count, _ := result.RowsAffected()
|
|
||||||
for i := int64(0); i < count; i++ {
|
|
||||||
rowId := lastId + i // MySQL: lastId 是第一条
|
|
||||||
if isLocalDB(rh.res.DB().Dialect()) {
|
|
||||||
rowId = lastId - count + 1 + i // SQLite: lastId 是最后一条
|
|
||||||
}
|
|
||||||
rows = append(rows, map[string]any{
|
|
||||||
"enabled": true,
|
|
||||||
"created_user": opUser,
|
|
||||||
"owned_user": opUser,
|
|
||||||
"resource_uuid": rh.res.Uuid,
|
|
||||||
"category": category,
|
|
||||||
"trace_id": traceId,
|
|
||||||
"row_id": rowId,
|
|
||||||
"snapshot": nil,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case "UPDATE", "DELETE":
|
|
||||||
for _, snap := range rh.snapshots {
|
|
||||||
rowId := int64(0)
|
|
||||||
if id, ok := snap["id"]; ok {
|
|
||||||
rowId = toChangeLogInt64(id)
|
|
||||||
}
|
|
||||||
snapJSON, _ := json.Marshal(snap)
|
|
||||||
rows = append(rows, map[string]any{
|
|
||||||
"enabled": true,
|
|
||||||
"created_user": opUser,
|
|
||||||
"owned_user": opUser,
|
|
||||||
"resource_uuid": rh.res.Uuid,
|
|
||||||
"category": category,
|
|
||||||
"trace_id": traceId,
|
|
||||||
"row_id": rowId,
|
|
||||||
"snapshot": string(snapJSON),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inserter := rh.res.DB().Insert("_change_log")
|
|
||||||
if rh.tx != nil {
|
|
||||||
inserter = rh.tx.Insert("_change_log")
|
|
||||||
}
|
|
||||||
if _, err := inserter.Rows(rows...).Executor().Exec(); err != nil {
|
|
||||||
log.Printf("_change_log write err: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toChangeLogInt64(v any) int64 {
|
|
||||||
switch val := v.(type) {
|
|
||||||
case int64:
|
|
||||||
return val
|
|
||||||
case int:
|
|
||||||
return int64(val)
|
|
||||||
case int32:
|
|
||||||
return int64(val)
|
|
||||||
case float64:
|
|
||||||
return int64(val)
|
|
||||||
case string:
|
|
||||||
n, _ := strconv.ParseInt(val, 10, 64)
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) Before(dataset interface{}) error {
|
|
||||||
switch d := dataset.(type) {
|
|
||||||
case *db.SelectDataset:
|
|
||||||
return rh.beforeSelectDataset(d)
|
|
||||||
case *db.InsertDataset:
|
|
||||||
return rh.beforeInsertDataset(d)
|
|
||||||
case *db.UpdateDataset:
|
|
||||||
return rh.beforeUpdateDataset(d)
|
|
||||||
case *db.DeleteDataset:
|
|
||||||
return rh.beforeDeleteDataset(d)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) After(dataset interface{}, result interface{}) error {
|
|
||||||
if rh.res.IsVirtual() {
|
|
||||||
// 返回鉴权后的 DB Builder
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户事件
|
|
||||||
// if rh.res.IsHistoryRecord {
|
|
||||||
// rh.res.onUserEvent(builder, user)
|
|
||||||
// }
|
|
||||||
|
|
||||||
switch dataset.(type) {
|
|
||||||
case *db.SelectDataset:
|
|
||||||
case *db.InsertDataset:
|
|
||||||
r := result.(sql.Result)
|
|
||||||
rh.res.onResEvent("INSERT", r)
|
|
||||||
if rh.isTracked() {
|
|
||||||
rh.writeChangeLogs("INSERT", r)
|
|
||||||
}
|
|
||||||
case *db.UpdateDataset:
|
|
||||||
r := result.(sql.Result)
|
|
||||||
rh.res.onResEvent("UPDATE", r)
|
|
||||||
if rh.isTracked() {
|
|
||||||
rh.writeChangeLogs("UPDATE", r)
|
|
||||||
}
|
|
||||||
case *db.DeleteDataset:
|
|
||||||
r := result.(sql.Result)
|
|
||||||
rh.res.onResEvent("DELETE", r)
|
|
||||||
if rh.isTracked() {
|
|
||||||
rh.writeChangeLogs("DELETE", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) beforeInsertDataset(id *db.InsertDataset) error {
|
|
||||||
switch true {
|
|
||||||
case id.GetClauses().HasRows():
|
|
||||||
return rh.beforeInsertRows(id)
|
|
||||||
case id.GetClauses().HasVals():
|
|
||||||
return rh.beforeInsertColsVals(id)
|
|
||||||
case id.GetClauses().HasFrom():
|
|
||||||
return rh.beforeInsertFromQuery(id)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) beforeInsertRows(id *db.InsertDataset) error {
|
|
||||||
rows := id.GetClauses().Rows()
|
|
||||||
|
|
||||||
for i := range rows {
|
|
||||||
rowValue := reflect.ValueOf(rows[i])
|
|
||||||
if rowValue.Kind() == reflect.Ptr {
|
|
||||||
rowValue = rowValue.Elem()
|
|
||||||
}
|
|
||||||
if rowValue.Kind() == reflect.Struct {
|
|
||||||
if row, err := exp.NewRecordFromStruct(rowValue.Interface(), true, false); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
rows[i] = row
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range rows {
|
|
||||||
row := rows[i].(map[string]any)
|
|
||||||
// 格式化保存数据
|
|
||||||
if err := rh.normalizeSaveValue(row); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// 填充默认数据
|
|
||||||
if err := rh.applyDefaultValue(row, true, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) beforeInsertColsVals(id *db.InsertDataset) error {
|
|
||||||
cols := id.GetClauses().Cols()
|
|
||||||
vals := id.GetClauses().Vals()
|
|
||||||
|
|
||||||
colsName := []string{}
|
|
||||||
for _, col := range cols.Columns() {
|
|
||||||
colsName = append(colsName, col.(exp.IdentifierExpression).GetCol().(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
colsVal := []any{}
|
|
||||||
for k, v := range lo.OmitByKeys(rh.getFieldsDefaultValue(true, false), colsName) {
|
|
||||||
cols = cols.Append(db.C(k))
|
|
||||||
colsVal = append(colsVal, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(vals); i++ {
|
|
||||||
for j := 0; j < len(vals[i]); j++ {
|
|
||||||
if filed, ok := rh.res.GetField(colsName[j]); ok {
|
|
||||||
vals[i][j] = filed.ToValue(vals[i][j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vals[i] = append(vals[i], colsVal...)
|
|
||||||
}
|
|
||||||
|
|
||||||
*id = *id.Cols(cols)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) beforeInsertFromQuery(id *db.InsertDataset) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 规范化保存数据
|
|
||||||
// 1. 移除系统缺省字段, 如果是 insert 需要调用 applyDefaultValue
|
|
||||||
// 2. 移除系统中没有的字段
|
|
||||||
func (rh *ResourceHooks) normalizeSaveValue(row db.Record) error {
|
|
||||||
for k, v := range row {
|
|
||||||
if k == "id" || k == "created_user" || k == "created_at" || k == "deleted_at" || k == "updated_at" {
|
|
||||||
delete(row, k)
|
|
||||||
} else if val, ok := v.(db.Expression); ok {
|
|
||||||
row[k] = val
|
|
||||||
} else if field, ok := rh.res.GetField(k); ok {
|
|
||||||
row[k] = field.ToValue(v)
|
|
||||||
} else {
|
|
||||||
delete(row, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 填充默认数据
|
|
||||||
func (rh *ResourceHooks) applyDefaultValue(row db.Record, forInsert, forUpdate bool) error {
|
|
||||||
// 填充默认数据
|
|
||||||
for k, v := range rh.getFieldsDefaultValue(forInsert, forUpdate) {
|
|
||||||
if _, ok := row[k]; !ok {
|
|
||||||
row[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) getFieldsDefaultValue(forInsert, forUpdate bool) map[string]db.Expression {
|
|
||||||
defs := map[string]db.Expression{}
|
|
||||||
|
|
||||||
if forInsert {
|
|
||||||
for _, item := range rh.res.Fields {
|
|
||||||
if item.GetCode() == "updated_at" || item.GetCode() == "created_user" || item.GetCode() == "owned_user" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(item.Default) > 4 && strings.ToLower(item.Default[0:4]) == "sql:" {
|
|
||||||
defs[item.GetCode()] = item.GetRawDefault()
|
|
||||||
} else if item.DataType == req.ResJson {
|
|
||||||
defs[item.GetCode()] = item.GetRawDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := defs["owned_user"]; !ok {
|
|
||||||
defs["owned_user"] = db.V(rh.u.Uuid())
|
|
||||||
}
|
|
||||||
|
|
||||||
defs["created_user"] = db.V(rh.u.Uuid())
|
|
||||||
}
|
|
||||||
|
|
||||||
if forUpdate && isLocalDB(rh.res.DB().Dialect()) {
|
|
||||||
defs["updated_at"] = db.L("CURRENT_TIMESTAMP")
|
|
||||||
}
|
|
||||||
|
|
||||||
return defs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) beforeSelectDataset(sd *db.SelectDataset) error {
|
|
||||||
sub, ex := rh.res.GetRolesCondition(rh.u)
|
|
||||||
if ex != nil {
|
|
||||||
*sd = *sd.Where(ex)
|
|
||||||
} else if sub != nil {
|
|
||||||
table := sd.GetClauses().From().Columns()[0]
|
|
||||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
|
||||||
*sd = *sd.From(db.V(sub.Expression()).As(alias.GetAs()))
|
|
||||||
} else {
|
|
||||||
*sd = *sd.From(db.V(sub.Expression()).As(table))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rh *ResourceHooks) beforeUpdateDataset(ud *db.UpdateDataset) error {
|
|
||||||
// 虚拟资源暂时不考虑鉴权
|
|
||||||
if rh.res.IsVirtual() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sub, ex := rh.res.GetRolesCondition(rh.u)
|
|
||||||
if ex != nil {
|
|
||||||
*ud = *ud.Where(ex)
|
|
||||||
} else if sub != nil {
|
|
||||||
table := ud.GetClauses().Table()
|
|
||||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
|
||||||
*ud = *ud.Where(alias.GetAs().Col("id").In(sub.Select(db.T(rh.res.GetCode()).Col("id"))))
|
|
||||||
} else {
|
|
||||||
*ud = *ud.Where(db.L("?.id", table).In(sub.Select(db.T(rh.res.GetCode()).Col("id"))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录变更前快照(role 条件已应用,WHERE 与实际 UPDATE 一致)
|
|
||||||
if rh.isTracked() {
|
|
||||||
rh.captureSnapshot(ud.GetClauses().Where())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化保存数据
|
|
||||||
if ud.GetClauses().HasSetValues() {
|
|
||||||
udv := ud.GetClauses().SetValues()
|
|
||||||
|
|
||||||
switch data := udv.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return rh.normalizeSaveValue(data)
|
|
||||||
case db.Record:
|
|
||||||
return rh.normalizeSaveValue(data)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type: %T", udv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (rh *ResourceHooks) beforeDeleteDataset(dd *db.DeleteDataset) error {
|
|
||||||
// 虚拟资源暂时不考虑鉴权
|
|
||||||
if rh.res.IsVirtual() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sub, ex := rh.res.GetRolesCondition(rh.u)
|
|
||||||
if ex != nil {
|
|
||||||
*dd = *dd.Where(ex)
|
|
||||||
} else if sub != nil {
|
|
||||||
table := dd.GetClauses().From().Columns()[0]
|
|
||||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
|
||||||
*dd = *dd.Where(alias.GetAs().Col("id").In(sub.Select(db.T(rh.res.GetCode()).Col("id"))))
|
|
||||||
} else {
|
|
||||||
*dd = *dd.Where(db.L("?.id", table).In(sub.Select(db.T(rh.res.GetCode()).Col("id"))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录变更前快照(role 条件已应用,WHERE 与实际 DELETE 一致)
|
|
||||||
if rh.isTracked() {
|
|
||||||
rh.captureSnapshot(dd.GetClauses().Where())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package base_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.fsdpf.net/go/contracts/base"
|
|
||||||
"git.fsdpf.net/go/db"
|
|
||||||
"git.fsdpf.net/go/db/engine"
|
|
||||||
"git.fsdpf.net/go/req"
|
|
||||||
"github.com/samber/do/v2"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
|
|
||||||
_ "git.fsdpf.net/go/db/schema/dialect/mysql"
|
|
||||||
_ "git.fsdpf.net/go/db/schema/dialect/sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type resourceTest struct {
|
|
||||||
suite.Suite
|
|
||||||
res *base.Resource
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResource(t *testing.T) {
|
|
||||||
suite.Run(t, new(resourceTest))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *resourceTest) SetupSuite() {
|
|
||||||
app := do.New()
|
|
||||||
|
|
||||||
do.Provide(app, func(container do.Injector) (engine.Engine, error) {
|
|
||||||
return engine.Open(map[string]engine.DBConfig{
|
|
||||||
"default": engine.NewDBConfig("mysql",
|
|
||||||
engine.WithHost(os.Getenv("MYSQL_HOST")),
|
|
||||||
engine.WithPort(os.Getenv("MYSQL_PORT")),
|
|
||||||
engine.WithDatabase(os.Getenv("MYSQL_DB")),
|
|
||||||
engine.WithUsername(os.Getenv("MYSQL_USER")),
|
|
||||||
engine.WithPassword(os.Getenv("MYSQL_PASSWD")),
|
|
||||||
),
|
|
||||||
}), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
fields := []base.ResField{
|
|
||||||
{Name: "字段", Code: "fields", DataType: req.ResJson, Default: "{}"},
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &base.Resource{
|
|
||||||
Uuid: "4bf3a311-cbe1-4236-bdda-c64ab04ae9b1",
|
|
||||||
Code: "User",
|
|
||||||
Name: "用户",
|
|
||||||
Table: "users",
|
|
||||||
Primarykey: "id",
|
|
||||||
Fields: fields,
|
|
||||||
}
|
|
||||||
|
|
||||||
res.InitContainer(app)
|
|
||||||
|
|
||||||
t.res = res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *resourceTest) TestInsertRows() {
|
|
||||||
type Temp struct {
|
|
||||||
Name string `db:"name"`
|
|
||||||
Fields map[string]any `db:"fields"`
|
|
||||||
}
|
|
||||||
|
|
||||||
result := "INSERT INTO `users` AS `User` (`created_user`, `fields`, `name`, `owned_user`) " +
|
|
||||||
"VALUES ('00000000-0000-0000-0000-000000000000', '{\\\"aa\\\":1}', '张三', '00000000-0000-0000-0000-000000000000')"
|
|
||||||
|
|
||||||
sql, _, _ := t.res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
|
||||||
db.Record{"name": "张三", "id": 1, "fields": map[string]any{"aa": 1}},
|
|
||||||
).Executor().ToSQL()
|
|
||||||
t.Equal(result, sql)
|
|
||||||
|
|
||||||
sql, _, _ = t.res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
|
||||||
&Temp{Name: "张三", Fields: map[string]any{"aa": 1}},
|
|
||||||
).Executor().ToSQL()
|
|
||||||
t.Equal(result, sql)
|
|
||||||
|
|
||||||
sql, _, _ = t.res.GetDBTable(base.GetAnonymous()).Insert().Rows(
|
|
||||||
Temp{Name: "张三", Fields: map[string]any{"aa": 1}},
|
|
||||||
).Executor().ToSQL()
|
|
||||||
t.Equal(result, sql)
|
|
||||||
}
|
|
||||||
func (t *resourceTest) TestInsertColsVals() {
|
|
||||||
result := "INSERT INTO `users` AS `User` (`name`, `fields`, `created_user`, `owned_user`) VALUES " +
|
|
||||||
"('李四', '{\\\"aa\\\":1}', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000'), " +
|
|
||||||
"('王五', '{\\\"aa\\\":3}', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000')"
|
|
||||||
|
|
||||||
sql, _, _ := t.res.GetDBTable(base.GetAnonymous()).Insert().Cols("name", "fields").Vals(
|
|
||||||
db.Vals{"李四", map[string]any{"aa": 1}},
|
|
||||||
db.Vals{"王五", map[string]any{"aa": 3}},
|
|
||||||
).Executor().ToSQL()
|
|
||||||
|
|
||||||
_ = result
|
|
||||||
_ = sql
|
|
||||||
|
|
||||||
// t.Equal(result, sql)
|
|
||||||
|
|
||||||
}
|
|
||||||
func (*resourceTest) TestInsertColsFromQuery() {}
|
|
||||||
+4
-16
@@ -53,36 +53,24 @@ func (this user) Runtime() req.UserRuntime {
|
|||||||
|
|
||||||
// GetAnonymous 获取匿名用户
|
// GetAnonymous 获取匿名用户
|
||||||
func GetAnonymous(opts ...req.UserRuntimeOption) req.User {
|
func GetAnonymous(opts ...req.UserRuntimeOption) req.User {
|
||||||
u := user{
|
return user{
|
||||||
id: 0,
|
id: 0,
|
||||||
uuid: "00000000-0000-0000-0000-000000000000",
|
uuid: "00000000-0000-0000-0000-000000000000",
|
||||||
username: "anonymous",
|
username: "anonymous",
|
||||||
nickname: "匿名者",
|
nickname: "匿名者",
|
||||||
roles: []string{"00000000-0000-0000-0000-000000000000"},
|
roles: []string{"00000000-0000-0000-0000-000000000000"},
|
||||||
runtime: req.UserRuntime{},
|
runtime: req.NewUserRuntime(opts...),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(&u.runtime)
|
|
||||||
}
|
|
||||||
|
|
||||||
return u
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSystemUser 系统用户
|
// GetSystemUser 系统用户
|
||||||
func GetSystemUser(opts ...req.UserRuntimeOption) req.User {
|
func GetSystemUser(opts ...req.UserRuntimeOption) req.User {
|
||||||
u := user{
|
return user{
|
||||||
id: -1,
|
id: -1,
|
||||||
uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||||
username: "system",
|
username: "system",
|
||||||
nickname: "系统",
|
nickname: "系统",
|
||||||
roles: []string{"ffffffff-ffff-ffff-ffff-ffffffffffff"},
|
roles: []string{"ffffffff-ffff-ffff-ffff-ffffffffffff"},
|
||||||
runtime: req.UserRuntime{},
|
runtime: req.NewUserRuntime(opts...),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(&u.runtime)
|
|
||||||
}
|
|
||||||
|
|
||||||
return u
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type UserAsset struct {
|
|||||||
ID int64 `db:"id"`
|
ID int64 `db:"id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
FileName string `db:"filename"`
|
FileName string `db:"filename"`
|
||||||
|
FileUrl string `db:"file_url"`
|
||||||
File string `db:"file"`
|
File string `db:"file"`
|
||||||
Mime string `db:"mime"`
|
Mime string `db:"mime"`
|
||||||
Size int64 `db:"size"`
|
Size int64 `db:"size"`
|
||||||
|
|||||||
Reference in New Issue
Block a user