From 7e0f1fac239565f36a7e644036f20809fa403ea7 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:58:05 -0800 Subject: [PATCH] feat: group statistics endpoint (#123) * group statistics endpoint * remove item store * return possible errors * add statistics tests --- backend/app/api/handlers/v1/v1_ctrl_group.go | 20 +++++++++ backend/app/api/routes.go | 1 + backend/app/api/static/docs/docs.go | 41 ++++++++++++++++++ backend/app/api/static/docs/swagger.json | 41 ++++++++++++++++++ backend/app/api/static/docs/swagger.yaml | 25 +++++++++++ backend/go.sum | 4 -- backend/internal/data/repo/repo_group.go | 25 +++++++++++ backend/internal/data/repo/repo_group_test.go | 13 ++++++ frontend/layouts/default.vue | 14 ------ frontend/lib/api/classes/group.ts | 8 +++- frontend/lib/api/types/data-contracts.ts | 7 +++ frontend/pages/home.vue | 43 +++++++++---------- frontend/stores/items.ts | 33 -------------- 13 files changed, 201 insertions(+), 74 deletions(-) delete mode 100644 frontend/stores/items.ts diff --git a/backend/app/api/handlers/v1/v1_ctrl_group.go b/backend/app/api/handlers/v1/v1_ctrl_group.go index a3e8992..b27622d 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_group.go +++ b/backend/app/api/handlers/v1/v1_ctrl_group.go @@ -24,6 +24,26 @@ type ( } ) +// HandleGroupGet godoc +// @Summary Get the current user's group +// @Tags Group +// @Produce json +// @Success 200 {object} repo.GroupStatistics +// @Router /v1/groups/statistics [Get] +// @Security Bearer +func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + ctx := services.NewContext(r.Context()) + + stats, err := ctrl.repo.Groups.GroupStatistics(ctx, ctx.GID) + if err != nil { + return validate.NewRequestError(err, http.StatusInternalServerError) + } + + return server.Respond(w, http.StatusOK, stats) + } +} + // HandleGroupGet godoc // @Summary Get the current user's group // @Tags Group diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 9074838..992e70e 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -76,6 +76,7 @@ func (a *app) mountRoutes(repos *repo.AllRepos) { a.server.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword(), a.mwAuthToken) a.server.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate(), a.mwAuthToken) + a.server.Get(v1Base("/groups/statistics"), v1Ctrl.HandleGroupStatistics(), a.mwAuthToken) // TODO: I don't like /groups being the URL for users a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), a.mwAuthToken) diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 7d19c80..a4105a1 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -113,6 +113,30 @@ const docTemplate = `{ } } }, + "/v1/groups/statistics": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get the current user's group", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.GroupStatistics" + } + } + } + } + }, "/v1/items": { "get": { "security": [ @@ -1178,6 +1202,23 @@ const docTemplate = `{ } } }, + "repo.GroupStatistics": { + "type": "object", + "properties": { + "totalItems": { + "type": "integer" + }, + "totalLabels": { + "type": "integer" + }, + "totalLocations": { + "type": "integer" + }, + "totalUsers": { + "type": "integer" + } + } + }, "repo.GroupUpdate": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 64bbf41..911f8f2 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -105,6 +105,30 @@ } } }, + "/v1/groups/statistics": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Group" + ], + "summary": "Get the current user's group", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.GroupStatistics" + } + } + } + } + }, "/v1/items": { "get": { "security": [ @@ -1170,6 +1194,23 @@ } } }, + "repo.GroupStatistics": { + "type": "object", + "properties": { + "totalItems": { + "type": "integer" + }, + "totalLabels": { + "type": "integer" + }, + "totalLocations": { + "type": "integer" + }, + "totalUsers": { + "type": "integer" + } + } + }, "repo.GroupUpdate": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index eb80be3..b4f8c78 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -22,6 +22,17 @@ definitions: updatedAt: type: string type: object + repo.GroupStatistics: + properties: + totalItems: + type: integer + totalLabels: + type: integer + totalLocations: + type: integer + totalUsers: + type: integer + type: object repo.GroupUpdate: properties: currency: @@ -560,6 +571,20 @@ paths: summary: Get the current user tags: - Group + /v1/groups/statistics: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/repo.GroupStatistics' + security: + - Bearer: [] + summary: Get the current user's group + tags: + - Group /v1/items: get: parameters: diff --git a/backend/go.sum b/backend/go.sum index a16fd64..af688c1 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -72,13 +72,11 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -90,8 +88,6 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index fa5b6b8..9a9ef7a 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -41,6 +41,12 @@ type ( Uses int `json:"uses"` Group Group `json:"group"` } + GroupStatistics struct { + TotalUsers int `json:"totalUsers"` + TotalItems int `json:"totalItems"` + TotalLocations int `json:"totalLocations"` + TotalLabels int `json:"totalLabels"` + } ) var ( @@ -70,6 +76,25 @@ func mapToGroupInvitation(g *ent.GroupInvitationToken) GroupInvitation { } } +func (r *GroupRepository) GroupStatistics(ctx context.Context, GID uuid.UUID) (GroupStatistics, error) { + q := ` + SELECT + (SELECT COUNT(*) FROM users WHERE group_users = ?) AS total_users, + (SELECT COUNT(*) FROM items WHERE group_items = ? AND items.archived = false) AS total_items, + (SELECT COUNT(*) FROM locations WHERE group_locations = ?) AS total_locations, + (SELECT COUNT(*) FROM labels WHERE group_labels = ?) AS total_labels +` + var stats GroupStatistics + row := r.db.Sql().QueryRowContext(ctx, q, GID, GID, GID, GID) + + err := row.Scan(&stats.TotalUsers, &stats.TotalItems, &stats.TotalLocations, &stats.TotalLabels) + if err != nil { + return GroupStatistics{}, err + } + + return stats, nil +} + func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group, error) { return mapToGroupErr(r.db.Group.Create(). SetName(name). diff --git a/backend/internal/data/repo/repo_group_test.go b/backend/internal/data/repo/repo_group_test.go index 941e06c..b608d16 100644 --- a/backend/internal/data/repo/repo_group_test.go +++ b/backend/internal/data/repo/repo_group_test.go @@ -31,3 +31,16 @@ func Test_Group_Update(t *testing.T) { assert.Equal(t, "test2", g.Name) assert.Equal(t, "EUR", g.Currency) } + +func Test_Group_GroupStatistics(t *testing.T) { + useItems(t, 20) + useLabels(t, 20) + + stats, err := tRepos.Groups.GroupStatistics(context.Background(), tGroup.ID) + + assert.NoError(t, err) + assert.Equal(t, 20, stats.TotalItems) + assert.Equal(t, 20, stats.TotalLabels) + assert.Equal(t, 1, stats.TotalUsers) + assert.Equal(t, 1, stats.TotalLocations) +} diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 1d87350..5b6d488 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -9,7 +9,6 @@ diff --git a/frontend/lib/api/classes/group.ts b/frontend/lib/api/classes/group.ts index 7468f09..9c8fefa 100644 --- a/frontend/lib/api/classes/group.ts +++ b/frontend/lib/api/classes/group.ts @@ -1,5 +1,5 @@ import { BaseAPI, route } from "../base"; -import { Group, GroupInvitation, GroupInvitationCreate, GroupUpdate } from "../types/data-contracts"; +import { Group, GroupInvitation, GroupInvitationCreate, GroupStatistics, GroupUpdate } from "../types/data-contracts"; export class GroupApi extends BaseAPI { createInvitation(data: GroupInvitationCreate) { @@ -21,4 +21,10 @@ export class GroupApi extends BaseAPI { url: route("/groups"), }); } + + statistics() { + return this.http.get({ + url: route("/groups/statistics"), + }); + } } diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index e4304e3..7fd055c 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -24,6 +24,13 @@ export interface Group { updatedAt: Date; } +export interface GroupStatistics { + totalItems: number; + totalLabels: number; + totalLocations: number; + totalUsers: number; +} + export interface GroupUpdate { currency: string; name: string; diff --git a/frontend/pages/home.vue b/frontend/pages/home.vue index 8bf0a54..1adc178 100644 --- a/frontend/pages/home.vue +++ b/frontend/pages/home.vue @@ -1,6 +1,5 @@