feat: pagination support
This commit is contained in:
parent
47fa47bdc7
commit
ebd85c2e6a
@ -143,10 +143,16 @@ func (q *Queries) ListAdmins(ctx context.Context) ([]User, error) {
|
||||
|
||||
const listUsers = `-- name: ListUsers :many
|
||||
SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
|
||||
func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
|
||||
rows, err := q.db.Query(ctx, listUsers)
|
||||
type ListUsersParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error) {
|
||||
rows, err := q.db.Query(ctx, listUsers, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ type TokenStore interface {
|
||||
// Mockable user related database operations interface.
|
||||
type UserStore interface {
|
||||
CreateUser(ctx context.Context, arg data.CreateUserParams) (data.User, error)
|
||||
ListUsers(ctx context.Context) ([]data.User, error)
|
||||
ListUsers(ctx context.Context, arg data.ListUsersParams) ([]data.User, error)
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (data.User, error)
|
||||
GetUserByUsername(ctx context.Context, username string) (data.User, error)
|
||||
UpdatePassword(ctx context.Context, arg data.UpdatePasswordParams) error
|
||||
@ -340,7 +340,11 @@ func (rs authResource) OwnerDelete(w http.ResponseWriter, r *http.Request) {
|
||||
// Handler for listing all users stored in the database. Should only be allowed to be called by
|
||||
// administrator level users.
|
||||
func (rs authResource) List(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := rs.Users.ListUsers(r.Context())
|
||||
limit, offset := getPaginationParams(r)
|
||||
users, err := rs.Users.ListUsers(r.Context(), data.ListUsersParams{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to retrieve users")
|
||||
return
|
||||
|
@ -2,13 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.umbrella.haus/ae/notatest/internal/data"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@ -292,36 +288,3 @@ func (rs *notesResource) GetFullVersion(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
respondJSON(w, http.StatusOK, fullVersion)
|
||||
}
|
||||
|
||||
// Parse `limit` and `offset` 32-bit integer URL parameters from the given request. Defaults to
|
||||
// limit of 50 and offset 0 if parameters are missing/invalid.
|
||||
func getPaginationParams(r *http.Request) (limit int32, offset int32) {
|
||||
defaultLimit := 50
|
||||
defaultOffset := 0
|
||||
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
if limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
defaultLimit = l
|
||||
}
|
||||
}
|
||||
|
||||
offsetStr := r.URL.Query().Get("offset")
|
||||
if offsetStr != "" {
|
||||
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
||||
defaultOffset = o
|
||||
}
|
||||
}
|
||||
|
||||
return int32(defaultLimit), int32(defaultOffset)
|
||||
}
|
||||
|
||||
// Concatenate the title and content strings, calculate a SHA-1 hash of the resulting string, and
|
||||
// return the resulting hash as a string.
|
||||
func sha1ContentHash(title, content string) string {
|
||||
hashContent := title + content
|
||||
hash := sha1.Sum([]byte(hashContent))
|
||||
hashStr := strings.ToUpper(hex.EncodeToString(hash[:]))
|
||||
|
||||
return hashStr
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@ -131,3 +132,36 @@ func validateUsername(username string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse `limit` and `offset` 32-bit integer URL parameters from the given request. Defaults to
|
||||
// limit of 50 and offset 0 if parameters are missing/invalid.
|
||||
func getPaginationParams(r *http.Request) (limit int32, offset int32) {
|
||||
defaultLimit := 50
|
||||
defaultOffset := 0
|
||||
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
if limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
defaultLimit = l
|
||||
}
|
||||
}
|
||||
|
||||
offsetStr := r.URL.Query().Get("offset")
|
||||
if offsetStr != "" {
|
||||
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
||||
defaultOffset = o
|
||||
}
|
||||
}
|
||||
|
||||
return int32(defaultLimit), int32(defaultOffset)
|
||||
}
|
||||
|
||||
// Concatenate the title and content strings, calculate a SHA-1 hash of the resulting string, and
|
||||
// return the resulting hash as a string.
|
||||
func sha1ContentHash(title, content string) string {
|
||||
hashContent := title + content
|
||||
hash := sha1.Sum([]byte(hashContent))
|
||||
hashStr := strings.ToUpper(hex.EncodeToString(hash[:]))
|
||||
|
||||
return hashStr
|
||||
}
|
||||
|
@ -51,19 +51,19 @@ func TestValidatePassword(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Mock HTTP client if needed
|
||||
if tt.mockHTTP != nil {
|
||||
if tc.mockHTTP != nil {
|
||||
mockClient := new(MockHTTPClient)
|
||||
tt.mockHTTP(mockClient)
|
||||
tc.mockHTTP(mockClient)
|
||||
}
|
||||
|
||||
err := validatePassword(tt.password)
|
||||
if tt.wantErr == "" {
|
||||
err := validatePassword(tc.password)
|
||||
if tc.wantErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tt.wantErr)
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -103,10 +103,10 @@ func TestPasswordEntropyCalculation(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.password, func(t *testing.T) {
|
||||
entropy := passwordvalidator.GetEntropy(tt.password)
|
||||
assert.InDelta(t, tt.entropy, entropy, 1.0)
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.password, func(t *testing.T) {
|
||||
entropy := passwordvalidator.GetEntropy(tc.password)
|
||||
assert.InDelta(t, tc.entropy, entropy, 1.0)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -139,13 +139,13 @@ func TestValidateUsername(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateUsername(tt.input)
|
||||
if tt.wantErr == "" {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateUsername(tc.input)
|
||||
if tc.wantErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tt.wantErr)
|
||||
assert.ErrorContains(t, err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -174,10 +174,119 @@ func TestNormalizeUsername(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeUsername(tt.input)
|
||||
assert.Equal(t, tt.want, got)
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := normalizeUsername(tc.input)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPaginationParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
expectedLimit int32
|
||||
expectedOffset int32
|
||||
}{
|
||||
{
|
||||
name: "defaults",
|
||||
query: "",
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "valid limit and offset",
|
||||
query: "limit=25&offset=30",
|
||||
expectedLimit: 25,
|
||||
expectedOffset: 30,
|
||||
},
|
||||
{
|
||||
name: "invalid limit",
|
||||
query: "limit=abc&offset=5",
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 5,
|
||||
},
|
||||
{
|
||||
name: "limit zero",
|
||||
query: "limit=0",
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "limit negative",
|
||||
query: "limit=-5",
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "offset negative",
|
||||
query: "offset=-10",
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "invalid offset",
|
||||
query: "offset=xyz",
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
{
|
||||
name: "valid offset zero",
|
||||
query: "offset=0",
|
||||
expectedLimit: 50,
|
||||
expectedOffset: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/test?"+tc.query, nil)
|
||||
assert.Nil(t, err)
|
||||
limit, offset := getPaginationParams(req)
|
||||
assert.Equal(t, tc.expectedLimit, limit)
|
||||
assert.Equal(t, tc.expectedOffset, offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA1ContentHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
title string
|
||||
content string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty strings",
|
||||
title: "",
|
||||
content: "",
|
||||
expected: "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709",
|
||||
},
|
||||
{
|
||||
name: "title only",
|
||||
title: "hello",
|
||||
content: "",
|
||||
expected: "AAF4C61DDCC5E8A2DABEDE0F3B482CD9AEA9434D",
|
||||
},
|
||||
{
|
||||
name: "content only",
|
||||
title: "",
|
||||
content: "world",
|
||||
expected: "7C211433F02071597741E6FF5A8EA34789ABBF43",
|
||||
},
|
||||
{
|
||||
name: "both title and content",
|
||||
title: "hello",
|
||||
content: "world",
|
||||
expected: "6ADFB183A4A2C94A2F92DAB5ADE762A47889A5A1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := sha1ContentHash(tc.title, tc.content)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ VALUES ($1, $2, true)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListUsers :many
|
||||
SELECT * FROM users;
|
||||
SELECT * FROM users
|
||||
LIMIT $1 OFFSET $2;
|
||||
|
||||
-- name: ListAdmins :many
|
||||
SELECT * FROM users
|
||||
|
Loading…
x
Reference in New Issue
Block a user