Compare commits
20 Commits
Author | SHA1 | Date |
---|---|---|
|
3b130c9d14 | |
|
8409716217 | |
|
368a9e868c | |
|
206932038e | |
|
5c09f1eefc | |
|
0c1c0da166 | |
|
01a671dff8 | |
|
2631943904 | |
|
e85cc0b8ca | |
|
3020468afb | |
|
bb048abbb1 | |
|
b32fd7d53f | |
|
43b48e1469 | |
|
5264ee005e | |
|
8de2f0e3a5 | |
|
7097610fad | |
|
44e6e2b34f | |
|
72b0de9c26 | |
|
68bdabddee | |
|
72d742a45d |
30
README.md
30
README.md
|
@ -1 +1,31 @@
|
||||||
# 数据库组件
|
# 数据库组件
|
||||||
|
|
||||||
|
组件提供了操作`mysql`一些相关的内容,通过组件可以方便的实现怎删改查的接口
|
||||||
|
|
||||||
|
|
||||||
|
## 插件
|
||||||
|
|
||||||
|
### 主键插件
|
||||||
|
|
||||||
|
主键插件是指的是用于生成数据库主键的插件,非自增长的ID主键插件,使用方式
|
||||||
|
|
||||||
|
```go
|
||||||
|
db.Use(&identified.Identify{})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据校验插件
|
||||||
|
|
||||||
|
数据校验插件用户增改的时候对数据格式进行校验,使用方式
|
||||||
|
|
||||||
|
```go
|
||||||
|
db.Use(validate.New())
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 分表插件
|
||||||
|
|
||||||
|
分表插件提供了自动分表的功能,使用方式
|
||||||
|
|
||||||
|
```go
|
||||||
|
db.Use(sharding.New())
|
||||||
|
```
|
18
condition.go
18
condition.go
|
@ -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
1
go.mod
|
@ -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
2
go.sum
|
@ -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
143
model.go
|
@ -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,
|
||||||
|
|
73
options.go
73
options.go
|
@ -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
102
query.go
|
@ -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,
|
||||||
|
|
|
@ -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
182
rest.go
|
@ -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
|
||||||
|
|
20
types.go
20
types.go
|
@ -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)
|
||||||
|
|
|
@ -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"` //下拉选项
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
21
utils.go
21
utils.go
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue