implement pagination for items

This commit is contained in:
Hayden 2022-10-12 21:08:22 -08:00
parent 85a96cde91
commit baa352d7ad
19 changed files with 435 additions and 327 deletions

View file

@ -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": {

View file

@ -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": {

View file

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

View file

@ -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"),
@ -39,9 +53,11 @@ func extractQuery(r *http.Request) repo.ItemQuery {
// @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} server.Results{items=[]repo.ItemSummary}
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/items [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc {
@ -53,7 +69,7 @@ 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)
}
}

View file

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

View file

@ -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().
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))
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.

View file

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

View file

@ -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<Results<ItemSummary>>({ url: route("/items", q) });
return this.http.get<PaginationResult<ItemSummary>>({ url: route("/items", q) });
}
create(item: ItemCreate) {

View file

@ -187,6 +187,7 @@ export interface LocationSummary {
updatedAt: Date;
}
export interface UserOut {
email: string;
groupId: string;

View file

@ -12,3 +12,10 @@ export type Result<T> = {
export type Results<T> = {
items: T[];
};
export interface PaginationResult<T> {
items: T[];
page: number;
pageSize: number;
total: number;
}

View file

@ -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"): " ",