Add yao.component.GetOptions and deprecated SelectOptions

This commit is contained in:
Max 2024-11-02 22:02:43 +08:00
parent a4bb4407d7
commit bb717a938a
2 changed files with 471 additions and 74 deletions

View file

@ -2,120 +2,292 @@ package component
import (
"fmt"
"regexp"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/model"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/kun/any"
"github.com/yaoapp/gou/query"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/kun/utils"
)
var varRe = regexp.MustCompile(`\[\[\s*\$([A-Za-z0-9_\-]+)\s*\]\]`)
// QueryProp query prop
type QueryProp struct {
Engine string `json:"engine"`
From string `json:"from"`
LabelField string `json:"labelField,omitempty"`
ValueField string `json:"valueField,omitempty"`
IconField string `json:"iconField,omitempty"`
LabelFormat string `json:"labelFormat,omitempty"`
ValueFormat string `json:"valueFormat,omitempty"`
IconFormat string `json:"iconFormat,omitempty"`
Wheres []map[string]interface{} `json:"wheres,omitempty"`
param model.QueryParam
dsl map[string]interface{}
props map[string]interface{}
}
// Option select option
type Option struct {
Label string `json:"label"`
Value interface{} `json:"value"`
Icon string `json:"icon,omitempty"`
}
// Export process
func exportProcess() {
process.Register("yao.component.getoptions", processGetOptions)
process.Register("yao.component.selectoptions", processSelectOptions)
process.Register("yao.component.selectoptions", processSelectOptions) // Deprecated
}
// processGetOptions get options
func processGetOptions(process *process.Process) interface{} {
utils.Dump(process.Args)
return []map[string]interface{}{
{"label": "Option 1", "value": "1"},
{"label": "Option 2", "value": "2"},
process.ValidateArgNums(2)
params := process.ArgsMap(0, map[string]interface{}{})
props := process.ArgsMap(1, map[string]interface{}{})
// Paser props
p, err := parseOptionsProps(params, props)
if err != nil {
exception.New(err.Error(), 400).Throw()
}
// Query the data
options := []Option{}
if p.Engine != "" {
engine, err := query.Select(p.Engine)
if err != nil {
exception.New(err.Error(), 400).Throw()
}
qb, err := engine.Load(p.dsl)
if err != nil {
exception.New(err.Error(), 400).Throw()
}
// Query the data
data := qb.Get(nil)
for _, row := range data {
p.format(&options, row)
}
return options
}
// Query param
m := model.Select(p.From)
data, err := m.Get(p.param)
if err != nil {
exception.New(err.Error(), 500).Throw()
}
// Format the data
for _, row := range data {
p.format(&options, row)
}
return options
}
func processSelectOptions(process *process.Process) interface{} {
process.ValidateArgNums(1)
query := process.ArgsMap(0, map[string]interface{}{})
if !query.Has("model") {
exception.New("query.model required", 400).Throw()
// parseOptionsProps parse options props
func parseOptionsProps(query, props map[string]interface{}) (*QueryProp, error) {
if props["query"] == nil {
exception.New("props.query is required", 400).Throw()
}
modelName, ok := query.Get("model").(string)
if !ok {
exception.New("query.model must be a string", 400).Throw()
// Read props
if v, ok := props["query"].(map[string]interface{}); ok {
props = v
}
m := model.Select(modelName)
valueField := query.Get("value")
if valueField == nil {
valueField = "id"
// Read query condition
var keywords string = ""
var selected interface{} = nil
if v, ok := query["keywords"].(string); ok {
keywords = v
}
value, ok := valueField.(string)
if !ok {
exception.New("query.value must be a string", 400).Throw()
if v, ok := query["selected"]; ok {
selected = v
}
labelField := query.Get("label")
if labelField == nil {
labelField = "name"
}
label, ok := labelField.(string)
if !ok {
exception.New("query.label must be a string", 400).Throw()
raw, err := jsoniter.Marshal(props)
if err != nil {
return nil, err
}
limit := 500
if query.Get("limit") != nil {
v := any.Of(query.Get("limit"))
if v.IsInt() || v.IsString() {
limit = v.CInt()
qprops := QueryProp{}
err = jsoniter.Unmarshal(raw, &qprops)
if err != nil {
return nil, err
}
qprops.props = props
err = qprops.parse(keywords, selected)
if err != nil {
return nil, err
}
return &qprops, nil
}
// format format the option
func (q *QueryProp) format(options *[]Option, row map[string]interface{}) {
label := row[q.LabelField]
value := row[q.ValueField]
option := Option{Label: fmt.Sprintf("%v", label), Value: value}
if q.IconField != "" {
option.Icon = fmt.Sprintf("%v", row[q.IconField])
}
if q.LabelFormat != "" {
option.Label = q.replaceString(q.LabelFormat, row)
}
if q.ValueFormat != "" {
option.Value = q.replaceString(q.ValueFormat, row)
}
if q.IconField != "" && q.IconFormat != "" {
option.Icon = q.replaceString(q.IconFormat, row)
}
// Update the option
*options = append(*options, option)
}
func (q *QueryProp) parse(keywords string, selected interface{}) error {
if q.Wheres == nil {
q.Wheres = []map[string]interface{}{}
}
// Validate the query param required fields
if q.Engine == "" {
if q.From == "" {
return fmt.Errorf("props.from is required")
}
if q.LabelField == "" {
return fmt.Errorf("props.labelField is required")
}
if q.ValueField == "" {
return fmt.Errorf("props.valueField is required")
}
}
wheres := []model.QueryWhere{}
switch input := query.Get("wheres").(type) {
case string:
where := model.QueryWhere{}
err := jsoniter.Unmarshal([]byte(input), &where)
if err != nil {
exception.New("query.wheres error %s", 400, err.Error()).Throw()
}
wheres = append(wheres, where)
break
case []string:
for _, data := range input {
where := model.QueryWhere{}
err := jsoniter.Unmarshal([]byte(data), &where)
if err != nil {
exception.New("query.wheres error %s", 400, err.Error()).Throw()
}
// Parse wheres
wheres := []map[string]interface{}{}
for _, where := range q.Wheres {
if q.replaceWhere(where, map[string]interface{}{"KEYWORDS": keywords, "SELECTED": selected}) {
wheres = append(wheres, where)
}
break
}
if data, ok := query.Get("wheres").(string); ok {
data = strings.TrimSpace(data)
if strings.HasPrefix(data, "{") && strings.HasSuffix(data, "}") {
data = fmt.Sprintf("[%s]", data)
// Update the props
props := map[string]interface{}{}
for key, value := range q.props {
props[key] = value
}
props["wheres"] = wheres
// Return the query dsl, if the engine is set
if q.Engine != "" {
if q.LabelField == "" {
q.LabelField = "label"
}
err := jsoniter.Unmarshal([]byte(data), &wheres)
if err != nil {
exception.New("query.wheres error %s", 400, err.Error()).Throw()
if q.ValueField == "" {
q.ValueField = "value"
}
if q.IconField == "" {
q.IconField = "icon"
}
q.dsl = props
return nil
}
rows, err := m.Get(model.QueryParam{
Select: []interface{}{valueField, labelField},
Wheres: wheres,
Limit: limit,
})
// Parse the query param from the props
q.param = model.QueryParam{
Model: q.From,
Select: []interface{}{q.LabelField, q.ValueField},
}
if q.IconField != "" {
q.param.Select = append(q.param.Select, q.IconField)
}
raw, err := jsoniter.Marshal(props)
if err != nil {
exception.New("%s", 500, err.Error()).Throw()
return err
}
res := []map[string]interface{}{}
for _, row := range rows {
res = append(res, map[string]interface{}{
"label": row.Get(label),
"value": row.Get(value),
})
err = jsoniter.Unmarshal(raw, &q.param)
if err != nil {
return err
}
return res
return nil
}
func (q *QueryProp) replaceString(format string, data map[string]interface{}) string {
if data == nil {
return format
}
matches := varRe.FindAllStringSubmatch(format, -1)
if len(matches) > 0 {
for _, match := range matches {
name := match[1]
orignal := match[0]
if val, ok := data[name]; ok {
format = strings.ReplaceAll(format, orignal, fmt.Sprintf("%v", val))
}
}
}
return format
}
// Replace replace the query where condition
// return true if the where condition is effective, otherwise return false
func (q *QueryProp) replaceWhere(where map[string]interface{}, data map[string]interface{}) bool {
if where == nil {
return false
}
for key, value := range where {
if v, ok := value.(string); ok {
matches := varRe.FindAllStringSubmatch(v, -1)
if len(matches) > 0 {
name := matches[0][1]
if val, ok := data[name]; ok {
// Check if the value is empty
if val == nil || val == "" {
return false
}
// Replace the value
where[key] = val
return true
}
return false
}
}
}
return true
}
// Deprecated: please use processGetOptions instead
// This function may cause security issue, please use processGetOptions instead
// It will be removed when the v0.10.4 released
func processSelectOptions(process *process.Process) interface{} {
message := "process yao.component.SelectOptions is deprecated, please use yao.component.GetOptions instead"
exception.New(message, 400).Throw()
return nil
}

View file

@ -0,0 +1,225 @@
package component
import (
"testing"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
)
func TestProcessGetOptions(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
props := prepare(t)
name := "yao.component.GetOptions"
for _, queryParam := range props {
args := []interface{}{
map[string]interface{}{},
map[string]interface{}{"query": queryParam},
}
p, err := process.Of(name, args...)
if err != nil {
t.Fatal(err)
}
err = p.Execute()
if err != nil {
t.Fatal(err)
}
defer p.Release()
res, ok := p.Value().([]Option)
if !ok {
t.Fatal("Result is not []Option")
}
if len(res) != 8 {
t.Fatal("Result length is not 8")
}
assert.Equal(t, "Category cat 1-active-1", res[0].Label)
assert.Equal(t, "1", res[0].Value)
assert.Equal(t, "active-1", res[0].Icon)
// With KEYWORDS
args = []interface{}{
map[string]interface{}{"keywords": "dog"},
map[string]interface{}{"query": queryParam},
}
p, err = process.Of(name, args...)
if err != nil {
t.Fatal(err)
}
err = p.Execute()
if err != nil {
t.Fatal(err)
}
res, ok = p.Value().([]Option)
if !ok {
t.Fatal("Result is not []Option")
}
if len(res) != 2 {
t.Fatal("Result length is not 2")
}
assert.Equal(t, "Category dog 7-active-7", res[0].Label)
assert.Equal(t, "7", res[0].Value)
assert.Equal(t, "active-7", res[0].Icon)
// With SELECTED
args = []interface{}{
map[string]interface{}{"selected": []interface{}{1}},
map[string]interface{}{"query": queryParam},
}
p, err = process.Of(name, args...)
if err != nil {
t.Fatal(err)
}
err = p.Execute()
if err != nil {
t.Fatal(err)
}
res, ok = p.Value().([]Option)
if !ok {
t.Fatal("Result is not []Option")
}
if len(res) != 1 {
t.Fatal("Result length is not 1")
}
assert.Equal(t, "Category cat 1-active-1", res[0].Label)
assert.Equal(t, "1", res[0].Value)
assert.Equal(t, "active-1", res[0].Icon)
// With KEYWORDS and SELECTED
args = []interface{}{
map[string]interface{}{"keywords": "dog", "selected": []interface{}{1, 2}},
map[string]interface{}{"query": queryParam},
}
p, err = process.Of(name, args...)
if err != nil {
t.Fatal(err)
}
err = p.Execute()
if err != nil {
t.Fatal(err)
}
res, ok = p.Value().([]Option)
if !ok {
t.Fatal("Result is not []Option")
}
if len(res) != 4 {
t.Fatal("Result length is not 4")
}
assert.Equal(t, "Category cat 1-active-1", res[0].Label)
assert.Equal(t, "1", res[0].Value)
assert.Equal(t, "active-1", res[0].Icon)
assert.Equal(t, "Category dog 7-active-7", res[2].Label)
assert.Equal(t, "7", res[2].Value)
assert.Equal(t, "active-7", res[2].Icon)
}
}
func TestProcessSelectOptions(t *testing.T) {
name := "yao.component.SelectOptions"
p, err := process.Of(name, nil)
if err != nil {
t.Fatal(err)
}
err = p.Execute()
assert.Contains(t, err.Error(), "process yao.component.SelectOptions is deprecated, please use yao.component.GetOptions instead")
}
func prepare(t *testing.T) map[string]map[string]interface{} {
exportProcess()
// Prepare data for testing
err := process.New("models.category.Migrate", true).Execute()
if err != nil {
t.Fatal(err)
}
err = process.New("models.category.Insert",
[]string{"name", "status"},
[][]interface{}{
{"Category cat 1", "active-1"},
{"Category cat 2", "active-2"},
{"Category cat 3", "active-3"},
{"Category cat 4", "active-4"},
{"Category cat 5", "active-5"},
{"Category cat 6", "active-6"},
{"Category dog 7", "active-7"},
{"Category dog 8", "active-8"},
}).Execute()
if err != nil {
t.Fatal(err)
}
queryParam := map[string]interface{}{}
queryDSL := map[string]interface{}{}
err = jsoniter.Unmarshal([]byte(`{
"labelField": "name",
"valueField": "id",
"iconField": "status",
"from": "category",
"wheres": [
{ "column": "name", "value": "[[ $KEYWORDS ]]", "op": "match" },
{
"method": "orwhere",
"column": "id",
"op": "in",
"value": "[[ $SELECTED ]]"
}
],
"limit": 20,
"labelFormat": "[[ $name ]]-[[ $status ]]",
"valueFormat": "[[ $id ]]",
"iconFormat": "[[ $status ]]"
}`), &queryParam)
if err != nil {
t.Fatal(err)
}
err = jsoniter.Unmarshal([]byte(`{
"engine": "query-test",
"select": ["name as label", "id as value", "status as icon"],
"from": "category",
"wheres": [
{ "field": "name", "match": "[[ $KEYWORDS ]]" },
{ "or": true, "field":"id", "in":"[[ $SELECTED ]]" }
],
"limit": 20,
"labelFormat": "[[ $label ]]-[[ $icon ]]",
"valueFormat": "[[ $value ]]",
"iconFormat": "[[ $icon ]]"
}`), &queryDSL)
if err != nil {
t.Fatal(err)
}
return map[string]map[string]interface{}{
"queryParam": queryParam,
"queryDSL": queryDSL,
}
}