diff --git a/service.go b/app.go similarity index 61% rename from service.go rename to app.go index fa7f22a..070180d 100644 --- a/service.go +++ b/app.go @@ -1,6 +1,6 @@ package contracts -type Service interface { +type App interface { LoadGoPlugin(file string) error RegControllers(items map[string]Controller) error RegListens(items map[string]ResListener) error @@ -8,10 +8,17 @@ type Service interface { RegJobs(items map[string]Job) error RegCrons(items map[string]Cron) error RegMqtts(items map[string]Mqtt) error + RegFlowDecisions(items map[string]FlowDecision) error + RegGoBridges(items map[string]GoBridge) error + RegResVirtualTables(items map[string]ResVirtualTable) error + GetResListener(code string) (ResListener, bool) GetAppController(code string) (Controller, bool) GetAppGRpc(code string) (GRpc, bool) GetAppJob(code string) (Job, bool) GetAppCron(code string) (Cron, bool) GetAppMqtt(code string) (Mqtt, bool) + GetAppFlowDecision(code string) (FlowDecision, bool) + GetAppGoBridge(code string) (GoBridge, bool) + GetAppResVirtualTable(code string) (ResVirtualTable, bool) } diff --git a/base/condflow.go b/base/condflow.go new file mode 100644 index 0000000..dbbea51 --- /dev/null +++ b/base/condflow.go @@ -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) diff --git a/base/condflow_case.go b/base/condflow_case.go new file mode 100644 index 0000000..81a0f3d --- /dev/null +++ b/base/condflow_case.go @@ -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 diff --git a/base/condflow_decision.go b/base/condflow_decision.go new file mode 100644 index 0000000..7faed0f --- /dev/null +++ b/base/condflow_decision.go @@ -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) diff --git a/base/data_list.go b/base/data_list.go index 9fea5b4..a079839 100644 --- a/base/data_list.go +++ b/base/data_list.go @@ -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"` diff --git a/base/data_list_field.go b/base/data_list_field.go index 0b70dc1..2f6d9e2 100644 --- a/base/data_list_field.go +++ b/base/data_list_field.go @@ -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"` } diff --git a/base/grid_layout.go b/base/grid_layout.go index 5bb4aca..41e76e5 100644 --- a/base/grid_layout.go +++ b/base/grid_layout.go @@ -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) diff --git a/base/grid_layout_field.go b/base/grid_layout_field.go index 0a47f04..c863a3b 100644 --- a/base/grid_layout_field.go +++ b/base/grid_layout_field.go @@ -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"` diff --git a/base/grid_layout_form.go b/base/grid_layout_form.go index 23dfab9..d8e390e 100644 --- a/base/grid_layout_form.go +++ b/base/grid_layout_form.go @@ -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"` diff --git a/base/pkg_version.go b/base/pkg_version.go index b082904..89b77fc 100644 --- a/base/pkg_version.go +++ b/base/pkg_version.go @@ -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 } diff --git a/base/query_field.go b/base/query_field.go index 8d4c3a3..e849787 100644 --- a/base/query_field.go +++ b/base/query_field.go @@ -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: diff --git a/base/res_api.go b/base/res_api.go index 2d5cfbd..3496205 100644 --- a/base/res_api.go +++ b/base/res_api.go @@ -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"` // 启用权限过滤 diff --git a/base/res_condition.go b/base/res_condition.go index 255bfe1..256662e 100644 --- a/base/res_condition.go +++ b/base/res_condition.go @@ -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), ) diff --git a/base/res_configure.go b/base/res_configure.go index 464e59f..4623fa5 100644 --- a/base/res_configure.go +++ b/base/res_configure.go @@ -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) diff --git a/base/res_field.go b/base/res_field.go index 5b8f7a2..a922fcf 100644 --- a/base/res_field.go +++ b/base/res_field.go @@ -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 { diff --git a/base/res_field_test.go b/base/res_field_test.go new file mode 100644 index 0000000..b15db40 --- /dev/null +++ b/base/res_field_test.go @@ -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) + }) +} diff --git a/base/res_fields.go b/base/res_fields.go deleted file mode 100644 index 32df8c1..0000000 --- a/base/res_fields.go +++ /dev/null @@ -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 -} diff --git a/base/resource.go b/base/resource.go index 212ca81..5b88780 100644 --- a/base/resource.go +++ b/base/resource.go @@ -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, } } diff --git a/base/resource_hooks.go b/base/resource_hooks.go new file mode 100644 index 0000000..08302d7 --- /dev/null +++ b/base/resource_hooks.go @@ -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 +} diff --git a/base/resource_test.go b/base/resource_test.go new file mode 100644 index 0000000..4c68025 --- /dev/null +++ b/base/resource_test.go @@ -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() {} diff --git a/base/route.go b/base/route.go index a6e0ac6..3d551d5 100644 --- a/base/route.go +++ b/base/route.go @@ -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"` } diff --git a/base/user.go b/base/user.go index 50d55af..e9c8094 100644 --- a/base/user.go +++ b/base/user.go @@ -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, diff --git a/base/user_asset.go b/base/user_asset.go new file mode 100644 index 0000000..50c8bf0 --- /dev/null +++ b/base/user_asset.go @@ -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) diff --git a/condflow.go b/condflow.go new file mode 100644 index 0000000..e024cda --- /dev/null +++ b/condflow.go @@ -0,0 +1,32 @@ +package contracts + +import ( + "git.fsdpf.net/go/reflux" + "git.fsdpf.net/go/req" + "github.com/samber/do/v2" +) + +type CondflowService interface { + Run(name string, input reflux.R, u req.User) error +} + +type FlowDecision interface { + Controller + Res() req.Resource +} + +type BaseFlowDecision struct { + Controller + res string +} + +func (be BaseFlowDecision) Res() req.Resource { + return do.MustInvoke[MustResource](be.Container())(be.res) +} + +func NewBaseFlowDecision(container do.Injector, res string) *BaseFlowDecision { + return &BaseFlowDecision{ + Controller: &BaseController{container}, + res: res, + } +} diff --git a/controller.go b/controller.go index 910bfa2..2672e13 100644 --- a/controller.go +++ b/controller.go @@ -2,23 +2,23 @@ package contracts import ( "git.fsdpf.net/go/req" - "github.com/samber/do" + "github.com/samber/do/v2" ) type Controller interface { // 初始化 Init() error // 获取 Container - Container() *do.Injector + Container() do.Injector // 派遣一个任务 Dispatch(job string, payload any, u req.User) error } type BaseController struct { - container *do.Injector + container do.Injector } -func (this BaseController) Container() *do.Injector { +func (this BaseController) Container() do.Injector { return this.container } @@ -30,6 +30,6 @@ func (this BaseController) Dispatch(job string, payload any, u req.User) error { return do.MustInvoke[JobService](this.Container()).Dispatch(job, payload, u) } -func NewBaseController(container *do.Injector) Controller { +func NewBaseController(container do.Injector) Controller { return &BaseController{container} } diff --git a/event_stream.go b/event_stream.go new file mode 100644 index 0000000..4aec3bb --- /dev/null +++ b/event_stream.go @@ -0,0 +1,67 @@ +package contracts + +import ( + "io" + "net/http" + + "git.fsdpf.net/go/req" + "github.com/samber/do/v2" +) + +type EventStream interface { + Controller + // 获取请求信息 + Request() *http.Request + // 请求处理 + Execute(req.GlobalParams) error + // 获取用户信息 + User() req.User + // 路由信息 + Route() req.Route + // 发送数据 + Sent(data, event string) error +} + +type EsHandleController struct { + Controller + w http.ResponseWriter + flusher http.Flusher + request *http.Request +} + +func (this EsHandleController) Request() *http.Request { + return this.request +} + +func (this EsHandleController) Route() req.Route { + return this.Request().Context().Value(req.RouteCtx{Name: "Route"}).(req.Route) +} + +func (this EsHandleController) User() req.User { + return this.Request().Context().Value(req.RouteCtx{Name: "User"}).(req.User) +} + +func (this EsHandleController) Execute(req.GlobalParams) error { + return ErrFuncNotImplemented +} + +func (this EsHandleController) Sent(data, event string) error { + if _, err := io.WriteString(this.w, "event: "+event+"\ndata: "+data+"\n\n"); err != nil { + return err + } + this.flusher.Flush() + return nil +} + +func NewEventStreamController(container do.Injector, request *http.Request, w http.ResponseWriter) EventStream { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + return &EsHandleController{ + Controller: &BaseController{container}, + request: request, + w: w, + flusher: w.(http.Flusher), + } +} diff --git a/executor.go b/executor.go new file mode 100644 index 0000000..258b86f --- /dev/null +++ b/executor.go @@ -0,0 +1,27 @@ +package contracts + +import ( + "git.fsdpf.net/go/req" + "github.com/samber/do/v2" +) + +type Executor interface { + Controller + Res() req.Resource +} + +type BaseExecutor struct { + Controller + res string +} + +func (be BaseExecutor) Res() req.Resource { + return do.MustInvoke[MustResource](be.Container())(be.res) +} + +func NewBaseExecutor(container do.Injector, res string) *BaseExecutor { + return &BaseExecutor{ + Controller: &BaseController{container}, + res: res, + } +} diff --git a/go.mod b/go.mod index 1319d6b..77aa5c6 100644 --- a/go.mod +++ b/go.mod @@ -1,56 +1,71 @@ module git.fsdpf.net/go/contracts -go 1.21 +go 1.24 +toolchain go1.24.10 + +replace ( + git.fsdpf.net/go/condition => ../condition-v2 + git.fsdpf.net/go/db => ../db-v2 + git.fsdpf.net/go/reflux => ../reflux + git.fsdpf.net/go/req => ../req-v2 +) + require ( - git.fsdpf.net/go/condition v0.0.0-20241126093638-50e1b3630e6b - git.fsdpf.net/go/db v0.0.0-20241125095839-f186c88c5c2a - github.com/go-chi/chi/v5 v5.0.12 - github.com/golang/protobuf v1.5.2 - github.com/google/uuid v1.3.0 - github.com/gorilla/websocket v1.5.0 - github.com/jhump/protoreflect v1.15.1 - github.com/lestrrat-go/jwx v1.2.25 - github.com/samber/do v1.6.0 - github.com/samber/lo v1.39.0 - github.com/spf13/cast v1.6.0 - github.com/spf13/viper v1.15.0 - github.com/tidwall/gjson v1.17.1 - github.com/tidwall/sjson v1.2.5 - google.golang.org/grpc v1.52.0 + git.fsdpf.net/go/condition v0.0.0-00010101000000-000000000000 + git.fsdpf.net/go/db v0.0.0-00010101000000-000000000000 + git.fsdpf.net/go/reflux v0.0.0-00010101000000-000000000000 + git.fsdpf.net/go/req v0.0.0-00010101000000-000000000000 + git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff + github.com/samber/do/v2 v2.0.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/mattn/go-sqlite3 v1.14.42 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/samber/go-type-to-string v1.8.0 // indirect ) require ( - git.fsdpf.net/go/req v0.0.0-20241126093511-f1774e5ca2f0 // indirect - git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff // indirect github.com/bufbuild/protocompile v0.4.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect - github.com/goccy/go-json v0.9.7 // indirect + github.com/go-chi/chi/v5 v5.0.12 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang/protobuf v1.5.2 + github.com/gorilla/websocket v1.5.0 github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jhump/protoreflect v1.15.1 github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.1 // indirect + github.com/lestrrat-go/jwx v1.2.25 github.com/lestrrat-go/option v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/samber/lo v1.49.1 github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/cast v1.10.0 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.15.0 github.com/subosito/gotenv v1.4.2 // indirect + github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect - golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect + google.golang.org/grpc v1.52.0 google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 068403e..7075ac5 100644 --- a/go.sum +++ b/go.sum @@ -36,50 +36,12 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -git.fsdpf.net/go/condition v0.0.0-20240509022115-37018837d1b0 h1:MHjkN+zrSCqb7Qswn9fFrHT4XaoDnGbXz0ssSKnWnQk= -git.fsdpf.net/go/condition v0.0.0-20240509022115-37018837d1b0/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q= -git.fsdpf.net/go/condition v0.0.0-20240509023514-a871ccfa75ac h1:pJQ0sFjPtv/RCy8k0mC/Kpn/j6As5bSqXyG0MWaylhc= -git.fsdpf.net/go/condition v0.0.0-20240509023514-a871ccfa75ac/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q= -git.fsdpf.net/go/condition v0.0.0-20240509024751-9694d9033266 h1:s5On5PKF1ZiIGgq6R2Nk/2GcURuZxDAVK1Sru54Ybn0= -git.fsdpf.net/go/condition v0.0.0-20240509024751-9694d9033266/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q= -git.fsdpf.net/go/condition v0.0.0-20240509030058-b0e337133e44 h1:XHJxWJuofEL7TLMDBsywi48nciaon8bEFOJLMYkOrNk= -git.fsdpf.net/go/condition v0.0.0-20240509030058-b0e337133e44/go.mod h1:xgX0fc3LEgrPk9tihGQqoQND4EWlNgYbLCi70TuCvvM= -git.fsdpf.net/go/condition v0.0.0-20240509034445-afc243ea916e h1:ZfwjqZS5URF4Y0JfQeytSdmXimJ4l74ooWRppza+3Tk= -git.fsdpf.net/go/condition v0.0.0-20240509034445-afc243ea916e/go.mod h1:K5ocyKVxgC5mI1k/crgrEozK0r51qAavWg0hweXo6PU= -git.fsdpf.net/go/condition v0.0.0-20241126084857-bb134581146e h1:GR7OmszSIcutwYfYkL0wGJXKlLzOHCszjN7Oy2jAcRU= -git.fsdpf.net/go/condition v0.0.0-20241126084857-bb134581146e/go.mod h1:cQhj0R7Q1GP4qxOvqRAwpGPSdYCX/c6dN3WSHhjmMvw= -git.fsdpf.net/go/condition v0.0.0-20241126090116-c3471706f4d4 h1:BMO+OujDukHDKD0RLUEu7w1ZZyHvtz8k69pHUiP7yPE= -git.fsdpf.net/go/condition v0.0.0-20241126090116-c3471706f4d4/go.mod h1:uKIEYgBdHFFnNQs6y7cvwsi4b/0Jk1QDneDgemo3Oa0= -git.fsdpf.net/go/condition v0.0.0-20241126093638-50e1b3630e6b h1:0Ez7ny8HNlZTMhYdlsGbSDcc9vkWt/V/OWlPi+sDw/s= -git.fsdpf.net/go/condition v0.0.0-20241126093638-50e1b3630e6b/go.mod h1:5N2xtaawQLquSsF8jJYPvOXhYNplBK+Iib783hTRg8c= -git.fsdpf.net/go/db v0.0.0-20230621051209-5740d112407f h1:H+/H6j+hyFMgSy+tUNmymRy7/PWQRjjp8qY/DrhwrsY= -git.fsdpf.net/go/db v0.0.0-20230621051209-5740d112407f/go.mod h1:397Sdx1cJS0OlHtTX1bVl//9k3Xn0Klnc6jC4MAkb6w= -git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b h1:fRgWNOQ9dAYuUZHQ24oi1XqRbJIcJvZGbnQDaAKI7IY= -git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b/go.mod h1:397Sdx1cJS0OlHtTX1bVl//9k3Xn0Klnc6jC4MAkb6w= -git.fsdpf.net/go/db v0.0.0-20241125095839-f186c88c5c2a h1:upXLpdDk7Oz7rNoMLx4uDRx50P3tLvwwlslywSkdygk= -git.fsdpf.net/go/db v0.0.0-20241125095839-f186c88c5c2a/go.mod h1:397Sdx1cJS0OlHtTX1bVl//9k3Xn0Klnc6jC4MAkb6w= -git.fsdpf.net/go/req v0.0.0-20240508133526-672fc634ef20 h1:PhjRtac2r0Vn1WBBzhRhz4//bJhHgkf/SHZe3mQYPto= -git.fsdpf.net/go/req v0.0.0-20240508133526-672fc634ef20/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= -git.fsdpf.net/go/req v0.0.0-20240509031332-f00b993f9c7d h1:TKNhQWDKyL50eVC/ptur1YuRkjRnKOlMsE1Y9tFCVbk= -git.fsdpf.net/go/req v0.0.0-20240509031332-f00b993f9c7d/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= -git.fsdpf.net/go/req v0.0.0-20240509031626-6501b5ed0f2b h1:KlVBAuWnFt1lcRTCcBU165MIldJqleO/LahWrd3FvUA= -git.fsdpf.net/go/req v0.0.0-20240509031626-6501b5ed0f2b/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= -git.fsdpf.net/go/req v0.0.0-20240509033523-c8cda2d8db56 h1:7ElgXUzt75brzOr44mItipTQTk++kZNoGOyTWHLxkTU= -git.fsdpf.net/go/req v0.0.0-20240509033523-c8cda2d8db56/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= -git.fsdpf.net/go/req v0.0.0-20240509033936-f8a11598db60 h1:hHrEcUSbtajC2LAo2fvEo7yrSSDeM/s5FAuQluP7yZQ= -git.fsdpf.net/go/req v0.0.0-20240509033936-f8a11598db60/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= -git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758 h1:nHys8Bwb29b/FvnE100KVH62aD2j5CEPxTQ/RRHo27k= -git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= -git.fsdpf.net/go/req v0.0.0-20241126084545-fd3625802b40 h1:Ik3m4G4fY0kEHGX7qg01L2Q7OXjXAvRRqg8ErwQd2Hs= -git.fsdpf.net/go/req v0.0.0-20241126084545-fd3625802b40/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= -git.fsdpf.net/go/req v0.0.0-20241126090024-bccb0382d886 h1:LOKgXrnJT4xmxf9KkvZBOZDQYEhNz3h0TZl13R2GCtk= -git.fsdpf.net/go/req v0.0.0-20241126090024-bccb0382d886/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= -git.fsdpf.net/go/req v0.0.0-20241126093511-f1774e5ca2f0 h1:XevOuJLH63dzRiE6Sni/M0xOUw+bmrqH5Rcum+Aayjo= -git.fsdpf.net/go/req v0.0.0-20241126093511-f1774e5ca2f0/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw= git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff h1:1hokQGKGIstoI7WDBir2N24OGgnussZ3GmucK4TQRuI= git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff/go.mod h1:NUoyQtFr905YT+pi850mvSf4YX0WghQIcMQkTvize5o= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -102,20 +64,20 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -157,6 +119,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -172,8 +135,6 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -192,10 +153,12 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= @@ -208,12 +171,16 @@ github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0ot github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo= +github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -223,19 +190,18 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/samber/do v1.6.0 h1:Jy/N++BXINDB6lAx5wBlbpHlUdl0FKpLWgGEV9YWqaU= -github.com/samber/do v1.6.0/go.mod h1:DWqBvumy8dyb2vEnYZE7D7zaVEB64J45B0NjTlY/M4k= -github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= -github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= -github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/samber/do/v2 v2.0.0 h1:tnunwWaoqSfJ9hxVIaJawIo7JXHQlqT9d9YBXlE9Keg= +github.com/samber/do/v2 v2.0.0/go.mod h1:ZSBCE7Xr6nTNIOVo4DBrkl2+ydUbIOzJjjdV8En5XO4= +github.com/samber/go-type-to-string v1.8.0 h1:5z6tDTjtXxkIAoAuHAZYMYR8mkBZjVgeSH7jcSLqc8w= +github.com/samber/go-type-to-string v1.8.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -245,6 +211,8 @@ github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jH github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -252,13 +220,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -285,8 +252,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -297,8 +264,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -354,8 +319,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -375,7 +340,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -412,8 +378,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -422,8 +388,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -575,8 +541,9 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743 h1:yqElulDvOF26oZ2O+2/aoX7mQ8DY/6+p39neytrycd8= google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/http.go b/http.go index d227541..baa6fef 100644 --- a/http.go +++ b/http.go @@ -5,7 +5,7 @@ import ( "net/http" "git.fsdpf.net/go/req" - "github.com/samber/do" + "github.com/samber/do/v2" ) var defaultWsClientGroup WsClientGroup = "__DEFAULT__" @@ -95,7 +95,7 @@ func (HttpHandleController) AuthDB() req.ResAuthDB { return req.ResAuthOn } -func NewHttpController(container *do.Injector, request *http.Request) HttpController { +func NewHttpController(container do.Injector, request *http.Request) HttpController { return &HttpHandleController{ Controller: &BaseController{container}, request: request, diff --git a/jwt_auth.go b/jwt_auth.go index c404cdb..5422437 100644 --- a/jwt_auth.go +++ b/jwt_auth.go @@ -4,11 +4,11 @@ import ( "net/http" "github.com/lestrrat-go/jwx/jwt" - "github.com/samber/do" + "github.com/samber/do/v2" ) type JWTAuth interface { - HttpMiddleware(container *do.Injector) func(next http.Handler) http.Handler + HttpMiddleware(container do.Injector) func(next http.Handler) http.Handler Encode(claims map[string]interface{}) (token string, err error) Decode(token string) (jwt.Token, error) } diff --git a/mcp.go b/mcp.go new file mode 100644 index 0000000..46bf364 --- /dev/null +++ b/mcp.go @@ -0,0 +1,7 @@ +package contracts + +import "net/http" + +type MCPService interface { + ServeHTTP(w http.ResponseWriter, r *http.Request) +} diff --git a/mqtt.go b/mqtt.go index 28b2ac1..9b575db 100644 --- a/mqtt.go +++ b/mqtt.go @@ -2,7 +2,7 @@ package contracts import ( "git.fsdpf.net/go/req" - "github.com/samber/do" + "github.com/samber/do/v2" ) type MqttService interface { @@ -88,7 +88,7 @@ func (this MqttHandle) OnPublish(g req.GlobalParams, topic string, retain bool, return nil, ErrFuncNotImplemented } -func NewMqttController(container *do.Injector) Mqtt { +func NewMqttController(container do.Injector) Mqtt { return &MqttHandle{ Controller: &BaseController{container}, } diff --git a/orm.go b/orm.go index d5820ec..276617f 100644 --- a/orm.go +++ b/orm.go @@ -1,10 +1,13 @@ package contracts import ( + "context" "reflect" "git.fsdpf.net/go/condition" "git.fsdpf.net/go/db" + "git.fsdpf.net/go/db/exp" + "git.fsdpf.net/go/reflux" "git.fsdpf.net/go/req" ) @@ -12,39 +15,35 @@ type OrmExecute int type RelationType string -type OrderByDirection string - const ( - OrmExecute_Query OrmExecute = iota - OrmExecute_Show - OrmExecute_Store - OrmExecute_Destroy + OrmQuery OrmExecute = iota + OrmShow + OrmStore + OrmDestroy ) const ( - RelationType_HasOne RelationType = "hasOne" - RelationType_HasMany RelationType = "hasMany" - RelationType_Inner RelationType = "inner" - RelationType_Left RelationType = "left" - RelationType_Right RelationType = "right" + OrmHasOne RelationType = "hasOne" + OrmHasMany RelationType = "hasMany" + OrmInnerJoin RelationType = "inner" + OrmLeftJoin RelationType = "left" + OrmRightJoin RelationType = "right" ) -const ( - OrderByDirection_ASC OrderByDirection = db.ORDER_ASC - OrderByDirection_DESC OrderByDirection = db.ORDER_DESC -) +type ModelDecorator func(Model) Model type Orm interface { - SetGlobalParams(g req.GlobalParams) Orm + SetGlobalParams(g req.UserAccessor) Orm GetModel(params ...string) Model SetRelationModel(m Model) Model SetQueryField(qf req.QueryField) error SetOrderBy(params []OrderBy) error SetCondition(cond *condition.Condition) error SetController(ctr OrmController) error - Execute(OrmExecute) (any, error) - Clone(tx *db.Transaction) Orm - Count() int + Controller() reflect.Value + Execute(OrmExecute) (reflux.R, error) + Clone(tx *db.TxDatabase) Orm + Count() int64 // Exists() bool } @@ -54,7 +53,6 @@ type Relation interface { Type() RelationType SetType(RelationType) SetCondition(*condition.Condition) *condition.Condition - SetPrependCondition(*condition.Condition) *condition.Condition GetCondition() *condition.Condition GetRelationResource() string GetRelationField() string @@ -66,8 +64,9 @@ type Join interface { GetCode() string GetResource() req.Resource // GetDependencies(items []ResRelation, dependencies ...string) []string - Inject(dbBuilder *db.Builder, m Model) + Inject(dbBuilder *db.SelectDataset, m Model) } + type Model interface { Relation condition.TokenValue @@ -76,28 +75,30 @@ type Model interface { GetPrimaryKey() string GetQueryFieldsStruct(extends ...reflect.StructField) any GetQueryFieldsSliceStruct(fields ...reflect.StructField) any - GetAttribute() map[string]any - GetResult() reflect.Value + GetAttribute() reflux.R + SetRelationAttribute(string, reflux.R) error + GetResult() reflux.R GetResource() req.Resource - QueryBuilder() *db.Builder - GetTransaction() *db.Transaction - SetQueryField(req.QueryField) - SetRelationResult(code string, vItems reflect.Value) + GetTransaction() *db.TxDatabase + SetQueryField(...req.QueryField) + // SetRelationResult(code string, items []reflux.R) GetJoinsCode() []string SetAuthDB(req.ResAuthDB) SetOrderBy(params ...OrderBy) Model SetGroupBy(params ...GroupBy) Model SetJoin(Join) Model - SetLimit(int) Model - SetOffset(int) Model - SetAttribute(data any) Model + SetLimit(uint) Model + SetOffset(uint) Model + // SetAttribute(data any) Model SetPrimaryKey(string) Model SetWithRecursive(QueryWithRecursive) Model DelQueryField(string) + SelectDataset() *db.SelectDataset + SelectDatasetWithUser(u req.User) *db.SelectDataset Query() error Store() error Destroy() error - Count() (int, error) + Count() (int64, error) // Exists() (bool, error) } @@ -106,18 +107,18 @@ type OrmController interface { } type OrderBy interface { - ToSql() db.Expression - Inject(dbBuilder *db.Builder, m Model) + ToSql() exp.OrderedExpression + Inject(sd *db.SelectDataset, m Model) } type GroupBy interface { - ToSql() db.Expression - Inject(dbBuilder *db.Builder, m Model) + ToSql() exp.ColumnListExpression + Inject(sd *db.SelectDataset, m Model) } type QueryWithRecursive interface { - ToQueryBuilder(*db.Builder, Model) (*db.Builder, any) - ToTreeData(any) any + ToQueryBuilder(*db.SelectDataset, Model) (*db.SelectDataset, any) + ToTreeData([]reflux.R) reflux.R } // @title 创建关联模型 @@ -130,18 +131,23 @@ type NewOrmRelation func(t RelationType, m Model, rResource, rField, rForeignKey type NewOrmJoin func(t RelationType, res req.Resource, alias, rResource, rField, rForeignKey string) Join -type NewOrmQueryWithRecursive func(pField, cField string, root any, isWithoutCondition bool, depth int) QueryWithRecursive +type NewOrmQueryWithRecursive func(pField, cField exp.IdentifierExpression, root any, isWithoutCondition bool, depth uint) QueryWithRecursive -type NewOrmOrderBy func(sql string, direction OrderByDirection) OrderBy +type NewOrmOrderBy func(sql db.Expression, direction exp.SortDirection) OrderBy -type NewOrmGroupBy func(sql string) GroupBy +type NewOrmGroupBy func(db.Expression) GroupBy type NewOrmModel func(res req.Resource, code, name string) Model -type NewOrm func(res req.Resource, tx *db.Transaction) Orm +type NewOrm func(res req.Resource, opts ...OrmOption) Orm type GetOrmConditions func(categoryUuid string, opts ...condition.Option) *condition.Condition type GetOrmOrderBy func(categoryUuid string) []OrderBy type GetOrmGroupBy func(categoryUuid string) []GroupBy + +type OrmOption func(Orm) +type WithTx func(tx *db.TxDatabase) OrmOption +type WithContext func(ctx context.Context) OrmOption +type WithUserAccessor func(ua req.UserAccessor) OrmOption diff --git a/res_event.go b/res_event.go index 906707f..fb2f218 100644 --- a/res_event.go +++ b/res_event.go @@ -2,7 +2,7 @@ package contracts import ( "git.fsdpf.net/go/req" - "github.com/samber/do" + "github.com/samber/do/v2" "github.com/samber/lo" ) @@ -21,7 +21,7 @@ type ResListener interface { } type BaseListener struct { - Container *do.Injector + Container do.Injector res req.Resource // 监听资源 code string // code events []string // 监听动作 @@ -51,6 +51,6 @@ func (BaseListener) Delete(old map[string]any, u req.User) error { return nil } -func NewBaseListener(code string, res req.Resource, container *do.Injector) *BaseListener { +func NewBaseListener(code string, res req.Resource, container do.Injector) *BaseListener { return &BaseListener{code: code, res: res, Container: container} } diff --git a/res_type/res_field_by_any.go b/res_type/res_field_by_any.go deleted file mode 100644 index 7118028..0000000 --- a/res_type/res_field_by_any.go +++ /dev/null @@ -1,27 +0,0 @@ -package res_type - -import ( - "database/sql/driver" - "encoding/json" - "fmt" -) - -// 解析map ---------------------------------- -type ResFieldByMap map[string]any - -func (c *ResFieldByMap) Scan(value any) error { - if value == nil { - return nil - } - - if err := json.Unmarshal(value.([]byte), c); err != nil { - return fmt.Errorf("ResFieldByMap json.Unmarshal Error, %s", err) - } - - return nil -} - -func (c ResFieldByMap) Value() (driver.Value, error) { - b, err := json.Marshal(c) - return string(b), err -} diff --git a/res_type/res_field_by_float.go b/res_type/res_field_by_float.go deleted file mode 100644 index 9dc4bda..0000000 --- a/res_type/res_field_by_float.go +++ /dev/null @@ -1,3 +0,0 @@ -package res_type - -type ResFieldByFloat float64 diff --git a/res_type/res_field_by_integer.go b/res_type/res_field_by_integer.go deleted file mode 100644 index a311815..0000000 --- a/res_type/res_field_by_integer.go +++ /dev/null @@ -1,3 +0,0 @@ -package res_type - -type ResFieldByInteger int64 diff --git a/res_type/res_field_by_map.go b/res_type/res_field_by_map.go deleted file mode 100644 index 0b563ff..0000000 --- a/res_type/res_field_by_map.go +++ /dev/null @@ -1,38 +0,0 @@ -package res_type - -import ( - "database/sql/driver" - "encoding/json" - "fmt" -) - -// 切片 ------------------------------------ -type ResFieldByAnys []any - -func (c *ResFieldByAnys) Scan(value any) error { - if value == nil { - return nil - } - - switch v := value.(type) { - case []byte: - if len(v) == 0 { - return nil - } - - if rune('[') != rune(v[0]) { - return nil - } - default: - } - - if err := json.Unmarshal(value.([]byte), c); err != nil { - return fmt.Errorf("ResFieldByAnys json.Unmarshal Error, %s, %s", value, err) - } - - return nil -} -func (c ResFieldByAnys) Value() (driver.Value, error) { - b, err := json.Marshal(c) - return string(b), err -} diff --git a/res_type/res_field_by_number.go b/res_type/res_field_by_number.go deleted file mode 100644 index 67bec11..0000000 --- a/res_type/res_field_by_number.go +++ /dev/null @@ -1,29 +0,0 @@ -package res_type - -import ( - "database/sql/driver" - - "github.com/spf13/cast" -) - -// Number 类型 -------------------------------- -type ResFieldByNumber float64 - -func (this *ResFieldByNumber) Scan(value any) error { - if value == nil { - return nil - } - - switch s := value.(type) { - case []byte: - *this = ResFieldByNumber(cast.ToFloat64(string(s))) - default: - *this = ResFieldByNumber(cast.ToFloat64(s)) - } - - return nil -} - -func (this ResFieldByNumber) Value() (driver.Value, error) { - return this, nil -} diff --git a/res_type/res_field_by_string.go b/res_type/res_field_by_string.go deleted file mode 100644 index e118bf2..0000000 --- a/res_type/res_field_by_string.go +++ /dev/null @@ -1,28 +0,0 @@ -package res_type - -import ( - "database/sql/driver" - "fmt" -) - -// 字符串 ------------------------------------- -type ResFieldByString string - -func (this *ResFieldByString) Scan(value any) error { - if value == nil { - return nil - } - - switch s := value.(type) { - case []byte: - *this = ResFieldByString(s) - default: - *this = ResFieldByString(fmt.Sprintf("%v", s)) - } - - return nil -} - -func (c ResFieldByString) Value() (driver.Value, error) { - return c, nil -} diff --git a/res_virtual_table.go b/res_virtual_table.go new file mode 100644 index 0000000..c6b4544 --- /dev/null +++ b/res_virtual_table.go @@ -0,0 +1,99 @@ +package contracts + +import ( + "git.fsdpf.net/go/db" + "git.fsdpf.net/go/reflux" + "git.fsdpf.net/go/req" + "github.com/samber/do/v2" +) + +// ResVirtualTable 虚拟表接口,由业务层实现,框架通过 SQLite vtab 机制调用。 +// Select 提供列表查询,Fetch 提供单条完整查询(含 Detail 补充字段), +// Insert/Update/Delete 支持写操作并自动同步缓存。 +type ResVirtualTable interface { + Controller + GetResource() req.Resource + + // Fetch 按主键获取单条完整数据,内部负责调用远端接口并补充 Detail 专属字段。 + // 框架在 item cache miss 时调用,结果写入 item cache 供后续查询复用。 + Fetch(pk any) (map[string]any, error) + + // Detail 对已有的 item 数据进行补充,填充列表接口不返回的详情字段。 + // changed=true 时框架会将修改后的数据回写 item cache。 + Detail(item reflux.R) (changed bool, err error) + + // Select 查询列表数据,filter 为过滤条件,pagesize/page 控制分页。 + // 返回的 items 每项为 map[string]any,total 为总记录数。 + Select(filter reflux.R, pagesize, page int) (items []any, total int64, err error) + + // Insert 新增一条记录,返回新记录的 rowid。 + Insert(item reflux.R) (rowid int64, err error) + + // Update 按 rowid 更新记录。 + Update(rowid any, item reflux.R) error + + // Delete 按 rowid 删除记录。 + Delete(rowid any) error +} + +type BaseResVirtualTable struct { + Controller + + res req.Resource + cache *db.Database +} + +func (b *BaseResVirtualTable) Cache() *db.Database { + // CREATE TABLE IF NOT EXISTS ` + res.Table + ` ( + // id INTEGER PRIMARY KEY AUTOINCREMENT, + // kind VARCHAR(8) NOT NULL DEFAULT 'list', + // key VARCHAR(255) NOT NULL, + // lmt INTEGER NOT NULL DEFAULT 0, + // page INTEGER NOT NULL DEFAULT 0, + // data TEXT NOT NULL, + // total INTEGER NOT NULL DEFAULT 0, + // expires_at BIGINT NOT NULL, + // UNIQUE(kind, key, lmt, page) + // ) + return b.cache +} + +func (b *BaseResVirtualTable) GetResource() req.Resource { + return b.res +} + +func (b *BaseResVirtualTable) GetExtraFields() []req.ResField { + return nil +} + +func (b *BaseResVirtualTable) Fetch(pk any) (map[string]any, error) { + return nil, nil +} + +func (b *BaseResVirtualTable) Detail(detail reflux.R) (changed bool, err error) { + return false, nil +} + +func (b *BaseResVirtualTable) Select(filter reflux.R, pagesize, page int) (items []any, total int64, err error) { + return nil, 0, ErrFuncNotImplemented +} + +func (b *BaseResVirtualTable) Insert(item reflux.R) (rowid int64, err error) { + return 0, ErrFuncNotImplemented +} + +func (b *BaseResVirtualTable) Update(rowid any, item reflux.R) error { + return ErrFuncNotImplemented +} + +func (b *BaseResVirtualTable) Delete(rowid any) error { + return ErrFuncNotImplemented +} + +func NewBaseResVirtualTable(container do.Injector, res req.Resource, conn *db.Database) ResVirtualTable { + return &BaseResVirtualTable{ + Controller: &BaseController{container}, + res: res, + cache: conn, + } +} diff --git a/resource.go b/resource.go index 36da174..db64849 100644 --- a/resource.go +++ b/resource.go @@ -1,9 +1,14 @@ package contracts -import "git.fsdpf.net/go/req" +import ( + "git.fsdpf.net/go/req" +) -// 资源 +// GetResource 资源 type GetResource func(code string) (req.Resource, bool) -// 获取用户 +// MustResource 资源, 没有则抛出错误 +type MustResource func(code string) req.Resource + +// GetUser 获取用户 type GetUser func(uuid string, options ...req.UserRuntimeOption) (req.User, error) diff --git a/support/list.go b/support/list.go index c7c59db..fb29de4 100644 --- a/support/list.go +++ b/support/list.go @@ -8,8 +8,9 @@ import ( "git.fsdpf.net/go/condition" "git.fsdpf.net/go/contracts" "git.fsdpf.net/go/db" + "git.fsdpf.net/go/db/exp" "git.fsdpf.net/go/utils" - "github.com/samber/do" + "github.com/samber/do/v2" "github.com/samber/lo" "github.com/spf13/cast" ) @@ -37,7 +38,7 @@ type OperationAccess struct { data map[string][]any } -func (o Operation) ToDBColumn(model contracts.Model, container *do.Injector) (column db.Expression, field reflect.StructField) { +func (o Operation) ToDBColumn(model contracts.Model, container do.Injector) (column db.Expression, field reflect.StructField) { GetOrmConditions := do.MustInvoke[contracts.GetOrmConditions](container) uuid := strings.ReplaceAll(o.Uuid, "-", "_") @@ -52,9 +53,9 @@ func (o Operation) ToDBColumn(model contracts.Model, container *do.Injector) (co } if cond.IsEmpty() { - column = db.Raw(pk + " as `u_" + uuid + "`") + column = db.I(pk).As("u_" + uuid) } else { - column = db.Raw("if(" + string(cond.ToSql(model)) + ", " + pk + ", 0) as `u_" + uuid + "`") + column = exp.NewSQLFunctionExpression("IF", cond.ToSql(model), db.I(pk), 0).As("u_" + uuid) } // 主键类型 diff --git a/support/response.go b/support/response.go index 644ab2d..7077936 100644 --- a/support/response.go +++ b/support/response.go @@ -37,7 +37,7 @@ var wsUpgrader = websocket.Upgrader{ func (this HttpStatusResponse) Get(path ...string) req.GlobalParams { if this.body == nil { - return req.NewGlobalParam("", nil) + return req.NewGlobalParams("", nil) } return this.body.Get(path...) } @@ -50,7 +50,7 @@ func (this HttpStatusResponse) Send(w http.ResponseWriter, r *http.Request) { } func (this RawResponse) Get(path ...string) req.GlobalParams { - return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(this.raw), nil), req.NewGlobalParam(string(this.raw), nil).Get(strings.Join(path, "."))) + return lo.Ternary(len(path) == 0, req.NewGlobalParams(string(this.raw), nil), req.NewGlobalParams(string(this.raw), nil).Get(strings.Join(path, "."))) } func (this RawResponse) Send(w http.ResponseWriter, r *http.Request) { @@ -72,7 +72,7 @@ func (this FileResponse) Get(path ...string) req.GlobalParams { "name": this.name, "disposition": this.disposition, }); err == nil { - return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(resp), nil), req.NewGlobalParam(string(resp), nil).Get(strings.Join(path, "."))) + return lo.Ternary(len(path) == 0, req.NewGlobalParams(string(resp), nil), req.NewGlobalParams(string(resp), nil).Get(strings.Join(path, "."))) } return nil diff --git a/user_approval.go b/user_approval.go index c4064e0..076f182 100644 --- a/user_approval.go +++ b/user_approval.go @@ -56,7 +56,7 @@ type UserApproval interface { // 判断发起审批请求权限 HasUserRoles(req.User) bool // 获取审批列表标记字段值 - Execute(id int64, status UserApprovalExecute, r *http.Request, tx *db.Transaction) (current, next UserApprovalFlow, err error) + Execute(id int64, status UserApprovalExecute, r *http.Request, tx *db.TxDatabase) (current, next UserApprovalFlow, err error) } type UserApprovalFlow interface { diff --git a/ws.go b/ws.go index 04d4fab..afc41a8 100644 --- a/ws.go +++ b/ws.go @@ -5,7 +5,7 @@ import ( "git.fsdpf.net/go/req" "github.com/gorilla/websocket" - "github.com/samber/do" + "github.com/samber/do/v2" ) type WsClientGroup string @@ -90,7 +90,7 @@ func (WsHandleController) ClientGroup(req.GlobalParams) WsClientGroup { return defaultWsClientGroup } -func NewWsController(container *do.Injector, request *http.Request, ws WsClient) WsController { +func NewWsController(container do.Injector, request *http.Request, ws WsClient) WsController { return &WsHandleController{ Controller: &BaseController{container}, request: request,