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 @@