diff --git a/backend/app/api/handlers/v1/v1_ctrl_locations.go b/backend/app/api/handlers/v1/v1_ctrl_locations.go index 9f14304..ee2785a 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_locations.go +++ b/backend/app/api/handlers/v1/v1_ctrl_locations.go @@ -15,6 +15,7 @@ import ( // @Summary Get All Locations // @Tags Locations // @Produce json +// @Param withItems query bool false "include items in response tree" // @Success 200 {object} server.Results{items=[]repo.TreeItem} // @Router /v1/locations/tree [GET] // @Security Bearer @@ -22,7 +23,18 @@ func (ctrl *V1Controller) HandleLocationTreeQuery() server.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { user := services.UseUserCtx(r.Context()) - locTree, err := ctrl.repo.Locations.Tree(r.Context(), user.GroupID) + q := r.URL.Query() + + withItems := queryBool(q.Get("withItems")) + + locTree, err := ctrl.repo.Locations.Tree( + r.Context(), + user.GroupID, + repo.TreeQuery{ + WithItems: withItems, + }, + ) + if err != nil { log.Err(err).Msg("failed to get locations tree") return validate.NewRequestError(err, http.StatusInternalServerError) diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 5cfe772..e8c89a1 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -1059,6 +1059,14 @@ const docTemplate = `{ "Locations" ], "summary": "Get All Locations", + "parameters": [ + { + "type": "boolean", + "description": "include items in response tree", + "name": "withItems", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -2173,6 +2181,9 @@ const docTemplate = `{ }, "name": { "type": "string" + }, + "type": { + "type": "string" } } }, diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 4f04b24..499f83f 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -1051,6 +1051,14 @@ "Locations" ], "summary": "Get All Locations", + "parameters": [ + { + "type": "boolean", + "description": "include items in response tree", + "name": "withItems", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -2165,6 +2173,9 @@ }, "name": { "type": "string" + }, + "type": { + "type": "string" } } }, diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index a94f1f5..1781f32 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -472,6 +472,8 @@ definitions: type: string name: type: string + type: + type: string type: object repo.UserOut: properties: @@ -1317,6 +1319,11 @@ paths: - Locations /v1/locations/tree: get: + parameters: + - description: include items in response tree + in: query + name: withItems + type: boolean produces: - application/json responses: diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index da4894f..2a5d8ea 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -232,45 +232,75 @@ func (r *LocationRepository) DeleteByGroup(ctx context.Context, GID, ID uuid.UUI type TreeItem struct { ID uuid.UUID `json:"id"` Name string `json:"name"` + Type string `json:"type"` Children []*TreeItem `json:"children"` } type FlatTreeItem struct { ID uuid.UUID Name string + Type string ParentID uuid.UUID Level int } -func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID) ([]TreeItem, error) { +type TreeQuery struct { + WithItems bool `json:"withItems"` +} + +func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQuery) ([]TreeItem, error) { query := ` - WITH recursive location_tree(id, NAME, location_children, level) AS + WITH recursive location_tree(id, NAME, location_children, level, node_type) AS ( SELECT id, NAME, location_children, - 0 AS level + 0 AS level, + 'location' AS node_type FROM locations WHERE location_children IS NULL AND group_locations = ? + UNION ALL SELECT c.id, c.NAME, c.location_children, - level + 1 + level + 1, + 'location' AS node_type FROM locations c JOIN location_tree p ON c.location_children = p.id WHERE level < 10 -- prevent infinite loop & excessive recursion + + {{ WITH_ITEMS }} ) SELECT id, NAME, level, - location_children + location_children, + node_type FROM location_tree ORDER BY level, + node_type DESC, -- sort locations before items NAME;` + if tq.WithItems { + itemQuery := ` + UNION ALL + SELECT i.id, + i.name, + location_items as location_children, + level + 1, + 'item' AS node_type + FROM items i + JOIN location_tree p + ON i.location_items = p.id + WHERE level < 10 -- prevent infinite loop & excessive recursion` + query = strings.ReplaceAll(query, "{{ WITH_ITEMS }}", itemQuery) + } else { + query = strings.ReplaceAll(query, "{{ WITH_ITEMS }}", "") + } + rows, err := lr.db.Sql().QueryContext(ctx, query, GID) if err != nil { return nil, err @@ -280,7 +310,7 @@ func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID) ([]TreeIt var locations []FlatTreeItem for rows.Next() { var location FlatTreeItem - if err := rows.Scan(&location.ID, &location.Name, &location.Level, &location.ParentID); err != nil { + if err := rows.Scan(&location.ID, &location.Name, &location.Level, &location.ParentID, &location.Type); err != nil { return nil, err } locations = append(locations, location) @@ -302,6 +332,7 @@ func ConvertLocationsToTree(locations []FlatTreeItem) []TreeItem { loc := &TreeItem{ ID: location.ID, Name: location.Name, + Type: location.Type, Children: []*TreeItem{}, } diff --git a/backend/internal/data/repo/repo_locations_test.go b/backend/internal/data/repo/repo_locations_test.go index 73ed743..9a72a63 100644 --- a/backend/internal/data/repo/repo_locations_test.go +++ b/backend/internal/data/repo/repo_locations_test.go @@ -134,7 +134,7 @@ func TestItemRepository_TreeQuery(t *testing.T) { }) assert.NoError(t, err) - locations, err := tRepos.Locations.Tree(context.Background(), tGroup.ID) + locations, err := tRepos.Locations.Tree(context.Background(), tGroup.ID, TreeQuery{WithItems: true}) assert.NoError(t, err)