mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat(api): error codes, idempotency keys, cursor pagination
- Stable machine-readable error codes alongside the error message (UNAUTHENTICATED, INVALID_TOKEN, ADMIN_REQUIRED, SCOPE_INSUFFICIENT, IDEMPOTENCY_IN_PROGRESS); CLI renders "CODE: message". - Idempotency middleware on authed/admin groups: POST requests with an Idempotency-Key header replay the first response for 24h (Redis-backed, caller-scoped); concurrent duplicates get 409. X-Idempotent-Replay header marks replays. - Shared pagination helpers: ?cursor= (opaque, preferred) alongside legacy ?page= on projects and deployments lists; pagination meta includes next_cursor; invalid cursors fail soft to page 1. - CLI --idempotency-key flag for safe scripted retries. - OpenAPI updated: error envelope, cursor params, idempotency contract. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
485e41a817
commit
18f78f00a3
15 files changed
+454
-56
No files matched your search
@@ -0,0 +1,52 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// pageWindow parses pagination from either ?page=&limit= (legacy) or
|
||||
// ?cursor=&limit= (agent-friendly). The cursor is an opaque base64 offset —
|
||||
// cheap to implement over the existing offset queries while the public
|
||||
// contract stays cursor-shaped for clients.
|
||||
func pageWindow(c *gin.Context, defaultLimit, maxLimit int) (offset, limit int) {
|
||||
limit = defaultLimit
|
||||
if v := c.Query("limit"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= maxLimit {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
if cur := c.Query("cursor"); cur != "" {
|
||||
if raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(cur, "o_")); err == nil {
|
||||
if n, err := strconv.Atoi(string(raw)); err == nil && n >= 0 {
|
||||
return n, limit
|
||||
}
|
||||
}
|
||||
return 0, limit // invalid cursor → first page, fail soft
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
return (page - 1) * limit, limit
|
||||
}
|
||||
|
||||
// paginationMeta renders the standard envelope: legacy page fields plus
|
||||
// next_cursor when another page exists.
|
||||
func paginationMeta(offset, limit, total int) gin.H {
|
||||
page := offset/limit + 1
|
||||
meta := gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"pages": (total + limit - 1) / limit,
|
||||
}
|
||||
if offset+limit < total {
|
||||
meta["next_cursor"] = "o_" + base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprint(offset+limit)))
|
||||
}
|
||||
return meta
|
||||
}
|
||||
Reference in new issue
Block a user