feat: 升级 samber/do v1 → v2
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
package base
|
||||
|
||||
// DataType 定义参数数据类型
|
||||
type DataType string
|
||||
|
||||
const (
|
||||
DataTypeString DataType = "string" // 字符串类型
|
||||
DataTypeInteger DataType = "integer" // 整数类型
|
||||
DataTypeFloat DataType = "float" // 浮点数类型
|
||||
DataTypeBoolean DataType = "boolean" // 布尔类型
|
||||
DataTypeJSON DataType = "json" // JSON类型
|
||||
)
|
||||
|
||||
// SchemaParam 工作流参数
|
||||
type SchemaParam struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
DataType DataType `db:"data_type" json:"data_type"`
|
||||
Required bool `db:"required" json:"required"`
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
Fields SchemaParams `db:"fields" json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type SchemaParams []SchemaParam
|
||||
|
||||
// Condflow 工作流
|
||||
type Condflow struct {
|
||||
Uuid string `db:"uuid" json:"uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
Params SchemaParams `db:"params" json:"params"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
UpdatedAt string `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type GetCondflow func(code string) (Condflow, bool)
|
||||
@@ -0,0 +1,23 @@
|
||||
package base
|
||||
|
||||
import "git.fsdpf.net/go/reflux/fieldx"
|
||||
|
||||
// CondflowCase 工作流条件分支
|
||||
type CondflowCase struct {
|
||||
Uuid string `db:"uuid" json:"uuid"`
|
||||
CondflowUuid string `db:"condflow_uuid" json:"condflow_uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
Priority int `db:"priority" json:"priority"`
|
||||
Action string `db:"action" json:"action"`
|
||||
ActionParams fieldx.Schema `db:"action_params" json:"action_params"` // 动作参数值, 对应 params_schema
|
||||
ActionConfig fieldx.Schema `db:"action_config" json:"action_config"` // 动作配置值, 对应 config_schema
|
||||
NextCaseOnSuccess bool `db:"next_case_on_success" json:"next_case_on_success"`
|
||||
NextFlowUuid string `db:"next_flow_uuid" json:"next_flow_uuid"`
|
||||
NextFlowParams fieldx.Schema `db:"next_flow_params" json:"next_flow_params"` // 流程间参数映射
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
UpdatedAt string `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type GetCondflowCases func(uuid string) []CondflowCase
|
||||
@@ -0,0 +1,25 @@
|
||||
package base
|
||||
|
||||
// CondflowDecisionAction 执行器动作定义
|
||||
type CondflowDecisionAction struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
ConfigSchema SchemaParams `db:"config_schema" json:"config_schema"` // 配置项Schema定义
|
||||
ParamsSchema SchemaParams `db:"params_schema" json:"params_schema"` // 参数Schema定义
|
||||
ResourceUuid string `db:"resource_uuid" json:"resource_uuid"`
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
}
|
||||
|
||||
// CondflowDecision 执行器注册表
|
||||
type CondflowDecision struct {
|
||||
Uuid string `db:"uuid" json:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid" json:"resource_uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
Actions []CondflowDecisionAction `db:"actions" json:"actions"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
UpdatedAt string `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type GetCondflowDecision func(code string) (CondflowDecision, bool)
|
||||
@@ -51,6 +51,7 @@ type DataList struct {
|
||||
ThemeConfig map[string]any `db:"themeConfig"` // 列表布局配置
|
||||
Roles []string `db:"roles"` // 列表权限
|
||||
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
|
||||
SelectionType string `db:"selectionType"` // 行选择类型
|
||||
Platform string `db:"platform"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
|
||||
@@ -25,6 +25,7 @@ type DataListField struct {
|
||||
FilterByJsSetting map[string]any `db:"filterByJsSetting"` // 字段筛选组件设置参数
|
||||
FilterOperator string `db:"filterOperator"` // 字段筛选符号
|
||||
EditableRoles []string `db:"editableRoles"` // 字段编辑权限
|
||||
Middlewares [][2]any `db:"middlewares"` // 字段数据转换
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
+19
-16
@@ -1,22 +1,25 @@
|
||||
package base
|
||||
|
||||
type GridLayout struct {
|
||||
Uuid string `db:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid"` // 所属资源UUID
|
||||
Code string `db:"code"` // 布局标识
|
||||
Name string `db:"name"` // 布局名称
|
||||
Type int `db:"type"` // 布局类型 [0=普通布局, 1=辅助布局]
|
||||
PrimaryKey string `db:"primaryKey"` // 主键
|
||||
MarginX int `db:"marginX"` // 栅格 x 间距
|
||||
MarginY int `db:"marginY"` // 栅格 y 间距
|
||||
Cols int `db:"cols"` // 网格列基数
|
||||
RowHeight float32 `db:"rowHeight"` // 栅格行高
|
||||
Css map[string]any `db:"css"` // 布局CSS
|
||||
Props []any `db:"props"` // 布局PROPS
|
||||
Platform string `db:"platform"`
|
||||
Roles []string `db:"roles"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
Uuid string `db:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid"` // 所属资源UUID
|
||||
Code string `db:"code"` // 布局标识
|
||||
Name string `db:"name"` // 布局名称
|
||||
Type int `db:"type"` // 布局类型 [0=普通布局, 1=辅助布局]
|
||||
PrimaryKey string `db:"primaryKey"` // 主键
|
||||
MarginX int `db:"marginX"` // 栅格 x 间距
|
||||
MarginY int `db:"marginY"` // 栅格 y 间距
|
||||
Cols int `db:"cols"` // 网格列基数
|
||||
RowHeight float32 `db:"rowHeight"` // 栅格行高
|
||||
Css map[string]any `db:"css"` // 布局CSS
|
||||
Props []any `db:"props"` // 布局PROPS
|
||||
Groups []map[string]any `db:"groups"` // 布局分组
|
||||
Theme string `db:"theme"` // 布局样式
|
||||
ThemeConfig map[string]any `db:"themeConfig"` // 布局样式配置
|
||||
Platform string `db:"platform"`
|
||||
Roles []string `db:"roles"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
type GetGridLayout func(code string) (GridLayout, bool)
|
||||
|
||||
@@ -3,6 +3,7 @@ package base
|
||||
type GridLayoutField struct {
|
||||
Uuid string `db:"uuid"`
|
||||
GridLayoutUuid string `db:"grid_layout_uuid"` // 所属布局 UUID
|
||||
Group string `db:"group"`
|
||||
Type string `db:"type"`
|
||||
X int `db:"x"`
|
||||
Y int `db:"y"`
|
||||
|
||||
@@ -14,7 +14,9 @@ type GridLayoutForm struct {
|
||||
MarginY int `db:"marginY"` // 栅格 y 间距
|
||||
Cols int `db:"cols"` // 网格列基数
|
||||
FormProps []map[string]string `db:"formProps"` // 表单PROPS
|
||||
Groups []map[string]any `db:"groups"` // 表单分组
|
||||
Groups []map[string]any `db:"groups"` // 布局分组
|
||||
Theme string `db:"theme"` // 布局样式
|
||||
ThemeConfig map[string]any `db:"themeConfig"` // 布局样式配置
|
||||
ListenChangeFields []string `db:"listenChangeFields"` // 监听字段
|
||||
ListenChangeFieldsFunc string `db:"listenChangeFieldsFunc"` // 监听字段回调方法
|
||||
Platform string `db:"platform"`
|
||||
|
||||
+15
-9
@@ -6,7 +6,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.fsdpf.net/go/contracts"
|
||||
"github.com/samber/do"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/samber/do/v2"
|
||||
)
|
||||
|
||||
type PkgVersion struct {
|
||||
@@ -39,7 +41,7 @@ func (this PkgVersion) GetSourceMapPath(prefix ...string) string {
|
||||
)
|
||||
}
|
||||
|
||||
func (this PkgVersion) GetFile(app *do.Injector) (file string, err error) {
|
||||
func (this PkgVersion) GetFile(app do.Injector, u req.User) (file string, err error) {
|
||||
if this.Id == 0 {
|
||||
return file, contracts.ErrPkgUnknown
|
||||
}
|
||||
@@ -50,23 +52,27 @@ func (this PkgVersion) GetFile(app *do.Injector) (file string, err error) {
|
||||
return file, contracts.ErrResNotFound
|
||||
}
|
||||
|
||||
_, err = res.GetDBTable().Where("id", this.Id).Value(&file, "file")
|
||||
ok, err = res.GetDBTable(u).Where(db.C("id").Eq(this.Id)).Select("file").ScanVal(&file)
|
||||
|
||||
if !ok {
|
||||
return file, contracts.ErrResNotFound
|
||||
}
|
||||
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (this PkgVersion) GetSourceMap(app *do.Injector) (file string, err error) {
|
||||
func (this PkgVersion) GetSourceMap(app do.Injector) (file string, err error) {
|
||||
if this.Id == 0 {
|
||||
return file, contracts.ErrPkgUnknown
|
||||
}
|
||||
|
||||
res, ok := do.MustInvoke[contracts.GetResource](app)("PkgVersion")
|
||||
// res, ok := do.MustInvoke[contracts.GetResource](app)("PkgVersion")
|
||||
|
||||
if !ok {
|
||||
return file, contracts.ErrResNotFound
|
||||
}
|
||||
// if !ok {
|
||||
// return file, contracts.ErrResNotFound
|
||||
// }
|
||||
|
||||
_, err = res.GetDBTable().Where("id", this.Id).Value(&file, "sourcemap")
|
||||
// _, err = res.GetDBTable().Where("id", this.Id).Value(&file, "sourcemap")
|
||||
|
||||
return file, err
|
||||
}
|
||||
|
||||
+7
-8
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"git.fsdpf.net/go/contracts/res_type"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
@@ -67,13 +66,13 @@ func (this *QueryField) ToSql() db.Expression {
|
||||
if this.Alias() != "" {
|
||||
if this.IsExpr() {
|
||||
if this.GetCode() == "" {
|
||||
return db.Raw("null as `" + this.Alias() + "`")
|
||||
return db.V(nil).As(this.Alias())
|
||||
}
|
||||
return db.Raw("(" + this.GetCode() + ") as `" + this.Alias() + "`")
|
||||
return db.L(this.GetCode()).As(this.Alias())
|
||||
}
|
||||
return db.Raw("`" + this.GetCodeResource() + "`.`" + this.GetCode() + "` as `" + this.Alias() + "`")
|
||||
return db.T(this.GetCodeResource()).Col(this.GetCode()).As(this.Alias())
|
||||
}
|
||||
return db.Raw("`" + this.GetCodeResource() + "`.`" + this.GetCode() + "`")
|
||||
return db.T(this.GetCodeResource()).Col(this.GetCode())
|
||||
}
|
||||
|
||||
func (this *QueryField) ToStructField(tags ...string) reflect.StructField {
|
||||
@@ -104,11 +103,11 @@ func (this *QueryField) ToStructField(tags ...string) reflect.StructField {
|
||||
case req.ReqString:
|
||||
typ = reflect.TypeOf(string(""))
|
||||
case req.ReqNumber:
|
||||
typ = reflect.TypeOf(res_type.ResFieldByNumber(0))
|
||||
typ = reflect.TypeOf(float64(0))
|
||||
case req.ReqInteger:
|
||||
typ = reflect.TypeOf(res_type.ResFieldByInteger(0))
|
||||
typ = reflect.TypeOf(int64(0))
|
||||
case req.ReqFloat:
|
||||
typ = reflect.TypeOf(res_type.ResFieldByFloat(0))
|
||||
typ = reflect.TypeOf(float64(0))
|
||||
case req.ReqBool:
|
||||
typ = reflect.TypeOf(true)
|
||||
case req.ReqArray:
|
||||
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
package base
|
||||
|
||||
type ResApiParam struct {
|
||||
Code string `db:"code"`
|
||||
Name string `db:"name"`
|
||||
Category string `db:"category"`
|
||||
DataType string `db:"type"`
|
||||
IsRequired bool `db:"isRequired"`
|
||||
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 {
|
||||
@@ -16,7 +16,7 @@ type ResApi struct {
|
||||
PrimaryKey string `db:"primaryKey"`
|
||||
ResourceUuid string `db:"resource_uuid"`
|
||||
Method string `db:"method"`
|
||||
Category string `db:"action"`
|
||||
Action string `db:"action"`
|
||||
Params []ResApiParam `db:"params"`
|
||||
Roles []string `db:"roles"`
|
||||
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package base
|
||||
|
||||
import "git.fsdpf.net/go/condition"
|
||||
import (
|
||||
"git.fsdpf.net/go/condition"
|
||||
)
|
||||
|
||||
type ResCondition struct {
|
||||
Id int `db:"id"`
|
||||
@@ -53,7 +55,7 @@ func NewConditionByRes(items []ResCondition, opts ...condition.Option) (root *co
|
||||
conditions[item.Id] = condition.NewExpr(
|
||||
item.ColumnResource, item.Column,
|
||||
condition.Token(item.Value, condition.TokenType(item.ValueType)),
|
||||
condition.Operator(condition.ConditionOperator(item.Operator)),
|
||||
condition.Operator(condition.ConditionOperator(condition.GetDBOperator(item.Operator))),
|
||||
condition.FieldSqlFn(item.ColumnSqlFunc, item.ColumnSqlFuncParam),
|
||||
condition.IgnoreEmptyParma(item.IgnoreEmptyParma),
|
||||
)
|
||||
|
||||
+43
-11
@@ -1,16 +1,48 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/reflux"
|
||||
"git.fsdpf.net/go/reflux/valuex"
|
||||
)
|
||||
|
||||
// role_uuid => value
|
||||
type ResRoleValue map[string]string
|
||||
|
||||
type ResConfigure struct {
|
||||
Uuid string `db:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid"`
|
||||
Key string `db:"key"`
|
||||
Value string `db:"value"`
|
||||
Label string `db:"label"`
|
||||
Type string `db:"type"`
|
||||
IsPrivate bool `db:"isPrivate"`
|
||||
Desc string `db:"desc"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
Uuid string `db:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid"`
|
||||
Key string `db:"key"`
|
||||
Value string `db:"value"`
|
||||
Label string `db:"label"`
|
||||
Type string `db:"type"`
|
||||
IsPrivate bool `db:"isPrivate"`
|
||||
Desc string `db:"desc"`
|
||||
RoleValues []ResRoleValue `db:"role_values"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
type GetResConfigures func(uuid string) []ResConfigure
|
||||
// GetValueByRoles 根据角色优先级获取配置值
|
||||
//
|
||||
// - roles: 角色UUIDs
|
||||
//
|
||||
// 返回: valuex.Accessor 对象和是否找到角色特定值的标志
|
||||
func (rc *ResConfigure) GetValueByRoles(roles ...string) (valuex.Accessor, bool) {
|
||||
// 如果没有角色值配置或没有传入角色,返回默认值
|
||||
if len(rc.RoleValues) == 0 || len(roles) == 0 {
|
||||
return reflux.New(rc.Value), false
|
||||
}
|
||||
|
||||
for _, rv := range rc.RoleValues {
|
||||
for _, role := range roles {
|
||||
if value, exists := rv[role]; exists {
|
||||
return reflux.New(value), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 没有找到匹配的角色值,返回默认值
|
||||
return reflux.New(rc.Value), false
|
||||
}
|
||||
|
||||
type GetResConfigure func(key string, roles ...string) (valuex.Accessor, bool)
|
||||
|
||||
+62
-54
@@ -7,24 +7,24 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.fsdpf.net/go/contracts/res_type"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/schema"
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// 资源字段
|
||||
// ResField 资源字段
|
||||
type ResField struct {
|
||||
Uuid string `db:"uuid"`
|
||||
Name string `db:"name"`
|
||||
Code string `db:"code"`
|
||||
CodeResource string `db:"codeResource"`
|
||||
DataType req.ResDataType `db:"table_type"`
|
||||
Length string `db:"length"`
|
||||
Comment string `db:"comment"`
|
||||
Default string `db:"default"`
|
||||
Uuid string `db:"uuid" json:"uuid"`
|
||||
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 {
|
||||
@@ -42,7 +42,7 @@ func (this ResField) ToStructField(tags ...string) reflect.StructField {
|
||||
req.ResTimestamp, req.ResDate, req.ResDatetime:
|
||||
typ = reflect.TypeOf(string(""))
|
||||
case req.ResInteger, req.ResSmallInteger:
|
||||
typ = reflect.TypeOf(res_type.ResFieldByInteger(0))
|
||||
typ = reflect.TypeOf(int64(0))
|
||||
case req.ResDecimal:
|
||||
typ = reflect.TypeOf(float64(0))
|
||||
case req.ResBoolean:
|
||||
@@ -62,6 +62,10 @@ func (this ResField) ToStructField(tags ...string) reflect.StructField {
|
||||
}
|
||||
}
|
||||
|
||||
func (this ResField) IsVirtual() bool {
|
||||
return this.Virtual
|
||||
}
|
||||
|
||||
func (this ResField) GetCode() string {
|
||||
return this.Code
|
||||
}
|
||||
@@ -101,30 +105,16 @@ func (this ResField) GetQueryDataType() req.RouteParamType {
|
||||
}
|
||||
|
||||
func (this ResField) ToValue(v any) any {
|
||||
switch this.DataType {
|
||||
case req.ResString, req.ResText, req.ResEnum,
|
||||
req.ResTimestamp, req.ResDate, req.ResDatetime:
|
||||
return strings.Trim(cast.ToString(v), " ")
|
||||
case req.ResInteger, req.ResSmallInteger:
|
||||
return cast.ToInt(v)
|
||||
case req.ResDecimal:
|
||||
return strings.Trim(cast.ToString(v), " ")
|
||||
case req.ResBoolean:
|
||||
if v, _ := strconv.ParseBool(fmt.Sprintf("%v", v)); v {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case req.ResJson:
|
||||
if this.DataType == req.ResJson {
|
||||
if v == nil {
|
||||
if this.Default != "" && this.Default[0:1] == "[" {
|
||||
return db.Raw("'[]'")
|
||||
return db.V("[]")
|
||||
} else if this.Default != "" && this.Default[0:1] == "{" {
|
||||
return db.Raw("'{}'")
|
||||
return db.V("{}")
|
||||
} else if this.Default == "" {
|
||||
return db.Raw("'{}'")
|
||||
return db.V("{}")
|
||||
}
|
||||
|
||||
return this.Default
|
||||
return this.GetRawDefault()
|
||||
}
|
||||
|
||||
if str, ok := v.(string); ok {
|
||||
@@ -137,52 +127,61 @@ func (this ResField) ToValue(v any) any {
|
||||
panic(fmt.Sprintf("%s, 类型转换错误, %s", this.Code, err))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Trim(cast.ToString(v), " ")
|
||||
return v
|
||||
}
|
||||
|
||||
func (this ResField) GetRawDefault(driver string) db.Expression {
|
||||
func (this ResField) GetRawDefault() db.Expression {
|
||||
if this.DataType == req.ResJson {
|
||||
if this.Default != "" && this.Default[0:1] == "[" {
|
||||
return db.Raw("'[]'")
|
||||
return db.V("[]")
|
||||
} else if this.Default != "" && this.Default[0:1] == "{" {
|
||||
return db.Raw("'{}'")
|
||||
return db.V("{}")
|
||||
} else if this.Default == "" {
|
||||
return db.Raw("'{}'")
|
||||
return db.V("{}")
|
||||
}
|
||||
} else if this.DataType == req.ResBoolean {
|
||||
if v, _ := strconv.ParseBool(this.Default); v {
|
||||
return db.Raw("'1'")
|
||||
return db.V(true)
|
||||
}
|
||||
return db.Raw("'0'")
|
||||
return db.V(false)
|
||||
}
|
||||
|
||||
if len(this.Default) > 4 && strings.ToLower(this.Default[0:4]) == "sql:" {
|
||||
sql := strings.ToLower(this.Default[4:])
|
||||
if sql == "uuid()" {
|
||||
if driver == "sqlite" {
|
||||
return db.Raw("'" + uuid.NewString() + "'")
|
||||
}
|
||||
return db.Raw("uuid()")
|
||||
}
|
||||
return db.Raw(sql)
|
||||
return db.L(this.Default[4:])
|
||||
}
|
||||
|
||||
if this.Default == "" {
|
||||
if this.GetDataType() == req.ResDate || this.GetDataType() == req.ResDatetime {
|
||||
return db.Raw("NULL")
|
||||
return db.V(nil)
|
||||
}
|
||||
return db.Raw("''")
|
||||
return db.V("")
|
||||
}
|
||||
|
||||
if strings.ToUpper(this.Default) == "CURRENT_TIMESTAMP" {
|
||||
return db.Raw(this.Default)
|
||||
return db.L(this.Default)
|
||||
}
|
||||
|
||||
return db.Raw("'" + 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)
|
||||
@@ -207,15 +206,15 @@ func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDe
|
||||
case "integer":
|
||||
// integer 默认长度 11
|
||||
temp = table.Integer(this.Code)
|
||||
case "date", "dateTime":
|
||||
case "date", "dateTime", "timestamp":
|
||||
if this.DataType == "date" {
|
||||
temp = table.Date(this.Code)
|
||||
} else {
|
||||
temp = table.DateTime(this.Code)
|
||||
}
|
||||
|
||||
if strings.ToUpper(this.Default) == "CURRENT_TIMESTAMP" {
|
||||
def = db.Raw(this.Default)
|
||||
if strings.ToUpper(this.Default) == "SQL:CURRENT_TIMESTAMP" {
|
||||
def = db.L("CURRENT_TIMESTAMP")
|
||||
} else if def == "" {
|
||||
isNull = true
|
||||
}
|
||||
@@ -242,6 +241,15 @@ func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDe
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/schema"
|
||||
"git.fsdpf.net/go/req"
|
||||
|
||||
_ "git.fsdpf.net/go/db/schema/dialect/mysql"
|
||||
_ "git.fsdpf.net/go/db/schema/dialect/sqlite3"
|
||||
)
|
||||
|
||||
func TestResFieldToBlueprint(t *testing.T) {
|
||||
t.Run("id", func(t *testing.T) {
|
||||
table := schema.NewBlueprint("users")
|
||||
|
||||
id := ResField{
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ResFields []ResField
|
||||
|
||||
func (c *ResFields) Scan(value any) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(value.([]byte), c); err != nil {
|
||||
return fmt.Errorf("ResFields json.Unmarshal Error, %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ResFields) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(c)
|
||||
return string(b), err
|
||||
}
|
||||
+222
-249
@@ -5,21 +5,26 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/samber/do"
|
||||
"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 {
|
||||
@@ -37,61 +42,72 @@ type ResChangeRecordTopicPayload struct {
|
||||
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
|
||||
container do.Injector
|
||||
initOnce *sync.Once
|
||||
|
||||
Uuid string `db:"uuid"`
|
||||
PUuid string `db:"pUuid"`
|
||||
Code string `db:"code"`
|
||||
Name string `db:"name"`
|
||||
IsResSystem bool `db:"isSystem"`
|
||||
IsResVirtual bool `db:"isVirtual"`
|
||||
Table string `db:"table"`
|
||||
Primarykey string `db:"primaryKey"`
|
||||
IsHistoryRecord bool `db:"isHistoryRecord"`
|
||||
HistoryCacheMax int `db:"historyCacheMax"`
|
||||
Fields ResFields `db:"fields"`
|
||||
Roles map[string]any `db:"roles"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
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) {
|
||||
func (this *Resource) InitContainer(container do.Injector) {
|
||||
this.container = container
|
||||
this.initOnce = &sync.Once{}
|
||||
}
|
||||
|
||||
// 资源UUID
|
||||
// GetUuid 资源UUID
|
||||
func (this Resource) GetUuid() string {
|
||||
return this.Uuid
|
||||
}
|
||||
|
||||
// 资源CODE
|
||||
// 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.IsResSystem
|
||||
return this.Conn == "service-support"
|
||||
}
|
||||
|
||||
// 资源字段
|
||||
// GetFields 资源字段
|
||||
func (this Resource) GetFields() (result []req.ResField) {
|
||||
for _, item := range this.Fields {
|
||||
result = append(result, item)
|
||||
@@ -99,137 +115,145 @@ func (this Resource) GetFields() (result []req.ResField) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// 开启事物
|
||||
func (this Resource) BeginTransaction() (*db.Transaction, error) {
|
||||
return this.GetDBConn().BeginTransaction()
|
||||
// BeginTransaction 开启事物
|
||||
func (this Resource) BeginTransaction() (*db.TxDatabase, error) {
|
||||
return this.DB().Begin()
|
||||
}
|
||||
|
||||
// 获取资源链接
|
||||
func (this Resource) GetDBConn() *db.Connection {
|
||||
db := do.MustInvoke[db.DB](this.container)
|
||||
// DB 获取资源链接
|
||||
func (this Resource) DB() *db.Database {
|
||||
dbEngine := do.MustInvoke[engine.Engine](this.container)
|
||||
conn := dbEngine.Connection(this.Conn)
|
||||
|
||||
if this.IsSystem() {
|
||||
return db.Connection("service-support")
|
||||
}
|
||||
|
||||
return db.Connection("default")
|
||||
}
|
||||
|
||||
// 获取资源对应的数据库连接
|
||||
func (this Resource) GetDBBuilder() *db.Builder {
|
||||
return this.GetDBConn().Query()
|
||||
}
|
||||
|
||||
// 获取资源对应的数据表
|
||||
func (this Resource) GetTable() db.Expression {
|
||||
if this.IsVirtual() {
|
||||
return db.Raw("(" + this.Table + ")")
|
||||
}
|
||||
|
||||
return db.Raw(this.Table)
|
||||
}
|
||||
|
||||
func (this Resource) GetDBDriver() string {
|
||||
return this.GetDBConn().GetConfig().Driver
|
||||
}
|
||||
|
||||
func (this Resource) GetAuthDBTable(u req.User, params ...any) *db.Builder {
|
||||
builder := this.GetDBTable(append(params, u)...)
|
||||
|
||||
// 数据权限过滤
|
||||
builder.Before(func(b *db.Builder, t string, data ...map[string]any) {
|
||||
if t == db.TYPE_SELECT || t == db.TYPE_UPDATE || t == db.TYPE_DELETE {
|
||||
this.WithRolesCondition(b, t, u)
|
||||
}
|
||||
})
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
// GetDBTable("Test", contracts.User)
|
||||
func (this Resource) GetDBTable(params ...any) *db.Builder {
|
||||
builder := this.GetDBBuilder()
|
||||
|
||||
var user req.User
|
||||
alias := this.Code
|
||||
|
||||
for _, param := range params {
|
||||
switch v := param.(type) {
|
||||
case *db.Transaction:
|
||||
builder.Tx = v
|
||||
case string:
|
||||
alias = v
|
||||
case req.User:
|
||||
user = v
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化数据库存储数据
|
||||
builder.Before(func(b *db.Builder, t string, data ...map[string]any) {
|
||||
if t == db.TYPE_UPDATE {
|
||||
// 格式化保存数据
|
||||
this.formatSaveValue(data[0])
|
||||
}
|
||||
if t == db.TYPE_INSERT {
|
||||
// 移除 table alias
|
||||
b.Table(string(this.GetTable()))
|
||||
for i := 0; i < len(data); i++ {
|
||||
// 格式化保存数据
|
||||
this.formatSaveValue(data[i])
|
||||
// 填充保存数据
|
||||
this.fillSaveValue(data[i], user, db.TYPE_INSERT)
|
||||
if isLocalDB(conn.Dialect()) && !this.IsResVirtual && this.Table != "" && this.initOnce != nil {
|
||||
this.initOnce.Do(func() {
|
||||
if err := this.autoCreateTable(conn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 资源事件
|
||||
this.onResEvent(builder)
|
||||
|
||||
// 用户事件
|
||||
if this.IsHistoryRecord {
|
||||
this.onUserEvent(builder, user)
|
||||
})
|
||||
}
|
||||
|
||||
// 虚拟资源暂时不考虑鉴权
|
||||
if !this.IsVirtual() {
|
||||
// 返回鉴权后的 DB Builder
|
||||
// return
|
||||
// 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 builder.Table(string(this.GetTable()), alias)
|
||||
return conn
|
||||
}
|
||||
|
||||
func (this Resource) WithRolesCondition(b *db.Builder, t string, u req.User) error {
|
||||
// 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)
|
||||
GetResource := do.MustInvoke[contracts.GetResource](this.container)
|
||||
GetOrmConditions := do.MustInvoke[contracts.GetOrmConditions](this.container)
|
||||
|
||||
items := do.MustInvoke[GetResRoles](this.container)(this.GetUuid(), u.Roles()...)
|
||||
|
||||
subTables := lo.Reduce(items, func(carry string, item ResRole, _ int) string {
|
||||
db := this.GetDBBuilder().Table(string(this.GetTable()), this.GetCode()).Select(db.Raw("distinct `" + this.GetCode() + "`.*"))
|
||||
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"
|
||||
@@ -249,106 +273,52 @@ func (this Resource) WithRolesCondition(b *db.Builder, t string, u req.User) err
|
||||
// 关联扩展条件
|
||||
join.SetCondition(GetOrmConditions(joins[i].Uuid, condition.Describe("关联扩展条件")))
|
||||
|
||||
join.Inject(db, NewOrmModel(rResource, rResource.GetCode(), rResource.GetName()))
|
||||
join.Inject(sd, NewOrmModel(rResource, rResource.GetCode(), rResource.GetName()))
|
||||
}
|
||||
|
||||
conditions := GetOrmConditions(item.Uuid, condition.Describe("关联扩展条件"))
|
||||
|
||||
if len(joins) == 0 && conditions.IsEmpty() {
|
||||
// 无权限, 直接跳过这个 unoin 语句
|
||||
if carry != "" {
|
||||
if carry != nil {
|
||||
return carry
|
||||
}
|
||||
// 第一个无权限除外, 避免所有用户所属角色都是无权限
|
||||
db.WhereRaw("false")
|
||||
sd = sd.Where(db.V(false))
|
||||
isFullNot = true
|
||||
} else if len(joins) == 0 && conditions.IsNotEmpty() && conditions.IsAlwaysRight() /* 1=1 的这种条件*/ {
|
||||
// 只要有1个满权限, 直接返回单条语句
|
||||
isFullRight = true
|
||||
return db.ToSql()
|
||||
return sd
|
||||
} else if conditions.IsNotEmpty() {
|
||||
oOrm := NewOrm(this, nil)
|
||||
oOrm.SetGlobalParams(req.NewGlobalParam("{}", u))
|
||||
oOrm.SetGlobalParams(req.NewGlobalParams("{}", u))
|
||||
|
||||
db.Where(conditions.ToSql(oOrm.GetModel()))
|
||||
sd = sd.Where(conditions.ToSql(oOrm.GetModel()))
|
||||
|
||||
// 如果前面是无权限的sql查看, 这直接返回本次查询
|
||||
if isFullNot {
|
||||
isFullNot = false
|
||||
return db.ToSql()
|
||||
return sd
|
||||
}
|
||||
}
|
||||
|
||||
if carry != "" {
|
||||
carry += " UNION "
|
||||
if carry != nil {
|
||||
return carry.Union(sd)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s(%s)", carry, db.ToSql())
|
||||
}, "")
|
||||
return sd
|
||||
}, nil)
|
||||
|
||||
if isFullRight {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if isFullNot {
|
||||
b.WhereRaw("false")
|
||||
} else if subTables != "" {
|
||||
if t == db.TYPE_SELECT {
|
||||
b.FromSub(subTables, b.TableAlias)
|
||||
} else {
|
||||
b.WhereRaw(fmt.Sprintf(
|
||||
"`%s`.`id` in (SELECT `temp`.`id` FROM (%s) as `temp`)",
|
||||
lo.Ternary(b.TableAlias != "", b.TableAlias, this.GetCode()),
|
||||
subTables,
|
||||
))
|
||||
}
|
||||
return nil, db.V(false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 格式化保存数据
|
||||
func (this Resource) formatSaveValue(data map[string]any) {
|
||||
//
|
||||
for k, v := range data {
|
||||
if k == "id" || k == "created_user" || k == "created_at" || k == "deleted_at" || k == "updated_at" {
|
||||
delete(data, k)
|
||||
} else if val, ok := v.(db.Expression); ok {
|
||||
data[k] = val
|
||||
} else if field, ok := this.GetField(k); ok {
|
||||
data[k] = field.ToValue(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 填充保存数据
|
||||
func (this Resource) fillSaveValue(data map[string]any, u req.User, t string) {
|
||||
for _, field := range this.GetFields() {
|
||||
fCode := field.GetCode()
|
||||
|
||||
if fCode == "id" || fCode == "created_user" || fCode == "created_at" || fCode == "deleted_at" || fCode == "updated_at" || fCode == "owned_user" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 只有新增默认字段
|
||||
if _, ok := data[fCode]; !ok {
|
||||
data[fCode] = field.GetRawDefault(this.GetDBDriver())
|
||||
}
|
||||
}
|
||||
|
||||
// 拥有者
|
||||
if _, ok := data["owned_user"]; !ok {
|
||||
data["owned_user"] = u.Uuid()
|
||||
}
|
||||
// 创建者
|
||||
data["created_user"] = u.Uuid()
|
||||
if this.GetDBDriver() == "sqlite" {
|
||||
// 更新时间
|
||||
// sqlite 不能自动更新时间, "DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
|
||||
data["updated_at"] = db.Raw("CURRENT_TIMESTAMP")
|
||||
} else {
|
||||
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (this Resource) GetStruct(extends ...reflect.StructField) any {
|
||||
@@ -379,67 +349,66 @@ func (this Resource) GetSliceStruct(extends ...reflect.StructField) any {
|
||||
return reflect.New(st).Interface()
|
||||
}
|
||||
|
||||
// 资源事件
|
||||
func (this Resource) onResEvent(builder *db.Builder) {
|
||||
builder.After(func(b *db.Builder, t string, result sql.Result, err error, data ...map[string]any) {
|
||||
if err != nil || t == db.TYPE_SELECT {
|
||||
return
|
||||
} else if num, err := result.RowsAffected(); num == 0 || err != nil {
|
||||
return
|
||||
}
|
||||
// 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 := do.MustInvoke[contracts.Queue](this.container).Publish(ResChangeEventTopic, ResChangeEventTopicPayload{
|
||||
if err := qu.Publish(ResChangeEventTopic, ResChangeEventTopicPayload{
|
||||
Type: t,
|
||||
Res: this,
|
||||
Result: result,
|
||||
}); err != nil {
|
||||
log.Println("Queue Publish Err:", ResChangeEventTopic, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 用户事件
|
||||
func (this Resource) onUserEvent(builder *db.Builder, user req.User) {
|
||||
old := []map[string]any{}
|
||||
// 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.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
|
||||
}
|
||||
// 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 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)
|
||||
}
|
||||
})
|
||||
// // 触发消息队列
|
||||
// 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, sql string, fields []ResField) req.Resource {
|
||||
func NewVirtualResource(pRes req.Resource, code, name string, sd *db.SelectDataset, fields []ResField) req.Resource {
|
||||
fieldsCopy := make([]ResField, len(fields))
|
||||
|
||||
copy(fieldsCopy, fields)
|
||||
@@ -448,17 +417,21 @@ func NewVirtualResource(pRes req.Resource, code, name, sql string, fields []ResF
|
||||
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,
|
||||
IsResSystem: pRes.IsSystem(),
|
||||
Table: sql,
|
||||
IsHistoryRecord: false,
|
||||
HistoryCacheMax: 0,
|
||||
Fields: fieldsCopy,
|
||||
Roles: nil,
|
||||
Uuid: code,
|
||||
PUuid: pRes.GetUuid(),
|
||||
Code: code,
|
||||
Name: name,
|
||||
IsResVirtual: true,
|
||||
Conn: pRes.GetConn(),
|
||||
Table: sql,
|
||||
Fields: fieldsCopy,
|
||||
Roles: nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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() {}
|
||||
@@ -15,6 +15,7 @@ type Route struct {
|
||||
Exact bool `db:"exact"`
|
||||
Platform string `db:"platform"`
|
||||
Extra map[string]any `db:"extra"`
|
||||
Roles []string `db:"roles"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@ func (this user) Runtime() req.UserRuntime {
|
||||
return this.runtime
|
||||
}
|
||||
|
||||
// 获取匿名用户
|
||||
// GetAnonymous 获取匿名用户
|
||||
func GetAnonymous(opts ...req.UserRuntimeOption) req.User {
|
||||
u := user{
|
||||
id: 0,
|
||||
@@ -69,7 +69,7 @@ func GetAnonymous(opts ...req.UserRuntimeOption) req.User {
|
||||
return u
|
||||
}
|
||||
|
||||
// 系统用户
|
||||
// GetSystemUser 系统用户
|
||||
func GetSystemUser(opts ...req.UserRuntimeOption) req.User {
|
||||
u := user{
|
||||
id: -1,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
type UserAsset struct {
|
||||
ID int64 `db:"id"`
|
||||
Name string `db:"name"`
|
||||
FileName string `db:"filename"`
|
||||
File string `db:"file"`
|
||||
Mime string `db:"mime"`
|
||||
Size int64 `db:"size"`
|
||||
Ext string `db:"ext"`
|
||||
Hit int64 `db:"hit"`
|
||||
IsPrivate bool `db:"isPrivate"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
type GetUserAsset func(user req.User, code string) (UserAsset, bool)
|
||||
|
||||
type UploadUserAsset func(user req.User, file string, name string) (UserAsset, error)
|
||||
Reference in New Issue
Block a user