From 51fae21f3d1e77eb88557f93e7e83e93dd4c43a4 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Thu, 18 Jul 2024 09:54:19 +0200 Subject: [PATCH 01/14] fix(cli,api) complex filter with typed attrs --- API/models/model.go | 28 +++++++++------- API/utils/util.go | 55 +++++++++++++++++++++++++++++--- CLI/completer.go | 18 ----------- CLI/controllers/shell.go | 4 +-- CLI/other/man/get.txt | 1 + CLI/other/man/getslot.txt | 12 ------- CLI/other/man/getu.txt | 9 ------ CLI/other/man/hc.txt | 11 ------- CLI/parser/ast.go | 46 --------------------------- CLI/parser/ast_test.go | 67 --------------------------------------- CLI/parser/astutil.go | 2 +- CLI/parser/parser.go | 25 +++------------ CLI/parser/parser_test.go | 1 - 13 files changed, 75 insertions(+), 204 deletions(-) delete mode 100644 CLI/other/man/getslot.txt delete mode 100644 CLI/other/man/getu.txt delete mode 100644 CLI/other/man/hc.txt diff --git a/API/models/model.go b/API/models/model.go index 2fdd1435d..442496d7d 100644 --- a/API/models/model.go +++ b/API/models/model.go @@ -345,29 +345,35 @@ func complexExpressionToMap(expressions []string) (map[string]any, error) { } // Base case: single filter expression - re := regexp.MustCompile(`^([\w-.]+)\s*(<=|>=|<|>|!=|=)\s*([\w-.*]+)$`) + return singleExpressionToMap(expressions) +} +func singleExpressionToMap(expressions []string) (map[string]any, error) { + re := regexp.MustCompile(`^([\w-.]+)\s*(<=|>=|<|>|!=|=)\s*((\[)*[\w-,.*]+(\])*)$`) ops := map[string]string{"<=": "$lte", ">=": "$gte", "<": "$lt", ">": "$gt", "!=": "$not"} if len(expressions) <= 3 { expression := strings.Join(expressions[:], "") - if match := re.FindStringSubmatch(expression); match != nil { - switch match[1] { + // convert filter value to proper type + filterName := match[1] // e.g. category + filterOp := match[2] // e.g. != + filterValue := u.ConvertString(match[3]) // e.g. device + switch filterName { case "startDate": - return map[string]any{"lastUpdated": map[string]any{"$gte": match[3]}}, nil + return map[string]any{"lastUpdated": map[string]any{"$gte": filterValue}}, nil case "endDate": - return map[string]any{"lastUpdated": map[string]any{"$lte": match[3]}}, nil + return map[string]any{"lastUpdated": map[string]any{"$lte": filterValue}}, nil case "id", "name", "category", "description", "domain", "createdDate", "lastUpdated", "slug": - if match[2] == "=" { - return map[string]any{match[1]: match[3]}, nil + if filterOp == "=" { + return map[string]any{filterName: filterValue}, nil } - return map[string]any{match[1]: map[string]any{ops[match[2]]: match[3]}}, nil + return map[string]any{filterName: map[string]any{ops[filterOp]: filterValue}}, nil default: - if match[2] == "=" { - return map[string]any{"attributes." + match[1]: match[3]}, nil + if filterOp == "=" { + return map[string]any{"attributes." + filterName: filterValue}, nil } - return map[string]any{"attributes." + match[1]: map[string]any{ops[match[2]]: match[3]}}, nil + return map[string]any{"attributes." + filterName: map[string]any{ops[filterOp]: filterValue}}, nil } } } diff --git a/API/utils/util.go b/API/utils/util.go index 11c93308f..89915501e 100644 --- a/API/utils/util.go +++ b/API/utils/util.go @@ -14,6 +14,7 @@ import ( "net/url" "os" "reflect" + "strconv" "strings" "time" @@ -194,16 +195,14 @@ func FilteredReqFromQueryParams(link *url.URL) bson.M { for key := range queryValues { if key != "fieldOnly" && key != "startDate" && key != "endDate" && key != "limit" && key != "namespace" { - keyValue := queryValues.Get(key) + keyValue := ConvertString(queryValues.Get(key)) AddFilterToReq(bsonMap, key, keyValue) } } return bsonMap } -func AddFilterToReq(bsonMap primitive.M, key string, value string) { - var keyValue interface{} - keyValue = value +func AddFilterToReq(bsonMap primitive.M, key string, keyValue any) { if key == "parentId" { regex := applyWildcards(keyValue.(string)) + `\.(` + NAME_REGEX + ")" bsonMap["id"] = regexToMongoFilter(regex) @@ -212,7 +211,8 @@ func AddFilterToReq(bsonMap primitive.M, key string, value string) { // tag is in tags list bsonMap["tags"] = bson.M{"$eq": keyValue} return - } else if strings.Contains(keyValue.(string), "*") { + } else if reflect.TypeOf(keyValue).Kind() == reflect.String && + strings.Contains(keyValue.(string), "*") { regex := applyWildcards(keyValue.(string)) keyValue = regexToMongoFilter(regex) } @@ -520,3 +520,48 @@ func GetFloat(unk interface{}) (float64, error) { fv := v.Convert(floatType) return fv.Float(), nil } + +func ConvertString(strValue string) any { + if num, err := strconv.ParseFloat(strValue, 64); err == nil { + // is number + return num + } else if nums, err := StringToFloatSlice(strValue); err == nil { + // is array of numbers + return nums + } else if strs, err := StringToStrSlice(strValue); err == nil { + // is array of strings + return strs + } else if boolean, err := strconv.ParseBool(strValue); err == nil { + // is boolean + return boolean + } + // is string + return strValue +} + +func StringToFloatSlice(strValue string) ([]float64, error) { + numbers := []float64{} + if len(strValue) < 2 || strValue[0] != '[' || strValue[len(strValue)-1:] != "]" { + return numbers, fmt.Errorf("not a vector") + } + strSplit := strings.Split(strValue[1:len(strValue)-1], ",") + for _, val := range strSplit { + if n, err := strconv.ParseFloat(val, 64); err == nil { + numbers = append(numbers, n) + } else { + return numbers, fmt.Errorf("invalid vector format") + } + } + return numbers, nil +} + +func StringToStrSlice(strValue string) ([]string, error) { + if len(strValue) < 2 || strValue[0] != '[' || strValue[len(strValue)-1:] != "]" { + return []string{}, fmt.Errorf("not a vector") + } + strs := strings.Split(strings.ReplaceAll(strValue[1:len(strValue)-1], "\"", ""), ",") + if len(strs[0]) <= 0 { + return strs, fmt.Errorf("invalid vector format") + } + return strs, nil +} diff --git a/CLI/completer.go b/CLI/completer.go index 9bf182f0b..011ae1c5d 100644 --- a/CLI/completer.go +++ b/CLI/completer.go @@ -278,9 +278,6 @@ func GetPrefixCompleter() *readline.PrefixCompleter { readline.PcItem("lspanel", false), readline.PcItem("lsenterprise", false), readline.PcItem("get", false), - readline.PcItem("getu", false), - readline.PcItem("getslot", false), - readline.PcItem("hc", false), readline.PcItem("drawable", false), readline.PcItem("draw", false), readline.PcItem("camera", false), @@ -315,11 +312,7 @@ func GetPrefixCompleter() *readline.PrefixCompleter { readline.PcItem("get", true, readline.PcItemDynamic(ListEntities, false)), - readline.PcItem("getu", true, - readline.PcItemDynamic(ListEntities, false)), - readline.PcItem("getslot", true, - readline.PcItemDynamic(ListEntities, false)), readline.PcItem("selection", false), readline.PcItem(".cmds:", true, readline.PcItemDynamic(ListLocal, false)), @@ -407,17 +400,6 @@ func GetPrefixCompleter() *readline.PrefixCompleter { readline.PcItem(">", true, readline.PcItemDynamic(ListEntities, false)), - readline.PcItem("hc", true, - readline.PcItemDynamic(ListEntities, false)), - /*readline.PcItem("gt", false, - readline.PcItem("site", false), - readline.PcItem("building", false), - readline.PcItem("room", false), - readline.PcItem("rack", false), - readline.PcItem("device", false), - readline.PcItem("subdevice", false), - readline.PcItem("subdevice1", false), - ),*/ readline.PcItem("link", true, readline.PcItemDynamic(UnLinkObjCompleter, false)), diff --git a/CLI/controllers/shell.go b/CLI/controllers/shell.go index a122df459..4ec40f06a 100644 --- a/CLI/controllers/shell.go +++ b/CLI/controllers/shell.go @@ -74,8 +74,8 @@ func Help(entry string) { switch entry { case "ls", "pwd", "print", "printf", "cd", "tree", "get", "clear", "lsog", "grep", "for", "while", "if", "env", - "cmds", "var", "unset", "selection", commands.Connect3D, commands.Disconnect3D, "camera", "ui", "hc", "drawable", - "link", "unlink", "draw", "getu", "getslot", "undraw", + "cmds", "var", "unset", "selection", commands.Connect3D, commands.Disconnect3D, "camera", "ui", "drawable", + "link", "unlink", "draw", "undraw", "lsenterprise", commands.Cp: path = "./other/man/" + entry + ".txt" diff --git a/CLI/other/man/get.txt b/CLI/other/man/get.txt index baab7cecd..320c88b0f 100644 --- a/CLI/other/man/get.txt +++ b/CLI/other/man/get.txt @@ -49,3 +49,4 @@ EXAMPLES get -r #racks get -r -m 1 -M 2 #racks get * -f category=rack & height>10 + get /P/RackID:separators diff --git a/CLI/other/man/getslot.txt b/CLI/other/man/getslot.txt deleted file mode 100644 index efcb927ca..000000000 --- a/CLI/other/man/getslot.txt +++ /dev/null @@ -1,12 +0,0 @@ -USAGE: getslot [PATH_TO_RACK] [SLOT] -Retrieves object located in a rack at slot [SLOT] location. If the path given does not refer to a rack then the command returns an error. If the value provided for the [SLOT] parameter does not exist in the rack then nothing will be returned. - -NOTE -If path is not specified then the current path will be used. - - -EXAMPLE - - getslot L01 - getslot RACKA/ X01 - getslot X05 diff --git a/CLI/other/man/getu.txt b/CLI/other/man/getu.txt deleted file mode 100644 index 2ab6b2ee2..000000000 --- a/CLI/other/man/getu.txt +++ /dev/null @@ -1,9 +0,0 @@ -USAGE: getu [PATH_TO_RACK] [NUM] -Retrieves object located in a rack at 'U' [NUM] location. If the path given does not refer to a rack then the command returns an error. A positive value must be provided for the [NUM] parameter. If there is nothing present at the given height, nothing will be returned. - - -EXAMPLE - - getu . 0 - getu RACKA/ 40 - getu . 19 diff --git a/CLI/other/man/hc.txt b/CLI/other/man/hc.txt deleted file mode 100644 index 75a552e2f..000000000 --- a/CLI/other/man/hc.txt +++ /dev/null @@ -1,11 +0,0 @@ -USAGE: hc [PATH] (optional) [NUM] (optional) -Displays object's hierarchy in JSON format. - -The PATH parameter specifies path to object. If PATH is not provided then the current path will be used. - - -The NUM parameter limits the depth of the hierarchy call. If the depth is not provided then 1 will be used by default which will only retrieve the current object and thus act as a get call. - -EXAMPLE - - hc DEMO/ALPHA/B 2 \ No newline at end of file diff --git a/CLI/parser/ast.go b/CLI/parser/ast.go index b1944262e..2b524e8e0 100644 --- a/CLI/parser/ast.go +++ b/CLI/parser/ast.go @@ -237,52 +237,6 @@ func (n *lsNode) execute() (interface{}, error) { } -type getUNode struct { - path node - u node -} - -func (n *getUNode) execute() (interface{}, error) { - path, err := nodeToString(n.path, "path") - if err != nil { - return nil, err - } - u, err := nodeToInt(n.u, "u") - if err != nil { - return nil, err - } - if u < 0 { - return nil, fmt.Errorf("the U value must be positive") - } - - if cmd.State.DryRun { - return nil, nil - } - - return nil, cmd.C.GetByAttr(path, u) -} - -type getSlotNode struct { - path node - slot node -} - -func (n *getSlotNode) execute() (interface{}, error) { - path, err := nodeToString(n.path, "path") - if err != nil { - return nil, err - } - slot, err := n.slot.execute() - if err != nil { - return nil, err - } - - if cmd.State.DryRun { - return nil, nil - } - return nil, cmd.C.GetByAttr(path, slot) -} - type loadNode struct { path node } diff --git a/CLI/parser/ast_test.go b/CLI/parser/ast_test.go index 7a106bb7d..eef9e7ee8 100644 --- a/CLI/parser/ast_test.go +++ b/CLI/parser/ast_test.go @@ -160,73 +160,6 @@ func TestLsNodeExecute(t *testing.T) { assert.Nil(t, value) } -func TestGetUNodeExecute(t *testing.T) { - _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - - rack := test_utils.GetEntity("rack", "rack", "site.building.room", "domain") - test_utils.MockGetObjectHierarchy(mockAPI, rack) - - uNode := getUNode{ - path: &pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}, - u: &valueNode{-42}, - } - value, err := uNode.execute() - - assert.Nil(t, value) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "the U value must be positive") - - uNode = getUNode{ - path: &pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}, - u: &valueNode{42}, - } - value, err = uNode.execute() - - assert.Nil(t, value) - assert.Nil(t, err) -} - -func TestGetSlotNodeExecute(t *testing.T) { - _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - - rack := test_utils.GetEntity("rack", "rack", "site.building.room", "domain") - rack["children"] = []any{map[string]any{ - "category": "device", - "attributes": map[string]any{ - "type": "chassis", - "slot": "slot", - }, - "children": []any{}, - "id": "BASIC.A.R1.A01.chT", - "name": "chT", - "parentId": "BASIC.A.R1.A01", - }} - rack["attributes"] = map[string]any{ - "slot": []any{ - map[string]any{ - "location": "slot", - "type": "u", - "elemOrient": []any{33.3, -44.4, 107}, - "elemPos": []any{58, 51, 44.45}, - "elemSize": []any{482.6, 1138, 44.45}, - "mandatory": "no", - "labelPos": "frontrear", - "color": "@color1", - }, - }, - } - test_utils.MockGetObjectHierarchy(mockAPI, rack) - - slotNode := getSlotNode{ - path: &pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}, - slot: &valueNode{"slot"}, - } - value, err := slotNode.execute() - - assert.Nil(t, value) - assert.Nil(t, err) -} - func TestPrintNodeExecute(t *testing.T) { executable := printNode{&formatStringNode{&valueNode{"%v"}, []node{&valueNode{5}}}} value, err := executable.execute() diff --git a/CLI/parser/astutil.go b/CLI/parser/astutil.go index 229881350..32091205b 100644 --- a/CLI/parser/astutil.go +++ b/CLI/parser/astutil.go @@ -121,7 +121,7 @@ func filtersToMapString(filters map[string]node) (map[string]string, error) { if err != nil { return nil, err } - filtersString[key] = filterVal.(string) + filtersString[key] = utils.Stringify(filterVal) } return filtersString, nil diff --git a/CLI/parser/parser.go b/CLI/parser/parser.go index 998c3732e..9bc99e32c 100644 --- a/CLI/parser/parser.go +++ b/CLI/parser/parser.go @@ -19,7 +19,7 @@ var lsCommands = []string{ "lspanel", "lscabinet", "lscorridor"} var manCommands = []string{ - "get", "getu", "getslot", + "get", "+", "-", "=", ">", ".cmds", ".template", ".var", commands.Connect3D, commands.Disconnect3D, "ui", "camera", @@ -27,7 +27,7 @@ var manCommands = []string{ "lssite", commands.LsBuilding, "lsroom", "lsrack", "lsdev", "lsac", "lspanel", "lscabinet", "lscorridor", "lsenterprise", "drawable", "draw", "undraw", - "tree", "lsog", "env", "cd", "pwd", "clear", "ls", "exit", "len", "man", "hc", + "tree", "lsog", "env", "cd", "pwd", "clear", "ls", "exit", "len", "man", "print", "printf", "unset", "selection", "for", "while", "if", commands.Cp, @@ -712,7 +712,7 @@ func (p *parser) parseFilters() map[string]node { first = false p.skipWhiteSpaces() attrName := p.parseAssign("attribute name") - attrVal := p.parseValue() + attrVal := p.parseText(p.parseQuotedStringToken, false, false) filters[attrName] = attrVal } @@ -725,12 +725,7 @@ func (p *parser) parseComplexFilters() map[string]node { for !p.commandEnd() { p.skipWhiteSpaces() - newComplexFilter := p.parseValue() - if p.parseExact(",") { - newFilterStr, _ := newComplexFilter.execute() - newComplexFilter = &valueNode{"(" + newFilterStr.(string) + ") & ("} - numArgs++ - } + newComplexFilter := p.parseText(p.parseQuotedStringToken, false, false) for p.parseExact(")") { newFilterStr, _ := newComplexFilter.execute() @@ -792,16 +787,6 @@ func (p *parser) parseGet() node { } } -func (p *parser) parseGetU() node { - defer un(trace(p, "getu")) - return &getUNode{p.parsePath(""), p.parseExpr("u")} -} - -func (p *parser) parseGetSlot() node { - defer un(trace(p, "getslot")) - return &getSlotNode{p.parsePath(""), p.parseString("slot name")} -} - func (p *parser) parseUndraw() node { defer un(trace(p, "undraw")) if p.commandEnd() { @@ -1431,8 +1416,6 @@ func newParser(buffer string) *parser { } p.commandDispatch = map[string]parseCommandFunc{ "get": p.parseGet, - "getu": p.parseGetU, - "getslot": p.parseGetSlot, "undraw": p.parseUndraw, "draw": p.parseDraw, "drawable": p.parseDrawable, diff --git a/CLI/parser/parser_test.go b/CLI/parser/parser_test.go index d2a7d2818..26d23fa00 100644 --- a/CLI/parser/parser_test.go +++ b/CLI/parser/parser_test.go @@ -342,7 +342,6 @@ var commandsMatching = map[string]node{ ">${toto}/tata": &focusNode{testPath}, "+site:${toto}/tata": &createSiteNode{testPath}, "+si:${toto}/tata": &createSiteNode{testPath}, - "getu rackA 42": &getUNode{&pathNode{path: &valueNode{"rackA"}}, &valueNode{42}}, "get ${toto}/tata": &getObjectNode{path: testPath, filters: map[string]node{}}, "get -r ${toto}/tata": &getObjectNode{path: testPath, filters: map[string]node{}, recursive: recursiveArgs{isRecursive: true}}, "get -r ${toto}/tata category=room": &getObjectNode{path: testPath, filters: map[string]node{"category": &valueNode{"room"}}, recursive: recursiveArgs{isRecursive: true}}, From 8250d10d5d2c0a4154b9185fa13bdb89b508e899 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:51:09 +0200 Subject: [PATCH 02/14] fix(cli) tests and doc --- CLI/other/man/ls.txt | 9 ++++---- CLI/other/man/lsobj.txt | 18 ++++++++++----- CLI/parser/lexer.go | 9 ++++++++ CLI/parser/parser.go | 10 ++------- CLI/parser/parser_test.go | 13 +++++------ ...[User-Guide]-CLI-\342\200\220-Language.md" | 22 +++++++------------ 6 files changed, 41 insertions(+), 40 deletions(-) diff --git a/CLI/other/man/ls.txt b/CLI/other/man/ls.txt index 0e97a6893..a53a89e84 100644 --- a/CLI/other/man/ls.txt +++ b/CLI/other/man/ls.txt @@ -9,12 +9,11 @@ DESCRIPTION If no PATH is given, then the current path will be used (equivalent to "ls ."). - [FILTERS] is an optional list of filters that can be used to only list the children that meet certain conditions. + [FILTERS] is an optional set of filters that can be used to only list the children that meet certain conditions. In order to use FILTERS, PATH must be present. - The format to use is attribute1=expected_value,attribute2=expected_value,.... - COMPLEX FILTERS can be used, composing complex boolean expressions with the operators `=`, `!=`, `<`, `<=`, `>`, `>=`, `&` and `|`. - In order to use COMPLEX FILTERS, PATH must be present. The option `-f` before the complex filter is also required. + The option `-f` before the complex filter is required. + If `-f` is not set, a simple filter (single condition) can be applied with the format attribute=expected_value OPTIONS -s attribute_name @@ -45,7 +44,6 @@ OPTIONS EXAMPLES ls - ls DEMO_RACK/DeviceA ls /Physical/SiteA ls $x ls -s height @@ -54,4 +52,5 @@ EXAMPLES ls . height=12 ls -r #racks ls -r -m 1 -M 2 #racks + ls . category=rack ls . -f category=rack & height>10 \ No newline at end of file diff --git a/CLI/other/man/lsobj.txt b/CLI/other/man/lsobj.txt index 71dc72426..2ecb324be 100644 --- a/CLI/other/man/lsobj.txt +++ b/CLI/other/man/lsobj.txt @@ -2,7 +2,7 @@ NAME lsOBJ - list children of a type SYNOPSIS - lsOBJ [OPTION]... [PATH] [FILTERS] + lsOBJ [OPTION]... [PATH] [COMPLEX FILTER OPTION] [FILTERS] DESCRIPTION Lists the children of type OBJ of the specified path (equivalent to ls with filter: category=[type]). @@ -19,16 +19,18 @@ DESCRIPTION If no PATH is given, then the current path will be used (equivalent to "ls ."). - [FILTERS] is an optional list of filters that can be used to only list the children that meet certain conditions. + [FILTERS] is an optional set of filters that can be used to only list the children that meet certain conditions. In order to use FILTERS, PATH must be present. - The format to use is attribute1=expected_value,attribute2=expected_value,.... + COMPLEX FILTERS can be used, composing complex boolean expressions with the operators `=`, `!=`, `<`, `<=`, `>`, `>=`, `&` and `|`. + The option `-f` before the complex filter is required. + If `-f` is not set, a simple filter (single condition) can be applied with the format attribute=expected_value OPTIONS -s attribute_name Specifies the attribute of the objects to be used to sort the results. By default, the results are listed in alphabetical order according to their name. - -f attributes + -a attributes The results are listed with the attributes present in format. The format of attributes is a list separated by : (attribute1:attribute2:...) @@ -46,14 +48,18 @@ OPTIONS Specifies the maximum number of children between the PATH and the listed results. Default is no limit. + -f + Specifies that you want to define a complex filter expression with the operators `=`, `!=`, `<`, `<=`, `>`, `>=`, `&` and `|`. + Regular filters can be used normally. + EXAMPLES lssite lsbuilding DEMO_RACK/DeviceA lsroom /Physical/SiteA lsrack $x lsdev -s height - lsac -f height:size - lscabinet -s height -f size + lsac -a height:size + lscabinet -s height -a size lscorridor . height=12 lspanel -r . lssite -r -m 1 -M 2 . \ No newline at end of file diff --git a/CLI/parser/lexer.go b/CLI/parser/lexer.go index 9e152929f..0db978c6c 100644 --- a/CLI/parser/lexer.go +++ b/CLI/parser/lexer.go @@ -334,6 +334,15 @@ func (p *parser) parseUnquotedStringToken() token { return p.lexUnquotedString() } +func (p *parser) lexUnquotedCommaString() token { + return p.lexText("@;})") +} + +func (p *parser) parseUnquotedCommaStringToken() token { + defer un(trace(p, "")) + return p.lexUnquotedCommaString() +} + func (p *parser) lexQuotedString() token { return p.lexText("\"") } diff --git a/CLI/parser/parser.go b/CLI/parser/parser.go index 9bc99e32c..1a995434c 100644 --- a/CLI/parser/parser.go +++ b/CLI/parser/parser.go @@ -703,16 +703,10 @@ func (p *parser) parseLs(category string) node { func (p *parser) parseFilters() map[string]node { filters := map[string]node{} - first := true for !p.commandEnd() { - p.skipWhiteSpaces() - if !first { - p.expect(",") - } - first = false p.skipWhiteSpaces() attrName := p.parseAssign("attribute name") - attrVal := p.parseText(p.parseQuotedStringToken, false, false) + attrVal := p.parseText(p.parseUnquotedCommaStringToken, true, false) filters[attrName] = attrVal } @@ -725,7 +719,7 @@ func (p *parser) parseComplexFilters() map[string]node { for !p.commandEnd() { p.skipWhiteSpaces() - newComplexFilter := p.parseText(p.parseQuotedStringToken, false, false) + newComplexFilter := p.parseText(p.parseUnquotedCommaStringToken, true, false) for p.parseExact(")") { newFilterStr, _ := newComplexFilter.execute() diff --git a/CLI/parser/parser_test.go b/CLI/parser/parser_test.go index 26d23fa00..d938b4603 100644 --- a/CLI/parser/parser_test.go +++ b/CLI/parser/parser_test.go @@ -248,18 +248,17 @@ func testCommand(buffer string, expected node, t *testing.T) bool { } func TestParseLs(t *testing.T) { - buffer := "lsbuilding -s height -a attr1:attr2 plouf.plaf attr1=a, attr2=b" + buffer := "lsbuilding -s height -a attr1:attr2 plouf.plaf attr1=a" path := &pathNode{path: &valueNode{"plouf.plaf"}} sort := "height" attrList := []string{"attr1", "attr2"} filters := map[string]node{ "category": &valueNode{"building"}, "attr1": &valueNode{"a"}, - "attr2": &valueNode{"b"}, } expected := &lsNode{path: path, filters: filters, sortAttr: sort, attrList: attrList} testCommand(buffer, expected, t) - buffer = "lsbuilding -s height -a \"attr1:attr2\" plouf.plaf attr1=a, attr2=b" + buffer = "lsbuilding -s height -a \"attr1:attr2\" plouf.plaf attr1=a" testCommand(buffer, expected, t) } @@ -284,9 +283,9 @@ func TestParseLsRecursive(t *testing.T) { func TestParseLsComplexFilter(t *testing.T) { path := &pathNode{path: &valueNode{"plouf.plaf"}} - buffer := "ls plouf.plaf -f category=building, attr1=a, attr2=b" + buffer := "ls plouf.plaf -f category=building & attr1=a & attr2=b" filters := map[string]node{ - "filter": &valueNode{"(category=building) & ((attr1=a) & (attr2=b))"}, + "filter": &valueNode{"category=building & attr1=a & attr2=b"}, } expected := &lsNode{path: path, filters: filters} testCommand(buffer, expected, t) @@ -345,7 +344,7 @@ var commandsMatching = map[string]node{ "get ${toto}/tata": &getObjectNode{path: testPath, filters: map[string]node{}}, "get -r ${toto}/tata": &getObjectNode{path: testPath, filters: map[string]node{}, recursive: recursiveArgs{isRecursive: true}}, "get -r ${toto}/tata category=room": &getObjectNode{path: testPath, filters: map[string]node{"category": &valueNode{"room"}}, recursive: recursiveArgs{isRecursive: true}}, - "get ${toto}/tata -f category=room, name=R1": &getObjectNode{path: testPath, filters: map[string]node{"filter": &valueNode{"(category=room) & (name=R1)"}}}, + "get ${toto}/tata -f category=room & name=R1": &getObjectNode{path: testPath, filters: map[string]node{"filter": &valueNode{"category=room & name=R1"}}}, "get ${toto}/tata -f category=room & (name!=R1 | height>5)": &getObjectNode{path: testPath, filters: map[string]node{"filter": &valueNode{"category=room & (name!=R1 | height>5)"}}}, "+building:${toto}/tata@[1., 2.]@3.@[.1, 2., 3.]": &createBuildingNode{testPath, vec2(1., 2.), &valueNode{3.}, vec3(.1, 2., 3.)}, "+room:${toto}/tata@[1., 2.]@3.@[.1, 2., 3.]@+x-y": &createRoomNode{testPath, vec2(1., 2.), &valueNode{3.}, vec3(.1, 2., 3.), &valueNode{"+x-y"}, nil, nil}, @@ -441,7 +440,7 @@ func TestFor(t *testing.T) { func TestIf(t *testing.T) { defer recoverFunc(t) for simpleCommand, tree := range commandsMatching { - buf := "true { " + simpleCommand + " }e" + buf := "true { " + simpleCommand + "}e" p := newParser(buf) expected := &ifNode{&valueNode{true}, tree, nil} result := p.parseIf() diff --git "a/wiki/\360\237\223\227-[User-Guide]-CLI-\342\200\220-Language.md" "b/wiki/\360\237\223\227-[User-Guide]-CLI-\342\200\220-Language.md" index d07609ae3..06b6aad1e 100644 --- "a/wiki/\360\237\223\227-[User-Guide]-CLI-\342\200\220-Language.md" +++ "b/wiki/\360\237\223\227-[User-Guide]-CLI-\342\200\220-Language.md" @@ -408,18 +408,15 @@ Filters can be added to the `get` command to get only the objects that meet a ce #### Simple filters -Simple filters are the ones that use the egality operator (`=`). These filters can be used to get only objects that meet certain characteristics. Objects can be filtered by `name`, `slug`, `id`, `category`, `description`, `domain`, `tag` and by any other attributes, such as `size`, `height` and `rotation`. +Simple filters are single condition set using the egality operator (`=`). Objects can be filtered by `name`, `slug`, `id`, `category`, `description`, `domain`, `tag` and by any other attributes, such as `size`, `height` and `rotation`. -It is also possible to specify a `startDate` and an `endDate`, to filter objects last modified _since_ and _up to_, respectively, the `startDate` and the `endDate`. Dates should be defined with the format `yyyy-mm-dd`. - -Simple filters can be combined with commas (`,`), performing a logical `AND` operation. +It is also possible to specify a `startDate` or an `endDate`, to filter objects last modified _since_ and _up to_ (respectively, the `startDate` and the `endDate`). Dates should be defined with the format `yyyy-mm-dd`. ``` get [path] tag=[tag_slug] get [path] category=[category] -get [path] domain=[domain], height=[height] -get [path] startDate=[yyyy-mm-dd], endDate=[yyyy-mm-dd] -get [path] name=[name], category=[category], startDate=[yyyy-mm-dd] +get [path] height=[height] +get [path] startDate=[yyyy-mm-dd] ``` #### Complex filters @@ -504,18 +501,15 @@ Filters can be added to the `ls` command to get only the children that meet a ce #### Simple filters -Simple filters are the ones that use the egality operator (`=`). These filters can be used to get only children that meet certain characteristics. Objects can be filtered by `name`, `slug`, `id`, `category`, `description`, `domain`, `tag` and by any other attributes, such as `size`, `height` and `rotation`. - -It is also possible to specify a `startDate` and an `endDate`, to filter objects last modified _since_ and _up to_, respectively, the `startDate` and the `endDate`. Dates should be defined with the format `yyyy-mm-dd`. +Simple filters are single condition set using the egality operator (`=`). Objects can be filtered by `name`, `slug`, `id`, `category`, `description`, `domain`, `tag` and by any other attributes, such as `size`, `height` and `rotation`. -Simple filters can be combined with commas (`,`), performing a logical `AND` operation. +It is also possible to specify a `startDate` or an `endDate`, to filter objects last modified _since_ and _up to_ (respectively, the `startDate` and the `endDate`). Dates should be defined with the format `yyyy-mm-dd`. ``` ls [path] tag=[tag_slug] ls [path] category=[category] -ls [path] domain=[domain], height=[height] -ls [path] startDate=[yyyy-mm-dd], endDate=[yyyy-mm-dd] -ls [path] name=[name], category=[category], startDate=[yyyy-mm-dd] +ls [path] height=[height] +ls [path] startDate=[yyyy-mm-dd] ``` #### Complex filters From 0d806aeaed4e007a68a36c5d7c50a9cbfcec9dbb Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Thu, 18 Jul 2024 14:03:38 +0200 Subject: [PATCH 03/14] fix(api) mongo not operator needs regex --- API/utils/wildcard.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/API/utils/wildcard.go b/API/utils/wildcard.go index b052150b5..0fdd5980b 100644 --- a/API/utils/wildcard.go +++ b/API/utils/wildcard.go @@ -21,10 +21,9 @@ func ApplyWildcardsOnComplexFilter(filter map[string]interface{}) { for key, val := range filter { switch v := val.(type) { case string: - if key == "$not" || !strings.HasPrefix(key, "$") { // only for '=' and '!=' operators - if strings.Contains(v, "*") { - filter[key] = regexToMongoFilter(applyWildcards(v)) - } + if key == "$not" || (!strings.HasPrefix(key, "$") && strings.Contains(v, "*")) { + // only for '=' with * and always for '!=' + filter[key] = regexToMongoFilter(applyWildcards(v)) } case []interface{}: for _, item := range v { From 5c4431069fbddd7a196d496c7323f0d70c558c13 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:50:16 +0200 Subject: [PATCH 04/14] fix(api) add trim and tes --- API/utils/util.go | 5 ++++- API/utils/util_test.go | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/API/utils/util.go b/API/utils/util.go index 89915501e..61d6b009a 100644 --- a/API/utils/util.go +++ b/API/utils/util.go @@ -546,7 +546,7 @@ func StringToFloatSlice(strValue string) ([]float64, error) { } strSplit := strings.Split(strValue[1:len(strValue)-1], ",") for _, val := range strSplit { - if n, err := strconv.ParseFloat(val, 64); err == nil { + if n, err := strconv.ParseFloat(strings.TrimSpace(val), 64); err == nil { numbers = append(numbers, n) } else { return numbers, fmt.Errorf("invalid vector format") @@ -563,5 +563,8 @@ func StringToStrSlice(strValue string) ([]string, error) { if len(strs[0]) <= 0 { return strs, fmt.Errorf("invalid vector format") } + for i := range strs { + strs[i] = strings.TrimSpace(strs[i]) + } return strs, nil } diff --git a/API/utils/util_test.go b/API/utils/util_test.go index 58ce7ad65..5cdf729fa 100644 --- a/API/utils/util_test.go +++ b/API/utils/util_test.go @@ -145,3 +145,23 @@ func TestFormatNotifyData(t *testing.T) { t.Error("Test Case 4 failed") } } + +func TestConvertString(t *testing.T) { + tests := []struct { + name string + toconvert string + expected any + }{ + {"StringToFloat", "1.2", 1.2}, + {"StringToFloatSlice", "[1.2, 1.3]", []float64{1.2, 1.3}}, + {"StringToStringSlice", "[hi, there]", []string{"hi", "there"}}, + {"StringToBoolean", "true", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + converted := ConvertString(tt.toconvert) + assert.Equal(t, tt.expected, converted) + }) + } +} From 3507c9492e1f8e69bf3cc6ceaded5f8c89acd84b Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:21:03 +0200 Subject: [PATCH 05/14] refactor(api) adjust pattern --- API/models/{transaction.go => db.go} | 53 + API/models/delete.go | 16 - .../{attributes.go => entity_attributes.go} | 0 API/models/entity_create.go | 65 + ...e_object_test.go => entity_create_test.go} | 0 API/models/entity_delete.go | 97 ++ API/models/entity_get.go | 175 +++ API/models/entity_get_hierarchy.go | 436 +++++++ ...{sanitise_entity.go => entity_sanitise.go} | 0 API/models/entity_update.go | 252 ++++ ...e_object_test.go => entity_update_test.go} | 0 ...{validate_entity.go => entity_validate.go} | 0 ...entity_test.go => entity_validate_test.go} | 0 API/models/filter.go | 128 ++ API/models/model.go | 1146 ----------------- 15 files changed, 1206 insertions(+), 1162 deletions(-) rename API/models/{transaction.go => db.go} (53%) delete mode 100644 API/models/delete.go rename API/models/{attributes.go => entity_attributes.go} (100%) create mode 100644 API/models/entity_create.go rename API/models/{create_object_test.go => entity_create_test.go} (100%) create mode 100644 API/models/entity_delete.go create mode 100644 API/models/entity_get.go create mode 100644 API/models/entity_get_hierarchy.go rename API/models/{sanitise_entity.go => entity_sanitise.go} (100%) create mode 100644 API/models/entity_update.go rename API/models/{update_object_test.go => entity_update_test.go} (100%) rename API/models/{validate_entity.go => entity_validate.go} (100%) rename API/models/{validate_entity_test.go => entity_validate_test.go} (100%) create mode 100644 API/models/filter.go delete mode 100644 API/models/model.go diff --git a/API/models/transaction.go b/API/models/db.go similarity index 53% rename from API/models/transaction.go rename to API/models/db.go index 1ce6b44b4..7f38c653e 100644 --- a/API/models/transaction.go +++ b/API/models/db.go @@ -1,12 +1,65 @@ package models import ( + "context" + "fmt" "p3/repository" u "p3/utils" + "strings" "go.mongodb.org/mongo-driver/mongo" ) +func CommandRunner(cmd interface{}) *mongo.SingleResult { + ctx, cancel := u.Connect() + result := repository.GetDB().RunCommand(ctx, cmd, nil) + defer cancel() + return result +} + +func GetDBName() string { + name := repository.GetDB().Name() + + //Remove the preceding 'ogree' at beginning of name + if strings.Index(name, "ogree") == 0 { + name = name[5:] //5=len('ogree') + } + return name +} + +func ExtractCursor(c *mongo.Cursor, ctx context.Context, entity int, userRoles map[string]Role) ([]map[string]interface{}, error) { + ans := []map[string]interface{}{} + for c.Next(ctx) { + x := map[string]interface{}{} + err := c.Decode(x) + if err != nil { + fmt.Println(err.Error()) + return nil, err + } + //Remove _id + x = fixID(x) + if u.IsEntityHierarchical(entity) && userRoles != nil { + //Check permissions + var domain string + if entity == u.DOMAIN { + domain = x["id"].(string) + } else { + domain = x["domain"].(string) + } + if permission := CheckUserPermissions(userRoles, entity, domain); permission >= READONLYNAME { + if permission == READONLYNAME { + x = FixReadOnlyName(x) + } + ans = append(ans, x) + } + } else { + ans = append(ans, x) + } + + } + return ans, nil +} + func WithTransaction[T any](callback func(mongo.SessionContext) (T, error)) (T, *u.Error) { ctx, cancel := u.Connect() defer cancel() diff --git a/API/models/delete.go b/API/models/delete.go deleted file mode 100644 index 16abb0935..000000000 --- a/API/models/delete.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -import ( - u "p3/utils" -) - -func DeleteObject(entityStr string, id string, userRoles map[string]Role) *u.Error { - entity := u.EntityStrToInt(entityStr) - if entity == u.TAG { - return DeleteTag(id) - } else if u.IsEntityNonHierarchical(entity) { - return DeleteNonHierarchicalObject(entityStr, id) - } else { - return DeleteHierarchicalObject(entityStr, id, userRoles) - } -} diff --git a/API/models/attributes.go b/API/models/entity_attributes.go similarity index 100% rename from API/models/attributes.go rename to API/models/entity_attributes.go diff --git a/API/models/entity_create.go b/API/models/entity_create.go new file mode 100644 index 000000000..51679c97b --- /dev/null +++ b/API/models/entity_create.go @@ -0,0 +1,65 @@ +package models + +import ( + "p3/repository" + u "p3/utils" + + "go.mongodb.org/mongo-driver/mongo" +) + +func CreateEntity(entity int, t map[string]interface{}, userRoles map[string]Role) (map[string]interface{}, *u.Error) { + tags, tagsPresent := getTags(t) + if tagsPresent { + err := verifyTagList(tags) + if err != nil { + return nil, err + } + } + + if err := prepareCreateEntity(entity, t, userRoles); err != nil { + return nil, err + } + + return WithTransaction(func(ctx mongo.SessionContext) (map[string]any, error) { + if entity == u.TAG { + err := createTagImage(ctx, t) + if err != nil { + return nil, err + } + } + + entStr := u.EntityToString(entity) + + _, err := repository.CreateObject(ctx, entStr, t) + if err != nil { + return nil, err + } + + fixID(t) + return t, nil + }) +} + +func prepareCreateEntity(entity int, t map[string]interface{}, userRoles map[string]Role) *u.Error { + if err := ValidateEntity(entity, t); err != nil { + return err + } + + // Check user permissions + if u.IsEntityHierarchical(entity) { + var domain string + if entity == u.DOMAIN { + domain = t["id"].(string) + } else { + domain = t["domain"].(string) + } + if permission := CheckUserPermissions(userRoles, entity, domain); permission < WRITE { + return &u.Error{Type: u.ErrUnauthorized, + Message: "User does not have permission to create this object"} + } + } + + delete(t, "parentId") + + return nil +} diff --git a/API/models/create_object_test.go b/API/models/entity_create_test.go similarity index 100% rename from API/models/create_object_test.go rename to API/models/entity_create_test.go diff --git a/API/models/entity_delete.go b/API/models/entity_delete.go new file mode 100644 index 000000000..dca426651 --- /dev/null +++ b/API/models/entity_delete.go @@ -0,0 +1,97 @@ +package models + +import ( + "os" + "p3/repository" + u "p3/utils" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +func DeleteObject(entityStr string, id string, userRoles map[string]Role) *u.Error { + entity := u.EntityStrToInt(entityStr) + if entity == u.TAG { + return DeleteTag(id) + } else if u.IsEntityNonHierarchical(entity) { + return DeleteNonHierarchicalObject(entityStr, id) + } else { + return DeleteHierarchicalObject(entityStr, id, userRoles) + } +} + +// DeleteHierarchicalObject: delete object of given hierarchyName +// search for all its children and delete them too, return: +// - success or fail message map +func DeleteHierarchicalObject(entity string, id string, userRoles map[string]Role) *u.Error { + // Special check for delete domain + if entity == "domain" { + if id == os.Getenv("db") { + return &u.Error{Type: u.ErrForbidden, Message: "Cannot delete tenant's default domain"} + } + if domainHasObjects(id) { + return &u.Error{Type: u.ErrForbidden, Message: "Cannot delete domain if it has at least one object"} + } + } + + // Delete with given id + req, ok := GetRequestFilterByDomain(userRoles) + if !ok { + return &u.Error{Type: u.ErrUnauthorized, Message: "User does not have permission to delete"} + } + + req["id"] = id + + _, err := WithTransaction(func(ctx mongo.SessionContext) (any, error) { + err := repository.DeleteObject(ctx, entity, req) + if err != nil { + // Unable to delete given id + return nil, err + } + + // Delete possible children + rangeEntities := getChildrenCollections(u.GROUP, entity) + for _, childEnt := range rangeEntities { + childEntName := u.EntityToString(childEnt) + pattern := primitive.Regex{Pattern: "^" + id + u.HN_DELIMETER, Options: ""} + + repository.GetDB().Collection(childEntName).DeleteMany(ctx, + bson.M{"id": pattern}) + } + + return nil, nil + }) + + return err +} + +func DeleteNonHierarchicalObject(entity, slug string) *u.Error { + req := bson.M{"slug": slug} + ctx, cancel := u.Connect() + defer cancel() + return repository.DeleteObject(ctx, entity, req) +} + +// Helper functions + +func domainHasObjects(domain string) bool { + data := map[string]interface{}{} + // Get all collections names + ctx, cancel := u.Connect() + db := repository.GetDB() + collNames, _ := db.ListCollectionNames(ctx, bson.D{}) + + // Check if at least one object belongs to domain + for _, collName := range collNames { + pattern := primitive.Regex{Pattern: "^" + domain, Options: ""} + e := db.Collection(collName).FindOne(ctx, bson.M{"domain": pattern}).Decode(&data) + if e == nil { + // Found one! + return true + } + } + + defer cancel() + return false +} diff --git a/API/models/entity_get.go b/API/models/entity_get.go new file mode 100644 index 000000000..1f93ecf6a --- /dev/null +++ b/API/models/entity_get.go @@ -0,0 +1,175 @@ +package models + +import ( + "fmt" + "maps" + "p3/repository" + u "p3/utils" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func GetObject(req bson.M, entityStr string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { + object, err := repository.GetObject(req, entityStr, filters) + + if err != nil { + return nil, err + } + + //Remove _id + object = fixID(object) + + entity := u.EntityStrToInt(entityStr) + + if shouldFillTags(entity, filters) { + object = fillTags(object) + } + + // Check permissions + if u.IsEntityHierarchical(entity) { + var domain string + if entity == u.DOMAIN { + domain = object["id"].(string) + } else { + domain = object["domain"].(string) + } + if userRoles != nil { + if permission := CheckUserPermissions(userRoles, u.EntityStrToInt(entityStr), domain); permission == NONE { + return nil, &u.Error{Type: u.ErrUnauthorized, + Message: "User does not have permission to see this object"} + } else if permission == READONLYNAME { + object = FixReadOnlyName(object) + } + } + } + + return object, nil +} + +func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, complexFilterExp string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { + ctx, cancel := u.Connect() + var err error + var c *mongo.Cursor + + var opts *options.FindOptions + if len(filters.FieldsToShow) > 0 { + compoundIndex := bson.D{bson.E{Key: "domain", Value: 1}, bson.E{Key: "id", Value: 1}} + for _, field := range filters.FieldsToShow { + if field != "domain" && field != "id" { + compoundIndex = append(compoundIndex, bson.E{Key: field, Value: 1}) + } + } + opts = options.Find().SetProjection(compoundIndex) + } + err = repository.GetDateFilters(req, filters.StartDate, filters.EndDate) + if err != nil { + return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } + + if complexFilterExp != "" { + if complexFilters, err := ComplexFilterToMap(complexFilterExp); err != nil { + return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } else { + err = getDatesFromComplexFilters(complexFilters) + if err != nil { + return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } + u.ApplyWildcardsOnComplexFilter(complexFilters) + maps.Copy(req, complexFilters) + } + } + + if opts != nil { + c, err = repository.GetDB().Collection(entityStr).Find(ctx, req, opts) + } else { + c, err = repository.GetDB().Collection(entityStr).Find(ctx, req) + } + if err != nil { + fmt.Println(err) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + defer cancel() + + entity := u.EntityStrToInt(entityStr) + data, e1 := ExtractCursor(c, ctx, entity, userRoles) + if e1 != nil { + fmt.Println(e1) + return nil, &u.Error{Type: u.ErrInternal, Message: e1.Error()} + } + + if shouldFillTags(entity, filters) { + for i := range data { + fillTags(data[i]) + } + } + + return data, nil +} + +func GetEntityCount(entity int) int64 { + ent := u.EntityToString(entity) + ctx, cancel := u.Connect() + ans, e := repository.GetDB().Collection(ent).CountDocuments(ctx, bson.M{}, nil) + if e != nil { + println(e.Error()) + return -1 + } + defer cancel() + return ans +} + +func GetStats() map[string]interface{} { + ans := map[string]interface{}{} + t := map[string]interface{}{} + latestDocArr := []map[string]interface{}{} + var latestTime interface{} + + for entity := range u.Entities { + num := GetEntityCount(entity) + if num == -1 { + num = 0 + } + + ans["Number of "+u.EntityToString(entity)+"s:"] = num + + //Retrieve the latest updated document in each collection + //and store into the latestDocArr array + obj := map[string]interface{}{} + filter := options.FindOne().SetSort(bson.M{"lastUpdated": -1}) + ctx, cancel := u.Connect() + + e := repository.GetDB().Collection(u.EntityToString(entity)).FindOne(ctx, bson.M{}, filter).Decode(&obj) + if e == nil { + latestDocArr = append(latestDocArr, obj) + } + defer cancel() + } + + //Get the latest update out of latestDocArr + value := -1 + for _, obj := range latestDocArr { + if int(obj["lastUpdated"].(primitive.DateTime)) > value { + value = int(obj["lastUpdated"].(primitive.DateTime)) + latestTime = obj["lastUpdated"] + } + } + + if latestTime == nil { + latestTime = "N/A" + } + + cmd := bson.D{{"dbStats", 1}, {"scale", 1024}} + + if e := CommandRunner(cmd).Decode(&t); e != nil { + println(e.Error()) + return nil + } + + ans["Number of Hierarchal Objects"] = t["collections"] + ans["Last Job Timestamp"] = latestTime + + return ans +} diff --git a/API/models/entity_get_hierarchy.go b/API/models/entity_get_hierarchy.go new file mode 100644 index 000000000..22a1da698 --- /dev/null +++ b/API/models/entity_get_hierarchy.go @@ -0,0 +1,436 @@ +package models + +import ( + "fmt" + "p3/repository" + u "p3/utils" + "strconv" + "strings" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func GetHierarchyObjectById(hierarchyName string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { + // Get possible collections for this name + rangeEntities := u.GetEntitiesById(u.PHierarchy, hierarchyName) + req := bson.M{"id": hierarchyName} + + // Search each collection + for _, entityStr := range rangeEntities { + data, _ := GetObject(req, entityStr, filters, userRoles) + if data != nil { + return data, nil + } + } + + return nil, &u.Error{Type: u.ErrNotFound, Message: "Unable to find object"} +} + +// GetCompleteHierarchy: gets all objects in db using hierachyName and returns: +// - tree: map with parents as key and their children as an array value +// tree: {parent:[children]} +// - categories: map with category name as key and corresponding objects +// as an array value +// categories: {categoryName:[children]} +func GetCompleteHierarchy(userRoles map[string]Role, filters u.HierarchyFilters) (map[string]interface{}, *u.Error) { + response := make(map[string]interface{}) + categories := make(map[string][]string) + hierarchy := make(map[string]interface{}) + + switch filters.Namespace { + case u.Any: + for _, ns := range []u.Namespace{u.Physical, u.Logical, u.Organisational} { + data, err := getHierarchyWithNamespace(ns, userRoles, filters, categories) + if err != nil { + return nil, err + } + hierarchy[u.NamespaceToString(ns)] = data + } + default: + data, err := getHierarchyWithNamespace(filters.Namespace, userRoles, filters, categories) + if err != nil { + return nil, err + } + hierarchy[u.NamespaceToString(filters.Namespace)] = data + + } + + response["tree"] = hierarchy + if filters.WithCategories { + categories["KeysOrder"] = []string{"site", "building", "room", "rack"} + response["categories"] = categories + } + return response, nil +} + +func getHierarchyWithNamespace(namespace u.Namespace, userRoles map[string]Role, filters u.HierarchyFilters, + categories map[string][]string) (map[string][]string, *u.Error) { + hierarchy := make(map[string][]string) + rootIdx := "*" + + ctx, cancel := u.Connect() + db := repository.GetDB() + dbFilter := bson.M{} + + // Depth of hierarchy defined by user + if filters.Limit != "" && namespace != u.PStray && + !strings.Contains(u.NamespaceToString(namespace), string(u.Logical)) { + if _, e := strconv.Atoi(filters.Limit); e == nil { + pattern := primitive.Regex{Pattern: "^" + u.NAME_REGEX + "(." + u.NAME_REGEX + "){0," + + filters.Limit + "}$", Options: ""} + dbFilter = bson.M{"id": pattern} + } + } + // User date filters + err := repository.GetDateFilters(dbFilter, filters.StartDate, filters.EndDate) + if err != nil { + return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } + + // Search collections according to namespace + entities := u.GetEntitiesById(namespace, "") + + for _, entityName := range entities { + // Get data + opts := options.Find().SetProjection(bson.D{{Key: "domain", Value: 1}, {Key: "id", Value: 1}, {Key: "category", Value: 1}}) + + if u.IsEntityNonHierarchical(u.EntityStrToInt(entityName)) { + opts = options.Find().SetProjection(bson.D{{Key: "slug", Value: 1}}) + } + + c, err := db.Collection(entityName).Find(ctx, dbFilter, opts) + if err != nil { + println(err.Error()) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + + data, e := ExtractCursor(c, ctx, u.EntityStrToInt(entityName), userRoles) + if e != nil { + return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} + } + + // Format data + for _, obj := range data { + if strings.Contains(u.NamespaceToString(namespace), string(u.Logical)) { + // Logical + var objId string + if u.IsEntityNonHierarchical(u.EntityStrToInt(entityName)) { + objId = obj["slug"].(string) + hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) + } else { + objId = obj["id"].(string) + categories[entityName] = append(categories[entityName], objId) + if strings.Contains(objId, ".") && obj["category"] != "group" { + // Physical or Org Children + fillHierarchyMap(objId, hierarchy) + } else { + hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) + } + } + } else if strings.Contains(obj["id"].(string), ".") { + // Physical or Org Children + categories[entityName] = append(categories[entityName], obj["id"].(string)) + fillHierarchyMap(obj["id"].(string), hierarchy) + } else { + // Physical or Org Roots + objId := obj["id"].(string) + categories[entityName] = append(categories[entityName], objId) + if u.EntityStrToInt(entityName) == u.STRAYOBJ { + hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) + } else if u.EntityStrToInt(entityName) != u.VIRTUALOBJ { + hierarchy[rootIdx] = append(hierarchy[rootIdx], objId) + } + } + } + } + + // For the root of VIRTUAL objects we also need to check for devices + for _, vobj := range hierarchy[rootIdx+u.EntityToString(u.VIRTUALOBJ)] { + if !strings.ContainsAny(vobj, u.HN_DELIMETER) { // is stray virtual + // Get device linked to this vobj through its virtual config + entityName := u.EntityToString(u.DEVICE) + dbFilter := bson.M{"attributes.virtual_config.clusterId": vobj} + opts := options.Find().SetProjection(bson.D{{Key: "domain", Value: 1}, {Key: "id", Value: 1}, {Key: "category", Value: 1}}) + c, err := db.Collection(entityName).Find(ctx, dbFilter, opts) + if err != nil { + println(err.Error()) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + data, e := ExtractCursor(c, ctx, u.EntityStrToInt(entityName), userRoles) + if e != nil { + return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} + } + // Add data + for _, obj := range data { + objId := obj["id"].(string) + categories[entityName] = append(categories[entityName], objId) + hierarchy[vobj] = append(hierarchy[vobj], objId) + } + } + } + + defer cancel() + return hierarchy, nil +} + +func GetCompleteHierarchyAttributes(userRoles map[string]Role) (map[string]interface{}, *u.Error) { + response := make(map[string]interface{}) + // Get all collections names + ctx, cancel := u.Connect() + db := repository.GetDB() + collNames, err := db.ListCollectionNames(ctx, bson.D{}) + if err != nil { + fmt.Println(err.Error()) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + + } + + for _, collName := range collNames { + if entInt := u.EntityStrToInt(collName); entInt > -1 { + projection := bson.D{{Key: "attributes", Value: 1}, + {Key: "domain", Value: 1}, {Key: "id", Value: 1}} + + opts := options.Find().SetProjection(projection) + + c, err := db.Collection(collName).Find(ctx, bson.M{}, opts) + if err != nil { + println(err.Error()) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + data, e := ExtractCursor(c, ctx, entInt, userRoles) + if e != nil { + return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} + } + + for _, obj := range data { + if obj["attributes"] != nil { + if id, isStr := obj["id"].(string); isStr && id != "" { + response[obj["id"].(string)] = obj["attributes"] + } else if obj["name"] != nil { + response[obj["name"].(string)] = obj["attributes"] + } + } + } + } + } + defer cancel() + return response, nil +} + +// GetHierarchyByName: get children objects of given parent. +// - Param limit: max relationship distance between parent and child, example: +// limit=1 only direct children, limit=2 includes nested children of children +func GetHierarchyByName(entity, hierarchyName string, limit int, filters u.RequestFilters) ([]map[string]interface{}, *u.Error) { + // Get all children and their relations + allChildren, hierarchy, err := getChildren(entity, hierarchyName, limit, filters) + if err != nil { + return nil, err + } + + // Organize the family according to relations (nest children) + return recursivelyGetChildrenFromMaps(hierarchyName, hierarchy, allChildren), nil +} + +func getChildren(entity, hierarchyName string, limit int, filters u.RequestFilters) (map[string]interface{}, + map[string][]string, *u.Error) { + allChildren := map[string]interface{}{} + hierarchy := make(map[string][]string) + + // Define in which collections we can find children + rangeEntities := getChildrenCollections(limit, entity) + + // Get children from all given collections + for _, checkEnt := range rangeEntities { + checkEntName := u.EntityToString(checkEnt) + // Obj should include parentName and not surpass limit range + pattern := primitive.Regex{Pattern: "^" + hierarchyName + + "(." + u.NAME_REGEX + "){1," + strconv.Itoa(limit) + "}$", Options: ""} + children, e1 := GetManyObjects(checkEntName, bson.M{"id": pattern}, filters, "", nil) + if e1 != nil { + println("SUBENT: ", checkEntName) + println("ERR: ", e1.Message) + return nil, nil, e1 + } + for _, child := range children { + // store child data + allChildren[child["id"].(string)] = child + // create hierarchy map + fillHierarchyMap(child["id"].(string), hierarchy) + } + } + + return allChildren, hierarchy, nil +} + +// GetHierarchyByCluster: get children devices and vobjs of given cluster +func GetHierarchyByCluster(clusterName string, limit int, filters u.RequestFilters) ([]map[string]interface{}, *u.Error) { + allChildren := map[string]interface{}{} + hierarchy := make(map[string][]string) + + // Get children from device and vobjs + for _, checkEnt := range []int{u.DEVICE, u.VIRTUALOBJ} { + checkEntName := u.EntityToString(checkEnt) + var dbFilter primitive.M + if checkEnt == u.VIRTUALOBJ { + // linked by Id + pattern := primitive.Regex{Pattern: "^" + clusterName + + "(." + u.NAME_REGEX + "){1," + strconv.Itoa(limit) + "}$", Options: ""} + dbFilter = bson.M{"id": pattern} + } else { + // DEVICE links to vobj via virtual config + dbFilter = bson.M{"attributes.virtual_config.clusterId": clusterName} + } + children, e1 := GetManyObjects(checkEntName, dbFilter, filters, "", nil) + if e1 != nil { + return nil, e1 + } + for _, child := range children { + if checkEnt == u.VIRTUALOBJ { + allChildren[child["id"].(string)] = child + fillHierarchyMap(child["id"].(string), hierarchy) + } else { + // add namespace prefix to devices + child["id"] = "Physical." + child["id"].(string) + allChildren[child["id"].(string)] = child + // add as direct child to cluster + hierarchy[clusterName] = append(hierarchy[clusterName], child["id"].(string)) + } + } + } + + // Organize vobj family according to relations (nest children) + return recursivelyGetChildrenFromMaps(clusterName, hierarchy, allChildren), nil +} + +// recursivelyGetChildrenFromMaps: nest children data as the array value of +// its parents "children" key +func recursivelyGetChildrenFromMaps(parentHierarchyName string, hierarchy map[string][]string, + allChildrenData map[string]interface{}) []map[string]interface{} { + var children []map[string]interface{} + for _, childName := range hierarchy[parentHierarchyName] { + // Get the child data and get its own children + child := allChildrenData[childName].(map[string]interface{}) + child["children"] = recursivelyGetChildrenFromMaps(childName, hierarchy, allChildrenData) + children = append(children, child) + } + return children +} + +// fillHierarchyMap: add obj id (hierarchyName) to the children array of its parent +func fillHierarchyMap(hierarchyName string, data map[string][]string) { + i := strings.LastIndex(hierarchyName, u.HN_DELIMETER) + if i > 0 { + parent := hierarchyName[:i] + data[parent] = append(data[parent], hierarchyName) + } +} + +// getChildrenCollections: get a list of entites where children of given parentEntStr +// may be found, considering limit as the max possible distance of child to parent +func getChildrenCollections(limit int, parentEntStr string) []int { + rangeEntities := []int{} + startEnt := u.EntityStrToInt(parentEntStr) + 1 + endEnt := startEnt + limit + if parentEntStr == u.EntityToString(u.DOMAIN) { + return []int{u.DOMAIN} + } else if parentEntStr == u.EntityToString(u.DEVICE) { + // device special case (devices can have devices and vobjs) + return []int{u.DEVICE, u.VIRTUALOBJ} + } else if parentEntStr == u.EntityToString(u.VIRTUALOBJ) { + return []int{u.VIRTUALOBJ} + } else if endEnt >= u.DEVICE { + // include AC, CABINET, CORRIDOR, PWRPNL and GROUP + // beacause of ROOM and RACK possible children + // but no need to search further than group + endEnt = u.GROUP + } + + for i := startEnt; i <= endEnt; i++ { + rangeEntities = append(rangeEntities, i) + } + + if startEnt == u.ROOM && endEnt == u.RACK { + // ROOM limit=1 special case should include extra + // ROOM children but avoiding DEVICE (big collection) + rangeEntities = append(rangeEntities, u.CORRIDOR, u.CABINET, u.PWRPNL, u.GROUP) + } + + return rangeEntities +} + +// GetSiteParentAttribute: search for the object of given ID, +// then search for its site parent and return its requested attribute +func GetSiteParentAttribute(id string, attribute string) (map[string]any, *u.Error) { + data := map[string]interface{}{} + + // Get all collections names + ctx, cancel := u.Connect() + db := repository.GetDB() + collNames, err := db.ListCollectionNames(ctx, bson.D{}) + if err != nil { + fmt.Println(err.Error()) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + // Find object + for _, collName := range collNames { + err := db.Collection(collName).FindOne(ctx, bson.M{"id": id}).Decode(&data) + if err == nil { + // Found object with given id + if data["category"].(string) == "site" { + // it's a site + break + } else { + // Find its parent site + nameSlice := strings.Split(data["id"].(string), u.HN_DELIMETER) + siteName := nameSlice[0] // CONSIDER SITE AS 0 + err := db.Collection("site").FindOne(ctx, bson.M{"id": siteName}).Decode(&data) + if err != nil { + return nil, &u.Error{Type: u.ErrNotFound, + Message: "Could not find parent site for given object"} + } + } + } + } + + defer cancel() + + if len(data) == 0 { + return nil, &u.Error{Type: u.ErrNotFound, Message: "No object found with given id"} + } else if attribute == "sitecolors" { + resp := map[string]any{} + for _, colorName := range []string{"reservedColor", "technicalColor", "usableColor"} { + if color := data["attributes"].(map[string]interface{})[colorName]; color != nil { + resp[colorName] = color + } else { + resp[colorName] = "" + } + } + return resp, nil + } else if attrValue := data["attributes"].(map[string]interface{})[attribute]; attrValue == nil { + return nil, &u.Error{Type: u.ErrNotFound, + Message: "Parent site has no temperatureUnit in attributes"} + } else { + return map[string]any{attribute: attrValue}, nil + } +} + +func GetEntitiesOfAncestor(id string, entStr, wantedEnt string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { + // Get parent object + req := bson.M{"id": id} + _, e := GetObject(req, entStr, u.RequestFilters{}, userRoles) + if e != nil { + return nil, e + } + + // Get sub entity objects + pattern := primitive.Regex{Pattern: "^" + id + u.HN_DELIMETER, Options: ""} + req = bson.M{"id": pattern} + sub, e1 := GetManyObjects(wantedEnt, req, u.RequestFilters{}, "", userRoles) + if e1 != nil { + return nil, e1 + } + + return sub, nil +} diff --git a/API/models/sanitise_entity.go b/API/models/entity_sanitise.go similarity index 100% rename from API/models/sanitise_entity.go rename to API/models/entity_sanitise.go diff --git a/API/models/entity_update.go b/API/models/entity_update.go new file mode 100644 index 000000000..989e15579 --- /dev/null +++ b/API/models/entity_update.go @@ -0,0 +1,252 @@ +package models + +import ( + "encoding/json" + "errors" + "p3/repository" + u "p3/utils" + "strings" + "time" + + "github.com/elliotchance/pie/v2" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var AttrsWithInnerObj = []string{"pillars", "separators", "breakers"} + +func UpdateObject(entityStr string, id string, updateData map[string]interface{}, isPatch bool, userRoles map[string]Role, isRecursive bool) (map[string]interface{}, *u.Error) { + var idFilter bson.M + if u.IsEntityNonHierarchical(u.EntityStrToInt(entityStr)) { + idFilter = bson.M{"slug": id} + } else { + idFilter = bson.M{"id": id} + } + + //Update timestamp requires first obj retrieval + //there isn't any way for mongoDB to make a field + //immutable in a document + var oldObj map[string]any + var err *u.Error + if entityStr == u.HIERARCHYOBJS_ENT { + oldObj, err = GetHierarchyObjectById(id, u.RequestFilters{}, userRoles) + if err == nil { + entityStr = oldObj["category"].(string) + } + } else { + oldObj, err = GetObject(idFilter, entityStr, u.RequestFilters{}, userRoles) + } + if err != nil { + return nil, err + } + + entity := u.EntityStrToInt(entityStr) + + // Check if permission is only readonly + if u.IsEntityHierarchical(entity) && oldObj["description"] == nil { + // Description is always present, unless GetEntity was called with readonly permission + return nil, &u.Error{Type: u.ErrUnauthorized, + Message: "User does not have permission to change this object"} + } + + tags, tagsPresent := getTags(updateData) + + // Update old object data with patch data + if isPatch { + if tagsPresent { + return nil, &u.Error{ + Type: u.ErrBadFormat, + Message: "Tags cannot be modified in this way, use tags+ and tags-", + } + } + + var formattedOldObj map[string]interface{} + // Convert primitive.A and similar types + bytes, _ := json.Marshal(oldObj) + json.Unmarshal(bytes, &formattedOldObj) + // Update old with new + err := updateOldObjWithPatch(formattedOldObj, updateData) + if err != nil { + return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } + + updateData = formattedOldObj + // Remove API set fields + delete(updateData, "id") + delete(updateData, "lastUpdated") + delete(updateData, "createdDate") + } else if tagsPresent { + err := verifyTagList(tags) + if err != nil { + return nil, err + } + } + + result, err := WithTransaction(func(ctx mongo.SessionContext) (interface{}, error) { + err = prepareUpdateObject(ctx, entity, id, updateData, oldObj, userRoles) + if err != nil { + return nil, err + } + + mongoRes := repository.GetDB().Collection(entityStr).FindOneAndReplace( + ctx, + idFilter, updateData, + options.FindOneAndReplace().SetReturnDocument(options.After), + ) + if mongoRes.Err() != nil { + return nil, mongoRes.Err() + } + + if oldObj["id"] != updateData["id"] { + // Changes to id should be propagated + if err := repository.PropagateParentIdChange( + ctx, + oldObj["id"].(string), + updateData["id"].(string), + entity, + ); err != nil { + return nil, err + } else if entity == u.DOMAIN { + if err := repository.PropagateDomainChange(ctx, + oldObj["id"].(string), + updateData["id"].(string), + ); err != nil { + return nil, err + } + } + } + if u.IsEntityHierarchical(entity) && (oldObj["domain"] != updateData["domain"]) { + if isRecursive { + // Change domain of all children too + if err := repository.PropagateDomainChangeToChildren( + ctx, + updateData["id"].(string), + updateData["domain"].(string), + ); err != nil { + return nil, err + } + } else { + // Check if children domains are compatible + if err := repository.CheckParentDomainChange(entity, updateData["id"].(string), + updateData["domain"].(string)); err != nil { + return nil, err + } + } + } + + return mongoRes, nil + }) + + if err != nil { + return nil, err + } + + var updatedDoc map[string]interface{} + result.(*mongo.SingleResult).Decode(&updatedDoc) + + return fixID(updatedDoc), nil +} + +func prepareUpdateObject(ctx mongo.SessionContext, entity int, id string, updateData, oldObject map[string]any, userRoles map[string]Role) *u.Error { + // Check user permissions in case domain is being updated + if entity != u.DOMAIN && u.IsEntityHierarchical(entity) && (oldObject["domain"] != updateData["domain"]) { + if perm := CheckUserPermissions(userRoles, entity, updateData["domain"].(string)); perm < WRITE { + return &u.Error{Type: u.ErrUnauthorized, Message: "User does not have permission to change this object"} + } + } + + // tag list edition support + err := addAndRemoveFromTags(entity, id, updateData) + if err != nil { + return err + } + + // Ensure the update is valid + err = ValidateEntity(entity, updateData) + if err != nil { + return err + } + + updateData["lastUpdated"] = primitive.NewDateTimeFromTime(time.Now()) + updateData["createdDate"] = oldObject["createdDate"] + delete(updateData, "parentId") + + if entity == u.TAG { + // tag slug edition support + if updateData["slug"].(string) != oldObject["slug"].(string) { + err := repository.UpdateTagSlugInEntities(ctx, oldObject["slug"].(string), updateData["slug"].(string)) + if err != nil { + return err + } + } + + err := updateTagImage(ctx, oldObject, updateData) + if err != nil { + return err + } + } + + return nil +} + +func updateOldObjWithPatch(old map[string]interface{}, patch map[string]interface{}) error { + for k, v := range patch { + switch patchValueCasted := v.(type) { + case map[string]interface{}: + if pie.Contains(AttrsWithInnerObj, k) { + old[k] = v + } else { + switch oldValueCasted := old[k].(type) { + case map[string]interface{}: + err := updateOldObjWithPatch(oldValueCasted, patchValueCasted) + if err != nil { + return err + } + default: + old[k] = v + } + } + default: + if k == "filter" && strings.HasPrefix(v.(string), "&") { + v = "(" + old["filter"].(string) + ") " + v.(string) + } + old[k] = v + } + } + + return nil +} + +// SwapEntity: use id to remove object from deleteEnt and then use data to create it in createEnt. +// Propagates id changes to children objects. For atomicity, all is done in a Mongo transaction. +func SwapEntity(createEnt, deleteEnt, id string, data map[string]interface{}, userRoles map[string]Role) *u.Error { + if err := prepareCreateEntity(u.EntityStrToInt(createEnt), data, userRoles); err != nil { + return err + } + + _, err := WithTransaction(func(ctx mongo.SessionContext) (any, error) { + // Create + if _, err := repository.CreateObject(ctx, createEnt, data); err != nil { + return nil, err + } + + // Propagate + if err := repository.PropagateParentIdChange(ctx, id, data["id"].(string), + u.EntityStrToInt(data["category"].(string))); err != nil { + return nil, err + } + + // Delete + if c, err := repository.GetDB().Collection(deleteEnt).DeleteOne(ctx, bson.M{"id": id}); err != nil { + return nil, err + } else if c.DeletedCount == 0 { + return nil, errors.New("Error deleting object: not found") + } + + return nil, nil + }) + + return err +} diff --git a/API/models/update_object_test.go b/API/models/entity_update_test.go similarity index 100% rename from API/models/update_object_test.go rename to API/models/entity_update_test.go diff --git a/API/models/validate_entity.go b/API/models/entity_validate.go similarity index 100% rename from API/models/validate_entity.go rename to API/models/entity_validate.go diff --git a/API/models/validate_entity_test.go b/API/models/entity_validate_test.go similarity index 100% rename from API/models/validate_entity_test.go rename to API/models/entity_validate_test.go diff --git a/API/models/filter.go b/API/models/filter.go new file mode 100644 index 000000000..da299166a --- /dev/null +++ b/API/models/filter.go @@ -0,0 +1,128 @@ +package models + +import ( + "errors" + "fmt" + u "p3/utils" + "regexp" + "strings" + "time" +) + +func ComplexFilterToMap(complexFilter string) (map[string]any, error) { + // Split the input string into individual filter expressions + chars := []string{"(", ")", "&", "|"} + for _, char := range chars { + complexFilter = strings.ReplaceAll(complexFilter, char, " "+char+" ") + } + return complexExpressionToMap(strings.Fields(complexFilter)) +} + +func complexExpressionToMap(expressions []string) (map[string]any, error) { + // Find the rightmost operator (AND, OR) outside of parentheses + parenCount := 0 + for i := len(expressions) - 1; i >= 0; i-- { + switch expressions[i] { + case "(": + parenCount++ + case ")": + parenCount-- + case "&": + if parenCount == 0 { + first, _ := complexExpressionToMap(expressions[:i]) + second, _ := complexExpressionToMap(expressions[i+1:]) + return map[string]any{"$and": []map[string]any{ + first, + second, + }}, nil + } + case "|": + if parenCount == 0 { + first, _ := complexExpressionToMap(expressions[:i]) + second, _ := complexExpressionToMap(expressions[i+1:]) + return map[string]any{"$or": []map[string]any{ + first, + second, + }}, nil + } + } + } + + // If there are no operators outside of parentheses, look for the innermost pair of parentheses + for i := 0; i < len(expressions); i++ { + if expressions[i] == "(" { + start, end := i+1, i+1 + for parenCount := 1; end < len(expressions) && parenCount > 0; end++ { + switch expressions[end] { + case "(": + parenCount++ + case ")": + parenCount-- + } + } + return complexExpressionToMap(append(expressions[:start-1], expressions[start:end-1]...)) + } + } + + // Base case: single filter expression + return singleExpressionToMap(expressions) +} + +func singleExpressionToMap(expressions []string) (map[string]any, error) { + re := regexp.MustCompile(`^([\w-.]+)\s*(<=|>=|<|>|!=|=)\s*((\[)*[\w-,.*]+(\])*)$`) + ops := map[string]string{"<=": "$lte", ">=": "$gte", "<": "$lt", ">": "$gt", "!=": "$not"} + + if len(expressions) <= 3 { + expression := strings.Join(expressions[:], "") + if match := re.FindStringSubmatch(expression); match != nil { + // convert filter value to proper type + filterName := match[1] // e.g. category + filterOp := match[2] // e.g. != + filterValue := u.ConvertString(match[3]) // e.g. device + switch filterName { + case "startDate": + return map[string]any{"lastUpdated": map[string]any{"$gte": filterValue}}, nil + case "endDate": + return map[string]any{"lastUpdated": map[string]any{"$lte": filterValue}}, nil + case "id", "name", "category", "description", "domain", "createdDate", "lastUpdated", "slug": + if filterOp == "=" { + return map[string]any{filterName: filterValue}, nil + } + return map[string]any{filterName: map[string]any{ops[filterOp]: filterValue}}, nil + default: + if filterOp == "=" { + return map[string]any{"attributes." + filterName: filterValue}, nil + } + return map[string]any{"attributes." + filterName: map[string]any{ops[filterOp]: filterValue}}, nil + } + } + } + + fmt.Println("Error: Invalid filter expression") + return nil, errors.New("invalid filter expression") +} + +func getDatesFromComplexFilters(req map[string]any) error { + for k, v := range req { + if k == "$and" || k == "$or" { + for _, complexFilter := range v.([]map[string]any) { + err := getDatesFromComplexFilters(complexFilter) + if err != nil { + return err + } + } + } else if k == "lastUpdated" { + for op, date := range v.(map[string]any) { + parsedDate, err := time.Parse("2006-01-02", date.(string)) + if err != nil { + return err + } + if op == "$lte" { + parsedDate = parsedDate.Add(time.Hour * 24) + } + req[k] = map[string]any{op: parsedDate} + } + } + } + return nil +} diff --git a/API/models/model.go b/API/models/model.go deleted file mode 100644 index 442496d7d..000000000 --- a/API/models/model.go +++ /dev/null @@ -1,1146 +0,0 @@ -package models - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "maps" - "os" - "p3/repository" - u "p3/utils" - "regexp" - "strconv" - "strings" - "time" - - "github.com/elliotchance/pie/v2" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -var AttrsWithInnerObj = []string{"pillars", "separators", "breakers"} - -// Helper functions - -func domainHasObjects(domain string) bool { - data := map[string]interface{}{} - // Get all collections names - ctx, cancel := u.Connect() - db := repository.GetDB() - collNames, _ := db.ListCollectionNames(ctx, bson.D{}) - - // Check if at least one object belongs to domain - for _, collName := range collNames { - pattern := primitive.Regex{Pattern: "^" + domain, Options: ""} - e := db.Collection(collName).FindOne(ctx, bson.M{"domain": pattern}).Decode(&data) - if e == nil { - // Found one! - return true - } - } - - defer cancel() - return false -} - -// fillHierarchyMap: add obj id (hierarchyName) to the children array of its parent -func fillHierarchyMap(hierarchyName string, data map[string][]string) { - i := strings.LastIndex(hierarchyName, u.HN_DELIMETER) - if i > 0 { - parent := hierarchyName[:i] - data[parent] = append(data[parent], hierarchyName) - } -} - -// getChildrenCollections: get a list of entites where children of given parentEntStr -// may be found, considering limit as the max possible distance of child to parent -func getChildrenCollections(limit int, parentEntStr string) []int { - rangeEntities := []int{} - startEnt := u.EntityStrToInt(parentEntStr) + 1 - endEnt := startEnt + limit - if parentEntStr == u.EntityToString(u.DOMAIN) { - return []int{u.DOMAIN} - } else if parentEntStr == u.EntityToString(u.DEVICE) { - // device special case (devices can have devices and vobjs) - return []int{u.DEVICE, u.VIRTUALOBJ} - } else if parentEntStr == u.EntityToString(u.VIRTUALOBJ) { - return []int{u.VIRTUALOBJ} - } else if endEnt >= u.DEVICE { - // include AC, CABINET, CORRIDOR, PWRPNL and GROUP - // beacause of ROOM and RACK possible children - // but no need to search further than group - endEnt = u.GROUP - } - - for i := startEnt; i <= endEnt; i++ { - rangeEntities = append(rangeEntities, i) - } - - if startEnt == u.ROOM && endEnt == u.RACK { - // ROOM limit=1 special case should include extra - // ROOM children but avoiding DEVICE (big collection) - rangeEntities = append(rangeEntities, u.CORRIDOR, u.CABINET, u.PWRPNL, u.GROUP) - } - - return rangeEntities -} - -func updateOldObjWithPatch(old map[string]interface{}, patch map[string]interface{}) error { - for k, v := range patch { - switch patchValueCasted := v.(type) { - case map[string]interface{}: - if pie.Contains(AttrsWithInnerObj, k) { - old[k] = v - } else { - switch oldValueCasted := old[k].(type) { - case map[string]interface{}: - err := updateOldObjWithPatch(oldValueCasted, patchValueCasted) - if err != nil { - return err - } - default: - old[k] = v - } - } - default: - if k == "filter" && strings.HasPrefix(v.(string), "&") { - v = "(" + old["filter"].(string) + ") " + v.(string) - } - old[k] = v - } - } - - return nil -} - -// Entity handlers - -func CreateEntity(entity int, t map[string]interface{}, userRoles map[string]Role) (map[string]interface{}, *u.Error) { - tags, tagsPresent := getTags(t) - if tagsPresent { - err := verifyTagList(tags) - if err != nil { - return nil, err - } - } - - if err := prepareCreateEntity(entity, t, userRoles); err != nil { - return nil, err - } - - return WithTransaction(func(ctx mongo.SessionContext) (map[string]any, error) { - if entity == u.TAG { - err := createTagImage(ctx, t) - if err != nil { - return nil, err - } - } - - entStr := u.EntityToString(entity) - - _, err := repository.CreateObject(ctx, entStr, t) - if err != nil { - return nil, err - } - - fixID(t) - return t, nil - }) -} - -func prepareCreateEntity(entity int, t map[string]interface{}, userRoles map[string]Role) *u.Error { - if err := ValidateEntity(entity, t); err != nil { - return err - } - - // Check user permissions - if u.IsEntityHierarchical(entity) { - var domain string - if entity == u.DOMAIN { - domain = t["id"].(string) - } else { - domain = t["domain"].(string) - } - if permission := CheckUserPermissions(userRoles, entity, domain); permission < WRITE { - return &u.Error{Type: u.ErrUnauthorized, - Message: "User does not have permission to create this object"} - } - } - - delete(t, "parentId") - - return nil -} - -func GetHierarchyObjectById(hierarchyName string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { - // Get possible collections for this name - rangeEntities := u.GetEntitiesById(u.PHierarchy, hierarchyName) - req := bson.M{"id": hierarchyName} - - // Search each collection - for _, entityStr := range rangeEntities { - data, _ := GetObject(req, entityStr, filters, userRoles) - if data != nil { - return data, nil - } - } - - return nil, &u.Error{Type: u.ErrNotFound, Message: "Unable to find object"} -} - -func GetObject(req bson.M, entityStr string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { - object, err := repository.GetObject(req, entityStr, filters) - - if err != nil { - return nil, err - } - - //Remove _id - object = fixID(object) - - entity := u.EntityStrToInt(entityStr) - - if shouldFillTags(entity, filters) { - object = fillTags(object) - } - - // Check permissions - if u.IsEntityHierarchical(entity) { - var domain string - if entity == u.DOMAIN { - domain = object["id"].(string) - } else { - domain = object["domain"].(string) - } - if userRoles != nil { - if permission := CheckUserPermissions(userRoles, u.EntityStrToInt(entityStr), domain); permission == NONE { - return nil, &u.Error{Type: u.ErrUnauthorized, - Message: "User does not have permission to see this object"} - } else if permission == READONLYNAME { - object = FixReadOnlyName(object) - } - } - } - - return object, nil -} - -func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, complexFilterExp string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { - ctx, cancel := u.Connect() - var err error - var c *mongo.Cursor - - var opts *options.FindOptions - if len(filters.FieldsToShow) > 0 { - compoundIndex := bson.D{bson.E{Key: "domain", Value: 1}, bson.E{Key: "id", Value: 1}} - for _, field := range filters.FieldsToShow { - if field != "domain" && field != "id" { - compoundIndex = append(compoundIndex, bson.E{Key: field, Value: 1}) - } - } - opts = options.Find().SetProjection(compoundIndex) - } - err = repository.GetDateFilters(req, filters.StartDate, filters.EndDate) - if err != nil { - return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} - } - - if complexFilterExp != "" { - if complexFilters, err := ComplexFilterToMap(complexFilterExp); err != nil { - return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} - } else { - err = getDatesFromComplexFilters(complexFilters) - if err != nil { - return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} - } - u.ApplyWildcardsOnComplexFilter(complexFilters) - maps.Copy(req, complexFilters) - } - } - - if opts != nil { - c, err = repository.GetDB().Collection(entityStr).Find(ctx, req, opts) - } else { - c, err = repository.GetDB().Collection(entityStr).Find(ctx, req) - } - if err != nil { - fmt.Println(err) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - } - defer cancel() - - entity := u.EntityStrToInt(entityStr) - data, e1 := ExtractCursor(c, ctx, entity, userRoles) - if e1 != nil { - fmt.Println(e1) - return nil, &u.Error{Type: u.ErrInternal, Message: e1.Error()} - } - - if shouldFillTags(entity, filters) { - for i := range data { - fillTags(data[i]) - } - } - - return data, nil -} - -func ComplexFilterToMap(complexFilter string) (map[string]any, error) { - // Split the input string into individual filter expressions - chars := []string{"(", ")", "&", "|"} - for _, char := range chars { - complexFilter = strings.ReplaceAll(complexFilter, char, " "+char+" ") - } - return complexExpressionToMap(strings.Fields(complexFilter)) -} - -func complexExpressionToMap(expressions []string) (map[string]any, error) { - // Find the rightmost operator (AND, OR) outside of parentheses - parenCount := 0 - for i := len(expressions) - 1; i >= 0; i-- { - switch expressions[i] { - case "(": - parenCount++ - case ")": - parenCount-- - case "&": - if parenCount == 0 { - first, _ := complexExpressionToMap(expressions[:i]) - second, _ := complexExpressionToMap(expressions[i+1:]) - return map[string]any{"$and": []map[string]any{ - first, - second, - }}, nil - } - case "|": - if parenCount == 0 { - first, _ := complexExpressionToMap(expressions[:i]) - second, _ := complexExpressionToMap(expressions[i+1:]) - return map[string]any{"$or": []map[string]any{ - first, - second, - }}, nil - } - } - } - - // If there are no operators outside of parentheses, look for the innermost pair of parentheses - for i := 0; i < len(expressions); i++ { - if expressions[i] == "(" { - start, end := i+1, i+1 - for parenCount := 1; end < len(expressions) && parenCount > 0; end++ { - switch expressions[end] { - case "(": - parenCount++ - case ")": - parenCount-- - } - } - return complexExpressionToMap(append(expressions[:start-1], expressions[start:end-1]...)) - } - } - - // Base case: single filter expression - return singleExpressionToMap(expressions) -} - -func singleExpressionToMap(expressions []string) (map[string]any, error) { - re := regexp.MustCompile(`^([\w-.]+)\s*(<=|>=|<|>|!=|=)\s*((\[)*[\w-,.*]+(\])*)$`) - ops := map[string]string{"<=": "$lte", ">=": "$gte", "<": "$lt", ">": "$gt", "!=": "$not"} - - if len(expressions) <= 3 { - expression := strings.Join(expressions[:], "") - if match := re.FindStringSubmatch(expression); match != nil { - // convert filter value to proper type - filterName := match[1] // e.g. category - filterOp := match[2] // e.g. != - filterValue := u.ConvertString(match[3]) // e.g. device - switch filterName { - case "startDate": - return map[string]any{"lastUpdated": map[string]any{"$gte": filterValue}}, nil - case "endDate": - return map[string]any{"lastUpdated": map[string]any{"$lte": filterValue}}, nil - case "id", "name", "category", "description", "domain", "createdDate", "lastUpdated", "slug": - if filterOp == "=" { - return map[string]any{filterName: filterValue}, nil - } - return map[string]any{filterName: map[string]any{ops[filterOp]: filterValue}}, nil - default: - if filterOp == "=" { - return map[string]any{"attributes." + filterName: filterValue}, nil - } - return map[string]any{"attributes." + filterName: map[string]any{ops[filterOp]: filterValue}}, nil - } - } - } - - fmt.Println("Error: Invalid filter expression") - return nil, errors.New("invalid filter expression") -} - -func getDatesFromComplexFilters(req map[string]any) error { - for k, v := range req { - if k == "$and" || k == "$or" { - for _, complexFilter := range v.([]map[string]any) { - err := getDatesFromComplexFilters(complexFilter) - if err != nil { - return err - } - } - } else if k == "lastUpdated" { - for op, date := range v.(map[string]any) { - parsedDate, err := time.Parse("2006-01-02", date.(string)) - if err != nil { - return err - } - if op == "$lte" { - parsedDate = parsedDate.Add(time.Hour * 24) - } - req[k] = map[string]any{op: parsedDate} - } - } - } - return nil -} - -// GetCompleteHierarchy: gets all objects in db using hierachyName and returns: -// - tree: map with parents as key and their children as an array value -// tree: {parent:[children]} -// - categories: map with category name as key and corresponding objects -// as an array value -// categories: {categoryName:[children]} -func GetCompleteHierarchy(userRoles map[string]Role, filters u.HierarchyFilters) (map[string]interface{}, *u.Error) { - response := make(map[string]interface{}) - categories := make(map[string][]string) - hierarchy := make(map[string]interface{}) - - switch filters.Namespace { - case u.Any: - for _, ns := range []u.Namespace{u.Physical, u.Logical, u.Organisational} { - data, err := getHierarchyWithNamespace(ns, userRoles, filters, categories) - if err != nil { - return nil, err - } - hierarchy[u.NamespaceToString(ns)] = data - } - default: - data, err := getHierarchyWithNamespace(filters.Namespace, userRoles, filters, categories) - if err != nil { - return nil, err - } - hierarchy[u.NamespaceToString(filters.Namespace)] = data - - } - - response["tree"] = hierarchy - if filters.WithCategories { - categories["KeysOrder"] = []string{"site", "building", "room", "rack"} - response["categories"] = categories - } - return response, nil -} - -func getHierarchyWithNamespace(namespace u.Namespace, userRoles map[string]Role, filters u.HierarchyFilters, - categories map[string][]string) (map[string][]string, *u.Error) { - hierarchy := make(map[string][]string) - rootIdx := "*" - - ctx, cancel := u.Connect() - db := repository.GetDB() - dbFilter := bson.M{} - - // Depth of hierarchy defined by user - if filters.Limit != "" && namespace != u.PStray && - !strings.Contains(u.NamespaceToString(namespace), string(u.Logical)) { - if _, e := strconv.Atoi(filters.Limit); e == nil { - pattern := primitive.Regex{Pattern: "^" + u.NAME_REGEX + "(." + u.NAME_REGEX + "){0," + - filters.Limit + "}$", Options: ""} - dbFilter = bson.M{"id": pattern} - } - } - // User date filters - err := repository.GetDateFilters(dbFilter, filters.StartDate, filters.EndDate) - if err != nil { - return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} - } - - // Search collections according to namespace - entities := u.GetEntitiesById(namespace, "") - - for _, entityName := range entities { - // Get data - opts := options.Find().SetProjection(bson.D{{Key: "domain", Value: 1}, {Key: "id", Value: 1}, {Key: "category", Value: 1}}) - - if u.IsEntityNonHierarchical(u.EntityStrToInt(entityName)) { - opts = options.Find().SetProjection(bson.D{{Key: "slug", Value: 1}}) - } - - c, err := db.Collection(entityName).Find(ctx, dbFilter, opts) - if err != nil { - println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - } - - data, e := ExtractCursor(c, ctx, u.EntityStrToInt(entityName), userRoles) - if e != nil { - return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} - } - - // Format data - for _, obj := range data { - if strings.Contains(u.NamespaceToString(namespace), string(u.Logical)) { - // Logical - var objId string - if u.IsEntityNonHierarchical(u.EntityStrToInt(entityName)) { - objId = obj["slug"].(string) - hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) - } else { - objId = obj["id"].(string) - categories[entityName] = append(categories[entityName], objId) - if strings.Contains(objId, ".") && obj["category"] != "group" { - // Physical or Org Children - fillHierarchyMap(objId, hierarchy) - } else { - hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) - } - } - } else if strings.Contains(obj["id"].(string), ".") { - // Physical or Org Children - categories[entityName] = append(categories[entityName], obj["id"].(string)) - fillHierarchyMap(obj["id"].(string), hierarchy) - } else { - // Physical or Org Roots - objId := obj["id"].(string) - categories[entityName] = append(categories[entityName], objId) - if u.EntityStrToInt(entityName) == u.STRAYOBJ { - hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) - } else if u.EntityStrToInt(entityName) != u.VIRTUALOBJ { - hierarchy[rootIdx] = append(hierarchy[rootIdx], objId) - } - } - } - } - - // For the root of VIRTUAL objects we also need to check for devices - for _, vobj := range hierarchy[rootIdx+u.EntityToString(u.VIRTUALOBJ)] { - if !strings.ContainsAny(vobj, u.HN_DELIMETER) { // is stray virtual - // Get device linked to this vobj through its virtual config - entityName := u.EntityToString(u.DEVICE) - dbFilter := bson.M{"attributes.virtual_config.clusterId": vobj} - opts := options.Find().SetProjection(bson.D{{Key: "domain", Value: 1}, {Key: "id", Value: 1}, {Key: "category", Value: 1}}) - c, err := db.Collection(entityName).Find(ctx, dbFilter, opts) - if err != nil { - println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - } - data, e := ExtractCursor(c, ctx, u.EntityStrToInt(entityName), userRoles) - if e != nil { - return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} - } - // Add data - for _, obj := range data { - objId := obj["id"].(string) - categories[entityName] = append(categories[entityName], objId) - hierarchy[vobj] = append(hierarchy[vobj], objId) - } - } - } - - defer cancel() - return hierarchy, nil -} - -func GetCompleteHierarchyAttributes(userRoles map[string]Role) (map[string]interface{}, *u.Error) { - response := make(map[string]interface{}) - // Get all collections names - ctx, cancel := u.Connect() - db := repository.GetDB() - collNames, err := db.ListCollectionNames(ctx, bson.D{}) - if err != nil { - fmt.Println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - - } - - for _, collName := range collNames { - if entInt := u.EntityStrToInt(collName); entInt > -1 { - projection := bson.D{{Key: "attributes", Value: 1}, - {Key: "domain", Value: 1}, {Key: "id", Value: 1}} - - opts := options.Find().SetProjection(projection) - - c, err := db.Collection(collName).Find(ctx, bson.M{}, opts) - if err != nil { - println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - } - data, e := ExtractCursor(c, ctx, entInt, userRoles) - if e != nil { - return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} - } - - for _, obj := range data { - if obj["attributes"] != nil { - if id, isStr := obj["id"].(string); isStr && id != "" { - response[obj["id"].(string)] = obj["attributes"] - } else if obj["name"] != nil { - response[obj["name"].(string)] = obj["attributes"] - } - } - } - } - } - defer cancel() - return response, nil -} - -// GetSiteParentAttribute: search for the object of given ID, -// then search for its site parent and return its requested attribute -func GetSiteParentAttribute(id string, attribute string) (map[string]any, *u.Error) { - data := map[string]interface{}{} - - // Get all collections names - ctx, cancel := u.Connect() - db := repository.GetDB() - collNames, err := db.ListCollectionNames(ctx, bson.D{}) - if err != nil { - fmt.Println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - } - // Find object - for _, collName := range collNames { - err := db.Collection(collName).FindOne(ctx, bson.M{"id": id}).Decode(&data) - if err == nil { - // Found object with given id - if data["category"].(string) == "site" { - // it's a site - break - } else { - // Find its parent site - nameSlice := strings.Split(data["id"].(string), u.HN_DELIMETER) - siteName := nameSlice[0] // CONSIDER SITE AS 0 - err := db.Collection("site").FindOne(ctx, bson.M{"id": siteName}).Decode(&data) - if err != nil { - return nil, &u.Error{Type: u.ErrNotFound, - Message: "Could not find parent site for given object"} - } - } - } - } - - defer cancel() - - if len(data) == 0 { - return nil, &u.Error{Type: u.ErrNotFound, Message: "No object found with given id"} - } else if attribute == "sitecolors" { - resp := map[string]any{} - for _, colorName := range []string{"reservedColor", "technicalColor", "usableColor"} { - if color := data["attributes"].(map[string]interface{})[colorName]; color != nil { - resp[colorName] = color - } else { - resp[colorName] = "" - } - } - return resp, nil - } else if attrValue := data["attributes"].(map[string]interface{})[attribute]; attrValue == nil { - return nil, &u.Error{Type: u.ErrNotFound, - Message: "Parent site has no temperatureUnit in attributes"} - } else { - return map[string]any{attribute: attrValue}, nil - } -} - -func GetEntityCount(entity int) int64 { - ent := u.EntityToString(entity) - ctx, cancel := u.Connect() - ans, e := repository.GetDB().Collection(ent).CountDocuments(ctx, bson.M{}, nil) - if e != nil { - println(e.Error()) - return -1 - } - defer cancel() - return ans -} - -func CommandRunner(cmd interface{}) *mongo.SingleResult { - ctx, cancel := u.Connect() - result := repository.GetDB().RunCommand(ctx, cmd, nil) - defer cancel() - return result -} - -func GetStats() map[string]interface{} { - ans := map[string]interface{}{} - t := map[string]interface{}{} - latestDocArr := []map[string]interface{}{} - var latestTime interface{} - - for entity := range u.Entities { - num := GetEntityCount(entity) - if num == -1 { - num = 0 - } - - ans["Number of "+u.EntityToString(entity)+"s:"] = num - - //Retrieve the latest updated document in each collection - //and store into the latestDocArr array - obj := map[string]interface{}{} - filter := options.FindOne().SetSort(bson.M{"lastUpdated": -1}) - ctx, cancel := u.Connect() - - e := repository.GetDB().Collection(u.EntityToString(entity)).FindOne(ctx, bson.M{}, filter).Decode(&obj) - if e == nil { - latestDocArr = append(latestDocArr, obj) - } - defer cancel() - } - - //Get the latest update out of latestDocArr - value := -1 - for _, obj := range latestDocArr { - if int(obj["lastUpdated"].(primitive.DateTime)) > value { - value = int(obj["lastUpdated"].(primitive.DateTime)) - latestTime = obj["lastUpdated"] - } - } - - if latestTime == nil { - latestTime = "N/A" - } - - cmd := bson.D{{"dbStats", 1}, {"scale", 1024}} - - if e := CommandRunner(cmd).Decode(&t); e != nil { - println(e.Error()) - return nil - } - - ans["Number of Hierarchal Objects"] = t["collections"] - ans["Last Job Timestamp"] = latestTime - - return ans -} - -func GetDBName() string { - name := repository.GetDB().Name() - - //Remove the preceding 'ogree' at beginning of name - if strings.Index(name, "ogree") == 0 { - name = name[5:] //5=len('ogree') - } - return name -} - -// DeleteHierarchicalObject: delete object of given hierarchyName -// search for all its children and delete them too, return: -// - success or fail message map -func DeleteHierarchicalObject(entity string, id string, userRoles map[string]Role) *u.Error { - // Special check for delete domain - if entity == "domain" { - if id == os.Getenv("db") { - return &u.Error{Type: u.ErrForbidden, Message: "Cannot delete tenant's default domain"} - } - if domainHasObjects(id) { - return &u.Error{Type: u.ErrForbidden, Message: "Cannot delete domain if it has at least one object"} - } - } - - // Delete with given id - req, ok := GetRequestFilterByDomain(userRoles) - if !ok { - return &u.Error{Type: u.ErrUnauthorized, Message: "User does not have permission to delete"} - } - - req["id"] = id - - _, err := WithTransaction(func(ctx mongo.SessionContext) (any, error) { - err := repository.DeleteObject(ctx, entity, req) - if err != nil { - // Unable to delete given id - return nil, err - } - - // Delete possible children - rangeEntities := getChildrenCollections(u.GROUP, entity) - for _, childEnt := range rangeEntities { - childEntName := u.EntityToString(childEnt) - pattern := primitive.Regex{Pattern: "^" + id + u.HN_DELIMETER, Options: ""} - - repository.GetDB().Collection(childEntName).DeleteMany(ctx, - bson.M{"id": pattern}) - } - - return nil, nil - }) - - return err -} - -func DeleteNonHierarchicalObject(entity, slug string) *u.Error { - req := bson.M{"slug": slug} - ctx, cancel := u.Connect() - defer cancel() - return repository.DeleteObject(ctx, entity, req) -} - -func prepareUpdateObject(ctx mongo.SessionContext, entity int, id string, updateData, oldObject map[string]any, userRoles map[string]Role) *u.Error { - // Check user permissions in case domain is being updated - if entity != u.DOMAIN && u.IsEntityHierarchical(entity) && (oldObject["domain"] != updateData["domain"]) { - if perm := CheckUserPermissions(userRoles, entity, updateData["domain"].(string)); perm < WRITE { - return &u.Error{Type: u.ErrUnauthorized, Message: "User does not have permission to change this object"} - } - } - - // tag list edition support - err := addAndRemoveFromTags(entity, id, updateData) - if err != nil { - return err - } - - // Ensure the update is valid - err = ValidateEntity(entity, updateData) - if err != nil { - return err - } - - updateData["lastUpdated"] = primitive.NewDateTimeFromTime(time.Now()) - updateData["createdDate"] = oldObject["createdDate"] - delete(updateData, "parentId") - - if entity == u.TAG { - // tag slug edition support - if updateData["slug"].(string) != oldObject["slug"].(string) { - err := repository.UpdateTagSlugInEntities(ctx, oldObject["slug"].(string), updateData["slug"].(string)) - if err != nil { - return err - } - } - - err := updateTagImage(ctx, oldObject, updateData) - if err != nil { - return err - } - } - - return nil -} - -func UpdateObject(entityStr string, id string, updateData map[string]interface{}, isPatch bool, userRoles map[string]Role, isRecursive bool) (map[string]interface{}, *u.Error) { - var idFilter bson.M - if u.IsEntityNonHierarchical(u.EntityStrToInt(entityStr)) { - idFilter = bson.M{"slug": id} - } else { - idFilter = bson.M{"id": id} - } - - //Update timestamp requires first obj retrieval - //there isn't any way for mongoDB to make a field - //immutable in a document - var oldObj map[string]any - var err *u.Error - if entityStr == u.HIERARCHYOBJS_ENT { - oldObj, err = GetHierarchyObjectById(id, u.RequestFilters{}, userRoles) - if err == nil { - entityStr = oldObj["category"].(string) - } - } else { - oldObj, err = GetObject(idFilter, entityStr, u.RequestFilters{}, userRoles) - } - if err != nil { - return nil, err - } - - entity := u.EntityStrToInt(entityStr) - - // Check if permission is only readonly - if u.IsEntityHierarchical(entity) && oldObj["description"] == nil { - // Description is always present, unless GetEntity was called with readonly permission - return nil, &u.Error{Type: u.ErrUnauthorized, - Message: "User does not have permission to change this object"} - } - - tags, tagsPresent := getTags(updateData) - - // Update old object data with patch data - if isPatch { - if tagsPresent { - return nil, &u.Error{ - Type: u.ErrBadFormat, - Message: "Tags cannot be modified in this way, use tags+ and tags-", - } - } - - var formattedOldObj map[string]interface{} - // Convert primitive.A and similar types - bytes, _ := json.Marshal(oldObj) - json.Unmarshal(bytes, &formattedOldObj) - // Update old with new - err := updateOldObjWithPatch(formattedOldObj, updateData) - if err != nil { - return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} - } - - updateData = formattedOldObj - // Remove API set fields - delete(updateData, "id") - delete(updateData, "lastUpdated") - delete(updateData, "createdDate") - } else if tagsPresent { - err := verifyTagList(tags) - if err != nil { - return nil, err - } - } - - result, err := WithTransaction(func(ctx mongo.SessionContext) (interface{}, error) { - err = prepareUpdateObject(ctx, entity, id, updateData, oldObj, userRoles) - if err != nil { - return nil, err - } - - mongoRes := repository.GetDB().Collection(entityStr).FindOneAndReplace( - ctx, - idFilter, updateData, - options.FindOneAndReplace().SetReturnDocument(options.After), - ) - if mongoRes.Err() != nil { - return nil, mongoRes.Err() - } - - if oldObj["id"] != updateData["id"] { - // Changes to id should be propagated - if err := repository.PropagateParentIdChange( - ctx, - oldObj["id"].(string), - updateData["id"].(string), - entity, - ); err != nil { - return nil, err - } else if entity == u.DOMAIN { - if err := repository.PropagateDomainChange(ctx, - oldObj["id"].(string), - updateData["id"].(string), - ); err != nil { - return nil, err - } - } - } - if u.IsEntityHierarchical(entity) && (oldObj["domain"] != updateData["domain"]) { - if isRecursive { - // Change domain of all children too - if err := repository.PropagateDomainChangeToChildren( - ctx, - updateData["id"].(string), - updateData["domain"].(string), - ); err != nil { - return nil, err - } - } else { - // Check if children domains are compatible - if err := repository.CheckParentDomainChange(entity, updateData["id"].(string), - updateData["domain"].(string)); err != nil { - return nil, err - } - } - } - - return mongoRes, nil - }) - - if err != nil { - return nil, err - } - - var updatedDoc map[string]interface{} - result.(*mongo.SingleResult).Decode(&updatedDoc) - - return fixID(updatedDoc), nil -} - -// GetHierarchyByName: get children objects of given parent. -// - Param limit: max relationship distance between parent and child, example: -// limit=1 only direct children, limit=2 includes nested children of children -func GetHierarchyByName(entity, hierarchyName string, limit int, filters u.RequestFilters) ([]map[string]interface{}, *u.Error) { - // Get all children and their relations - allChildren, hierarchy, err := getChildren(entity, hierarchyName, limit, filters) - if err != nil { - return nil, err - } - - // Organize the family according to relations (nest children) - return recursivelyGetChildrenFromMaps(hierarchyName, hierarchy, allChildren), nil -} - -func getChildren(entity, hierarchyName string, limit int, filters u.RequestFilters) (map[string]interface{}, - map[string][]string, *u.Error) { - allChildren := map[string]interface{}{} - hierarchy := make(map[string][]string) - - // Define in which collections we can find children - rangeEntities := getChildrenCollections(limit, entity) - - // Get children from all given collections - for _, checkEnt := range rangeEntities { - checkEntName := u.EntityToString(checkEnt) - // Obj should include parentName and not surpass limit range - pattern := primitive.Regex{Pattern: "^" + hierarchyName + - "(." + u.NAME_REGEX + "){1," + strconv.Itoa(limit) + "}$", Options: ""} - children, e1 := GetManyObjects(checkEntName, bson.M{"id": pattern}, filters, "", nil) - if e1 != nil { - println("SUBENT: ", checkEntName) - println("ERR: ", e1.Message) - return nil, nil, e1 - } - for _, child := range children { - // store child data - allChildren[child["id"].(string)] = child - // create hierarchy map - fillHierarchyMap(child["id"].(string), hierarchy) - } - } - - return allChildren, hierarchy, nil -} - -// GetHierarchyByCluster: get children devices and vobjs of given cluster -func GetHierarchyByCluster(clusterName string, limit int, filters u.RequestFilters) ([]map[string]interface{}, *u.Error) { - allChildren := map[string]interface{}{} - hierarchy := make(map[string][]string) - - // Get children from device and vobjs - for _, checkEnt := range []int{u.DEVICE, u.VIRTUALOBJ} { - checkEntName := u.EntityToString(checkEnt) - var dbFilter primitive.M - if checkEnt == u.VIRTUALOBJ { - // linked by Id - pattern := primitive.Regex{Pattern: "^" + clusterName + - "(." + u.NAME_REGEX + "){1," + strconv.Itoa(limit) + "}$", Options: ""} - dbFilter = bson.M{"id": pattern} - } else { - // DEVICE links to vobj via virtual config - dbFilter = bson.M{"attributes.virtual_config.clusterId": clusterName} - } - children, e1 := GetManyObjects(checkEntName, dbFilter, filters, "", nil) - if e1 != nil { - return nil, e1 - } - for _, child := range children { - if checkEnt == u.VIRTUALOBJ { - allChildren[child["id"].(string)] = child - fillHierarchyMap(child["id"].(string), hierarchy) - } else { - // add namespace prefix to devices - child["id"] = "Physical." + child["id"].(string) - allChildren[child["id"].(string)] = child - // add as direct child to cluster - hierarchy[clusterName] = append(hierarchy[clusterName], child["id"].(string)) - } - } - } - - // Organize vobj family according to relations (nest children) - return recursivelyGetChildrenFromMaps(clusterName, hierarchy, allChildren), nil -} - -// recursivelyGetChildrenFromMaps: nest children data as the array value of -// its parents "children" key -func recursivelyGetChildrenFromMaps(parentHierarchyName string, hierarchy map[string][]string, - allChildrenData map[string]interface{}) []map[string]interface{} { - var children []map[string]interface{} - for _, childName := range hierarchy[parentHierarchyName] { - // Get the child data and get its own children - child := allChildrenData[childName].(map[string]interface{}) - child["children"] = recursivelyGetChildrenFromMaps(childName, hierarchy, allChildrenData) - children = append(children, child) - } - return children -} - -func GetEntitiesOfAncestor(id string, entStr, wantedEnt string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { - // Get parent object - req := bson.M{"id": id} - _, e := GetObject(req, entStr, u.RequestFilters{}, userRoles) - if e != nil { - return nil, e - } - - // Get sub entity objects - pattern := primitive.Regex{Pattern: "^" + id + u.HN_DELIMETER, Options: ""} - req = bson.M{"id": pattern} - sub, e1 := GetManyObjects(wantedEnt, req, u.RequestFilters{}, "", userRoles) - if e1 != nil { - return nil, e1 - } - - return sub, nil -} - -// SwapEntity: use id to remove object from deleteEnt and then use data to create it in createEnt. -// Propagates id changes to children objects. For atomicity, all is done in a Mongo transaction. -func SwapEntity(createEnt, deleteEnt, id string, data map[string]interface{}, userRoles map[string]Role) *u.Error { - if err := prepareCreateEntity(u.EntityStrToInt(createEnt), data, userRoles); err != nil { - return err - } - - _, err := WithTransaction(func(ctx mongo.SessionContext) (any, error) { - // Create - if _, err := repository.CreateObject(ctx, createEnt, data); err != nil { - return nil, err - } - - // Propagate - if err := repository.PropagateParentIdChange(ctx, id, data["id"].(string), - u.EntityStrToInt(data["category"].(string))); err != nil { - return nil, err - } - - // Delete - if c, err := repository.GetDB().Collection(deleteEnt).DeleteOne(ctx, bson.M{"id": id}); err != nil { - return nil, err - } else if c.DeletedCount == 0 { - return nil, errors.New("Error deleting object: not found") - } - - return nil, nil - }) - - return err -} - -func ExtractCursor(c *mongo.Cursor, ctx context.Context, entity int, userRoles map[string]Role) ([]map[string]interface{}, error) { - ans := []map[string]interface{}{} - for c.Next(ctx) { - x := map[string]interface{}{} - err := c.Decode(x) - if err != nil { - fmt.Println(err.Error()) - return nil, err - } - //Remove _id - x = fixID(x) - if u.IsEntityHierarchical(entity) && userRoles != nil { - //Check permissions - var domain string - if entity == u.DOMAIN { - domain = x["id"].(string) - } else { - domain = x["domain"].(string) - } - if permission := CheckUserPermissions(userRoles, entity, domain); permission >= READONLYNAME { - if permission == READONLYNAME { - x = FixReadOnlyName(x) - } - ans = append(ans, x) - } - } else { - ans = append(ans, x) - } - - } - return ans, nil -} From 53e21c784faef43ddc976e54a09586aaa7dfa66e Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:12:58 +0200 Subject: [PATCH 06/14] refactor(api) extract cursor --- API/models/db.go | 18 ++++++------------ API/models/entity_create.go | 8 +------- API/models/entity_get.go | 20 ++++++-------------- API/models/rbac.go | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/API/models/db.go b/API/models/db.go index 7f38c653e..0006947d0 100644 --- a/API/models/db.go +++ b/API/models/db.go @@ -36,26 +36,20 @@ func ExtractCursor(c *mongo.Cursor, ctx context.Context, entity int, userRoles m fmt.Println(err.Error()) return nil, err } - //Remove _id + // Remove _id x = fixID(x) + // Check permissions if u.IsEntityHierarchical(entity) && userRoles != nil { - //Check permissions - var domain string - if entity == u.DOMAIN { - domain = x["id"].(string) - } else { - domain = x["domain"].(string) + permission := CheckUserPermissionsWithObject(userRoles, entity, x) + if permission == READONLYNAME { + x = FixReadOnlyName(x) } - if permission := CheckUserPermissions(userRoles, entity, domain); permission >= READONLYNAME { - if permission == READONLYNAME { - x = FixReadOnlyName(x) - } + if permission >= READONLYNAME { ans = append(ans, x) } } else { ans = append(ans, x) } - } return ans, nil } diff --git a/API/models/entity_create.go b/API/models/entity_create.go index 51679c97b..ad7eb29f4 100644 --- a/API/models/entity_create.go +++ b/API/models/entity_create.go @@ -47,13 +47,7 @@ func prepareCreateEntity(entity int, t map[string]interface{}, userRoles map[str // Check user permissions if u.IsEntityHierarchical(entity) { - var domain string - if entity == u.DOMAIN { - domain = t["id"].(string) - } else { - domain = t["domain"].(string) - } - if permission := CheckUserPermissions(userRoles, entity, domain); permission < WRITE { + if permission := CheckUserPermissionsWithObject(userRoles, entity, t); permission < WRITE { return &u.Error{Type: u.ErrUnauthorized, Message: "User does not have permission to create this object"} } diff --git a/API/models/entity_get.go b/API/models/entity_get.go index 1f93ecf6a..071ae27eb 100644 --- a/API/models/entity_get.go +++ b/API/models/entity_get.go @@ -29,20 +29,12 @@ func GetObject(req bson.M, entityStr string, filters u.RequestFilters, userRoles } // Check permissions - if u.IsEntityHierarchical(entity) { - var domain string - if entity == u.DOMAIN { - domain = object["id"].(string) - } else { - domain = object["domain"].(string) - } - if userRoles != nil { - if permission := CheckUserPermissions(userRoles, u.EntityStrToInt(entityStr), domain); permission == NONE { - return nil, &u.Error{Type: u.ErrUnauthorized, - Message: "User does not have permission to see this object"} - } else if permission == READONLYNAME { - object = FixReadOnlyName(object) - } + if u.IsEntityHierarchical(entity) && userRoles != nil { + if permission := CheckUserPermissionsWithObject(userRoles, u.EntityStrToInt(entityStr), object); permission == NONE { + return nil, &u.Error{Type: u.ErrUnauthorized, + Message: "User does not have permission to see this object"} + } else if permission == READONLYNAME { + object = FixReadOnlyName(object) } } diff --git a/API/models/rbac.go b/API/models/rbac.go index a5821d04c..b090396e8 100644 --- a/API/models/rbac.go +++ b/API/models/rbac.go @@ -64,6 +64,10 @@ func GetRequestFilterByDomain(userRoles map[string]Role) (bson.M, bool) { } } +func CheckUserPermissionsWithObject(userRoles map[string]Role, objEntity int, object map[string]any) Permission { + return CheckUserPermissions(userRoles, objEntity, getDomainFromObject(objEntity, object)) +} + func CheckUserPermissions(userRoles map[string]Role, objEntity int, objDomain string) Permission { permission := NONE if objEntity == u.DOMAIN { @@ -126,3 +130,14 @@ func CheckCanManageUser(callerRoles map[string]Role, newUserRoles map[string]Rol } return true } + +// Helpers +func getDomainFromObject(entity int, object map[string]any) string { + var domain string + if entity == u.DOMAIN { + domain = object["id"].(string) + } else { + domain = object["domain"].(string) + } + return domain +} From 156f3970a849559ecf5ea64b6c2581b9d17346a9 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:40:04 +0200 Subject: [PATCH 07/14] refactor(api) get many objects --- API/models/entity_get.go | 32 +++++++------------------------- API/models/filter.go | 17 +++++++++++++++++ API/repository/entity.go | 12 +----------- API/repository/filters.go | 27 +++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 36 deletions(-) diff --git a/API/models/entity_get.go b/API/models/entity_get.go index 071ae27eb..7e0bb49bd 100644 --- a/API/models/entity_get.go +++ b/API/models/entity_get.go @@ -2,7 +2,6 @@ package models import ( "fmt" - "maps" "p3/repository" u "p3/utils" @@ -46,34 +45,16 @@ func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, comp var err error var c *mongo.Cursor - var opts *options.FindOptions - if len(filters.FieldsToShow) > 0 { - compoundIndex := bson.D{bson.E{Key: "domain", Value: 1}, bson.E{Key: "id", Value: 1}} - for _, field := range filters.FieldsToShow { - if field != "domain" && field != "id" { - compoundIndex = append(compoundIndex, bson.E{Key: field, Value: 1}) - } - } - opts = options.Find().SetProjection(compoundIndex) - } - err = repository.GetDateFilters(req, filters.StartDate, filters.EndDate) - if err != nil { + // Filters + opts := repository.GetFieldsToShowFilter(filters.FieldsToShow) + if err := repository.GetDateFilters(req, filters.StartDate, filters.EndDate); err != nil { return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} } - - if complexFilterExp != "" { - if complexFilters, err := ComplexFilterToMap(complexFilterExp); err != nil { - return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} - } else { - err = getDatesFromComplexFilters(complexFilters) - if err != nil { - return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} - } - u.ApplyWildcardsOnComplexFilter(complexFilters) - maps.Copy(req, complexFilters) - } + if err := ApplyComplexFilter(complexFilterExp, req); err != nil { + return nil, err } + // Find if opts != nil { c, err = repository.GetDB().Collection(entityStr).Find(ctx, req, opts) } else { @@ -85,6 +66,7 @@ func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, comp } defer cancel() + // Format entity := u.EntityStrToInt(entityStr) data, e1 := ExtractCursor(c, ctx, entity, userRoles) if e1 != nil { diff --git a/API/models/filter.go b/API/models/filter.go index da299166a..f7a798b9e 100644 --- a/API/models/filter.go +++ b/API/models/filter.go @@ -3,12 +3,29 @@ package models import ( "errors" "fmt" + "maps" u "p3/utils" "regexp" "strings" "time" ) +func ApplyComplexFilter(complexFilterExp string, req map[string]any) *u.Error { + if complexFilterExp != "" { + if complexFilters, err := ComplexFilterToMap(complexFilterExp); err != nil { + return &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } else { + err = getDatesFromComplexFilters(complexFilters) + if err != nil { + return &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } + u.ApplyWildcardsOnComplexFilter(complexFilters) + maps.Copy(req, complexFilters) + } + } + return nil +} + func ComplexFilterToMap(complexFilter string) (map[string]any, error) { // Split the input string into individual filter expressions chars := []string{"(", ")", "&", "|"} diff --git a/API/repository/entity.go b/API/repository/entity.go index e3f2c29d7..bab6fd8e0 100644 --- a/API/repository/entity.go +++ b/API/repository/entity.go @@ -7,7 +7,6 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" u "p3/utils" ) @@ -17,16 +16,7 @@ func GetObject(req bson.M, ent string, filters u.RequestFilters) (map[string]int defer cancel() t := map[string]interface{}{} - var opts *options.FindOneOptions - if len(filters.FieldsToShow) > 0 { - compoundIndex := bson.D{bson.E{Key: "domain", Value: 1}, bson.E{Key: "id", Value: 1}} - for _, field := range filters.FieldsToShow { - if field != "domain" && field != "id" { - compoundIndex = append(compoundIndex, bson.E{Key: field, Value: 1}) - } - } - opts = options.FindOne().SetProjection(compoundIndex) - } + opts := GetFieldsToShowOneFilter(filters.FieldsToShow) err := GetDateFilters(req, filters.StartDate, filters.EndDate) if err != nil { diff --git a/API/repository/filters.go b/API/repository/filters.go index 65cf14665..4a186a97e 100644 --- a/API/repository/filters.go +++ b/API/repository/filters.go @@ -6,6 +6,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" ) func GetDateFilters(req bson.M, startDate string, endDate string) error { @@ -40,3 +41,29 @@ func GroupContentToOrFilter(content []any, parentId string) primitive.M { filter := bson.M{"$or": orReq} return filter } + +func GetFieldsToShowFilter(fieldsToShow []string) *options.FindOptions { + var opts *options.FindOptions + if len(fieldsToShow) > 0 { + opts = options.Find().SetProjection(GetFieldsToShowCompoundIndex(fieldsToShow)) + } + return opts +} + +func GetFieldsToShowOneFilter(fieldsToShow []string) *options.FindOneOptions { + var opts *options.FindOneOptions + if len(fieldsToShow) > 0 { + opts = options.FindOne().SetProjection(GetFieldsToShowCompoundIndex(fieldsToShow)) + } + return opts +} + +func GetFieldsToShowCompoundIndex(fieldsToShow []string) primitive.D { + compoundIndex := bson.D{bson.E{Key: "domain", Value: 1}, bson.E{Key: "id", Value: 1}} + for _, field := range fieldsToShow { + if field != "domain" && field != "id" { + compoundIndex = append(compoundIndex, bson.E{Key: field, Value: 1}) + } + } + return compoundIndex +} From fcf3b7a2c87ffa55a57bf82aeabf84a9fe90fb61 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:50:51 +0200 Subject: [PATCH 08/14] refacto(api) get hierarchy --- API/controllers/entity.go | 8 +- API/models/entity_get.go | 16 ++ API/models/entity_get_attribute.go | 66 +++++ API/models/entity_get_hierarchy.go | 379 ++++++++++++----------------- API/models/entity_update.go | 2 +- 5 files changed, 247 insertions(+), 224 deletions(-) create mode 100644 API/models/entity_get_attribute.go diff --git a/API/controllers/entity.go b/API/controllers/entity.go index 942b72f9b..e24373337 100644 --- a/API/controllers/entity.go +++ b/API/controllers/entity.go @@ -790,7 +790,7 @@ func GetEntity(w http.ResponseWriter, r *http.Request) { if id, canParse = mux.Vars(r)["id"]; canParse { var req primitive.M if entityStr == u.HIERARCHYOBJS_ENT { - data, modelErr = models.GetHierarchyObjectById(id, filters, user.Roles) + data, modelErr = models.GetHierarchicalObjectById(id, filters, user.Roles) } else { if u.IsEntityNonHierarchical(u.EntityStrToInt(entityStr)) { // Get by slug @@ -1108,7 +1108,7 @@ func DeleteEntity(w http.ResponseWriter, r *http.Request) { u.ErrLog("Error while parsing path parameters", "DELETE ENTITY", "", r) } else { if entityStr == u.HIERARCHYOBJS_ENT { - obj, err := models.GetHierarchyObjectById(id, u.RequestFilters{}, user.Roles) + obj, err := models.GetHierarchicalObjectById(id, u.RequestFilters{}, user.Roles) if err != nil { u.ErrLog("Error finding hierarchy obj to delete", "DELETE ENTITY", err.Message, r) u.RespondWithError(w, err) @@ -1639,7 +1639,7 @@ func GetHierarchyByName(w http.ResponseWriter, r *http.Request) { var data map[string]interface{} if entity == u.HIERARCHYOBJS_ENT { // Generic endpoint only for physical objs - data, modelErr = models.GetHierarchyObjectById(id, filters, user.Roles) + data, modelErr = models.GetHierarchicalObjectById(id, filters, user.Roles) if modelErr == nil { entity = data["category"].(string) } @@ -1879,7 +1879,7 @@ func LinkEntity(w http.ResponseWriter, r *http.Request) { // Get entity if id, canParse = mux.Vars(r)["id"]; canParse { if entityStr == u.HIERARCHYOBJS_ENT { - data, modelErr = models.GetHierarchyObjectById(id, u.RequestFilters{}, user.Roles) + data, modelErr = models.GetHierarchicalObjectById(id, u.RequestFilters{}, user.Roles) } else { data, modelErr = models.GetObject(bson.M{"id": id}, entityStr, u.RequestFilters{}, user.Roles) } diff --git a/API/models/entity_get.go b/API/models/entity_get.go index 7e0bb49bd..ed68919e0 100644 --- a/API/models/entity_get.go +++ b/API/models/entity_get.go @@ -83,6 +83,22 @@ func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, comp return data, nil } +func GetHierarchicalObjectById(hierarchyName string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { + // Get possible collections for this name + rangeEntities := u.GetEntitiesById(u.PHierarchy, hierarchyName) + req := bson.M{"id": hierarchyName} + + // Search each collection + for _, entityStr := range rangeEntities { + data, _ := GetObject(req, entityStr, filters, userRoles) + if data != nil { + return data, nil + } + } + + return nil, &u.Error{Type: u.ErrNotFound, Message: "Unable to find object"} +} + func GetEntityCount(entity int) int64 { ent := u.EntityToString(entity) ctx, cancel := u.Connect() diff --git a/API/models/entity_get_attribute.go b/API/models/entity_get_attribute.go new file mode 100644 index 000000000..64ca7e7a2 --- /dev/null +++ b/API/models/entity_get_attribute.go @@ -0,0 +1,66 @@ +package models + +import ( + "fmt" + "p3/repository" + u "p3/utils" + "strings" + + "go.mongodb.org/mongo-driver/bson" +) + +// GetSiteParentAttribute: search for the object of given ID, +// then search for its site parent and return its requested attribute +func GetSiteParentAttribute(id string, attribute string) (map[string]any, *u.Error) { + data := map[string]interface{}{} + + // Get all collections names + ctx, cancel := u.Connect() + db := repository.GetDB() + collNames, err := db.ListCollectionNames(ctx, bson.D{}) + if err != nil { + fmt.Println(err.Error()) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + // Find object + for _, collName := range collNames { + err := db.Collection(collName).FindOne(ctx, bson.M{"id": id}).Decode(&data) + if err == nil { + // Found object with given id + if data["category"].(string) == "site" { + // it's a site + break + } else { + // Find its parent site + nameSlice := strings.Split(data["id"].(string), u.HN_DELIMETER) + siteName := nameSlice[0] // CONSIDER SITE AS 0 + err := db.Collection("site").FindOne(ctx, bson.M{"id": siteName}).Decode(&data) + if err != nil { + return nil, &u.Error{Type: u.ErrNotFound, + Message: "Could not find parent site for given object"} + } + } + } + } + + defer cancel() + + if len(data) == 0 { + return nil, &u.Error{Type: u.ErrNotFound, Message: "No object found with given id"} + } else if attribute == "sitecolors" { + resp := map[string]any{} + for _, colorName := range []string{"reservedColor", "technicalColor", "usableColor"} { + if color := data["attributes"].(map[string]interface{})[colorName]; color != nil { + resp[colorName] = color + } else { + resp[colorName] = "" + } + } + return resp, nil + } else if attrValue := data["attributes"].(map[string]interface{})[attribute]; attrValue == nil { + return nil, &u.Error{Type: u.ErrNotFound, + Message: "Parent site has no temperatureUnit in attributes"} + } else { + return map[string]any{attribute: attrValue}, nil + } +} diff --git a/API/models/entity_get_hierarchy.go b/API/models/entity_get_hierarchy.go index 22a1da698..c90347daa 100644 --- a/API/models/entity_get_hierarchy.go +++ b/API/models/entity_get_hierarchy.go @@ -12,63 +12,25 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) -func GetHierarchyObjectById(hierarchyName string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { - // Get possible collections for this name - rangeEntities := u.GetEntitiesById(u.PHierarchy, hierarchyName) - req := bson.M{"id": hierarchyName} - - // Search each collection - for _, entityStr := range rangeEntities { - data, _ := GetObject(req, entityStr, filters, userRoles) - if data != nil { - return data, nil - } - } - - return nil, &u.Error{Type: u.ErrNotFound, Message: "Unable to find object"} -} - -// GetCompleteHierarchy: gets all objects in db using hierachyName and returns: -// - tree: map with parents as key and their children as an array value -// tree: {parent:[children]} -// - categories: map with category name as key and corresponding objects -// as an array value -// categories: {categoryName:[children]} -func GetCompleteHierarchy(userRoles map[string]Role, filters u.HierarchyFilters) (map[string]interface{}, *u.Error) { - response := make(map[string]interface{}) - categories := make(map[string][]string) - hierarchy := make(map[string]interface{}) - - switch filters.Namespace { - case u.Any: - for _, ns := range []u.Namespace{u.Physical, u.Logical, u.Organisational} { - data, err := getHierarchyWithNamespace(ns, userRoles, filters, categories) - if err != nil { - return nil, err - } - hierarchy[u.NamespaceToString(ns)] = data - } - default: - data, err := getHierarchyWithNamespace(filters.Namespace, userRoles, filters, categories) - if err != nil { - return nil, err - } - hierarchy[u.NamespaceToString(filters.Namespace)] = data +const rootIdx = "*" +// GetHierarchyByName: get children objects of given parent. +// - Param limit: max relationship distance between parent and child, example: +// limit=1 only direct children, limit=2 includes nested children of children +func GetHierarchyByName(entity, hierarchyName string, limit int, filters u.RequestFilters) ([]map[string]interface{}, *u.Error) { + // Get all children and their relations + allChildren, hierarchy, err := getChildren(entity, hierarchyName, limit, filters) + if err != nil { + return nil, err } - response["tree"] = hierarchy - if filters.WithCategories { - categories["KeysOrder"] = []string{"site", "building", "room", "rack"} - response["categories"] = categories - } - return response, nil + // Organize the family according to relations (nest children) + return recursivelyGetChildrenFromMaps(hierarchyName, hierarchy, allChildren), nil } -func getHierarchyWithNamespace(namespace u.Namespace, userRoles map[string]Role, filters u.HierarchyFilters, +func getHierarchyByNamespace(namespace u.Namespace, userRoles map[string]Role, filters u.HierarchyFilters, categories map[string][]string) (map[string][]string, *u.Error) { hierarchy := make(map[string][]string) - rootIdx := "*" ctx, cancel := u.Connect() db := repository.GetDB() @@ -91,15 +53,12 @@ func getHierarchyWithNamespace(namespace u.Namespace, userRoles map[string]Role, // Search collections according to namespace entities := u.GetEntitiesById(namespace, "") - for _, entityName := range entities { // Get data opts := options.Find().SetProjection(bson.D{{Key: "domain", Value: 1}, {Key: "id", Value: 1}, {Key: "category", Value: 1}}) - if u.IsEntityNonHierarchical(u.EntityStrToInt(entityName)) { opts = options.Find().SetProjection(bson.D{{Key: "slug", Value: 1}}) } - c, err := db.Collection(entityName).Find(ctx, dbFilter, opts) if err != nil { println(err.Error()) @@ -111,68 +70,94 @@ func getHierarchyWithNamespace(namespace u.Namespace, userRoles map[string]Role, return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} } - // Format data - for _, obj := range data { - if strings.Contains(u.NamespaceToString(namespace), string(u.Logical)) { - // Logical - var objId string - if u.IsEntityNonHierarchical(u.EntityStrToInt(entityName)) { - objId = obj["slug"].(string) - hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) - } else { - objId = obj["id"].(string) - categories[entityName] = append(categories[entityName], objId) - if strings.Contains(objId, ".") && obj["category"] != "group" { - // Physical or Org Children - fillHierarchyMap(objId, hierarchy) - } else { - hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) - } - } - } else if strings.Contains(obj["id"].(string), ".") { - // Physical or Org Children - categories[entityName] = append(categories[entityName], obj["id"].(string)) - fillHierarchyMap(obj["id"].(string), hierarchy) + // Fill hierarchy with formatted data + fillHierharchyData(data, namespace, entityName, hierarchy, categories) + } + + // For the root of VIRTUAL objects we also need to check for devices + if err := fillVirtualHierarchyByVconfig(userRoles, hierarchy, categories); err != nil { + return nil, err + } + + defer cancel() + return hierarchy, nil +} + +// GetHierarchyByCluster: get children devices and vobjs of given cluster +func GetHierarchyByCluster(clusterName string, limit int, filters u.RequestFilters) ([]map[string]interface{}, *u.Error) { + allChildren := map[string]interface{}{} + hierarchy := make(map[string][]string) + + // Get children from device and vobjs + for _, checkEnt := range []int{u.DEVICE, u.VIRTUALOBJ} { + checkEntName := u.EntityToString(checkEnt) + var dbFilter primitive.M + if checkEnt == u.VIRTUALOBJ { + // linked by Id + pattern := primitive.Regex{Pattern: "^" + clusterName + + "(." + u.NAME_REGEX + "){1," + strconv.Itoa(limit) + "}$", Options: ""} + dbFilter = bson.M{"id": pattern} + } else { + // DEVICE links to vobj via virtual config + dbFilter = bson.M{"attributes.virtual_config.clusterId": clusterName} + } + children, e1 := GetManyObjects(checkEntName, dbFilter, filters, "", nil) + if e1 != nil { + return nil, e1 + } + for _, child := range children { + if checkEnt == u.VIRTUALOBJ { + allChildren[child["id"].(string)] = child + fillHierarchyMap(child["id"].(string), hierarchy) } else { - // Physical or Org Roots - objId := obj["id"].(string) - categories[entityName] = append(categories[entityName], objId) - if u.EntityStrToInt(entityName) == u.STRAYOBJ { - hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) - } else if u.EntityStrToInt(entityName) != u.VIRTUALOBJ { - hierarchy[rootIdx] = append(hierarchy[rootIdx], objId) - } + // add namespace prefix to devices + child["id"] = "Physical." + child["id"].(string) + allChildren[child["id"].(string)] = child + // add as direct child to cluster + hierarchy[clusterName] = append(hierarchy[clusterName], child["id"].(string)) } } } - // For the root of VIRTUAL objects we also need to check for devices - for _, vobj := range hierarchy[rootIdx+u.EntityToString(u.VIRTUALOBJ)] { - if !strings.ContainsAny(vobj, u.HN_DELIMETER) { // is stray virtual - // Get device linked to this vobj through its virtual config - entityName := u.EntityToString(u.DEVICE) - dbFilter := bson.M{"attributes.virtual_config.clusterId": vobj} - opts := options.Find().SetProjection(bson.D{{Key: "domain", Value: 1}, {Key: "id", Value: 1}, {Key: "category", Value: 1}}) - c, err := db.Collection(entityName).Find(ctx, dbFilter, opts) + // Organize vobj family according to relations (nest children) + return recursivelyGetChildrenFromMaps(clusterName, hierarchy, allChildren), nil +} + +// GetCompleteHierarchy: gets all objects in db using hierachyName and returns: +// - tree: map with parents as key and their children as an array value +// tree: {parent:[children]} +// - categories: map with category name as key and corresponding objects +// as an array value +// categories: {categoryName:[children]} +func GetCompleteHierarchy(userRoles map[string]Role, filters u.HierarchyFilters) (map[string]interface{}, *u.Error) { + response := make(map[string]interface{}) + categories := make(map[string][]string) + hierarchy := make(map[string]interface{}) + + switch filters.Namespace { + case u.Any: + for _, ns := range []u.Namespace{u.Physical, u.Logical, u.Organisational} { + data, err := getHierarchyByNamespace(ns, userRoles, filters, categories) if err != nil { - println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - } - data, e := ExtractCursor(c, ctx, u.EntityStrToInt(entityName), userRoles) - if e != nil { - return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} - } - // Add data - for _, obj := range data { - objId := obj["id"].(string) - categories[entityName] = append(categories[entityName], objId) - hierarchy[vobj] = append(hierarchy[vobj], objId) + return nil, err } + hierarchy[u.NamespaceToString(ns)] = data + } + default: + data, err := getHierarchyByNamespace(filters.Namespace, userRoles, filters, categories) + if err != nil { + return nil, err } + hierarchy[u.NamespaceToString(filters.Namespace)] = data + } - defer cancel() - return hierarchy, nil + response["tree"] = hierarchy + if filters.WithCategories { + categories["KeysOrder"] = []string{"site", "building", "room", "rack"} + response["categories"] = categories + } + return response, nil } func GetCompleteHierarchyAttributes(userRoles map[string]Role) (map[string]interface{}, *u.Error) { @@ -219,18 +204,23 @@ func GetCompleteHierarchyAttributes(userRoles map[string]Role) (map[string]inter return response, nil } -// GetHierarchyByName: get children objects of given parent. -// - Param limit: max relationship distance between parent and child, example: -// limit=1 only direct children, limit=2 includes nested children of children -func GetHierarchyByName(entity, hierarchyName string, limit int, filters u.RequestFilters) ([]map[string]interface{}, *u.Error) { - // Get all children and their relations - allChildren, hierarchy, err := getChildren(entity, hierarchyName, limit, filters) - if err != nil { - return nil, err +func GetEntitiesOfAncestor(id string, entStr, wantedEnt string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { + // Get parent object + req := bson.M{"id": id} + _, e := GetObject(req, entStr, u.RequestFilters{}, userRoles) + if e != nil { + return nil, e } - // Organize the family according to relations (nest children) - return recursivelyGetChildrenFromMaps(hierarchyName, hierarchy, allChildren), nil + // Get sub entity objects + pattern := primitive.Regex{Pattern: "^" + id + u.HN_DELIMETER, Options: ""} + req = bson.M{"id": pattern} + sub, e1 := GetManyObjects(wantedEnt, req, u.RequestFilters{}, "", userRoles) + if e1 != nil { + return nil, e1 + } + + return sub, nil } func getChildren(entity, hierarchyName string, limit int, filters u.RequestFilters) (map[string]interface{}, @@ -264,46 +254,6 @@ func getChildren(entity, hierarchyName string, limit int, filters u.RequestFilte return allChildren, hierarchy, nil } -// GetHierarchyByCluster: get children devices and vobjs of given cluster -func GetHierarchyByCluster(clusterName string, limit int, filters u.RequestFilters) ([]map[string]interface{}, *u.Error) { - allChildren := map[string]interface{}{} - hierarchy := make(map[string][]string) - - // Get children from device and vobjs - for _, checkEnt := range []int{u.DEVICE, u.VIRTUALOBJ} { - checkEntName := u.EntityToString(checkEnt) - var dbFilter primitive.M - if checkEnt == u.VIRTUALOBJ { - // linked by Id - pattern := primitive.Regex{Pattern: "^" + clusterName + - "(." + u.NAME_REGEX + "){1," + strconv.Itoa(limit) + "}$", Options: ""} - dbFilter = bson.M{"id": pattern} - } else { - // DEVICE links to vobj via virtual config - dbFilter = bson.M{"attributes.virtual_config.clusterId": clusterName} - } - children, e1 := GetManyObjects(checkEntName, dbFilter, filters, "", nil) - if e1 != nil { - return nil, e1 - } - for _, child := range children { - if checkEnt == u.VIRTUALOBJ { - allChildren[child["id"].(string)] = child - fillHierarchyMap(child["id"].(string), hierarchy) - } else { - // add namespace prefix to devices - child["id"] = "Physical." + child["id"].(string) - allChildren[child["id"].(string)] = child - // add as direct child to cluster - hierarchy[clusterName] = append(hierarchy[clusterName], child["id"].(string)) - } - } - } - - // Organize vobj family according to relations (nest children) - return recursivelyGetChildrenFromMaps(clusterName, hierarchy, allChildren), nil -} - // recursivelyGetChildrenFromMaps: nest children data as the array value of // its parents "children" key func recursivelyGetChildrenFromMaps(parentHierarchyName string, hierarchy map[string][]string, @@ -360,77 +310,68 @@ func getChildrenCollections(limit int, parentEntStr string) []int { return rangeEntities } -// GetSiteParentAttribute: search for the object of given ID, -// then search for its site parent and return its requested attribute -func GetSiteParentAttribute(id string, attribute string) (map[string]any, *u.Error) { - data := map[string]interface{}{} - - // Get all collections names - ctx, cancel := u.Connect() - db := repository.GetDB() - collNames, err := db.ListCollectionNames(ctx, bson.D{}) - if err != nil { - fmt.Println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - } - // Find object - for _, collName := range collNames { - err := db.Collection(collName).FindOne(ctx, bson.M{"id": id}).Decode(&data) - if err == nil { - // Found object with given id - if data["category"].(string) == "site" { - // it's a site - break +func fillHierharchyData(data []map[string]any, namespace u.Namespace, entityName string, hierarchy, categories map[string][]string) { + for _, obj := range data { + if strings.Contains(u.NamespaceToString(namespace), string(u.Logical)) { + // Logical + var objId string + if u.IsEntityNonHierarchical(u.EntityStrToInt(entityName)) { + objId = obj["slug"].(string) + hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) } else { - // Find its parent site - nameSlice := strings.Split(data["id"].(string), u.HN_DELIMETER) - siteName := nameSlice[0] // CONSIDER SITE AS 0 - err := db.Collection("site").FindOne(ctx, bson.M{"id": siteName}).Decode(&data) - if err != nil { - return nil, &u.Error{Type: u.ErrNotFound, - Message: "Could not find parent site for given object"} + objId = obj["id"].(string) + categories[entityName] = append(categories[entityName], objId) + if strings.Contains(objId, ".") && obj["category"] != "group" { + // Physical or Org Children + fillHierarchyMap(objId, hierarchy) + } else { + hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) } } - } - } - - defer cancel() - - if len(data) == 0 { - return nil, &u.Error{Type: u.ErrNotFound, Message: "No object found with given id"} - } else if attribute == "sitecolors" { - resp := map[string]any{} - for _, colorName := range []string{"reservedColor", "technicalColor", "usableColor"} { - if color := data["attributes"].(map[string]interface{})[colorName]; color != nil { - resp[colorName] = color - } else { - resp[colorName] = "" + } else if strings.Contains(obj["id"].(string), ".") { + // Physical or Org Children + categories[entityName] = append(categories[entityName], obj["id"].(string)) + fillHierarchyMap(obj["id"].(string), hierarchy) + } else { + // Physical or Org Roots + objId := obj["id"].(string) + categories[entityName] = append(categories[entityName], objId) + if u.EntityStrToInt(entityName) == u.STRAYOBJ { + hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) + } else if u.EntityStrToInt(entityName) != u.VIRTUALOBJ { + hierarchy[rootIdx] = append(hierarchy[rootIdx], objId) } } - return resp, nil - } else if attrValue := data["attributes"].(map[string]interface{})[attribute]; attrValue == nil { - return nil, &u.Error{Type: u.ErrNotFound, - Message: "Parent site has no temperatureUnit in attributes"} - } else { - return map[string]any{attribute: attrValue}, nil } } -func GetEntitiesOfAncestor(id string, entStr, wantedEnt string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { - // Get parent object - req := bson.M{"id": id} - _, e := GetObject(req, entStr, u.RequestFilters{}, userRoles) - if e != nil { - return nil, e - } - - // Get sub entity objects - pattern := primitive.Regex{Pattern: "^" + id + u.HN_DELIMETER, Options: ""} - req = bson.M{"id": pattern} - sub, e1 := GetManyObjects(wantedEnt, req, u.RequestFilters{}, "", userRoles) - if e1 != nil { - return nil, e1 +func fillVirtualHierarchyByVconfig(userRoles map[string]Role, hierarchy, categories map[string][]string) *u.Error { + ctx, cancel := u.Connect() + db := repository.GetDB() + // For the root of VIRTUAL objects we also need to check for devices + for _, vobj := range hierarchy[rootIdx+u.EntityToString(u.VIRTUALOBJ)] { + if !strings.ContainsAny(vobj, u.HN_DELIMETER) { // is stray virtual + // Get device linked to this vobj through its virtual config + entityName := u.EntityToString(u.DEVICE) + dbFilter := bson.M{"attributes.virtual_config.clusterId": vobj} + opts := options.Find().SetProjection(bson.D{{Key: "domain", Value: 1}, {Key: "id", Value: 1}, {Key: "category", Value: 1}}) + c, err := db.Collection(entityName).Find(ctx, dbFilter, opts) + if err != nil { + println(err.Error()) + return &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + data, e := ExtractCursor(c, ctx, u.EntityStrToInt(entityName), userRoles) + if e != nil { + return &u.Error{Type: u.ErrInternal, Message: e.Error()} + } + // Add data + for _, obj := range data { + objId := obj["id"].(string) + categories[entityName] = append(categories[entityName], objId) + hierarchy[vobj] = append(hierarchy[vobj], objId) + } + } } - - return sub, nil + defer cancel() + return nil } diff --git a/API/models/entity_update.go b/API/models/entity_update.go index 989e15579..7bb22cdd4 100644 --- a/API/models/entity_update.go +++ b/API/models/entity_update.go @@ -31,7 +31,7 @@ func UpdateObject(entityStr string, id string, updateData map[string]interface{} var oldObj map[string]any var err *u.Error if entityStr == u.HIERARCHYOBJS_ENT { - oldObj, err = GetHierarchyObjectById(id, u.RequestFilters{}, userRoles) + oldObj, err = GetHierarchicalObjectById(id, u.RequestFilters{}, userRoles) if err == nil { entityStr = oldObj["category"].(string) } From 06c743b01669a4b49f8b5229fc6491e6769a875d Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Mon, 22 Jul 2024 12:02:30 +0200 Subject: [PATCH 09/14] refactor(api) get hierarchy --- API/models/entity_get_hierarchy.go | 54 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/API/models/entity_get_hierarchy.go b/API/models/entity_get_hierarchy.go index c90347daa..61cf8d16c 100644 --- a/API/models/entity_get_hierarchy.go +++ b/API/models/entity_get_hierarchy.go @@ -28,6 +28,7 @@ func GetHierarchyByName(entity, hierarchyName string, limit int, filters u.Reque return recursivelyGetChildrenFromMaps(hierarchyName, hierarchy, allChildren), nil } +// getHierarchyByNamespace: get complete hierarchy of a given namespace func getHierarchyByNamespace(namespace u.Namespace, userRoles map[string]Role, filters u.HierarchyFilters, categories map[string][]string) (map[string][]string, *u.Error) { hierarchy := make(map[string][]string) @@ -160,6 +161,7 @@ func GetCompleteHierarchy(userRoles map[string]Role, filters u.HierarchyFilters) return response, nil } +// GetCompleteHierarchyAttributes: get all objects with all its attributes func GetCompleteHierarchyAttributes(userRoles map[string]Role) (map[string]interface{}, *u.Error) { response := make(map[string]interface{}) // Get all collections names @@ -173,32 +175,37 @@ func GetCompleteHierarchyAttributes(userRoles map[string]Role) (map[string]inter } for _, collName := range collNames { - if entInt := u.EntityStrToInt(collName); entInt > -1 { - projection := bson.D{{Key: "attributes", Value: 1}, - {Key: "domain", Value: 1}, {Key: "id", Value: 1}} + var entInt int + if entInt = u.EntityStrToInt(collName); entInt == -1 { + continue + } - opts := options.Find().SetProjection(projection) + // Get attributes + projection := bson.D{{Key: "attributes", Value: 1}, + {Key: "domain", Value: 1}, {Key: "id", Value: 1}} + opts := options.Find().SetProjection(projection) + c, err := db.Collection(collName).Find(ctx, bson.M{}, opts) + if err != nil { + println(err.Error()) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + data, e := ExtractCursor(c, ctx, entInt, userRoles) + if e != nil { + return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} + } - c, err := db.Collection(collName).Find(ctx, bson.M{}, opts) - if err != nil { - println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} - } - data, e := ExtractCursor(c, ctx, entInt, userRoles) - if e != nil { - return nil, &u.Error{Type: u.ErrInternal, Message: e.Error()} + // Add to response + for _, obj := range data { + if obj["attributes"] == nil { + continue } - - for _, obj := range data { - if obj["attributes"] != nil { - if id, isStr := obj["id"].(string); isStr && id != "" { - response[obj["id"].(string)] = obj["attributes"] - } else if obj["name"] != nil { - response[obj["name"].(string)] = obj["attributes"] - } - } + if id, isStr := obj["id"].(string); isStr && id != "" { + response[id] = obj["attributes"] + } else if obj["name"] != nil { + response[obj["name"].(string)] = obj["attributes"] } } + } defer cancel() return response, nil @@ -314,12 +321,11 @@ func fillHierharchyData(data []map[string]any, namespace u.Namespace, entityName for _, obj := range data { if strings.Contains(u.NamespaceToString(namespace), string(u.Logical)) { // Logical - var objId string if u.IsEntityNonHierarchical(u.EntityStrToInt(entityName)) { - objId = obj["slug"].(string) + objId := obj["slug"].(string) hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) } else { - objId = obj["id"].(string) + objId := obj["id"].(string) categories[entityName] = append(categories[entityName], objId) if strings.Contains(objId, ".") && obj["category"] != "group" { // Physical or Org Children From 7f23c9a240cd4d3bc75fb7b4fcb22882b6ae2bcf Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Mon, 22 Jul 2024 15:14:43 +0200 Subject: [PATCH 10/14] refactor(api) update --- API/controllers/entity.go | 15 +-- API/models/db.go | 12 ++ API/models/entity_get.go | 41 ++++--- API/models/entity_get_attribute.go | 29 +++-- API/models/entity_update.go | 180 +++++++++++++++-------------- 5 files changed, 146 insertions(+), 131 deletions(-) diff --git a/API/controllers/entity.go b/API/controllers/entity.go index e24373337..7806f74f2 100644 --- a/API/controllers/entity.go +++ b/API/controllers/entity.go @@ -788,20 +788,7 @@ func GetEntity(w http.ResponseWriter, r *http.Request) { // Get entity if id, canParse = mux.Vars(r)["id"]; canParse { - var req primitive.M - if entityStr == u.HIERARCHYOBJS_ENT { - data, modelErr = models.GetHierarchicalObjectById(id, filters, user.Roles) - } else { - if u.IsEntityNonHierarchical(u.EntityStrToInt(entityStr)) { - // Get by slug - req = bson.M{"slug": id} - - } else { - req = bson.M{"id": id} - } - - data, modelErr = models.GetObject(req, entityStr, filters, user.Roles) - } + data, modelErr = models.GetObjectById(id, entityStr, filters, user.Roles) } else { w.WriteHeader(http.StatusBadRequest) u.Respond(w, u.Message("Error while parsing path parameters")) diff --git a/API/models/db.go b/API/models/db.go index 0006947d0..eafa1d280 100644 --- a/API/models/db.go +++ b/API/models/db.go @@ -7,6 +7,8 @@ import ( u "p3/utils" "strings" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" ) @@ -105,3 +107,13 @@ func castResult[T any](result any) T { return result.(T) } + +func GetIdReqByEntity(entityStr, id string) primitive.M { + var idFilter primitive.M + if u.IsEntityNonHierarchical(u.EntityStrToInt(entityStr)) { + idFilter = bson.M{"slug": id} + } else { + idFilter = bson.M{"id": id} + } + return idFilter +} diff --git a/API/models/entity_get.go b/API/models/entity_get.go index ed68919e0..f0bfee027 100644 --- a/API/models/entity_get.go +++ b/API/models/entity_get.go @@ -11,6 +11,15 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +func GetObjectById(id, entityStr string, filters u.RequestFilters, userRoles map[string]Role) (map[string]any, *u.Error) { + if entityStr == u.HIERARCHYOBJS_ENT { + return GetHierarchicalObjectById(id, filters, userRoles) + } else { + req := GetIdReqByEntity(entityStr, id) + return GetObject(req, entityStr, filters, userRoles) + } +} + func GetObject(req bson.M, entityStr string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { object, err := repository.GetObject(req, entityStr, filters) @@ -40,6 +49,22 @@ func GetObject(req bson.M, entityStr string, filters u.RequestFilters, userRoles return object, nil } +func GetHierarchicalObjectById(hierarchyName string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { + // Get possible collections for this name + rangeEntities := u.GetEntitiesById(u.PHierarchy, hierarchyName) + req := bson.M{"id": hierarchyName} + + // Search each collection + for _, entityStr := range rangeEntities { + data, _ := GetObject(req, entityStr, filters, userRoles) + if data != nil { + return data, nil + } + } + + return nil, &u.Error{Type: u.ErrNotFound, Message: "Unable to find object"} +} + func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, complexFilterExp string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { ctx, cancel := u.Connect() var err error @@ -83,22 +108,6 @@ func GetManyObjects(entityStr string, req bson.M, filters u.RequestFilters, comp return data, nil } -func GetHierarchicalObjectById(hierarchyName string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { - // Get possible collections for this name - rangeEntities := u.GetEntitiesById(u.PHierarchy, hierarchyName) - req := bson.M{"id": hierarchyName} - - // Search each collection - for _, entityStr := range rangeEntities { - data, _ := GetObject(req, entityStr, filters, userRoles) - if data != nil { - return data, nil - } - } - - return nil, &u.Error{Type: u.ErrNotFound, Message: "Unable to find object"} -} - func GetEntityCount(entity int) int64 { ent := u.EntityToString(entity) ctx, cancel := u.Connect() diff --git a/API/models/entity_get_attribute.go b/API/models/entity_get_attribute.go index 64ca7e7a2..9f883864f 100644 --- a/API/models/entity_get_attribute.go +++ b/API/models/entity_get_attribute.go @@ -24,21 +24,20 @@ func GetSiteParentAttribute(id string, attribute string) (map[string]any, *u.Err } // Find object for _, collName := range collNames { - err := db.Collection(collName).FindOne(ctx, bson.M{"id": id}).Decode(&data) - if err == nil { - // Found object with given id - if data["category"].(string) == "site" { - // it's a site - break - } else { - // Find its parent site - nameSlice := strings.Split(data["id"].(string), u.HN_DELIMETER) - siteName := nameSlice[0] // CONSIDER SITE AS 0 - err := db.Collection("site").FindOne(ctx, bson.M{"id": siteName}).Decode(&data) - if err != nil { - return nil, &u.Error{Type: u.ErrNotFound, - Message: "Could not find parent site for given object"} - } + if err := db.Collection(collName).FindOne(ctx, bson.M{"id": id}).Decode(&data); err != nil { + continue + } + // Found object with given id + if data["category"].(string) == "site" { + // it's a site + break + } else { + // Find its parent site + nameSlice := strings.Split(data["id"].(string), u.HN_DELIMETER) + siteName := nameSlice[0] // CONSIDER SITE AS 0 + if err := db.Collection("site").FindOne(ctx, bson.M{"id": siteName}).Decode(&data); err != nil { + return nil, &u.Error{Type: u.ErrNotFound, + Message: "Could not find parent site for given object"} } } } diff --git a/API/models/entity_update.go b/API/models/entity_update.go index 7bb22cdd4..472b70dd1 100644 --- a/API/models/entity_update.go +++ b/API/models/entity_update.go @@ -3,6 +3,7 @@ package models import ( "encoding/json" "errors" + "fmt" "p3/repository" u "p3/utils" "strings" @@ -18,30 +19,14 @@ import ( var AttrsWithInnerObj = []string{"pillars", "separators", "breakers"} func UpdateObject(entityStr string, id string, updateData map[string]interface{}, isPatch bool, userRoles map[string]Role, isRecursive bool) (map[string]interface{}, *u.Error) { - var idFilter bson.M - if u.IsEntityNonHierarchical(u.EntityStrToInt(entityStr)) { - idFilter = bson.M{"slug": id} - } else { - idFilter = bson.M{"id": id} - } - - //Update timestamp requires first obj retrieval - //there isn't any way for mongoDB to make a field - //immutable in a document - var oldObj map[string]any - var err *u.Error - if entityStr == u.HIERARCHYOBJS_ENT { - oldObj, err = GetHierarchicalObjectById(id, u.RequestFilters{}, userRoles) - if err == nil { - entityStr = oldObj["category"].(string) - } - } else { - oldObj, err = GetObject(idFilter, entityStr, u.RequestFilters{}, userRoles) - } + // Update timestamp requires, first, obj retrieval + oldObj, err := GetObjectById(id, entityStr, u.RequestFilters{}, userRoles) if err != nil { return nil, err + } else if entityStr == u.HIERARCHYOBJS_ENT { + // overwrite category + entityStr = oldObj["category"].(string) } - entity := u.EntityStrToInt(entityStr) // Check if permission is only readonly @@ -55,41 +40,39 @@ func UpdateObject(entityStr string, id string, updateData map[string]interface{} // Update old object data with patch data if isPatch { - if tagsPresent { - return nil, &u.Error{ - Type: u.ErrBadFormat, - Message: "Tags cannot be modified in this way, use tags+ and tags-", - } - } - - var formattedOldObj map[string]interface{} - // Convert primitive.A and similar types - bytes, _ := json.Marshal(oldObj) - json.Unmarshal(bytes, &formattedOldObj) - // Update old with new - err := updateOldObjWithPatch(formattedOldObj, updateData) - if err != nil { - return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + println("is PATCH") + if patchData, err := preparePatch(tagsPresent, updateData, oldObj); err != nil { + return nil, err + } else { + updateData = patchData } - - updateData = formattedOldObj - // Remove API set fields - delete(updateData, "id") - delete(updateData, "lastUpdated") - delete(updateData, "createdDate") } else if tagsPresent { - err := verifyTagList(tags) - if err != nil { + if err := verifyTagList(tags); err != nil { return nil, err } } - result, err := WithTransaction(func(ctx mongo.SessionContext) (interface{}, error) { - err = prepareUpdateObject(ctx, entity, id, updateData, oldObj, userRoles) + fmt.Println(updateData) + result, err := UpdateTransaction(entity, id, isRecursive, updateData, oldObj, userRoles) + if err != nil { + return nil, err + } + + var updatedDoc map[string]interface{} + result.(*mongo.SingleResult).Decode(&updatedDoc) + + return fixID(updatedDoc), nil +} + +func UpdateTransaction(entity int, id string, isRecursive bool, updateData, oldObj map[string]any, userRoles map[string]Role) (any, *u.Error) { + entityStr := u.EntityToString(entity) + return WithTransaction(func(ctx mongo.SessionContext) (interface{}, error) { + err := prepareUpdateObject(ctx, entity, id, updateData, oldObj, userRoles) if err != nil { return nil, err } + idFilter := GetIdReqByEntity(entityStr, id) mongoRes := repository.GetDB().Collection(entityStr).FindOneAndReplace( ctx, idFilter, updateData, @@ -99,54 +82,39 @@ func UpdateObject(entityStr string, id string, updateData map[string]interface{} return nil, mongoRes.Err() } - if oldObj["id"] != updateData["id"] { - // Changes to id should be propagated - if err := repository.PropagateParentIdChange( - ctx, - oldObj["id"].(string), - updateData["id"].(string), - entity, - ); err != nil { - return nil, err - } else if entity == u.DOMAIN { - if err := repository.PropagateDomainChange(ctx, - oldObj["id"].(string), - updateData["id"].(string), - ); err != nil { - return nil, err - } - } - } - if u.IsEntityHierarchical(entity) && (oldObj["domain"] != updateData["domain"]) { - if isRecursive { - // Change domain of all children too - if err := repository.PropagateDomainChangeToChildren( - ctx, - updateData["id"].(string), - updateData["domain"].(string), - ); err != nil { - return nil, err - } - } else { - // Check if children domains are compatible - if err := repository.CheckParentDomainChange(entity, updateData["id"].(string), - updateData["domain"].(string)); err != nil { - return nil, err - } - } + if err := propagateUpdateChanges(ctx, entity, oldObj, updateData, isRecursive); err != nil { + return nil, err } return mongoRes, nil }) +} - if err != nil { - return nil, err +func preparePatch(tagsPresent bool, updateData, oldObj map[string]any) (map[string]any, *u.Error) { + if tagsPresent { + return nil, &u.Error{ + Type: u.ErrBadFormat, + Message: "Tags cannot be modified in this way, use tags+ and tags-", + } } - var updatedDoc map[string]interface{} - result.(*mongo.SingleResult).Decode(&updatedDoc) + var formattedOldObj map[string]interface{} + // Convert primitive.A and similar types + bytes, _ := json.Marshal(oldObj) + json.Unmarshal(bytes, &formattedOldObj) + // Update old with new + err := updateOldObjWithPatch(formattedOldObj, updateData) + if err != nil { + return nil, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } - return fixID(updatedDoc), nil + updateData = formattedOldObj + // Remove API set fields + delete(updateData, "id") + delete(updateData, "lastUpdated") + delete(updateData, "createdDate") + fmt.Println(updateData) + return updateData, nil } func prepareUpdateObject(ctx mongo.SessionContext, entity int, id string, updateData, oldObject map[string]any, userRoles map[string]Role) *u.Error { @@ -219,6 +187,46 @@ func updateOldObjWithPatch(old map[string]interface{}, patch map[string]interfac return nil } +func propagateUpdateChanges(ctx mongo.SessionContext, entity int, oldObj, updateData map[string]any, isRecursive bool) error { + if oldObj["id"] != updateData["id"] { + // Changes to id should be propagated + if err := repository.PropagateParentIdChange( + ctx, + oldObj["id"].(string), + updateData["id"].(string), + entity, + ); err != nil { + return err + } else if entity == u.DOMAIN { + if err := repository.PropagateDomainChange(ctx, + oldObj["id"].(string), + updateData["id"].(string), + ); err != nil { + return err + } + } + } + if u.IsEntityHierarchical(entity) && (oldObj["domain"] != updateData["domain"]) { + if isRecursive { + // Change domain of all children too + if err := repository.PropagateDomainChangeToChildren( + ctx, + updateData["id"].(string), + updateData["domain"].(string), + ); err != nil { + return err + } + } else { + // Check if children domains are compatible + if err := repository.CheckParentDomainChange(entity, updateData["id"].(string), + updateData["domain"].(string)); err != nil { + return err + } + } + } + return nil +} + // SwapEntity: use id to remove object from deleteEnt and then use data to create it in createEnt. // Propagates id changes to children objects. For atomicity, all is done in a Mongo transaction. func SwapEntity(createEnt, deleteEnt, id string, data map[string]interface{}, userRoles map[string]Role) *u.Error { From 14dc62f9a5bc00ebee1d99aa8ffedc45365c0c11 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:33:48 +0200 Subject: [PATCH 11/14] refactor(api) fill hierarchy --- API/models/entity_get_hierarchy.go | 33 +++++++++++++++++------------- API/models/entity_update.go | 3 ++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/API/models/entity_get_hierarchy.go b/API/models/entity_get_hierarchy.go index 61cf8d16c..e2b55fb36 100644 --- a/API/models/entity_get_hierarchy.go +++ b/API/models/entity_get_hierarchy.go @@ -72,7 +72,7 @@ func getHierarchyByNamespace(namespace u.Namespace, userRoles map[string]Role, f } // Fill hierarchy with formatted data - fillHierharchyData(data, namespace, entityName, hierarchy, categories) + fillHierarchyData(data, namespace, entityName, hierarchy, categories) } // For the root of VIRTUAL objects we also need to check for devices @@ -317,7 +317,7 @@ func getChildrenCollections(limit int, parentEntStr string) []int { return rangeEntities } -func fillHierharchyData(data []map[string]any, namespace u.Namespace, entityName string, hierarchy, categories map[string][]string) { +func fillHierarchyData(data []map[string]any, namespace u.Namespace, entityName string, hierarchy, categories map[string][]string) { for _, obj := range data { if strings.Contains(u.NamespaceToString(namespace), string(u.Logical)) { // Logical @@ -334,19 +334,24 @@ func fillHierharchyData(data []map[string]any, namespace u.Namespace, entityName hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) } } - } else if strings.Contains(obj["id"].(string), ".") { - // Physical or Org Children - categories[entityName] = append(categories[entityName], obj["id"].(string)) - fillHierarchyMap(obj["id"].(string), hierarchy) } else { - // Physical or Org Roots - objId := obj["id"].(string) - categories[entityName] = append(categories[entityName], objId) - if u.EntityStrToInt(entityName) == u.STRAYOBJ { - hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) - } else if u.EntityStrToInt(entityName) != u.VIRTUALOBJ { - hierarchy[rootIdx] = append(hierarchy[rootIdx], objId) - } + fillHierarchyDataWithHierarchicalObj(entityName, obj, hierarchy, categories) + } + } +} + +func fillHierarchyDataWithHierarchicalObj(entityName string, obj map[string]any, hierarchy, categories map[string][]string) { + objId := obj["id"].(string) + categories[entityName] = append(categories[entityName], objId) + if strings.Contains(objId, ".") { + // Physical or Org Children + fillHierarchyMap(objId, hierarchy) + } else { + // Physical or Org Roots + if u.EntityStrToInt(entityName) == u.STRAYOBJ { + hierarchy[rootIdx+entityName] = append(hierarchy[rootIdx+entityName], objId) + } else if u.EntityStrToInt(entityName) != u.VIRTUALOBJ { + hierarchy[rootIdx] = append(hierarchy[rootIdx], objId) } } } diff --git a/API/models/entity_update.go b/API/models/entity_update.go index 472b70dd1..f0f8b23ec 100644 --- a/API/models/entity_update.go +++ b/API/models/entity_update.go @@ -197,7 +197,8 @@ func propagateUpdateChanges(ctx mongo.SessionContext, entity int, oldObj, update entity, ); err != nil { return err - } else if entity == u.DOMAIN { + } + if entity == u.DOMAIN { if err := repository.PropagateDomainChange(ctx, oldObj["id"].(string), updateData["id"].(string), From 297ae3a03406349bb1fe8efb4dec3b12e6ff4981 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:58:43 +0200 Subject: [PATCH 12/14] refactor(api) get hierarchy --- API/models/entity_get_attribute.go | 35 ++-------------------------- API/models/entity_get_hierarchy.go | 25 +++++++++++--------- API/repository/entity.go | 37 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/API/models/entity_get_attribute.go b/API/models/entity_get_attribute.go index 9f883864f..580d740fd 100644 --- a/API/models/entity_get_attribute.go +++ b/API/models/entity_get_attribute.go @@ -1,48 +1,17 @@ package models import ( - "fmt" "p3/repository" u "p3/utils" - "strings" - - "go.mongodb.org/mongo-driver/bson" ) // GetSiteParentAttribute: search for the object of given ID, // then search for its site parent and return its requested attribute func GetSiteParentAttribute(id string, attribute string) (map[string]any, *u.Error) { - data := map[string]interface{}{} - - // Get all collections names - ctx, cancel := u.Connect() - db := repository.GetDB() - collNames, err := db.ListCollectionNames(ctx, bson.D{}) + data, err := repository.GetObjectSiteParent(id) if err != nil { - fmt.Println(err.Error()) - return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + return nil, err } - // Find object - for _, collName := range collNames { - if err := db.Collection(collName).FindOne(ctx, bson.M{"id": id}).Decode(&data); err != nil { - continue - } - // Found object with given id - if data["category"].(string) == "site" { - // it's a site - break - } else { - // Find its parent site - nameSlice := strings.Split(data["id"].(string), u.HN_DELIMETER) - siteName := nameSlice[0] // CONSIDER SITE AS 0 - if err := db.Collection("site").FindOne(ctx, bson.M{"id": siteName}).Decode(&data); err != nil { - return nil, &u.Error{Type: u.ErrNotFound, - Message: "Could not find parent site for given object"} - } - } - } - - defer cancel() if len(data) == 0 { return nil, &u.Error{Type: u.ErrNotFound, Message: "No object found with given id"} diff --git a/API/models/entity_get_hierarchy.go b/API/models/entity_get_hierarchy.go index e2b55fb36..8bbe462e9 100644 --- a/API/models/entity_get_hierarchy.go +++ b/API/models/entity_get_hierarchy.go @@ -195,22 +195,25 @@ func GetCompleteHierarchyAttributes(userRoles map[string]Role) (map[string]inter } // Add to response - for _, obj := range data { - if obj["attributes"] == nil { - continue - } - if id, isStr := obj["id"].(string); isStr && id != "" { - response[id] = obj["attributes"] - } else if obj["name"] != nil { - response[obj["name"].(string)] = obj["attributes"] - } - } - + formatCompleteHierarchyAttributes(data, response) } defer cancel() return response, nil } +func formatCompleteHierarchyAttributes(data []map[string]any, response map[string]any) { + for _, obj := range data { + if obj["attributes"] == nil { + continue + } + if id, isStr := obj["id"].(string); isStr && id != "" { + response[id] = obj["attributes"] + } else if obj["name"] != nil { + response[obj["name"].(string)] = obj["attributes"] + } + } +} + func GetEntitiesOfAncestor(id string, entStr, wantedEnt string, userRoles map[string]Role) ([]map[string]interface{}, *u.Error) { // Get parent object req := bson.M{"id": id} diff --git a/API/repository/entity.go b/API/repository/entity.go index bab6fd8e0..dfe4410ac 100644 --- a/API/repository/entity.go +++ b/API/repository/entity.go @@ -2,6 +2,8 @@ package repository import ( "context" + "fmt" + "strings" "time" "go.mongodb.org/mongo-driver/bson" @@ -39,6 +41,41 @@ func GetObject(req bson.M, ent string, filters u.RequestFilters) (map[string]int return t, nil } +func GetObjectSiteParent(objId string) (map[string]any, *u.Error) { + data := map[string]any{} + + // Get all collections names + ctx, cancel := u.Connect() + db := GetDB() + collNames, err := db.ListCollectionNames(ctx, bson.D{}) + if err != nil { + fmt.Println(err.Error()) + return nil, &u.Error{Type: u.ErrDBError, Message: err.Error()} + } + + // Find object + for _, collName := range collNames { + if err := db.Collection(collName).FindOne(ctx, bson.M{"id": objId}).Decode(&data); err != nil { + continue + } + // Found object with given id + if data["category"].(string) == "site" { + // it's a site + break + } else { + // Find its parent site + nameSlice := strings.Split(data["id"].(string), u.HN_DELIMETER) + siteName := nameSlice[0] // CONSIDER SITE AS 0 + if err := db.Collection("site").FindOne(ctx, bson.M{"id": siteName}).Decode(&data); err != nil { + return nil, &u.Error{Type: u.ErrNotFound, + Message: "Could not find parent site for given object"} + } + } + } + defer cancel() + return data, nil +} + func GetByAutogeneratedID[T any](collection, id string) (*T, *u.Error) { ctx, cancel := u.Connect() defer cancel() From 4f62275378bdc83f3711c759c048c483c53d4ad4 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:20:03 +0200 Subject: [PATCH 13/14] refactor(api) refactor propagate --- API/models/entity_update.go | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/API/models/entity_update.go b/API/models/entity_update.go index f0f8b23ec..8d5c84ac8 100644 --- a/API/models/entity_update.go +++ b/API/models/entity_update.go @@ -208,21 +208,28 @@ func propagateUpdateChanges(ctx mongo.SessionContext, entity int, oldObj, update } } if u.IsEntityHierarchical(entity) && (oldObj["domain"] != updateData["domain"]) { - if isRecursive { - // Change domain of all children too - if err := repository.PropagateDomainChangeToChildren( - ctx, - updateData["id"].(string), - updateData["domain"].(string), - ); err != nil { - return err - } - } else { - // Check if children domains are compatible - if err := repository.CheckParentDomainChange(entity, updateData["id"].(string), - updateData["domain"].(string)); err != nil { - return err - } + if err := propagateObjDomainChange(ctx, entity, isRecursive, updateData); err != nil { + return err + } + } + return nil +} + +func propagateObjDomainChange(ctx mongo.SessionContext, entity int, isRecursive bool, updateData map[string]any) error { + if isRecursive { + // Change domain of all children too + if err := repository.PropagateDomainChangeToChildren( + ctx, + updateData["id"].(string), + updateData["domain"].(string), + ); err != nil { + return err + } + } else { + // Check if children domains are compatible + if err := repository.CheckParentDomainChange(entity, updateData["id"].(string), + updateData["domain"].(string)); err != nil { + return err } } return nil From be4d058931d283a4523806fe0ae8ca8bc0820782 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:41:43 +0200 Subject: [PATCH 14/14] refactor(api) lastupdated filter --- API/models/filter.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/API/models/filter.go b/API/models/filter.go index f7a798b9e..2da60dfb2 100644 --- a/API/models/filter.go +++ b/API/models/filter.go @@ -129,17 +129,25 @@ func getDatesFromComplexFilters(req map[string]any) error { } } } else if k == "lastUpdated" { - for op, date := range v.(map[string]any) { - parsedDate, err := time.Parse("2006-01-02", date.(string)) - if err != nil { - return err - } - if op == "$lte" { - parsedDate = parsedDate.Add(time.Hour * 24) - } - req[k] = map[string]any{op: parsedDate} + var err error + if req[k], err = parseLastUpdatedFilter(v.(map[string]any)); err != nil { + return err } } } return nil } + +func parseLastUpdatedFilter(lastUpdatedMap map[string]any) (map[string]any, error) { + for op, date := range lastUpdatedMap { + parsedDate, err := time.Parse("2006-01-02", date.(string)) + if err != nil { + return nil, err + } + if op == "$lte" { + parsedDate = parsedDate.Add(time.Hour * 24) + } + return map[string]any{op: parsedDate}, nil + } + return nil, nil +}