Compare commits

..

No commits in common. "master" and "v0.0.6" have entirely different histories.

7 changed files with 70 additions and 354 deletions

View File

@ -84,14 +84,9 @@ func BuildConditions(ctx context.Context, r *http.Request, query *Query, schemas
} }
//如果是多选的话直接使用IN操作 //如果是多选的话直接使用IN操作
columnName := row.Column + "[]" columnName := row.Column + "[]"
if qs.Has(columnName) { if qs.Has(columnName) && len(qs[columnName]) > 1 {
if len(qs[columnName]) > 1 { query.AndFilterWhere(newConditionWithOperator("IN", row.Column, qs[columnName]))
query.AndFilterWhere(newConditionWithOperator("IN", row.Column, qs[columnName])) continue
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 {

154
model.go
View File

@ -26,22 +26,19 @@ import (
) )
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 //禁用域 schemaLookup types.SchemaLookupFunc //获取schema的函数
permissionChecker types.PermissionChecker //权限检查 valueLookup types.ValueLookupFunc //查看域
schemaLookup types.SchemaLookupFunc //获取schema的函数 statement *gorm.Statement //字段声明
valueLookup types.ValueLookupFunc //查看域 formatter *Formatter //格式化
statement *gorm.Statement //字段声明 response types.HttpWriter //HTTP响应
formatter *Formatter //格式化 hookMgr *hookManager //钩子管理器
response types.HttpWriter //HTTP响应 dirname string //存放文件目录
hookMgr *hookManager //钩子管理器
dirname string //存放文件目录
scenarios []string //场景
} }
var ( var (
@ -68,18 +65,7 @@ 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 设置字段的值
@ -172,7 +158,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 := range modelType.NumField() { for i := 0; i < modelType.NumField(); i++ {
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) {
@ -233,28 +219,7 @@ func (m *Model) ModuleName() string {
// TableName 表的名称 // TableName 表的名称
func (m *Model) TableName() string { func (m *Model) TableName() string {
return m.naming.TableName return m.naming.ModuleName
}
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 返回搜索的模型的字段
@ -262,36 +227,9 @@ 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, 0, 4) ss := make([]string, 4)
if m.urlPrefix != "" { if m.urlPrefix != "" {
ss = append(ss, m.urlPrefix) ss = append(ss, m.urlPrefix)
} }
@ -336,7 +274,6 @@ 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
@ -355,14 +292,6 @@ 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"))
@ -463,14 +392,6 @@ 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 {
@ -552,14 +473,6 @@ 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()
@ -651,14 +564,6 @@ 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()
@ -714,14 +619,6 @@ 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())
@ -760,12 +657,8 @@ 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.ScenarioExport) { if !m.hasScenario(types.ScenarioList) {
m.response.Failure(w, types.RequestDenied, scenarioNotAllow, nil) m.response.Failure(w, types.RequestDenied, "request denied", 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)
@ -1006,14 +899,6 @@ 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)
@ -1089,7 +974,6 @@ 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

@ -6,35 +6,17 @@ import (
) )
type options struct { type options struct {
urlPrefix string urlPrefix string
moduleName string moduleName string
disableDomain bool disableDomain bool
db *gorm.DB db *gorm.DB
router types.HttpRouter router types.HttpRouter
writer types.HttpWriter writer types.HttpWriter
permissionChecker types.PermissionChecker formatter *Formatter
valueLookup types.ValueLookupFunc
formatter *Formatter
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 // WithDB 设置DB
func WithDB(db *gorm.DB) Option { func WithDB(db *gorm.DB) Option {
return func(o *options) { return func(o *options) {
@ -49,12 +31,6 @@ func WithUriPrefix(s string) Option {
} }
} }
func WithValueLookup(f types.ValueLookupFunc) Option {
return func(o *options) {
o.valueLookup = f
}
}
// WithModuleName 模块名称 // WithModuleName 模块名称
func WithModuleName(s string) Option { func WithModuleName(s string) Option {
return func(o *options) { return func(o *options) {
@ -89,17 +65,3 @@ func WithFormatter(s *Formatter) Option {
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
}
}

131
rest.go
View File

@ -492,13 +492,21 @@ func Init(cbs ...Option) (err error) {
} }
// AutoMigrate 自动合并表的schema // AutoMigrate 自动合并表的schema
func AutoMigrate(ctx context.Context, model any, cbs ...Option) (modelValue *Model, err error) { func AutoMigrate(ctx context.Context, model any, cbs ...Option) (err error) {
var ( var (
opts *options opts *options
table string table string
router types.HttpRouter router types.HttpRouter
) )
opts = globalOpts.Clone() opts = &options{
db: globalOpts.db,
router: globalOpts.router,
writer: globalOpts.writer,
formatter: globalOpts.formatter,
moduleName: globalOpts.moduleName,
urlPrefix: globalOpts.urlPrefix,
disableDomain: globalOpts.disableDomain,
}
for _, cb := range cbs { for _, cb := range cbs {
cb(opts) cb(opts)
} }
@ -506,22 +514,14 @@ func AutoMigrate(ctx context.Context, model any, cbs ...Option) (modelValue *Mod
return return
} }
//路由模块处理 //路由模块处理
singularizeTable := inflector.Singularize(table) modelValue := newModel(model, opts.db, types.Naming{
modelValue = newModel(model, opts.db, types.Naming{ Pluralize: inflector.Pluralize(table),
Pluralize: inflector.Pluralize(singularizeTable), Singular: inflector.Singularize(table),
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
} }
@ -652,128 +652,63 @@ func VisibleSchemas(ctx context.Context, db *gorm.DB, domain, moduleName, tableN
} }
// ModelTypes 查询指定模型的类型 // ModelTypes 查询指定模型的类型
func ModelTypes[T any](ctx context.Context, db *gorm.DB, model any, domainName, labelColumn, valueColumn string) (values []*types.TypeValue[T], err error) { func ModelTypes(ctx context.Context, db *gorm.DB, model any, domainName, labelColumn, valueColumn string) (values []*types.TypeValue) {
tx := db.WithContext(ctx) tx := db.WithContext(ctx)
result := make([]map[string]any, 0, 10) result := make([]map[string]any, 0, 10)
if domainName == "" { if domainName == "" {
err = tx.Model(model).Select(labelColumn, valueColumn).Scan(&result).Error tx.Model(model).Select(labelColumn, valueColumn).Scan(&result)
} else { } else {
err = tx.Model(model).Select(labelColumn, valueColumn).Where("domain=?", domainName).Scan(&result).Error tx.Model(model).Select(labelColumn, valueColumn).Where("domain=?", domainName).Scan(&result)
} }
if err != nil { values = make([]*types.TypeValue, 0, len(result))
return
}
values = make([]*types.TypeValue[T], 0, len(result))
for _, pairs := range result { for _, pairs := range result {
feed := &types.TypeValue[T]{} feed := &types.TypeValue{}
for k, v := range pairs { for k, v := range pairs {
if k == labelColumn { if k == labelColumn {
if s, ok := v.(string); ok { feed.Label = v
feed.Label = s
} else {
feed.Label = fmt.Sprint(s)
}
continue
} }
if k == valueColumn { if k == valueColumn {
if p, ok := v.(T); ok { feed.Value = v
feed.Value = p
}
continue
} }
} }
values = append(values, feed) values = append(values, feed)
} }
return values, nil return values
}
// 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) any { func GetFieldValue(stmt *gorm.Statement, refValue reflect.Value, column string) interface{} {
var ( var (
rawField *schema.Field idx = -1
) )
refVal := reflect.Indirect(refValue) refVal := reflect.Indirect(refValue)
for _, field := range stmt.Schema.Fields { for i, field := range stmt.Schema.Fields {
if field.DBName == column || field.Name == column { if field.DBName == column || field.Name == column {
rawField = field idx = i
break break
} }
} }
if rawField == nil { if idx > -1 {
return nil return refVal.Field(idx).Interface()
} }
var targetValue reflect.Value return nil
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 (
rawField *schema.Field idx = -1
) )
refVal := reflect.Indirect(refValue) refVal := reflect.Indirect(refValue)
for _, field := range stmt.Schema.Fields { for i, field := range stmt.Schema.Fields {
if field.DBName == column || field.Name == column { if field.DBName == column || field.Name == column {
rawField = field idx = i
break break
} }
} }
if rawField == nil { if idx > -1 {
return refVal.Field(idx).Set(reflect.ValueOf(value))
} }
var targetValue reflect.Value
targetValue = refVal
for _, i := range rawField.StructField.Index {
targetValue = targetValue.Field(i)
}
targetValue.Set(reflect.ValueOf(value))
} }
// SafeSetFileValue 安全设置模型某个字段的值 // SafeSetFileValue 安全设置模型某个字段的值

View File

@ -21,10 +21,6 @@ var (
allowMethods = []string{http.MethodPut, http.MethodPost} allowMethods = []string{http.MethodPut, http.MethodPost}
) )
var (
scenarioNotAllow = "request not allowed"
)
type ( type (
httpWriter struct { httpWriter struct {
} }
@ -44,11 +40,6 @@ 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

@ -2,10 +2,9 @@ package types
import ( import (
"context" "context"
"gorm.io/gorm"
"net/http" "net/http"
"time" "time"
"gorm.io/gorm"
) )
const ( const (
@ -108,36 +107,17 @@ 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[T any] struct { TypeValue struct {
Label string `json:"label"` Label any `json:"label"`
Value T `json:"value"` Value any `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[T any] struct { NestedValue struct {
Label string `json:"label"` Label any `json:"label"`
Value T `json:"value"` Value any `json:"value"`
Children []*NestedValue[T] `json:"children,omitempty"` Children []*NestedValue `json:"children,omitempty"`
} }
//ValueLookupFunc 查找域的函数 //ValueLookupFunc 查找域的函数
@ -249,17 +229,3 @@ 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

@ -4,8 +4,6 @@ import (
"reflect" "reflect"
"slices" "slices"
"strings" "strings"
"git.nobla.cn/golang/rest/types"
) )
func hasToken(hack string, need string) bool { func hasToken(hack string, need string) bool {
@ -45,18 +43,3 @@ 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
}