From baa352d7ad206c1bf59f2eceea29226ec547b10a Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Wed, 12 Oct 2022 21:08:22 -0800 Subject: [PATCH] implement pagination for items --- backend/app/api/docs/docs.go | 77 ++++++++----- backend/app/api/docs/swagger.json | 77 ++++++++----- backend/app/api/docs/swagger.yaml | 57 +++++---- backend/app/api/main.go | 20 ++-- backend/app/api/v1/controller.go | 10 +- backend/app/api/v1/v1_ctrl_auth.go | 42 +++---- backend/app/api/v1/v1_ctrl_group.go | 14 +-- backend/app/api/v1/v1_ctrl_items.go | 108 ++++++++++-------- .../app/api/v1/v1_ctrl_items_attachments.go | 84 +++++++------- backend/app/api/v1/v1_ctrl_labels.go | 68 +++++------ backend/app/api/v1/v1_ctrl_locations.go | 68 +++++------ backend/app/api/v1/v1_ctrl_user.go | 74 ++++++------ backend/internal/repo/pagination.go | 12 ++ backend/internal/repo/repo_items.go | 34 +++++- backend/internal/services/service_items.go | 2 +- frontend/lib/api/classes/items.ts | 6 +- frontend/lib/api/types/data-contracts.ts | 1 + frontend/lib/api/types/non-generated.ts | 7 ++ scripts/process-types.py | 1 + 19 files changed, 435 insertions(+), 327 deletions(-) create mode 100644 backend/internal/repo/pagination.go diff --git a/backend/app/api/docs/docs.go b/backend/app/api/docs/docs.go index 1e00622..5663e3f 100644 --- a/backend/app/api/docs/docs.go +++ b/backend/app/api/docs/docs.go @@ -77,6 +77,18 @@ const docTemplate = `{ "name": "q", "in": "query" }, + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "items per page", + "name": "pageSize", + "in": "query" + }, { "type": "array", "items": { @@ -102,22 +114,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.Results" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.ItemSummary" - } - } - } - } - ] + "$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary" } } } @@ -181,7 +178,7 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -282,7 +279,7 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -382,7 +379,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "" + "description": "OK" } } } @@ -498,7 +495,7 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -662,7 +659,7 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -826,7 +823,7 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -874,7 +871,7 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -931,7 +928,7 @@ const docTemplate = `{ "summary": "User Logout", "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -950,7 +947,7 @@ const docTemplate = `{ "summary": "User Token Refresh", "responses": { "200": { - "description": "" + "description": "OK" } } } @@ -977,7 +974,7 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -1077,7 +1074,7 @@ const docTemplate = `{ "summary": "Deletes the user account", "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -1098,7 +1095,7 @@ const docTemplate = `{ "summary": "Update the current user's password // TODO:", "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -1516,6 +1513,26 @@ const docTemplate = `{ } } }, + "repo.PaginationResult-repo_ItemSummary": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/repo.ItemSummary" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "repo.UserOut": { "type": "object", "properties": { @@ -1569,9 +1586,7 @@ const docTemplate = `{ "server.Results": { "type": "object", "properties": { - "items": { - "type": "any" - } + "items": {} } }, "server.ValidationError": { diff --git a/backend/app/api/docs/swagger.json b/backend/app/api/docs/swagger.json index 84d37a9..d625365 100644 --- a/backend/app/api/docs/swagger.json +++ b/backend/app/api/docs/swagger.json @@ -69,6 +69,18 @@ "name": "q", "in": "query" }, + { + "type": "integer", + "description": "page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "items per page", + "name": "pageSize", + "in": "query" + }, { "type": "array", "items": { @@ -94,22 +106,7 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/server.Results" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.ItemSummary" - } - } - } - } - ] + "$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary" } } } @@ -173,7 +170,7 @@ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -274,7 +271,7 @@ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -374,7 +371,7 @@ ], "responses": { "200": { - "description": "" + "description": "OK" } } } @@ -490,7 +487,7 @@ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -654,7 +651,7 @@ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -818,7 +815,7 @@ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -866,7 +863,7 @@ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -923,7 +920,7 @@ "summary": "User Logout", "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -942,7 +939,7 @@ "summary": "User Token Refresh", "responses": { "200": { - "description": "" + "description": "OK" } } } @@ -969,7 +966,7 @@ ], "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -1069,7 +1066,7 @@ "summary": "Deletes the user account", "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -1090,7 +1087,7 @@ "summary": "Update the current user's password // TODO:", "responses": { "204": { - "description": "" + "description": "No Content" } } } @@ -1508,6 +1505,26 @@ } } }, + "repo.PaginationResult-repo_ItemSummary": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/repo.ItemSummary" + } + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "repo.UserOut": { "type": "object", "properties": { @@ -1561,9 +1578,7 @@ "server.Results": { "type": "object", "properties": { - "items": { - "type": "any" - } + "items": {} } }, "server.ValidationError": { diff --git a/backend/app/api/docs/swagger.yaml b/backend/app/api/docs/swagger.yaml index e6d2af0..8c69cec 100644 --- a/backend/app/api/docs/swagger.yaml +++ b/backend/app/api/docs/swagger.yaml @@ -275,6 +275,19 @@ definitions: updatedAt: type: string type: object + repo.PaginationResult-repo_ItemSummary: + properties: + items: + items: + $ref: '#/definitions/repo.ItemSummary' + type: array + page: + type: integer + pageSize: + type: integer + total: + type: integer + type: object repo.UserOut: properties: email: @@ -310,8 +323,7 @@ definitions: type: object server.Results: properties: - items: - type: any + items: {} type: object server.ValidationError: properties: @@ -431,6 +443,14 @@ paths: in: query name: q type: string + - description: page number + in: query + name: page + type: integer + - description: items per page + in: query + name: pageSize + type: integer - collectionFormat: multi description: label Ids in: query @@ -451,14 +471,7 @@ paths: "200": description: OK schema: - allOf: - - $ref: '#/definitions/server.Results' - - properties: - items: - items: - $ref: '#/definitions/repo.ItemSummary' - type: array - type: object + $ref: '#/definitions/repo.PaginationResult-repo_ItemSummary' security: - Bearer: [] summary: Get All Items @@ -496,7 +509,7 @@ paths: - application/json responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: deletes a item @@ -602,7 +615,7 @@ paths: type: string responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: retrieves an attachment for an item @@ -677,7 +690,7 @@ paths: - application/octet-stream responses: "200": - description: "" + description: OK security: - Bearer: [] summary: retrieves an attachment for an item @@ -695,7 +708,7 @@ paths: - application/json responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: imports items into the database @@ -754,7 +767,7 @@ paths: - application/json responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: deletes a label @@ -851,7 +864,7 @@ paths: - application/json responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: deletes a location @@ -918,7 +931,7 @@ paths: $ref: '#/definitions/v1.ChangePassword' responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: Updates the users password @@ -954,7 +967,7 @@ paths: post: responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: User Logout @@ -967,7 +980,7 @@ paths: This does not validate that the user still exists within the database. responses: "200": - description: "" + description: OK security: - Bearer: [] summary: User Token Refresh @@ -986,7 +999,7 @@ paths: - application/json responses: "204": - description: "" + description: No Content summary: Get the current user tags: - User @@ -996,7 +1009,7 @@ paths: - application/json responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: Deletes the user account @@ -1051,7 +1064,7 @@ paths: - application/json responses: "204": - description: "" + description: No Content security: - Bearer: [] summary: 'Update the current user''s password // TODO:' diff --git a/backend/app/api/main.go b/backend/app/api/main.go index a1275d5..f963808 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -25,16 +25,16 @@ var ( BuildTime = "now" ) -// @title Go API Templates -// @version 1.0 -// @description This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!. -// @contact.name Don't -// @license.name MIT -// @BasePath /api -// @securityDefinitions.apikey Bearer -// @in header -// @name Authorization -// @description "Type 'Bearer TOKEN' to correctly set the API Key" +// @title Go API Templates +// @version 1.0 +// @description This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!. +// @contact.name Don't +// @license.name MIT +// @BasePath /api +// @securityDefinitions.apikey Bearer +// @in header +// @name Authorization +// @description "Type 'Bearer TOKEN' to correctly set the API Key" func main() { cfg, err := config.New() if err != nil { diff --git a/backend/app/api/v1/controller.go b/backend/app/api/v1/controller.go index ee4810f..365feef 100644 --- a/backend/app/api/v1/controller.go +++ b/backend/app/api/v1/controller.go @@ -66,11 +66,11 @@ func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller)) type ReadyFunc func() bool // HandleBase godoc -// @Summary Retrieves the basic information about the API -// @Tags Base -// @Produce json -// @Success 200 {object} ApiSummary -// @Router /v1/status [GET] +// @Summary Retrieves the basic information about the API +// @Tags Base +// @Produce json +// @Success 200 {object} ApiSummary +// @Router /v1/status [GET] func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { server.Respond(w, http.StatusOK, ApiSummary{ diff --git a/backend/app/api/v1/v1_ctrl_auth.go b/backend/app/api/v1/v1_ctrl_auth.go index 0a4f990..d410a86 100644 --- a/backend/app/api/v1/v1_ctrl_auth.go +++ b/backend/app/api/v1/v1_ctrl_auth.go @@ -23,15 +23,15 @@ type ( ) // HandleAuthLogin godoc -// @Summary User Login -// @Tags Authentication -// @Accept x-www-form-urlencoded -// @Accept application/json -// @Param username formData string false "string" example(admin@admin.com) -// @Param password formData string false "string" example(admin) -// @Produce json -// @Success 200 {object} TokenResponse -// @Router /v1/users/login [POST] +// @Summary User Login +// @Tags Authentication +// @Accept x-www-form-urlencoded +// @Accept application/json +// @Param username formData string false "string" example(admin@admin.com) +// @Param password formData string false "string" example(admin) +// @Produce json +// @Success 200 {object} TokenResponse +// @Router /v1/users/login [POST] func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { loginForm := &LoginForm{} @@ -80,11 +80,11 @@ func (ctrl *V1Controller) HandleAuthLogin() http.HandlerFunc { } // HandleAuthLogout godoc -// @Summary User Logout -// @Tags Authentication -// @Success 204 -// @Router /v1/users/logout [POST] -// @Security Bearer +// @Summary User Logout +// @Tags Authentication +// @Success 204 +// @Router /v1/users/logout [POST] +// @Security Bearer func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := services.UseTokenCtx(r.Context()) @@ -106,13 +106,13 @@ func (ctrl *V1Controller) HandleAuthLogout() http.HandlerFunc { } // HandleAuthLogout godoc -// @Summary User Token Refresh -// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token. -// @Description This does not validate that the user still exists within the database. -// @Tags Authentication -// @Success 200 -// @Router /v1/users/refresh [GET] -// @Security Bearer +// @Summary User Token Refresh +// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token. +// @Description This does not validate that the user still exists within the database. +// @Tags Authentication +// @Success 200 +// @Router /v1/users/refresh [GET] +// @Security Bearer func (ctrl *V1Controller) HandleAuthRefresh() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { requestToken := services.UseTokenCtx(r.Context()) diff --git a/backend/app/api/v1/v1_ctrl_group.go b/backend/app/api/v1/v1_ctrl_group.go index faa52a5..8b78fd8 100644 --- a/backend/app/api/v1/v1_ctrl_group.go +++ b/backend/app/api/v1/v1_ctrl_group.go @@ -23,13 +23,13 @@ type ( ) // HandleUserSelf godoc -// @Summary Get the current user -// @Tags User -// @Produce json -// @Param payload body GroupInvitationCreate true "User Data" -// @Success 200 {object} GroupInvitation -// @Router /v1/groups/invitations [Post] -// @Security Bearer +// @Summary Get the current user +// @Tags User +// @Produce json +// @Param payload body GroupInvitationCreate true "User Data" +// @Success 200 {object} GroupInvitation +// @Router /v1/groups/invitations [Post] +// @Security Bearer func (ctrl *V1Controller) HandleGroupInvitationsCreate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { data := GroupInvitationCreate{} diff --git a/backend/app/api/v1/v1_ctrl_items.go b/backend/app/api/v1/v1_ctrl_items.go index ab231e4..686842d 100644 --- a/backend/app/api/v1/v1_ctrl_items.go +++ b/backend/app/api/v1/v1_ctrl_items.go @@ -4,6 +4,7 @@ import ( "encoding/csv" "net/http" "net/url" + "strconv" "github.com/google/uuid" "github.com/hay-kot/homebox/backend/internal/repo" @@ -24,10 +25,23 @@ func uuidList(params url.Values, key string) []uuid.UUID { return ids } +func intOrNegativeOne(s string) int { + i, err := strconv.Atoi(s) + if err != nil { + return -1 + } + return i +} + func extractQuery(r *http.Request) repo.ItemQuery { params := r.URL.Query() + page := intOrNegativeOne(params.Get("page")) + perPage := intOrNegativeOne(params.Get("perPage")) + return repo.ItemQuery{ + Page: page, + PageSize: perPage, Search: params.Get("q"), LocationIDs: uuidList(params, "locations"), LabelIDs: uuidList(params, "labels"), @@ -35,15 +49,17 @@ func extractQuery(r *http.Request) repo.ItemQuery { } // HandleItemsGetAll godoc -// @Summary Get All Items -// @Tags Items -// @Produce json -// @Param q query string false "search string" -// @Param labels query []string false "label Ids" collectionFormat(multi) -// @Param locations query []string false "location Ids" collectionFormat(multi) -// @Success 200 {object} server.Results{items=[]repo.ItemSummary} -// @Router /v1/items [GET] -// @Security Bearer +// @Summary Get All Items +// @Tags Items +// @Produce json +// @Param q query string false "search string" +// @Param page query int false "page number" +// @Param pageSize query int false "items per page" +// @Param labels query []string false "label Ids" collectionFormat(multi) +// @Param locations query []string false "location Ids" collectionFormat(multi) +// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{} +// @Router /v1/items [GET] +// @Security Bearer func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := services.NewContext(r.Context()) @@ -53,18 +69,18 @@ func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc { server.RespondServerError(w) return } - server.Respond(w, http.StatusOK, server.Results{Items: items}) + server.Respond(w, http.StatusOK, items) } } // HandleItemsCreate godoc -// @Summary Create a new item -// @Tags Items -// @Produce json -// @Param payload body repo.ItemCreate true "Item Data" -// @Success 200 {object} repo.ItemSummary -// @Router /v1/items [POST] -// @Security Bearer +// @Summary Create a new item +// @Tags Items +// @Produce json +// @Param payload body repo.ItemCreate true "Item Data" +// @Success 200 {object} repo.ItemSummary +// @Router /v1/items [POST] +// @Security Bearer func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { createData := repo.ItemCreate{} @@ -87,13 +103,13 @@ func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc { } // HandleItemDelete godocs -// @Summary deletes a item -// @Tags Items -// @Produce json -// @Param id path string true "Item ID" -// @Success 204 -// @Router /v1/items/{id} [DELETE] -// @Security Bearer +// @Summary deletes a item +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Success 204 +// @Router /v1/items/{id} [DELETE] +// @Security Bearer func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, user, err := ctrl.partialParseIdAndUser(w, r) @@ -112,13 +128,13 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc { } // HandleItemGet godocs -// @Summary Gets a item and fields -// @Tags Items -// @Produce json -// @Param id path string true "Item ID" -// @Success 200 {object} repo.ItemOut -// @Router /v1/items/{id} [GET] -// @Security Bearer +// @Summary Gets a item and fields +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Success 200 {object} repo.ItemOut +// @Router /v1/items/{id} [GET] +// @Security Bearer func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, user, err := ctrl.partialParseIdAndUser(w, r) @@ -137,14 +153,14 @@ func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc { } // HandleItemUpdate godocs -// @Summary updates a item -// @Tags Items -// @Produce json -// @Param id path string true "Item ID" -// @Param payload body repo.ItemUpdate true "Item Data" -// @Success 200 {object} repo.ItemOut -// @Router /v1/items/{id} [PUT] -// @Security Bearer +// @Summary updates a item +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Param payload body repo.ItemUpdate true "Item Data" +// @Success 200 {object} repo.ItemOut +// @Router /v1/items/{id} [PUT] +// @Security Bearer func (ctrl *V1Controller) HandleItemUpdate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body := repo.ItemUpdate{} @@ -170,13 +186,13 @@ func (ctrl *V1Controller) HandleItemUpdate() http.HandlerFunc { } // HandleItemsImport godocs -// @Summary imports items into the database -// @Tags Items -// @Produce json -// @Success 204 -// @Param csv formData file true "Image to upload" -// @Router /v1/items/import [Post] -// @Security Bearer +// @Summary imports items into the database +// @Tags Items +// @Produce json +// @Success 204 +// @Param csv formData file true "Image to upload" +// @Router /v1/items/import [Post] +// @Security Bearer func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/backend/app/api/v1/v1_ctrl_items_attachments.go b/backend/app/api/v1/v1_ctrl_items_attachments.go index c8ff194..a32aaab 100644 --- a/backend/app/api/v1/v1_ctrl_items_attachments.go +++ b/backend/app/api/v1/v1_ctrl_items_attachments.go @@ -21,17 +21,17 @@ type ( ) // HandleItemsImport godocs -// @Summary imports items into the database -// @Tags Items -// @Produce json -// @Param id path string true "Item ID" -// @Param file formData file true "File attachment" -// @Param type formData string true "Type of file" -// @Param name formData string true "name of the file including extension" -// @Success 200 {object} repo.ItemOut -// @Failure 422 {object} []server.ValidationError -// @Router /v1/items/{id}/attachments [POST] -// @Security Bearer +// @Summary imports items into the database +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Param file formData file true "File attachment" +// @Param type formData string true "Type of file" +// @Param name formData string true "name of the file including extension" +// @Success 200 {object} repo.ItemOut +// @Failure 422 {object} []server.ValidationError +// @Router /v1/items/{id}/attachments [POST] +// @Security Bearer func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(ctrl.maxUploadSize << 20) @@ -98,14 +98,14 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc { } // HandleItemAttachmentGet godocs -// @Summary retrieves an attachment for an item -// @Tags Items -// @Produce application/octet-stream -// @Param id path string true "Item ID" -// @Param token query string true "Attachment token" -// @Success 200 -// @Router /v1/items/{id}/attachments/download [GET] -// @Security Bearer +// @Summary retrieves an attachment for an item +// @Tags Items +// @Produce application/octet-stream +// @Param id path string true "Item ID" +// @Param token query string true "Attachment token" +// @Success 200 +// @Router /v1/items/{id}/attachments/download [GET] +// @Security Bearer func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := server.GetParam(r, "token", "") @@ -125,39 +125,39 @@ func (ctrl *V1Controller) HandleItemAttachmentDownload() http.HandlerFunc { } // HandleItemAttachmentToken godocs -// @Summary retrieves an attachment for an item -// @Tags Items -// @Produce application/octet-stream -// @Param id path string true "Item ID" -// @Param attachment_id path string true "Attachment ID" -// @Success 200 {object} ItemAttachmentToken -// @Router /v1/items/{id}/attachments/{attachment_id} [GET] -// @Security Bearer +// @Summary retrieves an attachment for an item +// @Tags Items +// @Produce application/octet-stream +// @Param id path string true "Item ID" +// @Param attachment_id path string true "Attachment ID" +// @Success 200 {object} ItemAttachmentToken +// @Router /v1/items/{id}/attachments/{attachment_id} [GET] +// @Security Bearer func (ctrl *V1Controller) HandleItemAttachmentToken() http.HandlerFunc { return ctrl.handleItemAttachmentsHandler } // HandleItemAttachmentDelete godocs -// @Summary retrieves an attachment for an item -// @Tags Items -// @Param id path string true "Item ID" -// @Param attachment_id path string true "Attachment ID" -// @Success 204 -// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE] -// @Security Bearer +// @Summary retrieves an attachment for an item +// @Tags Items +// @Param id path string true "Item ID" +// @Param attachment_id path string true "Attachment ID" +// @Success 204 +// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE] +// @Security Bearer func (ctrl *V1Controller) HandleItemAttachmentDelete() http.HandlerFunc { return ctrl.handleItemAttachmentsHandler } // HandleItemAttachmentUpdate godocs -// @Summary retrieves an attachment for an item -// @Tags Items -// @Param id path string true "Item ID" -// @Param attachment_id path string true "Attachment ID" -// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update" -// @Success 200 {object} repo.ItemOut -// @Router /v1/items/{id}/attachments/{attachment_id} [PUT] -// @Security Bearer +// @Summary retrieves an attachment for an item +// @Tags Items +// @Param id path string true "Item ID" +// @Param attachment_id path string true "Attachment ID" +// @Param payload body repo.ItemAttachmentUpdate true "Attachment Update" +// @Success 200 {object} repo.ItemOut +// @Router /v1/items/{id}/attachments/{attachment_id} [PUT] +// @Security Bearer func (ctrl *V1Controller) HandleItemAttachmentUpdate() http.HandlerFunc { return ctrl.handleItemAttachmentsHandler } diff --git a/backend/app/api/v1/v1_ctrl_labels.go b/backend/app/api/v1/v1_ctrl_labels.go index f4811e3..8400748 100644 --- a/backend/app/api/v1/v1_ctrl_labels.go +++ b/backend/app/api/v1/v1_ctrl_labels.go @@ -11,12 +11,12 @@ import ( ) // HandleLabelsGetAll godoc -// @Summary Get All Labels -// @Tags Labels -// @Produce json -// @Success 200 {object} server.Results{items=[]repo.LabelOut} -// @Router /v1/labels [GET] -// @Security Bearer +// @Summary Get All Labels +// @Tags Labels +// @Produce json +// @Success 200 {object} server.Results{items=[]repo.LabelOut} +// @Router /v1/labels [GET] +// @Security Bearer func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := services.UseUserCtx(r.Context()) @@ -31,13 +31,13 @@ func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc { } // HandleLabelsCreate godoc -// @Summary Create a new label -// @Tags Labels -// @Produce json -// @Param payload body repo.LabelCreate true "Label Data" -// @Success 200 {object} repo.LabelSummary -// @Router /v1/labels [POST] -// @Security Bearer +// @Summary Create a new label +// @Tags Labels +// @Produce json +// @Param payload body repo.LabelCreate true "Label Data" +// @Success 200 {object} repo.LabelSummary +// @Router /v1/labels [POST] +// @Security Bearer func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { createData := repo.LabelCreate{} @@ -61,13 +61,13 @@ func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc { } // HandleLabelDelete godocs -// @Summary deletes a label -// @Tags Labels -// @Produce json -// @Param id path string true "Label ID" -// @Success 204 -// @Router /v1/labels/{id} [DELETE] -// @Security Bearer +// @Summary deletes a label +// @Tags Labels +// @Produce json +// @Param id path string true "Label ID" +// @Success 204 +// @Router /v1/labels/{id} [DELETE] +// @Security Bearer func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, user, err := ctrl.partialParseIdAndUser(w, r) @@ -86,13 +86,13 @@ func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc { } // HandleLabelGet godocs -// @Summary Gets a label and fields -// @Tags Labels -// @Produce json -// @Param id path string true "Label ID" -// @Success 200 {object} repo.LabelOut -// @Router /v1/labels/{id} [GET] -// @Security Bearer +// @Summary Gets a label and fields +// @Tags Labels +// @Produce json +// @Param id path string true "Label ID" +// @Success 200 {object} repo.LabelOut +// @Router /v1/labels/{id} [GET] +// @Security Bearer func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, user, err := ctrl.partialParseIdAndUser(w, r) @@ -118,13 +118,13 @@ func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc { } // HandleLabelUpdate godocs -// @Summary updates a label -// @Tags Labels -// @Produce json -// @Param id path string true "Label ID" -// @Success 200 {object} repo.LabelOut -// @Router /v1/labels/{id} [PUT] -// @Security Bearer +// @Summary updates a label +// @Tags Labels +// @Produce json +// @Param id path string true "Label ID" +// @Success 200 {object} repo.LabelOut +// @Router /v1/labels/{id} [PUT] +// @Security Bearer func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body := repo.LabelUpdate{} diff --git a/backend/app/api/v1/v1_ctrl_locations.go b/backend/app/api/v1/v1_ctrl_locations.go index 5dabc00..3505ff4 100644 --- a/backend/app/api/v1/v1_ctrl_locations.go +++ b/backend/app/api/v1/v1_ctrl_locations.go @@ -11,12 +11,12 @@ import ( ) // HandleLocationGetAll godoc -// @Summary Get All Locations -// @Tags Locations -// @Produce json -// @Success 200 {object} server.Results{items=[]repo.LocationOutCount} -// @Router /v1/locations [GET] -// @Security Bearer +// @Summary Get All Locations +// @Tags Locations +// @Produce json +// @Success 200 {object} server.Results{items=[]repo.LocationOutCount} +// @Router /v1/locations [GET] +// @Security Bearer func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := services.UseUserCtx(r.Context()) @@ -32,13 +32,13 @@ func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc { } // HandleLocationCreate godoc -// @Summary Create a new location -// @Tags Locations -// @Produce json -// @Param payload body repo.LocationCreate true "Location Data" -// @Success 200 {object} repo.LocationSummary -// @Router /v1/locations [POST] -// @Security Bearer +// @Summary Create a new location +// @Tags Locations +// @Produce json +// @Param payload body repo.LocationCreate true "Location Data" +// @Success 200 {object} repo.LocationSummary +// @Router /v1/locations [POST] +// @Security Bearer func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { createData := repo.LocationCreate{} @@ -61,13 +61,13 @@ func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc { } // HandleLocationDelete godocs -// @Summary deletes a location -// @Tags Locations -// @Produce json -// @Param id path string true "Location ID" -// @Success 204 -// @Router /v1/locations/{id} [DELETE] -// @Security Bearer +// @Summary deletes a location +// @Tags Locations +// @Produce json +// @Param id path string true "Location ID" +// @Success 204 +// @Router /v1/locations/{id} [DELETE] +// @Security Bearer func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, user, err := ctrl.partialParseIdAndUser(w, r) @@ -86,13 +86,13 @@ func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc { } // HandleLocationGet godocs -// @Summary Gets a location and fields -// @Tags Locations -// @Produce json -// @Param id path string true "Location ID" -// @Success 200 {object} repo.LocationOut -// @Router /v1/locations/{id} [GET] -// @Security Bearer +// @Summary Gets a location and fields +// @Tags Locations +// @Produce json +// @Param id path string true "Location ID" +// @Success 200 {object} repo.LocationOut +// @Router /v1/locations/{id} [GET] +// @Security Bearer func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, user, err := ctrl.partialParseIdAndUser(w, r) @@ -123,13 +123,13 @@ func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc { } // HandleLocationUpdate godocs -// @Summary updates a location -// @Tags Locations -// @Produce json -// @Param id path string true "Location ID" -// @Success 200 {object} repo.LocationOut -// @Router /v1/locations/{id} [PUT] -// @Security Bearer +// @Summary updates a location +// @Tags Locations +// @Produce json +// @Param id path string true "Location ID" +// @Success 200 {object} repo.LocationOut +// @Router /v1/locations/{id} [PUT] +// @Security Bearer func (ctrl *V1Controller) HandleLocationUpdate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body := repo.LocationUpdate{} diff --git a/backend/app/api/v1/v1_ctrl_user.go b/backend/app/api/v1/v1_ctrl_user.go index bb5f617..10fdbb1 100644 --- a/backend/app/api/v1/v1_ctrl_user.go +++ b/backend/app/api/v1/v1_ctrl_user.go @@ -11,12 +11,12 @@ import ( ) // HandleUserSelf godoc -// @Summary Get the current user -// @Tags User -// @Produce json -// @Param payload body services.UserRegistration true "User Data" -// @Success 204 -// @Router /v1/users/register [Post] +// @Summary Get the current user +// @Tags User +// @Produce json +// @Param payload body services.UserRegistration true "User Data" +// @Success 204 +// @Router /v1/users/register [Post] func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { regData := services.UserRegistration{} @@ -39,12 +39,12 @@ func (ctrl *V1Controller) HandleUserRegistration() http.HandlerFunc { } // HandleUserSelf godoc -// @Summary Get the current user -// @Tags User -// @Produce json -// @Success 200 {object} server.Result{item=repo.UserOut} -// @Router /v1/users/self [GET] -// @Security Bearer +// @Summary Get the current user +// @Tags User +// @Produce json +// @Success 200 {object} server.Result{item=repo.UserOut} +// @Router /v1/users/self [GET] +// @Security Bearer func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := services.UseTokenCtx(r.Context()) @@ -60,13 +60,13 @@ func (ctrl *V1Controller) HandleUserSelf() http.HandlerFunc { } // HandleUserSelfUpdate godoc -// @Summary Update the current user -// @Tags User -// @Produce json -// @Param payload body repo.UserUpdate true "User Data" -// @Success 200 {object} server.Result{item=repo.UserUpdate} -// @Router /v1/users/self [PUT] -// @Security Bearer +// @Summary Update the current user +// @Tags User +// @Produce json +// @Param payload body repo.UserUpdate true "User Data" +// @Success 200 {object} server.Result{item=repo.UserUpdate} +// @Router /v1/users/self [PUT] +// @Security Bearer func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { updateData := repo.UserUpdate{} @@ -90,24 +90,24 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() http.HandlerFunc { } // HandleUserUpdatePassword godoc -// @Summary Update the current user's password // TODO: -// @Tags User -// @Produce json -// @Success 204 -// @Router /v1/users/self/password [PUT] -// @Security Bearer +// @Summary Update the current user's password // TODO: +// @Tags User +// @Produce json +// @Success 204 +// @Router /v1/users/self/password [PUT] +// @Security Bearer func (ctrl *V1Controller) HandleUserUpdatePassword() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { } } // HandleUserSelfDelete godoc -// @Summary Deletes the user account -// @Tags User -// @Produce json -// @Success 204 -// @Router /v1/users/self [DELETE] -// @Security Bearer +// @Summary Deletes the user account +// @Tags User +// @Produce json +// @Success 204 +// @Router /v1/users/self [DELETE] +// @Security Bearer func (ctrl *V1Controller) HandleUserSelfDelete() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { actor := services.UseUserCtx(r.Context()) @@ -128,12 +128,12 @@ type ( ) // HandleUserSelfChangePassword godoc -// @Summary Updates the users password -// @Tags User -// @Success 204 -// @Param payload body ChangePassword true "Password Payload" -// @Router /v1/users/change-password [PUT] -// @Security Bearer +// @Summary Updates the users password +// @Tags User +// @Success 204 +// @Param payload body ChangePassword true "Password Payload" +// @Router /v1/users/change-password [PUT] +// @Security Bearer func (ctrl *V1Controller) HandleUserSelfChangePassword() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if ctrl.isDemo { diff --git a/backend/internal/repo/pagination.go b/backend/internal/repo/pagination.go new file mode 100644 index 0000000..d8878d0 --- /dev/null +++ b/backend/internal/repo/pagination.go @@ -0,0 +1,12 @@ +package repo + +type PaginationResult[T any] struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + Total int `json:"total"` + Items []T `json:"items"` +} + +func calculateOffset(page, pageSize int) int { + return (page - 1) * pageSize +} diff --git a/backend/internal/repo/repo_items.go b/backend/internal/repo/repo_items.go index 57a4dbb..7dd81ae 100644 --- a/backend/internal/repo/repo_items.go +++ b/backend/internal/repo/repo_items.go @@ -19,6 +19,8 @@ type ItemsRepository struct { type ( ItemQuery struct { + Page int + PageSize int Search string `json:"search"` LocationIDs []uuid.UUID `json:"locationIds"` LabelIDs []uuid.UUID `json:"labelIds"` @@ -215,7 +217,7 @@ func (e *ItemsRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID) } // QueryByGroup returns a list of items that belong to a specific group based on the provided query. -func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q ItemQuery) ([]ItemSummary, error) { +func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q ItemQuery) (PaginationResult[ItemSummary], error) { qb := e.db.Item.Query().Where(item.HasGroupWith(group.ID(gid))) if len(q.LabelIDs) > 0 { @@ -243,9 +245,33 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite ) } - return mapItemsSummaryErr(qb.WithLabel(). - WithLocation(). - All(ctx)) + if q.Page != -1 || q.PageSize != -1 { + qb = qb. + Offset(calculateOffset(q.Page, q.PageSize)). + Limit(q.PageSize) + } + + items, err := mapItemsSummaryErr( + qb.WithLabel(). + WithLocation(). + All(ctx), + ) + if err != nil { + return PaginationResult[ItemSummary]{}, err + } + + count, err := qb.Count(ctx) + if err != nil { + return PaginationResult[ItemSummary]{}, err + } + + return PaginationResult[ItemSummary]{ + Page: q.Page, + PageSize: q.PageSize, + Total: count, + Items: items, + }, nil + } // GetAll returns all the items in the database with the Labels and Locations eager loaded. diff --git a/backend/internal/services/service_items.go b/backend/internal/services/service_items.go index 0a8e2df..e944e14 100644 --- a/backend/internal/services/service_items.go +++ b/backend/internal/services/service_items.go @@ -28,7 +28,7 @@ func (svc *ItemService) GetOne(ctx context.Context, gid uuid.UUID, id uuid.UUID) return svc.repo.Items.GetOneByGroup(ctx, gid, id) } -func (svc *ItemService) Query(ctx Context, q repo.ItemQuery) ([]repo.ItemSummary, error) { +func (svc *ItemService) Query(ctx Context, q repo.ItemQuery) (repo.PaginationResult[repo.ItemSummary], error) { return svc.repo.Items.QueryByGroup(ctx, ctx.GID, q) } diff --git a/frontend/lib/api/classes/items.ts b/frontend/lib/api/classes/items.ts index 04defce..cd268ff 100644 --- a/frontend/lib/api/classes/items.ts +++ b/frontend/lib/api/classes/items.ts @@ -8,9 +8,11 @@ import { ItemSummary, ItemUpdate, } from "../types/data-contracts"; -import { AttachmentTypes, Results } from "../types/non-generated"; +import { AttachmentTypes, PaginationResult } from "../types/non-generated"; export type ItemsQuery = { + page?: number; + pageSize?: number; locations?: string[]; labels?: string[]; q?: string; @@ -18,7 +20,7 @@ export type ItemsQuery = { export class ItemsApi extends BaseAPI { getAll(q: ItemsQuery = {}) { - return this.http.get>({ url: route("/items", q) }); + return this.http.get>({ url: route("/items", q) }); } create(item: ItemCreate) { diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 24520bb..e160c5d 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -187,6 +187,7 @@ export interface LocationSummary { updatedAt: Date; } + export interface UserOut { email: string; groupId: string; diff --git a/frontend/lib/api/types/non-generated.ts b/frontend/lib/api/types/non-generated.ts index 26f4778..e248ab1 100644 --- a/frontend/lib/api/types/non-generated.ts +++ b/frontend/lib/api/types/non-generated.ts @@ -12,3 +12,10 @@ export type Result = { export type Results = { items: T[]; }; + +export interface PaginationResult { + items: T[]; + page: number; + pageSize: number; + total: number; +} diff --git a/scripts/process-types.py b/scripts/process-types.py index c2fcb88..00896b7 100644 --- a/scripts/process-types.py +++ b/scripts/process-types.py @@ -22,6 +22,7 @@ def date_types(*names: list[str]) -> dict[re.Pattern, str]: regex_replace: dict[re.Pattern, str] = { + re.compile(r" PaginationResultRepo"): "PaginationResult", re.compile(r" Repo"): " ", re.compile(r" Services"): " ", re.compile(r" V1"): " ",