Compare commits

...

20 Commits

Author SHA1 Message Date
Yavolte 3b130c9d14 fix tier 2025-06-26 18:30:17 +08:00
Yavolte 8409716217 add types 2025-06-26 14:07:30 +08:00
Yavolte 368a9e868c fix tier loop 2025-06-25 11:15:08 +08:00
Yavolte 206932038e add tier value support 2025-06-25 11:10:20 +08:00
Yavolte 5c09f1eefc update mulit value 2025-06-23 22:50:31 +08:00
Yavolte 0c1c0da166 add generic type 2025-06-18 14:47:19 +08:00
Yavolte 01a671dff8 添加valueLookup支持配置 2025-06-17 18:27:09 +08:00
Yavolte 2631943904 add permission model 2025-06-17 17:17:02 +08:00
Yavolte e85cc0b8ca fix singularize table names 2025-06-12 10:48:10 +08:00
Yavolte 3020468afb fix bugs 2025-06-12 10:37:21 +08:00
Yavolte bb048abbb1 fix model bugs 2025-06-12 10:10:34 +08:00
Yavolte b32fd7d53f export some method 2025-06-11 22:20:44 +08:00
Yavolte 43b48e1469 add permission checker 2025-06-11 17:43:22 +08:00
Yavolte 5264ee005e optimization module init 2025-06-11 11:36:56 +08:00
Yavolte 8de2f0e3a5 move arrays.exists to slices.Contains 2025-06-10 17:05:52 +08:00
Yavolte 7097610fad 修改多选的逻辑 2025-04-11 15:24:49 +08:00
Yavolte 44e6e2b34f 修改搜索支持多选 2025-04-11 14:12:23 +08:00
fancl 72b0de9c26 fix match method 2024-12-31 15:46:32 +08:00
fancl 68bdabddee fix bug 2024-12-13 09:50:13 +08:00
fancl 72d742a45d add default domain 2024-12-13 09:31:31 +08:00
14 changed files with 663 additions and 149 deletions

View File

@ -1 +1,31 @@
# 数据库组件 # 数据库组件
组件提供了操作`mysql`一些相关的内容,通过组件可以方便的实现怎删改查的接口
## 插件
### 主键插件
主键插件是指的是用于生成数据库主键的插件非自增长的ID主键插件使用方式
```go
db.Use(&identified.Identify{})
```
### 数据校验插件
数据校验插件用户增改的时候对数据格式进行校验,使用方式
```go
db.Use(validate.New())
```
### 分表插件
分表插件提供了自动分表的功能,使用方式
```go
db.Use(sharding.New())
```

View File

@ -3,10 +3,11 @@ package rest
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"git.nobla.cn/golang/kos/util/arrays"
"git.nobla.cn/golang/rest/types"
"net/http" "net/http"
"slices"
"strings" "strings"
"git.nobla.cn/golang/rest/types"
) )
func findCondition(schema *types.Schema, conditions []*types.Condition) *types.Condition { func findCondition(schema *types.Schema, conditions []*types.Condition) *types.Condition {
@ -30,7 +31,7 @@ func BuildConditions(ctx context.Context, r *http.Request, query *Query, schemas
return return
} }
} }
if arrays.Exists(r.Method, []string{http.MethodPut, http.MethodPost}) { if slices.Contains(allowMethods, r.Method) {
conditions := make([]*types.Condition, 0) conditions := make([]*types.Condition, 0)
if err = json.NewDecoder(r.Body).Decode(&conditions); err != nil { if err = json.NewDecoder(r.Body).Decode(&conditions); err != nil {
return return
@ -81,6 +82,17 @@ func BuildConditions(ctx context.Context, r *http.Request, query *Query, schemas
if row.Native == 0 { if row.Native == 0 {
continue continue
} }
//如果是多选的话直接使用IN操作
columnName := row.Column + "[]"
if qs.Has(columnName) {
if len(qs[columnName]) > 1 {
query.AndFilterWhere(newConditionWithOperator("IN", row.Column, qs[columnName]))
continue
} else if len(qs[columnName]) == 1 {
query.AndFilterWhere(newCondition(row.Column, qs[columnName][0]))
continue
}
}
formValue = qs.Get(row.Column) formValue = qs.Get(row.Column)
switch row.Format { switch row.Format {
case types.FormatString, types.FormatText: case types.FormatString, types.FormatText:

1
go.mod
View File

@ -19,6 +19,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
golang.org/x/crypto v0.19.0 // indirect golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.17.0 // indirect

2
go.sum
View File

@ -24,6 +24,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/longbridgeapp/sqlparser v0.3.2 h1:FV0dgMiv8VcksT3p10hJeqfPs8bodoehmUJ7MhBds+Y= github.com/longbridgeapp/sqlparser v0.3.2 h1:FV0dgMiv8VcksT3p10hJeqfPs8bodoehmUJ7MhBds+Y=
github.com/longbridgeapp/sqlparser v0.3.2/go.mod h1:GIHaUq8zvYyHLCLMJJykx1CdM6LHtkUih/QaJXySSx4= github.com/longbridgeapp/sqlparser v0.3.2/go.mod h1:GIHaUq8zvYyHLCLMJJykx1CdM6LHtkUih/QaJXySSx4=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=

143
model.go
View File

@ -5,13 +5,6 @@ import (
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.nobla.cn/golang/kos/util/arrays"
"git.nobla.cn/golang/kos/util/pool"
"git.nobla.cn/golang/rest/inflector"
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
@ -19,18 +12,28 @@ import (
"os" "os"
"path" "path"
"reflect" "reflect"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"git.nobla.cn/golang/kos/util/pool"
"git.nobla.cn/golang/rest/inflector"
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
) )
type Model struct { type Model struct {
naming types.Naming //命名规则 naming types.Naming //命名规则
value reflect.Value //模块值 value reflect.Value //模块值
modelValue any //模块实例
db *gorm.DB //数据库 db *gorm.DB //数据库
primaryKey string //主键 primaryKey string //主键
urlPrefix string //url前缀 urlPrefix string //url前缀
disableDomain bool //禁用域 disableDomain bool //禁用域
permissionChecker types.PermissionChecker //权限检查
schemaLookup types.SchemaLookupFunc //获取schema的函数 schemaLookup types.SchemaLookupFunc //获取schema的函数
valueLookup types.ValueLookupFunc //查看域 valueLookup types.ValueLookupFunc //查看域
statement *gorm.Statement //字段声明 statement *gorm.Statement //字段声明
@ -38,6 +41,7 @@ type Model struct {
response types.HttpWriter //HTTP响应 response types.HttpWriter //HTTP响应
hookMgr *hookManager //钩子管理器 hookMgr *hookManager //钩子管理器
dirname string //存放文件目录 dirname string //存放文件目录
scenarios []string //场景
} }
var ( var (
@ -64,8 +68,19 @@ func (m *Model) getHook() *hookManager {
// hasScenario 判断是否有该场景 // hasScenario 判断是否有该场景
func (m *Model) hasScenario(s string) bool { func (m *Model) hasScenario(s string) bool {
if len(m.scenarios) == 0 {
return true return true
} }
return slices.Contains(m.scenarios, s)
}
// hasPermission 判断是否有权限
func (m *Model) hasPermission(ctx context.Context, s string) (err error) {
if m.permissionChecker != nil {
return m.permissionChecker.CheckPermission(ctx, m.Permission(s))
}
return nil
}
// setValue 设置字段的值 // setValue 设置字段的值
func (m *Model) setValue(refValue reflect.Value, column string, value any) { func (m *Model) setValue(refValue reflect.Value, column string, value any) {
@ -157,7 +172,7 @@ func (m *Model) buildReporterCountColumns(ctx context.Context, dest types.Report
modelType = modelType.Elem() modelType = modelType.Elem()
} }
columns := make([]string, 0) columns := make([]string, 0)
for i := 0; i < modelType.NumField(); i++ { for i := range modelType.NumField() {
field := modelType.Field(i) field := modelType.Field(i)
scenarios := field.Tag.Get("scenarios") scenarios := field.Tag.Get("scenarios")
if !hasToken(types.ScenarioList, scenarios) { if !hasToken(types.ScenarioList, scenarios) {
@ -218,7 +233,28 @@ func (m *Model) ModuleName() string {
// TableName 表的名称 // TableName 表的名称
func (m *Model) TableName() string { func (m *Model) TableName() string {
return m.naming.ModuleName return m.naming.TableName
}
func (m *Model) Naming() types.Naming {
return m.naming
}
func (m *Model) Value() reflect.Value {
return m.value
}
func (m *Model) SetDB(db *gorm.DB) *gorm.DB {
m.db = db
return m.db
}
func (m *Model) HasScenario(s string) bool {
return m.hasScenario(s)
}
func (m *Model) HasPermission(ctx context.Context, s string) (err error) {
return m.hasPermission(ctx, s)
} }
// Fields 返回搜索的模型的字段 // Fields 返回搜索的模型的字段
@ -226,9 +262,36 @@ func (m *Model) Fields() []*schema.Field {
return m.statement.Schema.Fields return m.statement.Schema.Fields
} }
// Permission 获取权限
// 权限示例: "module.model.scenario"
// 比如: organize:user:list, organize:user:create
func (m *Model) Permission(scenario string) string {
if pm, ok := m.modelValue.(PermissionModel); ok {
return pm.GetPermission(scenario)
}
ss := make([]string, 0, 4)
switch scenario {
case types.ScenarioList:
ss = append(ss, m.naming.ModuleName, m.naming.Singular, "list")
case types.ScenarioView:
ss = append(ss, m.naming.ModuleName, m.naming.Singular, "detail")
case types.ScenarioCreate:
ss = append(ss, m.naming.ModuleName, m.naming.Singular, "create")
case types.ScenarioUpdate:
ss = append(ss, m.naming.ModuleName, m.naming.Singular, "update")
case types.ScenarioDelete:
ss = append(ss, m.naming.ModuleName, m.naming.Singular, "delete")
case types.ScenarioExport:
ss = append(ss, m.naming.ModuleName, m.naming.Singular, "export")
case types.ScenarioImport:
ss = append(ss, m.naming.ModuleName, m.naming.Singular, "import")
}
return strings.Join(ss, ":")
}
// Uri 获取请求的uri // Uri 获取请求的uri
func (m *Model) Uri(scenario string) string { func (m *Model) Uri(scenario string) string {
ss := make([]string, 4) ss := make([]string, 0, 4)
if m.urlPrefix != "" { if m.urlPrefix != "" {
ss = append(ss, m.urlPrefix) ss = append(ss, m.urlPrefix)
} }
@ -273,6 +336,7 @@ func (m *Model) Method(scenario string) string {
// Search 实现通过HTTP方法查找数据 // Search 实现通过HTTP方法查找数据
func (m *Model) Search(w http.ResponseWriter, r *http.Request) { func (m *Model) Search(w http.ResponseWriter, r *http.Request) {
var ( var (
ok bool ok bool
err error err error
@ -291,6 +355,14 @@ func (m *Model) Search(w http.ResponseWriter, r *http.Request) {
reporter types.Reporter reporter types.Reporter
namerTable tableNamer namerTable tableNamer
) )
if !m.hasScenario(types.ScenarioList) {
m.response.Failure(w, types.RequestDenied, scenarioNotAllow, nil)
return
}
if err = m.hasPermission(r.Context(), types.ScenarioList); err != nil {
m.response.Failure(w, types.RequestDenied, err.Error(), nil)
return
}
qs = r.URL.Query() qs = r.URL.Query()
page, _ = strconv.Atoi(qs.Get("page")) page, _ = strconv.Atoi(qs.Get("page"))
pageSize, _ = strconv.Atoi(qs.Get("pagesize")) pageSize, _ = strconv.Atoi(qs.Get("pagesize"))
@ -325,7 +397,7 @@ func (m *Model) Search(w http.ResponseWriter, r *http.Request) {
return return
} }
scenario = types.ScenarioList scenario = types.ScenarioList
if arrays.Exists(r.FormValue("scenario"), allowScenario) { if slices.Contains(allowScenario, r.FormValue("scenario")) {
scenario = r.FormValue("scenario") scenario = r.FormValue("scenario")
} }
if listSchemas, err = m.schemaLookup(childCtx, m.getDB(), domainName, m.naming.ModuleName, m.naming.TableName, scenario); err != nil { if listSchemas, err = m.schemaLookup(childCtx, m.getDB(), domainName, m.naming.ModuleName, m.naming.TableName, scenario); err != nil {
@ -391,6 +463,14 @@ func (m *Model) Create(w http.ResponseWriter, r *http.Request) {
domainName string domainName string
modelValue reflect.Value modelValue reflect.Value
) )
if !m.hasScenario(types.ScenarioCreate) {
m.response.Failure(w, types.RequestDenied, scenarioNotAllow, nil)
return
}
if err = m.hasPermission(r.Context(), types.ScenarioCreate); err != nil {
m.response.Failure(w, types.RequestDenied, err.Error(), nil)
return
}
modelValue = reflect.New(m.value.Type()) modelValue = reflect.New(m.value.Type())
model = modelValue.Interface() model = modelValue.Interface()
if err = json.NewDecoder(r.Body).Decode(modelValue.Interface()); err != nil { if err = json.NewDecoder(r.Body).Decode(modelValue.Interface()); err != nil {
@ -472,6 +552,14 @@ func (m *Model) Update(w http.ResponseWriter, r *http.Request) {
modelValue reflect.Value modelValue reflect.Value
oldValues map[string]any oldValues map[string]any
) )
if !m.hasScenario(types.ScenarioUpdate) {
m.response.Failure(w, types.RequestDenied, scenarioNotAllow, nil)
return
}
if err = m.hasPermission(r.Context(), types.ScenarioUpdate); err != nil {
m.response.Failure(w, types.RequestDenied, err.Error(), nil)
return
}
idStr := m.findPrimaryKey(m.Uri(types.ScenarioUpdate), r) idStr := m.findPrimaryKey(m.Uri(types.ScenarioUpdate), r)
modelValue = reflect.New(m.value.Type()) modelValue = reflect.New(m.value.Type())
model = modelValue.Interface() model = modelValue.Interface()
@ -563,6 +651,14 @@ func (m *Model) Delete(w http.ResponseWriter, r *http.Request) {
model any model any
modelValue reflect.Value modelValue reflect.Value
) )
if !m.hasScenario(types.ScenarioDelete) {
m.response.Failure(w, types.RequestDenied, scenarioNotAllow, nil)
return
}
if err = m.hasPermission(r.Context(), types.ScenarioDelete); err != nil {
m.response.Failure(w, types.RequestDenied, err.Error(), nil)
return
}
idStr := m.findPrimaryKey(m.Uri(types.ScenarioDelete), r) idStr := m.findPrimaryKey(m.Uri(types.ScenarioDelete), r)
modelValue = reflect.New(m.value.Type()) modelValue = reflect.New(m.value.Type())
model = modelValue.Interface() model = modelValue.Interface()
@ -618,6 +714,14 @@ func (m *Model) View(w http.ResponseWriter, r *http.Request) {
scenario string scenario string
domainName string domainName string
) )
if !m.hasScenario(types.ScenarioView) {
m.response.Failure(w, types.RequestDenied, scenarioNotAllow, nil)
return
}
if err = m.hasPermission(r.Context(), types.ScenarioView); err != nil {
m.response.Failure(w, types.RequestDenied, err.Error(), nil)
return
}
qs = r.URL.Query() qs = r.URL.Query()
idStr := m.findPrimaryKey(m.Uri(types.ScenarioUpdate), r) idStr := m.findPrimaryKey(m.Uri(types.ScenarioUpdate), r)
modelValue = reflect.New(m.value.Type()) modelValue = reflect.New(m.value.Type())
@ -656,8 +760,12 @@ func (m *Model) Export(w http.ResponseWriter, r *http.Request) {
fp *os.File fp *os.File
modelValue reflect.Value modelValue reflect.Value
) )
if !m.hasScenario(types.ScenarioList) { if !m.hasScenario(types.ScenarioExport) {
m.response.Failure(w, types.RequestDenied, "request denied", nil) m.response.Failure(w, types.RequestDenied, scenarioNotAllow, nil)
return
}
if err = m.hasPermission(r.Context(), types.ScenarioExport); err != nil {
m.response.Failure(w, types.RequestDenied, err.Error(), nil)
return return
} }
domainName = m.valueLookup(types.FieldDomain, w, r) domainName = m.valueLookup(types.FieldDomain, w, r)
@ -898,6 +1006,14 @@ func (m *Model) Import(w http.ResponseWriter, r *http.Request) {
qs url.Values qs url.Values
extraFields map[string]string extraFields map[string]string
) )
if !m.hasScenario(types.ScenarioImport) {
m.response.Failure(w, types.RequestDenied, scenarioNotAllow, nil)
return
}
if err = m.hasPermission(r.Context(), types.ScenarioImport); err != nil {
m.response.Failure(w, types.RequestDenied, err.Error(), nil)
return
}
domainName = m.valueLookup(types.FieldDomain, w, r) domainName = m.valueLookup(types.FieldDomain, w, r)
if schemas, err = m.schemaLookup(r.Context(), m.getDB(), domainName, m.naming.ModuleName, m.naming.TableName, types.ScenarioCreate); err != nil { if schemas, err = m.schemaLookup(r.Context(), m.getDB(), domainName, m.naming.ModuleName, m.naming.TableName, types.ScenarioCreate); err != nil {
m.response.Failure(w, types.RequestRecordNotFound, "record not found", nil) m.response.Failure(w, types.RequestRecordNotFound, "record not found", nil)
@ -973,6 +1089,7 @@ func newModel(v any, db *gorm.DB, naming types.Naming) *Model {
value: reflect.Indirect(reflect.ValueOf(v)), value: reflect.Indirect(reflect.ValueOf(v)),
valueLookup: defaultValueLookup, valueLookup: defaultValueLookup,
} }
model.modelValue = reflect.New(model.value.Type()).Interface()
model.statement = &gorm.Statement{ model.statement = &gorm.Statement{
DB: model.getDB(), DB: model.getDB(),
ConnPool: model.getDB().ConnPool, ConnPool: model.getDB().ConnPool,

View File

@ -1,52 +1,105 @@
package rest package rest
import "git.nobla.cn/golang/rest/types" import (
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
)
type Options struct { type options struct {
urlPrefix string urlPrefix string
moduleName string moduleName string
disableDomain bool disableDomain bool
db *gorm.DB
router types.HttpRouter router types.HttpRouter
writer types.HttpWriter writer types.HttpWriter
permissionChecker types.PermissionChecker
valueLookup types.ValueLookupFunc
formatter *Formatter formatter *Formatter
dirname string //文件目录 resourceDirectory string
} }
type Option func(o *Options) type Option func(o *options)
func (o *options) Clone() *options {
return &options{
urlPrefix: o.urlPrefix,
moduleName: o.moduleName,
disableDomain: o.disableDomain,
db: o.db,
router: o.router,
writer: o.writer,
permissionChecker: o.permissionChecker,
formatter: o.formatter,
resourceDirectory: o.resourceDirectory,
valueLookup: o.valueLookup,
}
}
// WithDB 设置DB
func WithDB(db *gorm.DB) Option {
return func(o *options) {
o.db = db
}
}
// WithUriPrefix 模块前缀
func WithUriPrefix(s string) Option { func WithUriPrefix(s string) Option {
return func(o *Options) { return func(o *options) {
o.urlPrefix = s o.urlPrefix = s
} }
} }
func WithValueLookup(f types.ValueLookupFunc) Option {
return func(o *options) {
o.valueLookup = f
}
}
// WithModuleName 模块名称
func WithModuleName(s string) Option { func WithModuleName(s string) Option {
return func(o *Options) { return func(o *options) {
o.moduleName = s o.moduleName = s
} }
} }
// WithoutDomain 禁用域 // WithoutDomain 禁用域
func WithoutDomain() Option { func WithoutDomain() Option {
return func(o *Options) { return func(o *options) {
o.disableDomain = true o.disableDomain = true
} }
} }
// WithHttpRouter 设置HttpRouter
func WithHttpRouter(s types.HttpRouter) Option { func WithHttpRouter(s types.HttpRouter) Option {
return func(o *Options) { return func(o *options) {
o.router = s o.router = s
} }
} }
// WithHttpWriter 配置HttpWriter
func WithHttpWriter(s types.HttpWriter) Option { func WithHttpWriter(s types.HttpWriter) Option {
return func(o *Options) { return func(o *options) {
o.writer = s o.writer = s
} }
} }
// WithFormatter 配置Formatter
func WithFormatter(s *Formatter) Option { func WithFormatter(s *Formatter) Option {
return func(o *Options) { return func(o *options) {
o.formatter = s o.formatter = s
} }
} }
// WithPermissionChecker 配置PermissionChecker
func WithPermissionChecker(s types.PermissionChecker) Option {
return func(o *options) {
o.permissionChecker = s
}
}
// WithResourceDirectory 配置资源目录
func WithResourceDirectory(s string) Option {
return func(o *options) {
o.resourceDirectory = s
}
}

102
query.go
View File

@ -3,11 +3,13 @@ package rest
import ( import (
"context" "context"
"fmt" "fmt"
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"unicode"
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
) )
type ( type (
@ -79,7 +81,35 @@ func (query *Query) compile() (*gorm.DB, error) {
return db, nil return db, nil
} }
func (query *Query) decodeValue(v any) string { // quoteName 编码数据库字段
func (quote *Query) quoteName(name string) string {
if len(name) == 0 {
return name
}
name = strings.Map(func(r rune) rune {
if unicode.IsControl(r) {
return -1
}
if unicode.IsSpace(r) {
return -1
}
return r
}, name)
isQuoted := len(name) >= 2 &&
name[0] == '`' &&
name[len(name)-1] == '`'
if !isQuoted {
var b strings.Builder
b.Grow(len(name) + 2)
b.WriteByte('`')
b.WriteString(name)
b.WriteByte('`')
return b.String()
}
return name
}
func (query *Query) quoteValue(v any) string {
refVal := reflect.Indirect(reflect.ValueOf(v)) refVal := reflect.Indirect(reflect.ValueOf(v))
switch refVal.Kind() { switch refVal.Kind() {
case reflect.Bool: case reflect.Bool:
@ -88,13 +118,18 @@ func (query *Query) decodeValue(v any) string {
} else { } else {
return "0" return "0"
} }
case reflect.Int8, reflect.Int, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint, reflect.Uint32, reflect.Uint64: case reflect.Uint8, reflect.Uint, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(refVal.Uint(), 10)
case reflect.Int8, reflect.Int, reflect.Int32, reflect.Int64:
return strconv.FormatInt(refVal.Int(), 10) return strconv.FormatInt(refVal.Int(), 10)
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(refVal.Float(), 'f', -1, 64) return strconv.FormatFloat(refVal.Float(), 'f', -1, 64)
case reflect.String: case reflect.String:
return "'" + refVal.String() + "'" return strconv.Quote(refVal.String())
default: default:
if v == nil {
return "IS NULL"
}
return fmt.Sprint(v) return fmt.Sprint(v)
} }
} }
@ -103,7 +138,7 @@ func (query *Query) buildConditions(operator string, filter bool, conditions ...
var ( var (
sb strings.Builder sb strings.Builder
) )
params = make([]interface{}, 0) params = make([]any, 0)
for _, cond := range conditions { for _, cond := range conditions {
if filter { if filter {
if isEmpty(cond.Value) { if isEmpty(cond.Value) {
@ -116,42 +151,65 @@ func (query *Query) buildConditions(operator string, filter bool, conditions ...
switch strings.ToUpper(cond.Expr) { switch strings.ToUpper(cond.Expr) {
case "=", "<>", ">", "<", ">=", "<=", "!=": case "=", "<>", ">", "<", ">=", "<=", "!=":
if sb.Len() > 0 { if sb.Len() > 0 {
sb.WriteString(" " + operator + " ") sb.WriteString(" ")
sb.WriteString(operator)
sb.WriteString(" ")
} }
if cond.Expr == "=" && cond.Value == nil { if cond.Expr == "=" && cond.Value == nil {
sb.WriteString("`" + cond.Field + "` IS NULL") sb.WriteString(query.quoteName(cond.Field))
sb.WriteString(" IS NULL")
} else { } else {
sb.WriteString("`" + cond.Field + "` " + cond.Expr + " ?") sb.WriteString(query.quoteName(cond.Field))
sb.WriteString(" ")
sb.WriteString(cond.Expr)
sb.WriteString(" ?")
params = append(params, cond.Value) params = append(params, cond.Value)
} }
case "LIKE": case "LIKE":
if sb.Len() > 0 { if sb.Len() > 0 {
sb.WriteString(" " + operator + " ") sb.WriteString(" ")
sb.WriteString(operator)
sb.WriteString(" ")
} }
cond.Value = fmt.Sprintf("%%%s%%", cond.Value) cond.Value = fmt.Sprintf("%%%s%%", cond.Value)
sb.WriteString("`" + cond.Field + "` LIKE ?") sb.WriteString(query.quoteName(cond.Field))
sb.WriteString(" LIKE ?")
params = append(params, cond.Value) params = append(params, cond.Value)
case "IN": case "IN":
if sb.Len() > 0 { if sb.Len() > 0 {
sb.WriteString(" " + operator + " ") sb.WriteString(" ")
sb.WriteString(operator)
sb.WriteString(" ")
} }
refVal := reflect.Indirect(reflect.ValueOf(cond.Value)) refVal := reflect.Indirect(reflect.ValueOf(cond.Value))
switch refVal.Kind() { switch refVal.Kind() {
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:
ss := make([]string, refVal.Len()) ss := make([]string, refVal.Len())
for i := 0; i < refVal.Len(); i++ { for i := range refVal.Len() {
ss[i] = query.decodeValue(refVal.Index(i)) ss[i] = query.quoteValue(refVal.Index(i).Interface())
} }
sb.WriteString("`" + cond.Field + "` IN (" + strings.Join(ss, ",") + ")") sb.WriteString(query.quoteName(cond.Field))
sb.WriteString(" IN (")
sb.WriteString(strings.Join(ss, ","))
sb.WriteString(") ")
case reflect.String: case reflect.String:
sb.WriteString("`" + cond.Field + "` IN (" + refVal.String() + ")") sb.WriteString(query.quoteName(cond.Field))
sb.WriteString(" IN (")
sb.WriteString(refVal.String())
sb.WriteString(")")
default: default:
} }
case "BETWEEN": case "BETWEEN":
if sb.Len() > 0 {
sb.WriteString(" ")
sb.WriteString(operator)
sb.WriteString(" ")
}
refVal := reflect.ValueOf(cond.Value) refVal := reflect.ValueOf(cond.Value)
if refVal.Kind() == reflect.Slice && refVal.Len() == 2 { if refVal.Kind() == reflect.Slice && refVal.Len() == 2 {
sb.WriteString("`" + cond.Field + "` BETWEEN ? AND ?") sb.WriteString(query.quoteName(cond.Field))
params = append(params, refVal.Index(0), refVal.Index(1)) sb.WriteString(" BETWEEN ? AND ?")
params = append(params, refVal.Index(0).Interface(), refVal.Index(1).Interface())
} }
} }
} }
@ -341,7 +399,7 @@ func (query *Query) Count(v interface{}) (i int64) {
return return
} }
func (query *Query) One(v interface{}) (err error) { func (query *Query) One(v any) (err error) {
var ( var (
db *gorm.DB db *gorm.DB
) )
@ -353,7 +411,7 @@ func (query *Query) One(v interface{}) (err error) {
return return
} }
func (query *Query) All(v interface{}) (err error) { func (query *Query) All(v any) (err error) {
var ( var (
db *gorm.DB db *gorm.DB
) )
@ -376,7 +434,7 @@ func NewCondition(column, opera string, value any) *condition {
} }
} }
func newCondition(field string, value interface{}) *condition { func newCondition(field string, value any) *condition {
return &condition{ return &condition{
Field: field, Field: field,
Value: value, Value: value,
@ -384,7 +442,7 @@ func newCondition(field string, value interface{}) *condition {
} }
} }
func newConditionWithOperator(operator, field string, value interface{}) *condition { func newConditionWithOperator(operator, field string, value any) *condition {
cond := &condition{ cond := &condition{
Field: field, Field: field,
Value: value, Value: value,

79
query_test.go 100644
View File

@ -0,0 +1,79 @@
package rest
import (
"testing"
)
func TestQuoteName(t *testing.T) {
q := &Query{}
tests := []struct {
name string
input string
expected string
}{
{"Empty string", "", ""},
{"Control chars", "a\x00b\nc", "`abc`"}, // \x00 and \n should be filtered
{"Spaces", " test name ", "`testname`"}, // Spaces should be filtered
{"Properly quoted", "`valid`", "`valid`"}, // Already quoted
{"Left quote only", "`invalid", "``invalid`"}, // Add missing right quote
{"Normal unquoted", "normal", "`normal`"}, // Add quotes
{"All filtered", "\t\r\n", "``"}, // Filter all characters
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := q.quoteName(tt.input)
if got != tt.expected {
t.Errorf("quoteName(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}
func TestQuery_quoteValue(t *testing.T) {
type testCase struct {
name string
input any
expected string
}
tests := []testCase{
// Boolean values
{"bool true", true, "1"},
{"bool false", false, "0"},
{"bool pointer", new(bool), "0"}, // *bool with zero value
// Integer family
{"int", 42, "42"},
{"int8", int8(127), "127"},
{"uint", uint(100), "100"},
{"uint64", uint64(1<<64 - 1), "18446744073709551615"},
// Floating points
{"float64", 3.14, "3.14"},
{"float64 scientific", 1e10, "10000000000"},
{"float32", float32(1.5), "1.5"},
// Strings
{"simple string", "hello", `"hello"`},
{"string with quotes", `"quoted"`, `"\"quoted\""`},
{"string with newline", "line\nbreak", `"line\nbreak"`},
// Default cases
{"struct", struct{}{}, "{}"},
{"slice", []int{1, 2}, "[1 2]"},
{"nil", nil, "IS NULL"},
}
q := &Query{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := q.quoteValue(tt.input)
if got != tt.expected {
t.Errorf("quoteValue(%v) = %v, want %v", tt.input, got, tt.expected)
}
})
}
}

182
rest.go
View File

@ -4,23 +4,24 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"git.nobla.cn/golang/kos/util/arrays" "net/http"
"reflect"
"slices"
"strconv"
"strings"
"time"
"git.nobla.cn/golang/kos/util/reflection" "git.nobla.cn/golang/kos/util/reflection"
"git.nobla.cn/golang/rest/inflector" "git.nobla.cn/golang/rest/inflector"
"git.nobla.cn/golang/rest/types" "git.nobla.cn/golang/rest/types"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
"gorm.io/gorm/schema" "gorm.io/gorm/schema"
"net/http"
"reflect"
"strconv"
"strings"
"time"
) )
var ( var (
modelEntities []*Model modelEntities []*Model
httpRouter types.HttpRouter globalOpts *options
hookMgr *hookManager hookMgr *hookManager
timeKind = reflect.TypeOf(time.Time{}).Kind() timeKind = reflect.TypeOf(time.Time{}).Kind()
timePtrKind = reflect.TypeOf(&time.Time{}).Kind() timePtrKind = reflect.TypeOf(&time.Time{}).Kind()
@ -33,6 +34,7 @@ var (
) )
func init() { func init() {
globalOpts = &options{}
hookMgr = &hookManager{} hookMgr = &hookManager{}
modelEntities = make([]*Model, 0) modelEntities = make([]*Model, 0)
} }
@ -166,12 +168,12 @@ func fieldRule(field *schema.Field) types.Rule {
if ls > 1 { if ls > 1 {
bs := strings.Split(vs[1], ",") bs := strings.Split(vs[1], ",")
for _, i := range bs { for _, i := range bs {
if arrays.Exists(i, []string{types.ScenarioCreate, types.ScenarioUpdate}) { if slices.Contains(requiredScenario, i) {
r.Required = append(r.Required, i) r.Required = append(r.Required, i)
} }
} }
} else { } else {
r.Required = []string{types.ScenarioCreate, types.ScenarioUpdate} r.Required = requiredScenario
} }
case "unique": case "unique":
r.Unique = true r.Unique = true
@ -256,7 +258,7 @@ func fieldAttribute(field *schema.Field) types.Attribute {
case "icon": case "icon":
attr.Icon = sv attr.Icon = sv
case "match": case "match":
if arrays.Exists(sv, matchEnums) { if slices.Contains(matchEnums, sv) {
attr.Match = sv attr.Match = sv
} }
case "endofnow", "end_of_now": case "endofnow", "end_of_now":
@ -280,7 +282,7 @@ func fieldAttribute(field *schema.Field) types.Attribute {
case "readonly": case "readonly":
bs := strings.Split(sv, ",") bs := strings.Split(sv, ",")
for _, i := range bs { for _, i := range bs {
if arrays.Exists(i, []string{types.ScenarioCreate, types.ScenarioUpdate}) { if slices.Contains(readonlyScenario, i) {
attr.Readonly = append(attr.Readonly, i) attr.Readonly = append(attr.Readonly, i)
} }
} }
@ -312,6 +314,12 @@ func fieldAttribute(field *schema.Field) types.Attribute {
attr.Live.Columns = strings.Split(kv[1], ",") attr.Live.Columns = strings.Split(kv[1], ",")
} }
} }
if attr.Live.Enable {
if attr.Live.Method == "" {
attr.Live.Method = "GET"
}
}
attr.Match = types.MatchExactly
} }
dropdown := field.Tag.Get("dropdown") dropdown := field.Tag.Get("dropdown")
@ -379,6 +387,7 @@ func fieldAttribute(field *schema.Field) types.Attribute {
} }
attr.Values = append(attr.Values, fv) attr.Values = append(attr.Values, fv)
} }
attr.Match = types.MatchExactly
} }
if !field.Creatable { if !field.Creatable {
attr.Disable = append(attr.Disable, types.ScenarioCreate) attr.Disable = append(attr.Disable, types.ScenarioCreate)
@ -471,40 +480,51 @@ func autoMigrate(ctx context.Context, db *gorm.DB, module string, model any) (na
return return
} }
// SetHttpRouter 设置HTTP路由 // Init 初始化
func SetHttpRouter(router types.HttpRouter) { func Init(cbs ...Option) (err error) {
httpRouter = router for _, cb := range cbs {
cb(globalOpts)
}
if globalOpts.db != nil {
err = globalOpts.db.AutoMigrate(&types.Schema{})
}
return
} }
// AutoMigrate 自动合并表的schema // AutoMigrate 自动合并表的schema
func AutoMigrate(ctx context.Context, db *gorm.DB, model any, cbs ...Option) (err error) { func AutoMigrate(ctx context.Context, model any, cbs ...Option) (modelValue *Model, err error) {
var ( var (
opts *Options opts *options
table string table string
router types.HttpRouter router types.HttpRouter
) )
opts = &Options{} opts = globalOpts.Clone()
for _, cb := range cbs { for _, cb := range cbs {
cb(opts) cb(opts)
} }
if table, err = autoMigrate(ctx, db, opts.moduleName, model); err != nil { if table, err = autoMigrate(ctx, opts.db, opts.moduleName, model); err != nil {
return return
} }
//路由模块处理 //路由模块处理
modelValue := newModel(model, db, types.Naming{ singularizeTable := inflector.Singularize(table)
Pluralize: inflector.Pluralize(table), modelValue = newModel(model, opts.db, types.Naming{
Singular: inflector.Singularize(table), Pluralize: inflector.Pluralize(singularizeTable),
Singular: singularizeTable,
ModuleName: opts.moduleName, ModuleName: opts.moduleName,
TableName: table, TableName: table,
}) })
if scenarioModel, ok := model.(types.ScenarioModel); ok {
modelValue.scenarios = scenarioModel.Scenario()
}
if opts.valueLookup != nil {
modelValue.valueLookup = opts.valueLookup
}
modelValue.hookMgr = hookMgr modelValue.hookMgr = hookMgr
modelValue.schemaLookup = VisibleSchemas modelValue.schemaLookup = VisibleSchemas
modelValue.permissionChecker = opts.permissionChecker
if opts.router != nil { if opts.router != nil {
router = opts.router router = opts.router
} }
if router == nil && httpRouter != nil {
router = httpRouter
}
if opts.urlPrefix != "" { if opts.urlPrefix != "" {
modelValue.urlPrefix = opts.urlPrefix modelValue.urlPrefix = opts.urlPrefix
} }
@ -552,6 +572,9 @@ func CloneSchemas(ctx context.Context, db *gorm.DB, domain string) (err error) {
models []*types.Schema models []*types.Schema
) )
tx := db.WithContext(ctx) tx := db.WithContext(ctx)
if domain == "" {
domain = defaultDomain
}
if err = tx.Where("domain=?", defaultDomain).Find(&values).Error; err != nil { if err = tx.Where("domain=?", defaultDomain).Find(&values).Error; err != nil {
return fmt.Errorf("schema not found") return fmt.Errorf("schema not found")
} }
@ -608,6 +631,9 @@ func GetSchemas(ctx context.Context, db *gorm.DB, domain, moduleName, tableName
// VisibleSchemas 获取某个场景下面的schema // VisibleSchemas 获取某个场景下面的schema
func VisibleSchemas(ctx context.Context, db *gorm.DB, domain, moduleName, tableName, scenario string) ([]*types.Schema, error) { func VisibleSchemas(ctx context.Context, db *gorm.DB, domain, moduleName, tableName, scenario string) ([]*types.Schema, error) {
if domain == "" {
domain = defaultDomain
}
schemas, err := GetSchemas(ctx, db, domain, moduleName, tableName) schemas, err := GetSchemas(ctx, db, domain, moduleName, tableName)
if err != nil { if err != nil {
return nil, err return nil, err
@ -626,61 +652,131 @@ func VisibleSchemas(ctx context.Context, db *gorm.DB, domain, moduleName, tableN
} }
// ModelTypes 查询指定模型的类型 // ModelTypes 查询指定模型的类型
func ModelTypes(ctx context.Context, db *gorm.DB, model any, domainName, labelColumn, valueColumn string) (values []*types.TypeValue) { func ModelTypes[T any](ctx context.Context, db *gorm.DB, model any, domainName, labelColumn, valueColumn string) (values []*types.TypeValue[T], err error) {
tx := db.WithContext(ctx) tx := db.WithContext(ctx)
result := make([]map[string]any, 0, 10) result := make([]map[string]any, 0, 10)
tx.Model(model).Select(labelColumn, valueColumn).Where("domain=?", domainName).Scan(&result) if domainName == "" {
values = make([]*types.TypeValue, 0, len(result)) err = tx.Model(model).Select(labelColumn, valueColumn).Scan(&result).Error
} else {
err = tx.Model(model).Select(labelColumn, valueColumn).Where("domain=?", domainName).Scan(&result).Error
}
if err != nil {
return
}
values = make([]*types.TypeValue[T], 0, len(result))
for _, pairs := range result { for _, pairs := range result {
feed := &types.TypeValue{} feed := &types.TypeValue[T]{}
for k, v := range pairs { for k, v := range pairs {
if k == labelColumn { if k == labelColumn {
feed.Label = v if s, ok := v.(string); ok {
feed.Label = s
} else {
feed.Label = fmt.Sprint(s)
}
continue
} }
if k == valueColumn { if k == valueColumn {
feed.Value = v if p, ok := v.(T); ok {
feed.Value = p
}
continue
} }
} }
values = append(values, feed) values = append(values, feed)
} }
return values return values, nil
}
// ModelTiers 查询指定模型的层级数据
func ModelTiers[T comparable](ctx context.Context, db *gorm.DB, model any, domainName, parentColumn, labelColumn, valueColumn string) (values []*types.TierValue[T], err error) {
tx := db.WithContext(ctx)
result := make([]map[string]any, 0, 10)
if domainName == "" {
err = tx.Model(model).Select(parentColumn, labelColumn, valueColumn).Scan(&result).Error
} else {
err = tx.Model(model).Select(parentColumn, labelColumn, valueColumn).Where("domain=?", domainName).Scan(&result).Error
}
if err != nil {
return
}
values = make([]*types.TierValue[T], 0, len(result))
for _, pairs := range result {
feed := &types.TierValue[T]{}
for k, v := range pairs {
if k == labelColumn {
if s, ok := v.(string); ok {
feed.Label = s
} else {
feed.Label = fmt.Sprint(s)
}
continue
}
if k == valueColumn {
if p, ok := v.(T); ok {
feed.Value = p
}
continue
}
if k == parentColumn {
if p, ok := v.(T); ok {
feed.Parent = p
}
continue
}
}
values = append(values, feed)
}
var none T
return recursiveTier(none, values), nil
} }
// GetFieldValue 获取模型某个字段的值 // GetFieldValue 获取模型某个字段的值
func GetFieldValue(stmt *gorm.Statement, refValue reflect.Value, column string) interface{} { func GetFieldValue(stmt *gorm.Statement, refValue reflect.Value, column string) any {
var ( var (
idx = -1 rawField *schema.Field
) )
refVal := reflect.Indirect(refValue) refVal := reflect.Indirect(refValue)
for i, field := range stmt.Schema.Fields { for _, field := range stmt.Schema.Fields {
if field.DBName == column || field.Name == column { if field.DBName == column || field.Name == column {
idx = i rawField = field
break break
} }
} }
if idx > -1 { if rawField == nil {
return refVal.Field(idx).Interface()
}
return nil return nil
} }
var targetValue reflect.Value
targetValue = refVal
for _, i := range rawField.StructField.Index {
targetValue = targetValue.Field(i)
}
return targetValue.Interface()
}
// SetFieldValue 设置模型某个字段的值 // SetFieldValue 设置模型某个字段的值
func SetFieldValue(stmt *gorm.Statement, refValue reflect.Value, column string, value any) { func SetFieldValue(stmt *gorm.Statement, refValue reflect.Value, column string, value any) {
var ( var (
idx = -1 rawField *schema.Field
) )
refVal := reflect.Indirect(refValue) refVal := reflect.Indirect(refValue)
for i, field := range stmt.Schema.Fields { for _, field := range stmt.Schema.Fields {
if field.DBName == column || field.Name == column { if field.DBName == column || field.Name == column {
idx = i rawField = field
break break
} }
} }
if idx > -1 { if rawField == nil {
refVal.Field(idx).Set(reflect.ValueOf(value)) return
} }
var targetValue reflect.Value
targetValue = refVal
for _, i := range rawField.StructField.Index {
targetValue = targetValue.Field(i)
}
targetValue.Set(reflect.ValueOf(value))
} }
// SafeSetFileValue 安全设置模型某个字段的值
func SafeSetFileValue(stmt *gorm.Statement, refValue reflect.Value, column string, value any) { func SafeSetFileValue(stmt *gorm.Statement, refValue reflect.Value, column string, value any) {
var ( var (
idx = -1 idx = -1

View File

@ -3,8 +3,10 @@ package rest
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"gorm.io/gorm"
"net/http" "net/http"
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
) )
const ( const (
@ -12,6 +14,17 @@ const (
defaultDomain = "localhost" defaultDomain = "localhost"
) )
var (
readonlyScenario = []string{types.ScenarioCreate, types.ScenarioUpdate}
requiredScenario = []string{types.ScenarioCreate, types.ScenarioUpdate}
allowMethods = []string{http.MethodPut, http.MethodPost}
)
var (
scenarioNotAllow = "request not allowed"
)
type ( type (
httpWriter struct { httpWriter struct {
} }
@ -31,6 +44,11 @@ type (
HttpTableName(req *http.Request) string HttpTableName(req *http.Request) string
} }
// 权限模型定义
PermissionModel interface {
GetPermission(scenario string) string
}
//创建后的回调,这个回调不在事物内 //创建后的回调,这个回调不在事物内
afterCreated interface { afterCreated interface {
AfterCreated(ctx context.Context, tx *gorm.DB) AfterCreated(ctx context.Context, tx *gorm.DB)

View File

@ -13,10 +13,10 @@ type (
DefaultValue string `json:"default_value"` //默认值 DefaultValue string `json:"default_value"` //默认值
Readonly []string `json:"readonly"` //只读场景 Readonly []string `json:"readonly"` //只读场景
Disable []string `json:"disable"` //禁用场景 Disable []string `json:"disable"` //禁用场景
Visible []VisibleCondition `json:"visible"` //可见条 Visible []VisibleCondition `json:"visible"` //显示场景
Invisible bool `json:"invisible"` //不可见的字段表示在UI界面看不到这个字段但是这个字段需要 Invisible bool `json:"invisible"` //不可见的字段表示在UI界面看不到这个字段但是这个字段需要
EndOfNow bool `json:"end_of_now"` //最大时间为当前时间 EndOfNow bool `json:"end_of_now"` //最大时间为当前时间
Values []EnumValue `json:"values"` // Values []EnumValue `json:"values"` //枚举的
Live LiveValue `json:"live"` //延时加载配置 Live LiveValue `json:"live"` //延时加载配置
UploadUrl string `json:"upload_url,omitempty"` //上传地址 UploadUrl string `json:"upload_url,omitempty"` //上传地址
Icon string `json:"icon,omitempty"` //显示图标 Icon string `json:"icon,omitempty"` //显示图标
@ -24,6 +24,7 @@ type (
Suffix string `json:"suffix,omitempty"` //追加内容 Suffix string `json:"suffix,omitempty"` //追加内容
Tooltip string `json:"tooltip,omitempty"` //字段提示信息 Tooltip string `json:"tooltip,omitempty"` //字段提示信息
Description string `json:"description,omitempty"` //字段说明信息 Description string `json:"description,omitempty"` //字段说明信息
MultipleForSearch bool `json:"multiple_for_search"` //下拉选择是否允许在搜索的时候多选
DropdownOption *DropdownOption `json:"dropdown,omitempty"` //下拉选项 DropdownOption *DropdownOption `json:"dropdown,omitempty"` //下拉选项
} }
) )

View File

@ -2,6 +2,7 @@ package types
import ( import (
"encoding/json" "encoding/json"
"slices"
) )
type ( type (
@ -23,7 +24,7 @@ type (
VisibleCondition struct { VisibleCondition struct {
Column string `json:"column"` Column string `json:"column"`
Values []interface{} `json:"values"` Values []any `json:"values"`
} }
DropdownOption struct { DropdownOption struct {
@ -58,12 +59,7 @@ type (
) )
func (n Scenarios) Has(str string) bool { func (n Scenarios) Has(str string) bool {
for _, v := range n { return slices.Contains(n, str)
if v == str {
return true
}
}
return false
} }
// Scan implements the Scanner interface. // Scan implements the Scanner interface.

View File

@ -2,9 +2,10 @@ package types
import ( import (
"context" "context"
"gorm.io/gorm"
"net/http" "net/http"
"time" "time"
"gorm.io/gorm"
) )
const ( const (
@ -107,17 +108,36 @@ type (
Handle(method string, uri string, handler http.HandlerFunc) Handle(method string, uri string, handler http.HandlerFunc)
} }
// 权限检测器
PermissionChecker interface {
CheckPermission(ctx context.Context, permission string) error
}
// 模型场景
ScenarioModel interface {
Scenario() []string
}
// TypeValue 键值对数据 // TypeValue 键值对数据
TypeValue struct { TypeValue[T any] struct {
Label any `json:"label"` Label string `json:"label"`
Value any `json:"value"` Value T `json:"value"`
}
//TierValue 层级数据
TierValue[T comparable] struct {
Label string `json:"label"`
Value T `json:"value"`
Parent T `json:"-"`
Used bool `json:"-"`
Children []*TierValue[T] `json:"children"`
} }
// NestedValue 层级数据 // NestedValue 层级数据
NestedValue struct { NestedValue[T any] struct {
Label any `json:"label"` Label string `json:"label"`
Value any `json:"value"` Value T `json:"value"`
Children []*NestedValue `json:"children,omitempty"` Children []*NestedValue[T] `json:"children,omitempty"`
} }
//ValueLookupFunc 查找域的函数 //ValueLookupFunc 查找域的函数
@ -229,3 +249,17 @@ type (
Status string `json:"status"` Status string `json:"status"`
} }
) )
func (t *TierValue[T]) HasChild(value T) bool {
for _, child := range t.Children {
if child.Value == value {
return true
}
if len(child.Children) > 0 {
if child.HasChild(value) {
return true
}
}
}
return false
}

View File

@ -1,9 +1,11 @@
package rest package rest
import ( import (
"git.nobla.cn/golang/kos/util/arrays"
"reflect" "reflect"
"slices"
"strings" "strings"
"git.nobla.cn/golang/rest/types"
) )
func hasToken(hack string, need string) bool { func hasToken(hack string, need string) bool {
@ -13,7 +15,7 @@ func hasToken(hack string, need string) bool {
char := []byte{',', ';'} char := []byte{',', ';'}
for _, c := range char { for _, c := range char {
if strings.IndexByte(need, c) > -1 { if strings.IndexByte(need, c) > -1 {
return arrays.Exists(hack, strings.Split(need, string(c))) return slices.Contains(strings.Split(need, string(c)), hack)
} }
} }
return false return false
@ -43,3 +45,18 @@ func isEmpty(val any) bool {
return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface()) return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface())
} }
} }
func recursiveTier[T comparable](parent T, values []*types.TierValue[T]) []*types.TierValue[T] {
items := make([]*types.TierValue[T], 0, len(values)/2)
for idx, row := range values {
if row.Used {
continue
}
if row.Parent == parent {
values[idx].Used = true
row.Children = recursiveTier(row.Value, values)
items = append(items, row)
}
}
return items
}