79 Commits

Author SHA1 Message Date
argoyle 84bc7a80ce Merge branch 'next-release' into 'main'
chore(release): prepare for v0.1.0

See merge request unboundsoftware/pagination!33
2026-01-01 23:39:25 +01:00
Unbound Release ace572f1d6 chore(release): prepare for v0.1.0 2026-01-01 23:39:25 +01:00
argoyle b38eccbc46 Merge branch 'test/validation-coverage' into 'main'
test: add comprehensive validation and cursor test coverage

See merge request unboundsoftware/pagination!63
2026-01-01 23:21:38 +01:00
argoyle 0d61ca4d92 Merge branch 'docs/godoc-comments' into 'main'
docs: add godoc comments to public functions and types

See merge request unboundsoftware/pagination!65
2026-01-01 22:22:21 +01:00
argoyle 3af635b6c3 test: add comprehensive validation and cursor test coverage 2026-01-01 21:41:32 +01:00
argoyle 3008e2c49b docs: add godoc comments to public functions and types 2026-01-01 21:41:08 +01:00
argoyle 250a5e0c04 Merge branch 'feat/total-count' into 'main'
feat: add TotalCount field to PageInfo

See merge request unboundsoftware/pagination!64
2026-01-01 21:37:46 +01:00
argoyle 4b5115fe26 Merge branch 'refactor/custom-error-types' into 'main'
refactor: add custom error types for better error handling

See merge request unboundsoftware/pagination!62
2026-01-01 21:35:55 +01:00
argoyle 6409d9b2aa refactor: add custom error types for better error handling 2026-01-01 20:39:59 +01:00
argoyle 718fb0b17c Merge branch 'fix/negative-slice-index' into 'main'
fix: prevent negative slice index when last exceeds items count

See merge request unboundsoftware/pagination!61
2026-01-01 16:49:46 +01:00
argoyle 7fd209b5df Merge branch 'docs-add-CLAUDE-and-update-gitignore' into 'main'
docs: add CLAUDE.md and update .gitignore

See merge request unboundsoftware/pagination!60
2026-01-01 16:48:40 +01:00
argoyle 3e5b220810 feat: add TotalCount field to PageInfo
test: add TotalCount assertions to existing tests
2026-01-01 15:37:19 +01:00
argoyle bead25aa6b fix: prevent negative slice index when last exceeds items count 2026-01-01 15:09:00 +01:00
argoyle 236399df03 docs: add CLAUDE.md and update .gitignore 2026-01-01 15:08:10 +01:00
argoyle 02405c5ce1 Merge branch 'renovate/golang-1.25.5' into 'main'
chore(deps): update golang:1.25.5 docker digest to ad03ba9

See merge request unboundsoftware/pagination!59
2025-12-30 15:55:43 +01:00
Renovate 57111d2dfb chore(deps): update golang:1.25.5 docker digest to ad03ba9 2025-12-30 03:55:39 +00:00
argoyle f9586d2d1d Merge branch 'renovate/golang-1.25.5' into 'main'
chore(deps): update golang:1.25.5 docker digest to 0c27bcf

See merge request unboundsoftware/pagination!58
2025-12-09 09:53:22 +01:00
Renovate ef5de45119 chore(deps): update golang:1.25.5 docker digest to 0c27bcf 2025-12-09 02:08:40 +00:00
argoyle 57d72c54d1 Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.7.2

See merge request unboundsoftware/pagination!57
2025-12-08 09:40:22 +01:00
Renovate 78c5423940 chore(deps): update pre-commit hook golangci/golangci-lint to v2.7.2 2025-12-07 16:56:00 +00:00
argoyle a55fa056f6 Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.7.1

See merge request unboundsoftware/pagination!56
2025-12-04 16:24:06 +01:00
Renovate da5101f196 chore(deps): update pre-commit hook golangci/golangci-lint to v2.7.1 2025-12-04 14:56:19 +00:00
argoyle 63b91ee6ec Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.7.0

See merge request unboundsoftware/pagination!55
2025-12-04 08:27:17 +01:00
Renovate 484134fb5f chore(deps): update pre-commit hook golangci/golangci-lint to v2.7.0 2025-12-03 19:55:48 +00:00
argoyle 1f997f4b8f Merge branch 'renovate/golang-1.x' into 'main'
chore(deps): update golang docker tag to v1.25.5

See merge request unboundsoftware/pagination!54
2025-12-02 19:38:51 +01:00
Renovate f878072e6f chore(deps): update golang docker tag to v1.25.5 2025-12-02 18:07:49 +00:00
argoyle 61cbd44133 Merge branch 'renovate/gitleaks-gitleaks-8.x' into 'main'
chore(deps): update pre-commit hook gitleaks/gitleaks to v8.30.0

See merge request unboundsoftware/pagination!53
2025-11-27 00:03:47 +01:00
Renovate 6e24d707d7 chore(deps): update pre-commit hook gitleaks/gitleaks to v8.30.0 2025-11-26 18:55:53 +00:00
argoyle f879f0e13e Merge branch 'renovate/gitleaks-gitleaks-8.x' into 'main'
chore(deps): update pre-commit hook gitleaks/gitleaks to v8.29.1

See merge request unboundsoftware/pagination!52
2025-11-20 09:20:03 +01:00
Renovate 4881dda264 chore(deps): update pre-commit hook gitleaks/gitleaks to v8.29.1 2025-11-19 21:55:21 +00:00
argoyle 43a1a54748 Merge branch 'renovate/golang-1.25.4' into 'main'
chore(deps): update golang:1.25.4 docker digest to efe81fa

See merge request unboundsoftware/pagination!51
2025-11-18 16:19:33 +01:00
Renovate f94df9038f chore(deps): update golang:1.25.4 docker digest to efe81fa 2025-11-18 11:55:25 +00:00
argoyle 8f12a7cdc1 Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.2

See merge request unboundsoftware/pagination!50
2025-11-14 16:20:44 +01:00
Renovate 56537c188d chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.2 2025-11-14 13:55:05 +00:00
argoyle 09e72624f5 Merge branch 'renovate/golang-1.x' into 'main'
chore(deps): update golang docker tag to v1.25.4

See merge request unboundsoftware/pagination!49
2025-11-05 23:13:04 +01:00
Renovate 33c35f6290 chore(deps): update golang docker tag to v1.25.4 2025-11-05 22:06:42 +00:00
argoyle a8befab266 Merge branch 'renovate/gitleaks-gitleaks-8.x' into 'main'
chore(deps): update pre-commit hook gitleaks/gitleaks to v8.29.0

See merge request unboundsoftware/pagination!48
2025-11-05 06:51:44 +01:00
Renovate 196af118a4 chore(deps): update pre-commit hook gitleaks/gitleaks to v8.29.0 2025-11-05 01:55:46 +00:00
argoyle dd2c49d44c Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.1

See merge request unboundsoftware/pagination!47
2025-11-04 13:34:45 +01:00
argoyle ca2bf9629f Merge branch 'renovate/golang-1.25.3' into 'main'
chore(deps): update golang:1.25.3 docker digest to 9ac0edc

See merge request unboundsoftware/pagination!46
2025-11-04 13:34:01 +01:00
Renovate 83ed741568 chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.1 2025-11-04 11:55:22 +00:00
Renovate 83a3b5f2bd chore(deps): update golang:1.25.3 docker digest to 9ac0edc 2025-11-04 11:55:19 +00:00
argoyle 92f6545acd Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.0

See merge request unboundsoftware/pagination!45
2025-10-29 23:14:49 +01:00
Renovate 89f642e45b chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.0 2025-10-29 19:55:22 +00:00
argoyle 6c3576453a Merge branch 'feat/config-cleanup-remove-initial-tag' into 'main'
feat(config): remove initial tag configuration and cleanup

See merge request unboundsoftware/pagination!44
2025-10-21 16:14:48 +02:00
argoyle a32311acd9 feat(config): remove initial tag configuration and cleanup
Removes the initial tag setting from the configuration and cleans up 
commented sections for better readability. This simplifies the config 
file and enhances maintainability for future updates.
2025-10-21 15:31:54 +02:00
argoyle fb800730bb Merge branch 'renovate/golang-1.25.3' into 'main'
chore(deps): update golang:1.25.3 docker digest to 69d1009

See merge request unboundsoftware/pagination!43
2025-10-21 14:18:08 +02:00
Renovate afada26260 chore(deps): update golang:1.25.3 docker digest to 69d1009 2025-10-21 09:54:56 +00:00
argoyle 90e56fa252 Merge branch 'renovate/golang-1.x' into 'main'
chore(deps): update golang docker tag to v1.25.3

See merge request unboundsoftware/pagination!42
2025-10-14 08:58:28 +02:00
Renovate e21b681edf chore(deps): update golang docker tag to v1.25.3 2025-10-13 23:55:21 +00:00
argoyle 040faacceb Merge branch 'renovate/golang-1.x' into 'main'
chore(deps): update golang docker tag to v1.25.2

See merge request unboundsoftware/pagination!41
2025-10-08 09:17:56 +02:00
Renovate 04991a05e4 chore(deps): update golang docker tag to v1.25.2 2025-10-07 21:54:45 +00:00
argoyle a558130411 Merge branch 'renovate/alessandrojcm-commitlint-pre-commit-hook-9.x' into 'main'
chore(deps): update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.23.0

See merge request unboundsoftware/pagination!40
2025-10-02 17:04:33 +02:00
Renovate 22e153e4e5 chore(deps): update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.23.0 2025-10-02 09:56:57 +00:00
argoyle 67ee742396 Merge branch 'renovate/golang-1.25.1' into 'main'
chore(deps): update golang:1.25.1 docker digest to 12640a4

See merge request unboundsoftware/pagination!39
2025-10-01 19:16:51 +02:00
Renovate b683384a36 chore(deps): update golang:1.25.1 docker digest to 12640a4 2025-10-01 13:54:51 +00:00
argoyle a989c9d399 Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.5.0

See merge request unboundsoftware/pagination!38
2025-09-22 11:21:15 +02:00
Renovate 945c8c342d chore(deps): update pre-commit hook golangci/golangci-lint to v2.5.0 2025-09-21 19:54:23 +00:00
argoyle 7d1420c60a Merge branch 'renovate/lietu-go-pre-commit-1.x' into 'main'
chore(deps): update pre-commit hook lietu/go-pre-commit to v1

See merge request unboundsoftware/pagination!37
2025-09-13 17:22:59 +02:00
Renovate 4a47d7da60 chore(deps): update pre-commit hook lietu/go-pre-commit to v1 2025-09-13 09:09:28 +00:00
Renovate 36f46ce79c chore(deps): update golang:1.25.1 docker digest to 53f7808 2025-09-08 23:10:02 +00:00
Renovate d7d3e3bf6a chore(deps): update golang docker tag to v1.25.1 2025-09-03 19:54:41 +00:00
Renovate e3d43560af fix(deps): update module github.com/stretchr/testify to v1.11.1 2025-08-27 11:54:56 +00:00
Renovate 46115c2353 fix(deps): update module github.com/stretchr/testify to v1.11.0 2025-08-24 16:54:35 +00:00
Renovate fca68e6577 chore(deps): update golang:1.25.0 docker digest to f6b9e1a 2025-08-22 18:54:47 +00:00
Renovate 47f9af0201 chore(deps): update golang docker tag to v1.25.0 2025-08-14 09:46:02 +02:00
Renovate 15242d2ace chore(deps): update pre-commit hook golangci/golangci-lint to v2.4.0 2025-08-13 20:54:43 +00:00
Renovate ab68f17fd7 chore(deps): update golang:1.24.6 docker digest to 958bfd1 2025-08-12 22:54:26 +00:00
Renovate 24cc9bb232 chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v6 2025-08-09 19:55:03 +00:00
Renovate 7d2dd78b76 chore(deps): update golang docker tag to v1.24.6 2025-08-06 20:54:32 +00:00
Renovate 78fb41e28e chore(deps): update pre-commit hook golangci/golangci-lint to v2.3.1 2025-08-02 21:54:43 +00:00
Renovate 4bdf933cc8 chore(deps): update golang:1.24.5 docker digest to 0a156a4 2025-07-22 04:54:17 +00:00
Renovate 10913325a8 chore(deps): update pre-commit hook golangci/golangci-lint to v2.3.0 2025-07-21 14:54:03 +00:00
Renovate 56a3e2a4a1 chore(deps): update pre-commit hook gitleaks/gitleaks to v8.28.0 2025-07-20 16:53:50 +00:00
Renovate 1f4ce0d0eb chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.2 2025-07-11 12:51:23 +00:00
Renovate 85596120ce chore(deps): update golang docker tag to v1.24.5 2025-07-09 18:51:34 +00:00
Renovate 575d935231 chore(deps): update golang:1.24.4 docker digest to 9f820b6 2025-07-01 05:51:24 +00:00
Renovate 1fd3f2ba0a chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.1 2025-06-29 21:51:20 +00:00
Renovate 3d8c9a37e3 chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.0 2025-06-28 20:51:30 +00:00
11 changed files with 355 additions and 42 deletions
+1
View File
@@ -1,2 +1,3 @@
.idea
.claude
/release
+2 -2
View File
@@ -7,7 +7,7 @@ include:
- project: unboundsoftware/ci-templates
file: Pre-Commit-Go.gitlab-ci.yml
image: amd64/golang:1.24.4@sha256:3494bbe140127d12656113203ec91b8e3ff34e8a2b06a0a22bb0d8a41cc69e53
image: amd64/golang:1.25.5@sha256:ad03ba93327b8a6143b49373790b5d92c28067bdb814418509466122ee9c9e63
stages:
- deps
@@ -32,7 +32,7 @@ test:
vulnerabilities:
stage: test
image: amd64/golang:1.24.4@sha256:3494bbe140127d12656113203ec91b8e3ff34e8a2b06a0a22bb0d8a41cc69e53
image: amd64/golang:1.25.5@sha256:ad03ba93327b8a6143b49373790b5d92c28067bdb814418509466122ee9c9e63
script:
- go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck ./...
+5 -5
View File
@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -18,7 +18,7 @@ repos:
- --project
- unboundsoftware/pagination
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.22.0
rev: v9.23.0
hooks:
- id: commitlint
stages: [ commit-msg ]
@@ -32,15 +32,15 @@ repos:
- -local
- gitlab.com/unboundsoftware/shiny/presenter
- repo: https://github.com/lietu/go-pre-commit
rev: v0.1.0
rev: v1.0.0
hooks:
- id: go-test
- id: gofumpt
- repo: https://github.com/golangci/golangci-lint
rev: v2.1.6
rev: v2.7.2
hooks:
- id: golangci-lint-full
- repo: https://github.com/gitleaks/gitleaks
rev: v8.27.2
rev: v8.30.0
hooks:
- id: gitleaks
+1
View File
@@ -0,0 +1 @@
{"version":"v0.1.0"}
+73
View File
@@ -2,6 +2,73 @@
All notable changes to this project will be documented in this file.
## [0.1.0] - 2026-01-01
### 🚀 Features
- *(config)* Remove initial tag configuration and cleanup
### 🐛 Bug Fixes
- *(deps)* Update module github.com/stretchr/testify to v1.11.0
- *(deps)* Update module github.com/stretchr/testify to v1.11.1
- Prevent negative slice index when last exceeds items count
### 🚜 Refactor
- Add custom error types for better error handling
### 📚 Documentation
- Add CLAUDE.md and update .gitignore
- Add godoc comments to public functions and types
### 🧪 Testing
- Add comprehensive validation and cursor test coverage
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.1
- *(deps)* Update golang:1.24.4 docker digest to 9f820b6
- *(deps)* Update golang docker tag to v1.24.5
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.2
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.28.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.3.0
- *(deps)* Update golang:1.24.5 docker digest to 0a156a4
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.3.1
- *(deps)* Update golang docker tag to v1.24.6
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v6
- *(deps)* Update golang:1.24.6 docker digest to 958bfd1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.4.0
- *(deps)* Update golang docker tag to v1.25.0
- *(deps)* Update golang:1.25.0 docker digest to f6b9e1a
- *(deps)* Update golang docker tag to v1.25.1
- *(deps)* Update golang:1.25.1 docker digest to 53f7808
- *(deps)* Update pre-commit hook lietu/go-pre-commit to v1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.5.0
- *(deps)* Update golang:1.25.1 docker digest to 12640a4
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.23.0
- *(deps)* Update golang docker tag to v1.25.2
- *(deps)* Update golang docker tag to v1.25.3
- *(deps)* Update golang:1.25.3 docker digest to 69d1009
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.0
- *(deps)* Update golang:1.25.3 docker digest to 9ac0edc
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.29.0
- *(deps)* Update golang docker tag to v1.25.4
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.2
- *(deps)* Update golang:1.25.4 docker digest to efe81fa
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.29.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.30.0
- *(deps)* Update golang docker tag to v1.25.5
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.2
- *(deps)* Update golang:1.25.5 docker digest to 0c27bcf
- *(deps)* Update golang:1.25.5 docker digest to ad03ba9
## [0.0.4] - 2025-06-16
### 🐛 Bug Fixes
@@ -21,6 +88,12 @@ All notable changes to this project will be documented in this file.
- Update Codecov badge URL in README
- *(pagination)* Handle out-of-bounds slicing in pagination
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.2
- *(deps)* Update golang:1.24.4 docker digest to 3494bbe
## [0.0.1] - 2025-06-07
### 🚀 Features
+33
View File
@@ -0,0 +1,33 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
This is a Go library providing cursor-based pagination utilities for GraphQL Relay-style pagination. It handles cursor encoding/decoding (base64) and page extraction with `first/after` and `last/before` parameters.
## Commands
```bash
# Run all tests
go test ./...
# Run tests with race detection and coverage (as in CI)
CGO_ENABLED=1 go test -race -coverprofile=coverage.txt -covermode=atomic ./...
# Run a single test
go test -run TestGetPage ./...
# Lint
golangci-lint run
# Check for vulnerabilities
govulncheck ./...
```
## Architecture
Single-package library with:
- `pagination.go` - Core functions: `Validate`, `GetPage`, `EncodeCursor`, `DecodeCursor`, and `PageInfo` struct
- Cursors are base64-encoded strings
- `GetPage` is generic and works with any type via a cursor extraction function
+17 -24
View File
@@ -5,9 +5,6 @@
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[bump]
initial_tag = "v0.0.1"
[changelog]
# template for the changelog header
header = """
@@ -39,7 +36,7 @@ footer = """
trim = true
# postprocessors
postprocessors = [
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
]
# render body even when there are no releases to process
# render_always = true
@@ -55,28 +52,25 @@ filter_unconventional = true
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
@@ -84,4 +78,3 @@ filter_commits = false
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+"
+1 -1
View File
@@ -2,7 +2,7 @@ module gitlab.com/unboundsoftware/pagination
go 1.24.4
require github.com/stretchr/testify v1.10.0
require github.com/stretchr/testify v1.11.1
require (
github.com/davecgh/go-spew v1.1.1 // indirect
+2 -2
View File
@@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+38 -8
View File
@@ -1,33 +1,51 @@
// Package pagination provides cursor-based pagination utilities for GraphQL Relay-style pagination.
// Cursors are base64-encoded strings that can be used with first/after and last/before parameters.
package pagination
import (
"encoding/base64"
"fmt"
"errors"
"slices"
)
// Pagination validation errors.
var (
ErrFirstAndLastProvided = errors.New("only one of first and last can be provided")
ErrFirstNegative = errors.New("first must be greater than 0")
ErrLastNegative = errors.New("last must be greater than 0")
ErrAfterAndBeforeProvided = errors.New("only one of after and before can be provided")
ErrInvalidAfterCursor = errors.New("after is not a valid cursor")
ErrInvalidBeforeCursor = errors.New("before is not a valid cursor")
ErrInvalidCursor = errors.New("invalid cursor")
)
// Validate checks that the pagination parameters are valid according to Relay specification.
// It ensures that first and last are not both provided, that they are non-negative,
// and that after and before cursors are valid base64-encoded strings.
func Validate(first *int, after *string, last *int, before *string) error {
if first != nil && last != nil {
return fmt.Errorf("only one of first and last can be provided")
return ErrFirstAndLastProvided
}
if first != nil && *first < 0 {
return fmt.Errorf("first must be greater than 0")
return ErrFirstNegative
}
if last != nil && *last < 0 {
return fmt.Errorf("last must be greater than 0")
return ErrLastNegative
}
if after != nil && len(*after) > 0 && before != nil && len(*before) > 0 {
return fmt.Errorf("only one of after and before can be provided")
return ErrAfterAndBeforeProvided
}
if ValidateCursor(after) != nil {
return fmt.Errorf("after is not a valid cursor")
return ErrInvalidAfterCursor
}
if ValidateCursor(before) != nil {
return fmt.Errorf("before is not a valid cursor")
return ErrInvalidBeforeCursor
}
return nil
}
// ValidateCursor checks if a cursor is a valid base64-encoded string.
// Returns nil if the cursor is nil or valid, ErrInvalidCursor otherwise.
func ValidateCursor(cursor *string) error {
_, err := DecodeCursor(cursor)
if err != nil {
@@ -36,21 +54,27 @@ func ValidateCursor(cursor *string) error {
return nil
}
// DecodeCursor decodes a base64-encoded cursor string.
// Returns an empty string if the cursor is nil.
func DecodeCursor(cursor *string) (string, error) {
if cursor == nil {
return "", nil
}
b64, err := base64.StdEncoding.DecodeString(*cursor)
if err != nil {
return "", err
return "", ErrInvalidCursor
}
return string(b64), nil
}
// EncodeCursor encodes a string value as a base64 cursor.
func EncodeCursor(cursor string) string {
return base64.StdEncoding.EncodeToString([]byte(cursor))
}
// GetPage returns a paginated slice of items based on the provided pagination parameters.
// The fn parameter extracts the cursor value from each item.
// If neither first nor last is provided, max is used as the default page size.
func GetPage[T any](items []T, first *int, after *string, last *int, before *string, max int, fn func(T) string) ([]T, PageInfo) {
if len(items) == 0 {
return nil, PageInfo{}
@@ -64,6 +88,9 @@ func GetPage[T any](items []T, first *int, after *string, last *int, before *str
} else if last != nil {
tmp = *last
sIx = len(items) - tmp
if sIx < 0 {
sIx = 0
}
eIx = len(items)
}
if cursor, err := DecodeCursor(after); err == nil && cursor != "" {
@@ -96,6 +123,7 @@ func GetPage[T any](items []T, first *int, after *string, last *int, before *str
HasNextPage: eIx < len(items),
HasPreviousPage: sIx > 0,
EndCursor: ptr(EncodeCursor(fn(page[len(page)-1]))),
TotalCount: len(items),
}
}
@@ -103,9 +131,11 @@ func ptr[T any](v T) *T {
return &v
}
// PageInfo contains pagination metadata for a page of results.
type PageInfo struct {
StartCursor *string
HasNextPage bool
HasPreviousPage bool
EndCursor *string
TotalCount int
}
+182
View File
@@ -1,6 +1,7 @@
package pagination
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
@@ -43,6 +44,7 @@ func TestGetPage(t *testing.T) {
StartCursor: ptr("MQ=="),
HasNextPage: true,
EndCursor: ptr("Mg=="),
TotalCount: 3,
},
},
{
@@ -59,6 +61,7 @@ func TestGetPage(t *testing.T) {
StartCursor: ptr("MQ=="),
HasNextPage: false,
EndCursor: ptr("Mw=="),
TotalCount: 3,
},
},
{
@@ -70,6 +73,7 @@ func TestGetPage(t *testing.T) {
HasNextPage: false,
HasPreviousPage: true,
EndCursor: ptr("NA=="),
TotalCount: 4,
},
},
{
@@ -81,6 +85,7 @@ func TestGetPage(t *testing.T) {
HasNextPage: false,
HasPreviousPage: true,
EndCursor: ptr("Mw=="),
TotalCount: 3,
},
},
{
@@ -92,6 +97,7 @@ func TestGetPage(t *testing.T) {
HasNextPage: false,
HasPreviousPage: true,
EndCursor: ptr("Mw=="),
TotalCount: 3,
},
},
{
@@ -103,6 +109,7 @@ func TestGetPage(t *testing.T) {
HasNextPage: true,
HasPreviousPage: false,
EndCursor: ptr("Mg=="),
TotalCount: 3,
},
},
{
@@ -114,6 +121,19 @@ func TestGetPage(t *testing.T) {
HasNextPage: true,
HasPreviousPage: false,
EndCursor: ptr("MQ=="),
TotalCount: 3,
},
},
{
name: "last exceeds items count",
args: args[string]{items: []string{"1", "2", "3"}, last: ptr(10), max: 10, fn: func(s string) string { return s }},
wantItems: []string{"1", "2", "3"},
wantPageInfo: PageInfo{
StartCursor: ptr("MQ=="),
HasNextPage: false,
HasPreviousPage: false,
EndCursor: ptr("Mw=="),
TotalCount: 3,
},
},
}
@@ -125,3 +145,165 @@ func TestGetPage(t *testing.T) {
})
}
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
first *int
after *string
last *int
before *string
wantErr error
}{
{
name: "valid first",
first: ptr(10),
wantErr: nil,
},
{
name: "valid last",
last: ptr(10),
wantErr: nil,
},
{
name: "first and last both provided",
first: ptr(10),
last: ptr(10),
wantErr: ErrFirstAndLastProvided,
},
{
name: "negative first",
first: ptr(-1),
wantErr: ErrFirstNegative,
},
{
name: "negative last",
last: ptr(-1),
wantErr: ErrLastNegative,
},
{
name: "after and before both provided",
after: ptr("MQ=="),
before: ptr("Mg=="),
wantErr: ErrAfterAndBeforeProvided,
},
{
name: "invalid after cursor",
after: ptr("not-valid-base64!@#"),
wantErr: ErrInvalidAfterCursor,
},
{
name: "invalid before cursor",
before: ptr("not-valid-base64!@#"),
wantErr: ErrInvalidBeforeCursor,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Validate(tt.first, tt.after, tt.last, tt.before)
if tt.wantErr != nil {
assert.True(t, errors.Is(err, tt.wantErr), "Validate() error = %v, wantErr %v", err, tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}
func TestDecodeCursor(t *testing.T) {
tests := []struct {
name string
cursor *string
want string
wantErr error
}{
{
name: "nil cursor",
cursor: nil,
want: "",
wantErr: nil,
},
{
name: "valid cursor",
cursor: ptr("dGVzdA=="),
want: "test",
wantErr: nil,
},
{
name: "invalid base64",
cursor: ptr("not-valid-base64!@#"),
want: "",
wantErr: ErrInvalidCursor,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := DecodeCursor(tt.cursor)
if tt.wantErr != nil {
assert.True(t, errors.Is(err, tt.wantErr), "DecodeCursor() error = %v, wantErr %v", err, tt.wantErr)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func TestValidateCursor(t *testing.T) {
tests := []struct {
name string
cursor *string
wantErr error
}{
{
name: "nil cursor",
cursor: nil,
wantErr: nil,
},
{
name: "valid cursor",
cursor: ptr("dGVzdA=="),
wantErr: nil,
},
{
name: "invalid cursor",
cursor: ptr("not-valid-base64!@#"),
wantErr: ErrInvalidCursor,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateCursor(tt.cursor)
if tt.wantErr != nil {
assert.True(t, errors.Is(err, tt.wantErr), "ValidateCursor() error = %v, wantErr %v", err, tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}
func TestEncodeCursor(t *testing.T) {
tests := []struct {
name string
cursor string
want string
}{
{
name: "simple string",
cursor: "test",
want: "dGVzdA==",
},
{
name: "empty string",
cursor: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := EncodeCursor(tt.cursor)
assert.Equal(t, tt.want, got)
})
}
}