{"id":34916269,"url":"https://github.com/rvflash/cursor","last_synced_at":"2026-05-23T06:06:03.538Z","repository":{"id":322167230,"uuid":"1086488027","full_name":"rvflash/cursor","owner":"rvflash","description":"Lightweight, generic cursor-based pagination package","archived":false,"fork":false,"pushed_at":"2025-11-14T13:13:14.000Z","size":43,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-14T15:14:38.407Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rvflash.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-30T13:41:47.000Z","updated_at":"2025-11-14T13:13:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rvflash/cursor","commit_stats":null,"previous_names":["rvflash/cursor"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/rvflash/cursor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvflash%2Fcursor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvflash%2Fcursor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvflash%2Fcursor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvflash%2Fcursor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rvflash","download_url":"https://codeload.github.com/rvflash/cursor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rvflash%2Fcursor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33384606,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T04:15:53.637Z","status":"ssl_error","status_checked_at":"2026-05-23T04:15:53.242Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-26T12:29:09.271Z","updated_at":"2026-05-23T06:06:03.533Z","avatar_url":"https://github.com/rvflash.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cursor\n\n[![GoDoc](https://godoc.org/github.com/rvflash/cursor?status.svg)](https://godoc.org/github.com/rvflash/cursor)\n[![Build Status](https://github.com/rvflash/cursor/workflows/build/badge.svg)](https://github.com/rvflash/cursor/actions?workflow=build)\n[![Code Coverage](https://codecov.io/gh/rvflash/cursor/branch/main/graph/badge.svg)](https://codecov.io/gh/rvflash/cursor)\n[![Go Report Card](https://goreportcard.com/badge/github.com/rvflash/cursor?)](https://goreportcard.com/report/github.com/rvflash/cursor)\n\n\nA lightweight, generic cursor-based pagination package for Go.\nDesigned for MySQL and MariaDB, `cursor` lets you build encrypted, \nstateless cursors that encode pagination state and query parameters safely.\n\nCursor-based pagination is preferred to OFFSET for better performance on large tables.\nIt stores the last seen ID or timestamp in the cursor to build efficient WHERE clauses.\nIt is recommended to check the expiration date with `IsExpired(maxAge)` to avoid using outdated cursors.\n\n3 main types:\n- `Cursor` allows computation of data necessary for pagination.\n- `Statement` builds based on a Cursor SQL query parts, to use to perform a SELECT statement.\n- `Pointer` defines the data types that can be used as a cursor to filter the query.\nSuch as `Int64` to manage the auto-increment field.\nSee also `String` or List to manage a set of `Pointer` as `Pointer`.\nFinally, `RowCount` can be used as `Pointer` to transform the cursor into a standard LIMIT statement, \nwith offset and row count (also see Statement.Offset).\n\n\n### Cursor Encoding Format\n\n- Internally serialized as JSON, then encoded as Base64 (URL-safe).\n- Optionally encrypted or signed using HMAC for integrity.\n- Fully stateless — no server session needed.\n\n\n## Features\n\n- 🔒 Encrypted cursors — opaque Base64 tokens with or not HMAC signing.\n- 📜 Cursor-based pagination — no offset drift, efficient for large datasets.\n- 🧠 Stateless by design — all state is encoded in the cursor.\n- 💡 Generic — supports any data type T.\n- 🧩 SQL helpers for LIMIT, ORDER BY, and conditional pagination queries.\n- ⏱️ Expiration support — cursors can self-expire based on max age.\n\n\n## Installation\n\n```go\ngo get github.com/rvflash/cursor\n```\n\n## Example Usage\n\nCodes with multiple shortcuts for demonstration purposes only.\n\n### SQL statement\n\n\n```go\nfunc ListFromDatabase(ctx context.Context, cur *cursor.Cursor[cursor.Int64]) ([]User, error) {\n    // Create a Statement based on the Cursor.\n    var st = cursor.Statement[cursor.Int64]{\n        Cursor:          cur,\n        DescendingOrder: false,\n    }\n    // WHERE uses the cursor semantics (e.g., \"id \u003c ?\") under descending order\n    where, args := st.WhereCondition(\"id\")\n    if len(args) \u003e 0 {\n        where = \" WHERE \" + where\n    }\n    // LIMIT +1 to check if there is a next page.\n    args = append(args, st.Limit())\n    // ORDER BY applies the desired ordering for the limited page (e.g., \"ORDER BY id DESC\")\n    query := `SELECT id, name FROM users` + where + \" ORDER BY\" + st.OrderBy(\"id\") + \" LIMIT ?\"\n    // Reset allows to reuse the current cursor to build the next ones.\n    cur.Reset()\n\n    rows, err := DB.QueryContext(ctx, query, args...)\n    if err != nil {\n        return nil, err\n    }\n    defer func() { _ = rows.Close() }()\n    \n    var (\n\t\tres []User\n        u User\n\t)\n    for rows.Next() {\n        if err = rows.Scan(\u0026u.ID, \u0026u.Name); err != nil {\n            return nil, err\n        }\n        res = append(res, u)\n        cur.Add(cursor.Int64(u.ID)) // we’re pointing by ID in this example\n    }\n    return res[:min(len(res), st.Cursor.Limit)], rows.Err()\n}\n\ntype User struct {\n    ID        int64  `json:\"id\"`\n    Name      string `json:\"name\"`\n}\n```\n \n### Integrating with an HTTP API\n\nExample of returning paginated results in a REST response:\n```go\nfunc HTTPHandler(w http.ResponseWriter, r *http.Request) {\n    var (\n        secret = []byte(os.Getenv(\"CURSOR_SECRET\"))\n        cur    *cursor.Cursor[cursor.Int64]\n        err    error\n    )\n    if tok := r.URL.Query().Get(\"cursor\"); tok != \"\" {\n        // Decrypt verifies HMAC and returns the cursor state\n        cur, err = cursor.Decrypt[cursor.Int64]([]byte(tok), secret)\n        if err != nil || cur.IsExpired(time.Hour) {\n         http.Error(w, \"invalid or expired cursor\", http.StatusBadRequest)\n            return\n        }\n\t} else {\n        // New(limit, total). If you don’t know total, you can pass 0 (or compute it separately)\n        cur = cursor.New[cursor.Int64](20, 0)\n    }\n    // SQL query\n    rows, err := ListFromDatabase(r.Context(), cur)\n    if err != nil {\n        http.Error(w, \"query error\", http.StatusInternalServerError)\n        return\n    }\n    // Build pagination tokens: first/prev/next/last.\n    pg, err := cursor.Paginate(cur, secret)\n    if err != nil {\n        http.Error(w, \"pagination error\", http.StatusInternalServerError)\n        return\n    }\n    w.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n    _ = json.NewEncoder(w).Encode(usersResponse{\n        Data:       rows,\n        Pagination: pg,\n    })\n}\n\ntype usersResponse struct {\n    Data        []User  `json:\"data\"`\n    Pagination  *cursor.Pagination `json:\"cursor\"`\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frvflash%2Fcursor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frvflash%2Fcursor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frvflash%2Fcursor/lists"}