diff --git a/API/controllers/entityController.go b/API/controllers/entityController.go index a73e7c581..c5d87d8b1 100644 --- a/API/controllers/entityController.go +++ b/API/controllers/entityController.go @@ -619,6 +619,110 @@ func GetEntity(w http.ResponseWriter, r *http.Request) { } } +// swagger:operation GET /api/layers/{id}/objects Objects GetLayerObjects +// Gets the object of a given layer. +// Apply the layer filters to get children objects of a given root query param. +// --- +// security: +// - bearer: [] +// produces: +// - application/json +// parameters: +// - name: id +// in: path +// description: 'ID of desired layer.' +// required: true +// type: string +// default: "layer_slug" +// - name: root +// in: query +// description: 'Mandatory, accepts IDs. The root object from where to apply the layer' +// required: true +// - name: recursive +// in: query +// description: 'Accepts true or false. If true, get objects +// from all levels beneath root. If false, get objects directly under root.' +// responses: +// '200': +// description: 'Found. A response body will be returned with +// a meaningful message.' +// '400': +// description: Bad request. An error message will be returned. +// '404': +// description: Not Found. An error message will be returned. + +func GetLayerObjects(w http.ResponseWriter, r *http.Request) { + fmt.Println("******************************************************") + fmt.Println("FUNCTION CALL: GetLayerObjects ") + fmt.Println("******************************************************") + DispRequestMetaData(r) + var data map[string]interface{} + var id string + var canParse bool + var modelErr *u.Error + + // Get user roles for permissions + user := getUserFromToken(w, r) + if user == nil { + return + } + + // Get query params + var filters u.LayerObjsFilters + decoder.Decode(&filters, r.URL.Query()) + if filters.Root == "" { + //error + w.WriteHeader(http.StatusBadRequest) + u.Respond(w, u.Message("Query param root is mandatory")) + return + } + + if id, canParse = mux.Vars(r)["slug"]; canParse { + // Get layer + data, modelErr = models.GetObject(bson.M{"slug": id}, u.EntityToString(u.LAYER), u.RequestFilters{}, user.Roles) + if modelErr != nil { + u.RespondWithError(w, modelErr) + } + + // Apply layer to get objects request + req := bson.M{} + for filterName, filterValue := range data["filters"].(map[string]interface{}) { + u.AddFilterToReq(req, filterName, filterValue.(string)) + } + var searchId string + if filters.IsRecursive { + searchId = filters.Root + ".**.*" + } else { + searchId = filters.Root + ".*" + } + u.AddFilterToReq(req, "id", searchId) + + // Get objects + matchingObjects := []map[string]interface{}{} + entities := u.GetEntitiesByNamespace(u.Any, searchId) + for _, entStr := range entities { + entData, err := models.GetManyObjects(entStr, req, u.RequestFilters{}, user.Roles) + if err != nil { + u.RespondWithError(w, err) + return + } + matchingObjects = append(matchingObjects, entData...) + } + + // Respond + if r.Method == "OPTIONS" { + w.Header().Add("Content-Type", "application/json") + w.Header().Add("Allow", "GET, DELETE, OPTIONS, PATCH, PUT") + } else { + u.Respond(w, u.RespDataWrapper("successfully processed request", matchingObjects)) + } + } else { + w.WriteHeader(http.StatusBadRequest) + u.Respond(w, u.Message("Error while parsing path parameters")) + return + } +} + // swagger:operation GET /api/{entity} Objects GetAllEntities // Gets all present objects for specified entity (category). // Returns JSON body with all specified objects of type. diff --git a/API/router/router.go b/API/router/router.go index 01e64a46b..7768af478 100644 --- a/API/router/router.go +++ b/API/router/router.go @@ -112,6 +112,10 @@ func Router(jwt func(next http.Handler) http.Handler) *mux.Router { router.HandleFunc("/api/{entity}s/{id}", controllers.GetEntity).Methods("GET", "HEAD", "OPTIONS") + //GET LAYER + router.HandleFunc("/api/layers/{slug}/objects", + controllers.GetLayerObjects).Methods("GET", "HEAD", "OPTIONS") + // GET ALL ENTITY router.HandleFunc("/api/{entity}s", controllers.GetAllEntities).Methods("HEAD", "GET") diff --git a/API/swagger.json b/API/swagger.json index 4fa708a80..c1f7c87b8 100644 --- a/API/swagger.json +++ b/API/swagger.json @@ -168,6 +168,56 @@ } } }, + "/api/layers/{id}/objects": { + "get": { + "security": [ + { + "bearer": [] + } + ], + "description": "Apply the layer filters to get children objects of a given root query param.", + "produces": [ + "application/json" + ], + "tags": [ + "Objects" + ], + "summary": "Gets the object of a given layer.", + "operationId": "GetLayerObjects", + "parameters": [ + { + "type": "string", + "default": "layer_slug", + "description": "ID of desired layer.", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Mandatory, accepts IDs. The root object from where to apply the layer", + "name": "root", + "in": "query", + "required": true + }, + { + "description": "Accepts true or false. If true, get objects from all levels beneath root. If false, get objects directly under root.", + "name": "recursive", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Found. A response body will be returned with a meaningful message." + }, + "400": { + "description": "Bad request. An error message will be returned." + }, + "404": { + "description": "Not Found. An error message will be returned." + } + } + } + }, "/api/login": { "post": { "description": "Create a new JWT Key. This can also be used to verify credentials\nThe authorize and 'Try it out' buttons don't work", diff --git a/API/utils/util.go b/API/utils/util.go index bb42bbda4..d7aa4153a 100644 --- a/API/utils/util.go +++ b/API/utils/util.go @@ -16,6 +16,7 @@ import ( "github.com/elliotchance/pie/v2" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" ) var BuildHash string @@ -75,6 +76,11 @@ type RequestFilters struct { Id string `schema:"id"` } +type LayerObjsFilters struct { + Root string `schema:"root"` + IsRecursive bool `schema:"recursive"` +} + type HierarchyFilters struct { Namespace Namespace `schema:"namespace"` StartDate string `schema:"startDate"` @@ -173,35 +179,39 @@ func FilteredReqFromQueryParams(link *url.URL) bson.M { for key := range queryValues { if key != "fieldOnly" && key != "startDate" && key != "endDate" && key != "limit" && key != "namespace" { - var keyValue interface{} - keyValue = queryValues.Get(key) - - if key == "parentId" { - regex := applyWildcards(keyValue.(string)) + `\.(` + NAME_REGEX + ")" - bsonMap["id"] = regexToMongoFilter(regex) - continue - } else if key == "tag" { - // tag is in tags list - bsonMap["tags"] = bson.M{"$eq": keyValue} - continue - } else if strings.Contains(keyValue.(string), "*") { - regex := applyWildcards(keyValue.(string)) - keyValue = regexToMongoFilter(regex) - } - - switch key { - case "id", "name", "category", - "description", "domain", - "createdDate", "lastUpdated", "slug": - bsonMap[key] = keyValue - default: - bsonMap["attributes."+key] = keyValue - } + keyValue := queryValues.Get(key) + AddFilterToReq(bsonMap, key, keyValue) } } return bsonMap } +func AddFilterToReq(bsonMap primitive.M, key string, value string) { + var keyValue interface{} + keyValue = value + if key == "parentId" { + regex := applyWildcards(keyValue.(string)) + `\.(` + NAME_REGEX + ")" + bsonMap["id"] = regexToMongoFilter(regex) + return + } else if key == "tag" { + // tag is in tags list + bsonMap["tags"] = bson.M{"$eq": keyValue} + return + } else if strings.Contains(keyValue.(string), "*") { + regex := applyWildcards(keyValue.(string)) + keyValue = regexToMongoFilter(regex) + } + + switch key { + case "id", "name", "category", + "description", "domain", + "createdDate", "lastUpdated", "slug": + bsonMap[key] = keyValue + default: + bsonMap["attributes."+key] = keyValue + } +} + func ErrTypeToStatusCode(errType ErrType) int { switch errType { case ErrForbidden: diff --git a/CLI/ast.go b/CLI/ast.go index 4b2834e8c..584752804 100644 --- a/CLI/ast.go +++ b/CLI/ast.go @@ -1262,6 +1262,8 @@ func (n *createTagNode) execute() (interface{}, error) { type createLayerNode struct { slug node applicability node + filterName string + filterValue node } func (n *createLayerNode) execute() (interface{}, error) { @@ -1275,7 +1277,12 @@ func (n *createLayerNode) execute() (interface{}, error) { return nil, err } - return nil, cmd.C.CreateLayer(slug, applicability) + filterValue, err := nodeToString(n.filterValue, "filterValue") + if err != nil { + return nil, err + } + + return nil, cmd.C.CreateLayer(slug, applicability, n.filterName, filterValue) } type createCorridorNode struct { diff --git a/CLI/controllers/create.go b/CLI/controllers/create.go index b2306af43..7d41dd05d 100644 --- a/CLI/controllers/create.go +++ b/CLI/controllers/create.go @@ -18,7 +18,11 @@ func (controller Controller) PostObj(ent int, entity string, data map[string]any if models.EntityCreationMustBeInformed(ent) && IsInObjForUnity(entity) { entInt := models.EntityStrToInt(entity) - Ogree3D.InformOptional("PostObj", entInt, map[string]any{"type": "create", "data": resp.Body["data"]}) + createType := "create" + if entInt == models.LAYER { + createType = "create-layer" + } + Ogree3D.InformOptional("PostObj", entInt, map[string]any{"type": createType, "data": resp.Body["data"]}) } if models.IsLayer(path) { diff --git a/CLI/controllers/delete.go b/CLI/controllers/delete.go index f7aab606e..bdb2cd1b6 100644 --- a/CLI/controllers/delete.go +++ b/CLI/controllers/delete.go @@ -26,6 +26,9 @@ func (controller Controller) DeleteObj(path string) ([]string, error) { } if models.IsLayer(path) { State.Hierarchy.Children["Logical"].Children["Layers"].IsCached = false + if IsEntityTypeForOGrEE3D(models.LAYER) { + controller.Ogree3D.InformOptional("DeleteObj", -1, map[string]any{"type": "delete-layer", "data": obj["slug"].(string)}) + } } } if path == State.CurrPath { diff --git a/CLI/controllers/initController.go b/CLI/controllers/initController.go index 51937504f..cb4162a41 100755 --- a/CLI/controllers/initController.go +++ b/CLI/controllers/initController.go @@ -183,6 +183,7 @@ func SetObjsForUnity(objs []string) []int { } res = append(res, models.DOMAIN) res = append(res, models.TAG) + res = append(res, models.LAYER) } return res } diff --git a/CLI/controllers/layer.go b/CLI/controllers/layer.go index 85a79186e..dec86160f 100644 --- a/CLI/controllers/layer.go +++ b/CLI/controllers/layer.go @@ -140,7 +140,7 @@ func (controller Controller) GetLayer(id string) (string, models.Layer, error) { return realID, layer, nil } -func (controller Controller) CreateLayer(slug, applicability string) error { +func (controller Controller) CreateLayer(slug, applicability, filterName, filterValue string) error { applicability, err := TranslateApplicability(applicability) if err != nil { return err @@ -148,7 +148,7 @@ func (controller Controller) CreateLayer(slug, applicability string) error { return controller.PostObj(models.LAYER, models.EntityToString(models.LAYER), map[string]any{ "slug": slug, - models.LayerFilters: map[string]any{}, + models.LayerFilters: map[string]any{filterName: filterValue}, models.LayerApplicability: applicability, }, models.LayersPath+slug) } @@ -166,10 +166,18 @@ func (controller Controller) UpdateLayer(path string, attributeName string, valu _, err = controller.UpdateObj(path, map[string]any{attributeName: applicability}) case models.LayerFiltersRemove: _, err = controller.UpdateObj(path, map[string]any{attributeName: value}) - default: - filters := map[string]any{attributeName: value} - + case models.LayerFiltersAdd: + values := strings.Split(value.(string), "=") + if len(values) != 2 || len(values[0]) == 0 || len(values[0]) == 0 { + return fmt.Errorf("invalid filter format") + } + filters := map[string]any{values[0]: values[1]} _, err = controller.UpdateObj(path, map[string]any{models.LayerFilters: filters}) + default: + _, err = controller.UpdateObj(path, map[string]any{attributeName: value}) + if attributeName == "slug" { + State.Hierarchy.Children["Logical"].Children["Layers"].IsCached = false + } } return err diff --git a/CLI/controllers/layers_test.go b/CLI/controllers/layers_test.go index 137a57d78..4ab29eee6 100644 --- a/CLI/controllers/layers_test.go +++ b/CLI/controllers/layers_test.go @@ -1119,23 +1119,6 @@ func TestLsNotShowLayerIfNotMatchWithDoubleStar(t *testing.T) { assert.Len(t, objects, 0) } -func TestLsNotShowLayerIfDoubleStarIsAtTheEndAndZeroFoldersAreFound(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - mockGetObjectsByEntity(mockAPI, "layers", []any{ - map[string]any{ - "slug": "test", - models.LayerApplicability: "BASIC.A.R1.**", - models.LayerFilters: map[string]any{"any": "yes"}, - }, - }) - mockGetObjectHierarchy(mockAPI, roomWithoutChildren) - - objects, err := controller.Ls("/Physical/BASIC/A/R1", nil, nil) - assert.Nil(t, err) - assert.Len(t, objects, 0) -} - func TestLsNotShowLayerIfNotMatchWithDoubleStarAndMore(t *testing.T) { controller, mockAPI, _ := layersSetup(t) @@ -1164,10 +1147,10 @@ func TestLsReturnsLayerCreatedAfterLastUpdate(t *testing.T) { mockCreateObject(mockAPI, "layer", map[string]any{ "slug": "test", - models.LayerFilters: map[string]any{}, + models.LayerFilters: map[string]any{"key": "value"}, models.LayerApplicability: "BASIC.A.R1", }) - err = controller.CreateLayer("test", "/Physical/BASIC/A/R1") + err = controller.CreateLayer("test", "/Physical/BASIC/A/R1", "key", "value") assert.Nil(t, err) objects, err = controller.Ls("/Logical/Layers", nil, nil) @@ -1177,7 +1160,7 @@ func TestLsReturnsLayerCreatedAfterLastUpdate(t *testing.T) { } func TestLsReturnsLayerCreatedAndUpdatedAfterLastUpdate(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) + controller, mockAPI, mockOgree3D := layersSetup(t) mockGetObjectsByEntity(mockAPI, "layers", []any{}) @@ -1187,12 +1170,12 @@ func TestLsReturnsLayerCreatedAndUpdatedAfterLastUpdate(t *testing.T) { testLayer := map[string]any{ "slug": "test", - models.LayerFilters: map[string]any{}, + models.LayerFilters: map[string]any{"key": "value"}, models.LayerApplicability: "BASIC.A.R1", } mockCreateObject(mockAPI, "layer", testLayer) - err = controller.CreateLayer("test", "/Physical/BASIC/A/R1") + err = controller.CreateLayer("test", "/Physical/BASIC/A/R1", "key", "value") assert.Nil(t, err) mockGetObjectByEntity(mockAPI, "layers", testLayer) @@ -1203,7 +1186,14 @@ func TestLsReturnsLayerCreatedAndUpdatedAfterLastUpdate(t *testing.T) { models.LayerFilters: map[string]any{"category": "device"}, models.LayerApplicability: "BASIC.A.R1", }) - err = controller.UpdateLayer("/Logical/Layers/test", "category", "device") + mockOgree3D.On( + "InformOptional", "UpdateObj", + models.LAYER, map[string]any{"data": map[string]interface{}{ + "layer": map[string]interface{}{ + "applicability": "BASIC.A.R1", "filters": map[string]interface{}{"category": "device"}, + "slug": "test"}, "old-slug": "test"}, "type": "modify-layer"}, + ).Return(nil) + err = controller.UpdateLayer("/Logical/Layers/test", models.LayerFiltersAdd, "category=device") assert.Nil(t, err) objects, err = controller.Ls("/Logical/Layers", nil, nil) @@ -1220,7 +1210,7 @@ func TestLsReturnsLayerCreatedAndUpdatedAfterLastUpdate(t *testing.T) { } func TestLsOnLayerUpdatedAfterLastUpdateDoesUpdatedFilter(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) + controller, mockAPI, mockOgree3D := layersSetup(t) testLayer := map[string]any{ "slug": "test", @@ -1246,7 +1236,14 @@ func TestLsOnLayerUpdatedAfterLastUpdateDoesUpdatedFilter(t *testing.T) { models.LayerFilters: map[string]any{"category": "device"}, models.LayerApplicability: "BASIC.A.R1", }) - err = controller.UpdateLayer("/Logical/Layers/test", "category", "device") + mockOgree3D.On( + "InformOptional", "UpdateObj", + models.LAYER, map[string]any{"data": map[string]interface{}{ + "layer": map[string]interface{}{ + "applicability": "BASIC.A.R1", "filters": map[string]interface{}{"category": "device"}, + "slug": "test"}, "old-slug": "test"}, "type": "modify-layer"}, + ).Return(nil) + err = controller.UpdateLayer("/Logical/Layers/test", models.LayerFiltersAdd, "category=device") assert.Nil(t, err) mockGetObjects(mockAPI, "category=device&id=BASIC.A.R1.*&namespace=physical.hierarchy", []any{}) diff --git a/CLI/controllers/update.go b/CLI/controllers/update.go index c5ac09765..eecc77fae 100644 --- a/CLI/controllers/update.go +++ b/CLI/controllers/update.go @@ -40,10 +40,15 @@ func (controller Controller) UpdateObj(pathStr string, data map[string]any) (map entityType = models.TAG } else if models.IsLayer(pathStr) { // For layers, update the object to the hierarchy in order to be cached - _, err = State.Hierarchy.AddObjectInPath(resp.Body["data"].(map[string]any), pathStr) + data := resp.Body["data"].(map[string]any) + _, err = State.Hierarchy.AddObjectInPath(data, pathStr) if err != nil { return nil, err } + if len(data["filters"].(map[string]any)) == 0 { + println("Attention: this layer is never shown as an option since it has no filters") + } + entityType = models.LAYER } message := map[string]any{} @@ -99,6 +104,17 @@ func (controller Controller) UpdateObj(pathStr string, data map[string]any) (map "old-slug": path.ObjectID, "tag": resp.Body["data"], } + } else if entityType == models.LAYER { + path, err := controller.SplitPath(pathStr) + if err != nil { + return nil, err + } + + message["type"] = "modify-layer" + message["data"] = map[string]any{ + "old-slug": path.ObjectID, + "layer": resp.Body["data"], + } } else { message["type"] = "modify" message["data"] = resp.Body["data"] diff --git a/CLI/models/entity.go b/CLI/models/entity.go index 62b844ee6..7d9f6c37f 100644 --- a/CLI/models/entity.go +++ b/CLI/models/entity.go @@ -136,5 +136,5 @@ func GetParentOfEntity(ent int) int { } func EntityCreationMustBeInformed(entity int) bool { - return entity != TAG && entity != LAYER + return entity != TAG } diff --git a/CLI/models/layer.go b/CLI/models/layer.go index 6ff6e7802..e168c3747 100644 --- a/CLI/models/layer.go +++ b/CLI/models/layer.go @@ -12,6 +12,7 @@ const ( LayerApplicability = "applicability" LayerFilters = "filters" LayerFiltersRemove = LayerFilters + "-" + LayerFiltersAdd = LayerFilters + "+" ) var pluralizeClient = pluralize.NewClient() @@ -52,12 +53,6 @@ func (layer UserDefinedLayer) Matches(path string) bool { "/", ) - // special case different to the library used, - // path/to does not match path/to/** - if strings.HasSuffix(applicability, "/**") && strings.TrimSuffix(applicability, "/**") == path { - return false - } - match, err := doublestar.Match(applicability, path) return err == nil && match diff --git a/CLI/parser.go b/CLI/parser.go index a34523805..a667a0832 100644 --- a/CLI/parser.go +++ b/CLI/parser.go @@ -1083,8 +1083,14 @@ func (p *parser) parseCreateLayer() node { slug := p.parseString("slug") p.expect("@") applicability := p.parsePath(models.LayerApplicability) + p.expect("@") + filterName := p.parseSimpleWord("filterName") + p.skipWhiteSpaces() + p.expect("=") + p.skipWhiteSpaces() + filterValue := p.parseString("filterValue") - return &createLayerNode{slug, applicability} + return &createLayerNode{slug, applicability, filterName, filterValue} } func (p *parser) parseCreateCorridor() node { diff --git a/wiki/CLI-language.md b/wiki/CLI-language.md index 335ad3be2..df4160796 100644 --- a/wiki/CLI-language.md +++ b/wiki/CLI-language.md @@ -496,15 +496,15 @@ After the tag is created, it can be seen in /Logical/Tags. The command `get /Log Layers are identified by a slug. In addition, they have an applicability and the filters they apply. To create a layer, use: ``` -+layer:[slug]@[applicability] ++layer:[slug]@[applicability]@[filter] ``` The applicability is the path in which the layer should be added when doing ls. Patterns can be used in the applicability (see [Applicability Patterns](#applicability-patterns)). -Filters are automatically created as empty. To add filters, edit the layer using the object modification syntax (see [Modify object's attribute](#modify-objects-attribute)). Example: +A first filter in the format `field=value` should be given to create the layer. To add more filters, edit the layer using the following syntax: ``` -[layer_path]:category=device +[layer_path]:filters+=[filter_name]=[filter_value] ``` Where [layer_path] is `/Logical/Layers/[slug]` (or only `[slug]` if the current path is /Logical/Layers).