Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f41c7de3ac | |||
| aead750db4 | |||
| 1c1770f068 | |||
| 8a11a52ea1 | |||
|
d484be8e15
|
|||
| 3e62319fad | |||
|
ea0595f8cb
|
|||
| 26d3136eaa | |||
| 70308cb006 | |||
| feeae4d43c | |||
| 6976dec872 | |||
| 259610599d | |||
| 67dc073454 | |||
| 496539b836 | |||
|
583eaae203
|
|||
| 84bc7a80ce | |||
| ace572f1d6 | |||
| b38eccbc46 | |||
| 0d61ca4d92 | |||
|
3af635b6c3
|
|||
|
3008e2c49b
|
|||
| 250a5e0c04 | |||
| 4b5115fe26 | |||
|
6409d9b2aa
|
|||
| 718fb0b17c | |||
| 7fd209b5df | |||
|
3e5b220810
|
|||
|
bead25aa6b
|
|||
|
236399df03
|
|||
| 02405c5ce1 | |||
| 57111d2dfb | |||
| f9586d2d1d | |||
| ef5de45119 | |||
| 57d72c54d1 | |||
| 78c5423940 | |||
| a55fa056f6 | |||
| da5101f196 | |||
| 63b91ee6ec | |||
| 484134fb5f | |||
| 1f997f4b8f | |||
| f878072e6f | |||
| 61cbd44133 | |||
| 6e24d707d7 | |||
| f879f0e13e | |||
| 4881dda264 | |||
| 43a1a54748 | |||
| f94df9038f | |||
| 8f12a7cdc1 | |||
| 56537c188d | |||
| 09e72624f5 | |||
| 33c35f6290 | |||
| a8befab266 | |||
| 196af118a4 | |||
| dd2c49d44c | |||
| ca2bf9629f | |||
| 83ed741568 | |||
| 83a3b5f2bd | |||
| 92f6545acd | |||
| 89f642e45b | |||
| 6c3576453a | |||
|
a32311acd9
|
|||
| fb800730bb | |||
| afada26260 | |||
| 90e56fa252 | |||
| e21b681edf | |||
| 040faacceb | |||
| 04991a05e4 | |||
| a558130411 | |||
| 22e153e4e5 | |||
| 67ee742396 | |||
| b683384a36 | |||
| a989c9d399 | |||
| 945c8c342d | |||
| 7d1420c60a | |||
| 4a47d7da60 | |||
| 36f46ce79c | |||
| d7d3e3bf6a | |||
| e3d43560af | |||
| 46115c2353 | |||
| fca68e6577 | |||
| 47f9af0201 | |||
| 15242d2ace | |||
| ab68f17fd7 | |||
| 24cc9bb232 | |||
| 7d2dd78b76 | |||
| 78fb41e28e | |||
| 4bdf933cc8 | |||
| 10913325a8 | |||
| 56a3e2a4a1 | |||
| 1f4ce0d0eb | |||
| 85596120ce | |||
| 575d935231 | |||
| 1fd3f2ba0a | |||
| 3d8c9a37e3 | |||
| e18b226356 | |||
|
a8a1c613a4
|
|||
| e60459ddd4 | |||
|
8e8bdac22c
|
|||
| 704b755c4c | |||
|
bb3f7327cd
|
|||
| 0e8ba09d62 | |||
|
9978b6f900
|
|||
| bd468af1d2 | |||
| ec1003a094 |
@@ -0,0 +1,30 @@
|
|||||||
|
name: pagination
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -race -coverprofile=coverage.txt ./...
|
||||||
|
|
||||||
|
vulnerabilities:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
- name: Check vulnerabilities
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
govulncheck ./...
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: pre-commit
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SKIP: no-commit-to-branch
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
- name: Install goimports
|
||||||
|
run: go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: unboundsoftware/shared-workflows/.gitea/workflows/Release.yml@main
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.claude
|
||||||
/release
|
/release
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
include:
|
|
||||||
- template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Defaults.gitlab-ci.yml
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Release.gitlab-ci.yml
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Pre-Commit-Go.gitlab-ci.yml
|
|
||||||
|
|
||||||
image: amd64/golang:1.24.4@sha256:40891f7b63de861049787c5262bff91906d30cbe221753840e276b3e785a66ab
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- deps
|
|
||||||
- test
|
|
||||||
|
|
||||||
deps:
|
|
||||||
stage: deps
|
|
||||||
script:
|
|
||||||
- go mod download
|
|
||||||
|
|
||||||
test:
|
|
||||||
stage: test
|
|
||||||
dependencies:
|
|
||||||
- deps
|
|
||||||
script:
|
|
||||||
- CGO_ENABLED=1 go test -mod=readonly -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=$(go list ./... | tr '\n' , | sed 's/,$//') ./...
|
|
||||||
- go tool cover -html=coverage.txt -o coverage.html
|
|
||||||
- go tool cover -func=coverage.txt
|
|
||||||
- curl -Os https://uploader.codecov.io/latest/linux/codecov
|
|
||||||
- chmod +x codecov
|
|
||||||
- ./codecov -t ${CODECOV_TOKEN} -R $CI_PROJECT_DIR -C $CI_COMMIT_SHA -r $CI_PROJECT_PATH
|
|
||||||
|
|
||||||
vulnerabilities:
|
|
||||||
stage: test
|
|
||||||
image: amd64/golang:1.24.4@sha256:40891f7b63de861049787c5262bff91906d30cbe221753840e276b3e785a66ab
|
|
||||||
script:
|
|
||||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
||||||
- govulncheck ./...
|
|
||||||
+6
-13
@@ -2,7 +2,7 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@@ -10,15 +10,8 @@ repos:
|
|||||||
args:
|
args:
|
||||||
- --allow-multiple-documents
|
- --allow-multiple-documents
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://gitlab.com/devopshq/gitlab-ci-linter
|
|
||||||
rev: v1.0.6
|
|
||||||
hooks:
|
|
||||||
- id: gitlab-ci-linter
|
|
||||||
args:
|
|
||||||
- --project
|
|
||||||
- unboundsoftware/pagination
|
|
||||||
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||||
rev: v9.22.0
|
rev: v9.23.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: commitlint
|
- id: commitlint
|
||||||
stages: [ commit-msg ]
|
stages: [ commit-msg ]
|
||||||
@@ -30,17 +23,17 @@ repos:
|
|||||||
- id: go-imports
|
- id: go-imports
|
||||||
args:
|
args:
|
||||||
- -local
|
- -local
|
||||||
- gitlab.com/unboundsoftware/shiny/presenter
|
- git.unbound.se/unboundsoftware
|
||||||
- repo: https://github.com/lietu/go-pre-commit
|
- repo: https://github.com/lietu/go-pre-commit
|
||||||
rev: v0.1.0
|
rev: v1.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: go-test
|
- id: go-test
|
||||||
- id: gofumpt
|
- id: gofumpt
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.1.6
|
rev: v2.8.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-full
|
- id: golangci-lint-full
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
rev: v8.27.0
|
rev: v8.30.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: gitleaks
|
- id: gitleaks
|
||||||
|
|||||||
+106
@@ -2,6 +2,112 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-01-09
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add Gitea Actions workflow
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update actions/checkout action to v6
|
||||||
|
- *(deps)* Update actions/setup-go action to v6
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.8.0
|
||||||
|
- Migrate module path from GitLab to Gitea
|
||||||
|
- Add pre-commit and release workflows for Gitea Actions
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
- *(pagination)* Handle empty page return case
|
||||||
|
|
||||||
|
## [0.0.3] - 2025-06-16
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Update GetPage to check for empty items slice
|
||||||
|
|
||||||
|
## [0.0.2] - 2025-06-15
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- 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
|
## [0.0.1] - 2025-06-07
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
# Pagination helper
|
# Pagination helper
|
||||||
|
|
||||||
Pagination helper
|
Pagination helper
|
||||||
|
|
||||||
[](https://gitlab.com/unboundsoftware/pagination/commits/main)
|
|
||||||
[](https://codecov.io/gl/unboundsoftware:pagination)
|
|
||||||
|
|||||||
+17
-24
@@ -5,9 +5,6 @@
|
|||||||
# Configuration options are organized into tables and keys.
|
# Configuration options are organized into tables and keys.
|
||||||
# See documentation for more information on available options.
|
# See documentation for more information on available options.
|
||||||
|
|
||||||
[bump]
|
|
||||||
initial_tag = "v0.0.1"
|
|
||||||
|
|
||||||
[changelog]
|
[changelog]
|
||||||
# template for the changelog header
|
# template for the changelog header
|
||||||
header = """
|
header = """
|
||||||
@@ -39,7 +36,7 @@ footer = """
|
|||||||
trim = true
|
trim = true
|
||||||
# postprocessors
|
# postprocessors
|
||||||
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 body even when there are no releases to process
|
||||||
# render_always = true
|
# render_always = true
|
||||||
@@ -55,28 +52,25 @@ filter_unconventional = true
|
|||||||
split_commits = false
|
split_commits = false
|
||||||
# regex for preprocessing the commit messages
|
# regex for preprocessing the commit messages
|
||||||
commit_preprocessors = [
|
commit_preprocessors = [
|
||||||
# Replace issue numbers
|
# Replace issue numbers
|
||||||
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
||||||
# Check spelling of the commit with https://github.com/crate-ci/typos
|
# Check spelling of the commit with https://github.com/crate-ci/typos
|
||||||
# If the spelling is incorrect, it will be automatically fixed.
|
# If the spelling is incorrect, it will be automatically fixed.
|
||||||
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||||
]
|
]
|
||||||
# regex for parsing and grouping commits
|
# regex for parsing and grouping commits
|
||||||
commit_parsers = [
|
commit_parsers = [
|
||||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||||
{ message = "^chore\\(deps.*\\)", skip = true },
|
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||||
{ message = "^chore\\(pr\\)", skip = true },
|
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||||
{ message = "^chore\\(pull\\)", skip = true },
|
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||||
{ 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 out the commits that are not matched by commit parsers
|
||||||
filter_commits = false
|
filter_commits = false
|
||||||
@@ -84,4 +78,3 @@ filter_commits = false
|
|||||||
topo_order = false
|
topo_order = false
|
||||||
# sort the commits inside sections by oldest/newest order
|
# sort the commits inside sections by oldest/newest order
|
||||||
sort_commits = "oldest"
|
sort_commits = "oldest"
|
||||||
tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+"
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
module gitlab.com/unboundsoftware/pagination
|
module git.unbound.se/unboundsoftware/pagination
|
||||||
|
|
||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.10.0
|
require github.com/stretchr/testify v1.11.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
+43
-10
@@ -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
|
package pagination
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"errors"
|
||||||
"slices"
|
"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 {
|
func Validate(first *int, after *string, last *int, before *string) error {
|
||||||
if first != nil && last != nil {
|
if first != nil && last != nil {
|
||||||
return fmt.Errorf("only one of first and last can be provided")
|
return ErrFirstAndLastProvided
|
||||||
}
|
}
|
||||||
if first != nil && *first < 0 {
|
if first != nil && *first < 0 {
|
||||||
return fmt.Errorf("first must be greater than 0")
|
return ErrFirstNegative
|
||||||
}
|
}
|
||||||
if last != nil && *last < 0 {
|
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 {
|
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 {
|
if ValidateCursor(after) != nil {
|
||||||
return fmt.Errorf("after is not a valid cursor")
|
return ErrInvalidAfterCursor
|
||||||
}
|
}
|
||||||
if ValidateCursor(before) != nil {
|
if ValidateCursor(before) != nil {
|
||||||
return fmt.Errorf("before is not a valid cursor")
|
return ErrInvalidBeforeCursor
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
func ValidateCursor(cursor *string) error {
|
||||||
_, err := DecodeCursor(cursor)
|
_, err := DecodeCursor(cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,23 +54,29 @@ func ValidateCursor(cursor *string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DecodeCursor decodes a base64-encoded cursor string.
|
||||||
|
// Returns an empty string if the cursor is nil.
|
||||||
func DecodeCursor(cursor *string) (string, error) {
|
func DecodeCursor(cursor *string) (string, error) {
|
||||||
if cursor == nil {
|
if cursor == nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
b64, err := base64.StdEncoding.DecodeString(*cursor)
|
b64, err := base64.StdEncoding.DecodeString(*cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", ErrInvalidCursor
|
||||||
}
|
}
|
||||||
return string(b64), nil
|
return string(b64), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncodeCursor encodes a string value as a base64 cursor.
|
||||||
func EncodeCursor(cursor string) string {
|
func EncodeCursor(cursor string) string {
|
||||||
return base64.StdEncoding.EncodeToString([]byte(cursor))
|
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) {
|
func GetPage[T any](items []T, first *int, after *string, last *int, before *string, max int, fn func(T) string) ([]T, PageInfo) {
|
||||||
if items == nil {
|
if len(items) == 0 {
|
||||||
return nil, PageInfo{}
|
return nil, PageInfo{}
|
||||||
}
|
}
|
||||||
tmp := min(max, len(items))
|
tmp := min(max, len(items))
|
||||||
@@ -64,6 +88,9 @@ func GetPage[T any](items []T, first *int, after *string, last *int, before *str
|
|||||||
} else if last != nil {
|
} else if last != nil {
|
||||||
tmp = *last
|
tmp = *last
|
||||||
sIx = len(items) - tmp
|
sIx = len(items) - tmp
|
||||||
|
if sIx < 0 {
|
||||||
|
sIx = 0
|
||||||
|
}
|
||||||
eIx = len(items)
|
eIx = len(items)
|
||||||
}
|
}
|
||||||
if cursor, err := DecodeCursor(after); err == nil && cursor != "" {
|
if cursor, err := DecodeCursor(after); err == nil && cursor != "" {
|
||||||
@@ -87,12 +114,16 @@ func GetPage[T any](items []T, first *int, after *string, last *int, before *str
|
|||||||
sIx = f
|
sIx = f
|
||||||
eIx = idx
|
eIx = idx
|
||||||
}
|
}
|
||||||
page := items[sIx:eIx]
|
page := items[sIx:min(eIx, len(items))]
|
||||||
|
if len(page) == 0 {
|
||||||
|
return nil, PageInfo{}
|
||||||
|
}
|
||||||
return page, PageInfo{
|
return page, PageInfo{
|
||||||
StartCursor: ptr(EncodeCursor(fn(page[0]))),
|
StartCursor: ptr(EncodeCursor(fn(page[0]))),
|
||||||
HasNextPage: eIx < len(items),
|
HasNextPage: eIx < len(items),
|
||||||
HasPreviousPage: sIx > 0,
|
HasPreviousPage: sIx > 0,
|
||||||
EndCursor: ptr(EncodeCursor(fn(page[len(page)-1]))),
|
EndCursor: ptr(EncodeCursor(fn(page[len(page)-1]))),
|
||||||
|
TotalCount: len(items),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +131,11 @@ func ptr[T any](v T) *T {
|
|||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageInfo contains pagination metadata for a page of results.
|
||||||
type PageInfo struct {
|
type PageInfo struct {
|
||||||
StartCursor *string
|
StartCursor *string
|
||||||
HasNextPage bool
|
HasNextPage bool
|
||||||
HasPreviousPage bool
|
HasPreviousPage bool
|
||||||
EndCursor *string
|
EndCursor *string
|
||||||
|
TotalCount int
|
||||||
}
|
}
|
||||||
|
|||||||
+205
-1
@@ -1,6 +1,7 @@
|
|||||||
package pagination
|
package pagination
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -24,11 +25,17 @@ func TestGetPage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
tests := []testCase[string]{
|
tests := []testCase[string]{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "nil items",
|
||||||
args: args[string]{max: 10},
|
args: args[string]{max: 10},
|
||||||
wantItems: nil,
|
wantItems: nil,
|
||||||
wantPageInfo: PageInfo{},
|
wantPageInfo: PageInfo{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "empty items",
|
||||||
|
args: args[string]{items: []string{}, max: 10},
|
||||||
|
wantItems: nil,
|
||||||
|
wantPageInfo: PageInfo{},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "first 2",
|
name: "first 2",
|
||||||
args: args[string]{items: []string{"1", "2", "3"}, first: ptr(2), max: 10, fn: func(s string) string { return s }},
|
args: args[string]{items: []string{"1", "2", "3"}, first: ptr(2), max: 10, fn: func(s string) string { return s }},
|
||||||
@@ -37,6 +44,24 @@ func TestGetPage(t *testing.T) {
|
|||||||
StartCursor: ptr("MQ=="),
|
StartCursor: ptr("MQ=="),
|
||||||
HasNextPage: true,
|
HasNextPage: true,
|
||||||
EndCursor: ptr("Mg=="),
|
EndCursor: ptr("Mg=="),
|
||||||
|
TotalCount: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no more items",
|
||||||
|
args: args[string]{items: []string{"1", "2", "3"}, first: ptr(2), after: ptr("Mw=="), max: 10, fn: func(s string) string { return s }},
|
||||||
|
wantItems: nil,
|
||||||
|
wantPageInfo: PageInfo{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first 10",
|
||||||
|
args: args[string]{items: []string{"1", "2", "3"}, first: ptr(10), max: 10, fn: func(s string) string { return s }},
|
||||||
|
wantItems: []string{"1", "2", "3"},
|
||||||
|
wantPageInfo: PageInfo{
|
||||||
|
StartCursor: ptr("MQ=="),
|
||||||
|
HasNextPage: false,
|
||||||
|
EndCursor: ptr("Mw=="),
|
||||||
|
TotalCount: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -48,6 +73,7 @@ func TestGetPage(t *testing.T) {
|
|||||||
HasNextPage: false,
|
HasNextPage: false,
|
||||||
HasPreviousPage: true,
|
HasPreviousPage: true,
|
||||||
EndCursor: ptr("NA=="),
|
EndCursor: ptr("NA=="),
|
||||||
|
TotalCount: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,6 +85,7 @@ func TestGetPage(t *testing.T) {
|
|||||||
HasNextPage: false,
|
HasNextPage: false,
|
||||||
HasPreviousPage: true,
|
HasPreviousPage: true,
|
||||||
EndCursor: ptr("Mw=="),
|
EndCursor: ptr("Mw=="),
|
||||||
|
TotalCount: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,6 +97,7 @@ func TestGetPage(t *testing.T) {
|
|||||||
HasNextPage: false,
|
HasNextPage: false,
|
||||||
HasPreviousPage: true,
|
HasPreviousPage: true,
|
||||||
EndCursor: ptr("Mw=="),
|
EndCursor: ptr("Mw=="),
|
||||||
|
TotalCount: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,6 +109,7 @@ func TestGetPage(t *testing.T) {
|
|||||||
HasNextPage: true,
|
HasNextPage: true,
|
||||||
HasPreviousPage: false,
|
HasPreviousPage: false,
|
||||||
EndCursor: ptr("Mg=="),
|
EndCursor: ptr("Mg=="),
|
||||||
|
TotalCount: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -92,6 +121,19 @@ func TestGetPage(t *testing.T) {
|
|||||||
HasNextPage: true,
|
HasNextPage: true,
|
||||||
HasPreviousPage: false,
|
HasPreviousPage: false,
|
||||||
EndCursor: ptr("MQ=="),
|
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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -103,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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user