[{"data":1,"prerenderedAt":2242},["ShallowReactive",2],{"navigation_docs_en":3,"-en-paginate-cursor":441,"-en-paginate-cursor-surround":2239},[4,45,60,81,112,129,146,173,197,213,242,279,356,372,384,403,429],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"queue","i-lucide-layers","\u002Fen\u002Fqueue","en\u002F10.queue",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Overview","\u002Fen\u002Fqueue\u002Foverview","en\u002F10.queue\u002F1.overview","i-lucide-list-ordered",{"title":57,"path":58,"stem":59,"icon":47},"Tasks","\u002Fen\u002Fqueue\u002Ftasks","en\u002F10.queue\u002F2.tasks",{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":44},"events","i-lucide-workflow","\u002Fen\u002Fevents","en\u002F11.events",[66,71,76],{"title":67,"path":68,"stem":69,"icon":70},"Events","\u002Fen\u002Fevents\u002Foverview","en\u002F11.events\u002F1.overview","i-lucide-radio",{"title":72,"path":73,"stem":74,"icon":75},"Outbox","\u002Fen\u002Fevents\u002Foutbox","en\u002F11.events\u002F2.outbox","i-lucide-inbox",{"title":77,"path":78,"stem":79,"icon":80},"Scheduling","\u002Fen\u002Fevents\u002Fscheduler","en\u002F11.events\u002F3.scheduler","i-lucide-calendar-clock",{"title":82,"icon":83,"path":84,"stem":85,"children":86,"page":44},"auth","i-lucide-shield-check","\u002Fen\u002Fauth","en\u002F12.auth",[87,92,97,102,107],{"title":88,"path":89,"stem":90,"icon":91},"Authentication","\u002Fen\u002Fauth\u002Foverview","en\u002F12.auth\u002F1.overview","i-lucide-key-round",{"title":93,"path":94,"stem":95,"icon":96},"RBAC","\u002Fen\u002Fauth\u002Frbac","en\u002F12.auth\u002F2.rbac","i-lucide-users",{"title":98,"path":99,"stem":100,"icon":101},"Policies","\u002Fen\u002Fauth\u002Fpolicy","en\u002F12.auth\u002F3.policy","i-lucide-gavel",{"title":103,"path":104,"stem":105,"icon":106},"Using auth standalone","\u002Fen\u002Fauth\u002Fstandalone","en\u002F12.auth\u002F4.standalone","i-lucide-plug",{"title":108,"path":109,"stem":110,"icon":111},"Social login with goth, standalone","\u002Fen\u002Fauth\u002Fsocial-standalone","en\u002F12.auth\u002F5.social-standalone","i-lucide-log-in",{"title":113,"icon":114,"path":115,"stem":116,"children":117,"page":44},"mail","i-lucide-mail","\u002Fen\u002Fmail","en\u002F13.mail",[118,121,125],{"title":52,"path":119,"stem":120,"icon":114},"\u002Fen\u002Fmail\u002Foverview","en\u002F13.mail\u002F1.overview",{"title":122,"path":123,"stem":124,"icon":114},"SMTP","\u002Fen\u002Fmail\u002Fsmtp","en\u002F13.mail\u002F2.smtp",{"title":126,"path":127,"stem":128,"icon":114},"MJML","\u002Fen\u002Fmail\u002Fmjml","en\u002F13.mail\u002F3.mjml",{"title":130,"icon":131,"path":132,"stem":133,"children":134,"page":44},"notify","i-lucide-bell","\u002Fen\u002Fnotify","en\u002F14.notify",[135,138,142],{"title":52,"path":136,"stem":137,"icon":131},"\u002Fen\u002Fnotify\u002Foverview","en\u002F14.notify\u002F1.overview",{"title":139,"path":140,"stem":141,"icon":131},"Database channel","\u002Fen\u002Fnotify\u002Fdatabase","en\u002F14.notify\u002F2.database",{"title":143,"path":144,"stem":145,"icon":131},"Broadcast channel","\u002Fen\u002Fnotify\u002Fbroadcast","en\u002F14.notify\u002F3.broadcast",{"title":147,"icon":148,"path":149,"stem":150,"children":151,"page":44},"storage","i-lucide-hard-drive","\u002Fen\u002Fstorage","en\u002F15.storage",[152,155,159,163,168],{"title":52,"path":153,"stem":154,"icon":148},"\u002Fen\u002Fstorage\u002Foverview","en\u002F15.storage\u002F1.overview",{"title":156,"path":157,"stem":158},"Disk API","\u002Fen\u002Fstorage\u002Fdisk-api","en\u002F15.storage\u002F2.disk-api",{"title":160,"path":161,"stem":162,"icon":148},"Memory and local disk","\u002Fen\u002Fstorage\u002Flocal","en\u002F15.storage\u002F3.local",{"title":164,"path":165,"stem":166,"icon":167},"S3","\u002Fen\u002Fstorage\u002Fs3","en\u002F15.storage\u002F4.s3","i-lucide-cloud",{"title":169,"path":170,"stem":171,"icon":172},"Testing","\u002Fen\u002Fstorage\u002Ftesting","en\u002F15.storage\u002F5.testing","i-lucide-flask-conical",{"title":174,"icon":175,"path":176,"stem":177,"children":178,"page":44},"telemetry","i-lucide-activity","\u002Fen\u002Ftelemetry","en\u002F16.telemetry",[179,182,187,192],{"title":52,"path":180,"stem":181,"icon":175},"\u002Fen\u002Ftelemetry\u002Foverview","en\u002F16.telemetry\u002F1.overview",{"title":183,"path":184,"stem":185,"icon":186},"Logging","\u002Fen\u002Ftelemetry\u002Flogging","en\u002F16.telemetry\u002F2.logging","i-lucide-scroll-text",{"title":188,"path":189,"stem":190,"icon":191},"OpenTelemetry","\u002Fen\u002Ftelemetry\u002Fopentelemetry","en\u002F16.telemetry\u002F3.opentelemetry","i-lucide-radar",{"title":193,"path":194,"stem":195,"icon":196},"Sentry","\u002Fen\u002Ftelemetry\u002Fsentry","en\u002F16.telemetry\u002F4.sentry","i-lucide-bug",{"title":198,"icon":199,"path":200,"stem":201,"children":202,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F17.reference",[203,208],{"title":204,"path":205,"stem":206,"icon":207},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F17.reference\u002F1.configuration","i-lucide-settings",{"title":209,"path":210,"stem":211,"icon":212},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F17.reference\u002F2.dependencies","i-lucide-package",{"title":214,"icon":215,"path":216,"stem":217,"children":218,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[219,223,228,232,237],{"title":220,"path":221,"stem":222,"icon":47},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture",{"title":224,"path":225,"stem":226,"icon":227},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F2.error-model","i-lucide-shield-alert",{"title":204,"path":229,"stem":230,"icon":231},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F3.configuration","i-lucide-settings-2",{"title":233,"path":234,"stem":235,"icon":236},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F4.codegen-pipeline","i-lucide-file-json",{"title":238,"path":239,"stem":240,"icon":241},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F5.design-patterns","i-lucide-puzzle",{"title":243,"icon":244,"path":245,"stem":246,"children":247,"page":44},"Kit","i-lucide-box","\u002Fen\u002Fkit","en\u002F3.kit",[248,251,256,261,266,270,275],{"title":52,"path":249,"stem":250,"icon":244},"\u002Fen\u002Fkit\u002Foverview","en\u002F3.kit\u002F1.overview",{"title":252,"path":253,"stem":254,"icon":255},"Application lifecycle","\u002Fen\u002Fkit\u002Fapp","en\u002F3.kit\u002F2.app","i-lucide-power",{"title":257,"path":258,"stem":259,"icon":260},"Server","\u002Fen\u002Fkit\u002Fserver","en\u002F3.kit\u002F3.server","i-lucide-server",{"title":262,"path":263,"stem":264,"icon":265},"Worker","\u002Fen\u002Fkit\u002Fworker","en\u002F3.kit\u002F4.worker","i-lucide-cog",{"title":267,"path":268,"stem":269,"icon":70},"Realtime","\u002Fen\u002Fkit\u002Frealtime","en\u002F3.kit\u002F5.realtime",{"title":271,"path":272,"stem":273,"icon":274},"Migrations","\u002Fen\u002Fkit\u002Fmigrations","en\u002F3.kit\u002F6.migrations","i-lucide-file-stack",{"title":276,"path":277,"stem":278,"icon":111},"Social login","\u002Fen\u002Fkit\u002Fsocial-login","en\u002F3.kit\u002F7.social-login",{"title":280,"icon":281,"path":282,"stem":283,"children":284,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F4.cli",[285,288,293,298,303,308,313,318,323,328,333,337,342,347,351],{"title":52,"path":286,"stem":287,"icon":281},"\u002Fen\u002Fcli\u002Foverview","en\u002F4.cli\u002F1.overview",{"title":289,"path":290,"stem":291,"icon":292},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F4.cli\u002F10.add-db","i-lucide-database-zap",{"title":294,"path":295,"stem":296,"icon":297},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F4.cli\u002F11.add-compose","i-lucide-container",{"title":299,"path":300,"stem":301,"icon":302},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F4.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":304,"path":305,"stem":306,"icon":307},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F4.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":309,"path":310,"stem":311,"icon":312},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F4.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":314,"path":315,"stem":316,"icon":317},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F4.cli\u002F15.add-seeder","i-lucide-sprout",{"title":319,"path":320,"stem":321,"icon":322},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F4.cli\u002F2.new-project","i-lucide-folder-plus",{"title":324,"path":325,"stem":326,"icon":327},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F4.cli\u002F3.new-module","i-lucide-blocks",{"title":329,"path":330,"stem":331,"icon":332},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F4.cli\u002F4.add-surface","i-lucide-layers-2",{"title":334,"path":335,"stem":336,"icon":244},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F4.cli\u002F5.add-core",{"title":338,"path":339,"stem":340,"icon":341},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F4.cli\u002F6.add-handler","i-lucide-webhook",{"title":343,"path":344,"stem":345,"icon":346},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F4.cli\u002F7.new-migration","i-lucide-file-plus",{"title":348,"path":349,"stem":350,"icon":265},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F4.cli\u002F8.worker-generators",{"title":352,"path":353,"stem":354,"icon":355},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F4.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"title":357,"icon":227,"path":358,"stem":359,"children":360,"page":44},"errs","\u002Fen\u002Ferrs","en\u002F5.errs",[361,364,368],{"title":52,"path":362,"stem":363,"icon":227},"\u002Fen\u002Ferrs\u002Foverview","en\u002F5.errs\u002F1.overview",{"title":365,"path":366,"stem":367},"API","\u002Fen\u002Ferrs\u002Fapi","en\u002F5.errs\u002F2.api",{"title":369,"path":370,"stem":371,"icon":227},"Integration","\u002Fen\u002Ferrs\u002Fintegration","en\u002F5.errs\u002F3.integration",{"title":373,"icon":231,"path":374,"stem":375,"children":376,"page":44},"envconf","\u002Fen\u002Fenvconf","en\u002F6.envconf",[377,380],{"title":52,"path":378,"stem":379,"icon":231},"\u002Fen\u002Fenvconf\u002Foverview","en\u002F6.envconf\u002F1.overview",{"title":381,"path":382,"stem":383},"Recipes","\u002Fen\u002Fenvconf\u002Frecipes","en\u002F6.envconf\u002F2.recipes",{"title":385,"icon":386,"path":387,"stem":388,"children":389,"page":44},"httperr","i-lucide-globe","\u002Fen\u002Fhttperr","en\u002F7.httperr",[390,393,398],{"title":52,"path":391,"stem":392,"icon":386},"\u002Fen\u002Fhttperr\u002Foverview","en\u002F7.httperr\u002F1.overview",{"title":394,"path":395,"stem":396,"icon":397},"Error responses","\u002Fen\u002Fhttperr\u002Ferror-responses","en\u002F7.httperr\u002F2.error-responses","i-lucide-octagon-alert",{"title":399,"path":400,"stem":401,"icon":402},"Validation","\u002Fen\u002Fhttperr\u002Fvalidation","en\u002F7.httperr\u002F3.validation","i-lucide-badge-check",{"title":404,"icon":405,"path":406,"stem":407,"children":408,"page":44},"dbx","i-lucide-database","\u002Fen\u002Fdbx","en\u002F8.dbx",[409,412,416,420,425],{"title":52,"path":410,"stem":411,"icon":405},"\u002Fen\u002Fdbx\u002Foverview","en\u002F8.dbx\u002F1.overview",{"title":413,"path":414,"stem":415,"icon":106},"pgx","\u002Fen\u002Fdbx\u002Fpg","en\u002F8.dbx\u002F2.pg",{"title":417,"path":418,"stem":419,"icon":47},"bun","\u002Fen\u002Fdbx\u002Fbunx","en\u002F8.dbx\u002F3.bunx",{"title":421,"path":422,"stem":423,"icon":424},"Transactions","\u002Fen\u002Fdbx\u002Ftransactions","en\u002F8.dbx\u002F4.transactions","i-lucide-git-merge",{"title":426,"path":427,"stem":428,"icon":317},"Seeders","\u002Fen\u002Fdbx\u002Fseed","en\u002F8.dbx\u002F5.seed",{"title":430,"icon":55,"path":431,"stem":432,"children":433,"page":44},"paginate","\u002Fen\u002Fpaginate","en\u002F9.paginate",[434,437],{"title":52,"path":435,"stem":436,"icon":55},"\u002Fen\u002Fpaginate\u002Foverview","en\u002F9.paginate\u002F1.overview",{"title":438,"path":439,"stem":440,"icon":19},"Cursor pagination","\u002Fen\u002Fpaginate\u002Fcursor","en\u002F9.paginate\u002F2.cursor",{"id":442,"title":438,"body":443,"description":2232,"extension":2233,"links":2234,"meta":2235,"navigation":2236,"path":439,"seo":2237,"stem":440,"__hash__":2238},"docs_en\u002Fen\u002F9.paginate\u002F2.cursor.md",{"type":444,"value":445,"toc":2224},"minimark",[446,476,873,913,918,929,1152,1353,1910,1954,1958,1965,2020,2024,2042,2060,2080,2084,2167,2173,2177,2203,2207,2220],[447,448,449,453,454,458,459,463,464,467,468,471,472,475],"p",{},[450,451,452],"a",{"href":435},"Offset pagination"," has two structural problems on large or frequently mutated tables: ",[455,456,457],"code",{},"OFFSET n"," computes and ",[460,461,462],"strong",{},"throws away"," the first ",[455,465,466],{},"n"," rows on every request (O(n), page 400 means combing through 8000 rows), and under concurrent inserts the pages ",[460,469,470],{},"drift",": the same row shows up on two pages, or on none. Cursor (keyset) pagination solves both: the client receives an opaque cursor encoding the sort key of the last row it saw, and the next page continues from there with an indexed seek (",[455,473,474],{},"WHERE (created_at, id) \u003C (...)","), at near-constant cost, however deep you page.",[477,478,483],"pre",{"className":479,"code":480,"language":481,"meta":482,"style":482},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","func NormalizeCursor(cursor *string, limit *int, sort string, opts ...Option) (CursorParams, error)\n\ntype CursorParams struct{ Limit int \u002F* + decoded keys *\u002F }\nfunc (p CursorParams) HasCursor() bool          \u002F\u002F was a cursor supplied, i.e. is this a page after the first\nfunc (p CursorParams) DecodeKey(dests ...any) error \u002F\u002F read out the key values, Scan-style\nfunc (p CursorParams) FetchLimit() int          \u002F\u002F Limit+1, how we detect whether another page follows\nfunc (p CursorParams) NextCursor(keys ...any) (string, error)\n\nfunc NewCursorPage[T any](rows []T, p CursorParams, key func(last T) []any) (CursorPage[T], error)\n\ntype CursorPage[T any] struct {\n    Items      []T\n    NextCursor string \u002F\u002F \"\" on the last page\n    HasMore    bool\n}\n\nvar ErrInvalidCursor \u002F\u002F errs definition: HTTP 400, code: paginate_invalid_cursor\n","go","",[455,484,485,560,567,592,618,651,675,712,717,790,795,818,830,841,850,856,861],{"__ignoreMap":482},[486,487,490,494,498,501,505,508,512,515,518,520,523,525,528,531,533,536,539,543,546,549,552,554,557],"span",{"class":488,"line":489},"line",1,[486,491,493],{"class":492},"sMK4o","func",[486,495,497],{"class":496},"s2Zo4"," NormalizeCursor",[486,499,500],{"class":492},"(",[486,502,504],{"class":503},"sHdIc","cursor",[486,506,507],{"class":492}," *",[486,509,511],{"class":510},"spNyl","string",[486,513,514],{"class":492},",",[486,516,517],{"class":503}," limit",[486,519,507],{"class":492},[486,521,522],{"class":510},"int",[486,524,514],{"class":492},[486,526,527],{"class":503}," sort",[486,529,530],{"class":510}," string",[486,532,514],{"class":492},[486,534,535],{"class":503}," opts",[486,537,538],{"class":492}," ...",[486,540,542],{"class":541},"sBMFI","Option",[486,544,545],{"class":492},")",[486,547,548],{"class":492}," (",[486,550,551],{"class":541},"CursorParams",[486,553,514],{"class":492},[486,555,556],{"class":510}," error",[486,558,559],{"class":492},")\n",[486,561,563],{"class":488,"line":562},2,[486,564,566],{"emptyLinePlaceholder":565},true,"\n",[486,568,570,573,576,579,583,585,589],{"class":488,"line":569},3,[486,571,572],{"class":492},"type",[486,574,575],{"class":541}," CursorParams",[486,577,578],{"class":492}," struct{",[486,580,582],{"class":581},"sTEyZ"," Limit ",[486,584,522],{"class":510},[486,586,588],{"class":587},"sHwdD"," \u002F* + decoded keys *\u002F",[486,590,591],{"class":492}," }\n",[486,593,595,597,599,602,604,606,609,612,615],{"class":488,"line":594},4,[486,596,493],{"class":492},[486,598,548],{"class":492},[486,600,601],{"class":503},"p ",[486,603,551],{"class":541},[486,605,545],{"class":492},[486,607,608],{"class":496}," HasCursor",[486,610,611],{"class":492},"()",[486,613,614],{"class":510}," bool",[486,616,617],{"class":587},"          \u002F\u002F was a cursor supplied, i.e. is this a page after the first\n",[486,619,621,623,625,627,629,631,634,636,639,641,644,646,648],{"class":488,"line":620},5,[486,622,493],{"class":492},[486,624,548],{"class":492},[486,626,601],{"class":503},[486,628,551],{"class":541},[486,630,545],{"class":492},[486,632,633],{"class":496}," DecodeKey",[486,635,500],{"class":492},[486,637,638],{"class":503},"dests",[486,640,538],{"class":492},[486,642,643],{"class":541},"any",[486,645,545],{"class":492},[486,647,556],{"class":510},[486,649,650],{"class":587}," \u002F\u002F read out the key values, Scan-style\n",[486,652,654,656,658,660,662,664,667,669,672],{"class":488,"line":653},6,[486,655,493],{"class":492},[486,657,548],{"class":492},[486,659,601],{"class":503},[486,661,551],{"class":541},[486,663,545],{"class":492},[486,665,666],{"class":496}," FetchLimit",[486,668,611],{"class":492},[486,670,671],{"class":510}," int",[486,673,674],{"class":587},"          \u002F\u002F Limit+1, how we detect whether another page follows\n",[486,676,678,680,682,684,686,688,691,693,696,698,700,702,704,706,708,710],{"class":488,"line":677},7,[486,679,493],{"class":492},[486,681,548],{"class":492},[486,683,601],{"class":503},[486,685,551],{"class":541},[486,687,545],{"class":492},[486,689,690],{"class":496}," NextCursor",[486,692,500],{"class":492},[486,694,695],{"class":503},"keys",[486,697,538],{"class":492},[486,699,643],{"class":541},[486,701,545],{"class":492},[486,703,548],{"class":492},[486,705,511],{"class":510},[486,707,514],{"class":492},[486,709,556],{"class":510},[486,711,559],{"class":492},[486,713,715],{"class":488,"line":714},8,[486,716,566],{"emptyLinePlaceholder":565},[486,718,720,722,725,728,731,734,737,740,743,745,747,750,752,754,757,760,763,766,768,770,772,774,776,779,781,783,786,788],{"class":488,"line":719},9,[486,721,493],{"class":492},[486,723,724],{"class":496}," NewCursorPage",[486,726,727],{"class":492},"[",[486,729,730],{"class":503},"T",[486,732,733],{"class":541}," any",[486,735,736],{"class":492},"](",[486,738,739],{"class":503},"rows",[486,741,742],{"class":492}," []",[486,744,730],{"class":541},[486,746,514],{"class":492},[486,748,749],{"class":503}," p",[486,751,575],{"class":541},[486,753,514],{"class":492},[486,755,756],{"class":503}," key",[486,758,759],{"class":492}," func(",[486,761,762],{"class":503},"last",[486,764,765],{"class":541}," T",[486,767,545],{"class":492},[486,769,742],{"class":492},[486,771,643],{"class":541},[486,773,545],{"class":492},[486,775,548],{"class":492},[486,777,778],{"class":541},"CursorPage",[486,780,727],{"class":492},[486,782,730],{"class":541},[486,784,785],{"class":492},"],",[486,787,556],{"class":510},[486,789,559],{"class":492},[486,791,793],{"class":488,"line":792},10,[486,794,566],{"emptyLinePlaceholder":565},[486,796,798,800,803,805,807,809,812,815],{"class":488,"line":797},11,[486,799,572],{"class":492},[486,801,802],{"class":541}," CursorPage",[486,804,727],{"class":492},[486,806,730],{"class":503},[486,808,733],{"class":541},[486,810,811],{"class":492},"]",[486,813,814],{"class":492}," struct",[486,816,817],{"class":492}," {\n",[486,819,821,824,827],{"class":488,"line":820},12,[486,822,823],{"class":581},"    Items      ",[486,825,826],{"class":492},"[]",[486,828,829],{"class":541},"T\n",[486,831,833,836,838],{"class":488,"line":832},13,[486,834,835],{"class":581},"    NextCursor ",[486,837,511],{"class":510},[486,839,840],{"class":587}," \u002F\u002F \"\" on the last page\n",[486,842,844,847],{"class":488,"line":843},14,[486,845,846],{"class":581},"    HasMore    ",[486,848,849],{"class":510},"bool\n",[486,851,853],{"class":488,"line":852},15,[486,854,855],{"class":492},"}\n",[486,857,859],{"class":488,"line":858},16,[486,860,566],{"emptyLinePlaceholder":565},[486,862,864,867,870],{"class":488,"line":863},17,[486,865,866],{"class":492},"var",[486,868,869],{"class":581}," ErrInvalidCursor ",[486,871,872],{"class":587},"\u002F\u002F errs definition: HTTP 400, code: paginate_invalid_cursor\n",[447,874,875,878,879,882,883,886,887,890,891,548,897,900,901,904,905,908,909,912],{},[455,876,877],{},"NormalizeCursor"," clamps ",[455,880,881],{},"limit"," with the same ",[455,884,885],{},"WithDefaultPerPage","\u002F",[455,888,889],{},"WithMaxPerPage"," options as ",[450,892,894],{"href":893},"\u002Fen\u002Fpaginate\u002Foverview#offset-pagination",[455,895,896],{},"Normalize",[455,898,899],{},"WithMaxPage"," is ignored, there is no page number here). The ",[455,902,903],{},"sort"," argument must be ",[460,906,907],{},"exactly"," the ",[455,910,911],{},"ORDER BY"," expression the query uses: its fingerprint is stamped into issued cursors, and a cursor issued for a different sort is later rejected.",[914,915,917],"h2",{"id":916},"the-shop-product-listing-end-to-end","The shop product listing, end to end",[447,919,920,921,924,925,928],{},"The full path of ",[455,922,923],{},"GET \u002Fapi\u002Fv1\u002Fshop\u002Fproducts?cursor=...&limit=..."," in the ",[450,926,927],{"href":41},"sample app",", handler → service → repository → response:",[477,930,933],{"className":479,"code":931,"filename":932,"language":481,"meta":482,"style":482},"func (h *Handler) ListProducts(ctx context.Context, req gen.ListProductsRequest) (gen.ListProductsResponse, error) {\n    page, err := h.svc.ListProducts(ctx, req.Params.Cursor, req.Params.Limit)\n    if err != nil {\n        return nil, service.ErrListProducts.Wrap(err, \"api: list products\")\n    }\n    return mapper.ToProductList(page), nil \u002F\u002F paginate.CursorPage[Product] → gen.CursorPageProduct\n}\n","internal\u002Fmodules\u002Fshop\u002Fsurfaces\u002Fapi\u002Fhttp\u002Fhandler.go",[455,934,935,1002,1061,1077,1117,1122,1148],{"__ignoreMap":482},[486,936,937,939,941,944,947,950,952,955,957,960,963,966,969,971,974,977,979,982,984,986,989,991,994,996,998,1000],{"class":488,"line":489},[486,938,493],{"class":492},[486,940,548],{"class":492},[486,942,943],{"class":503},"h ",[486,945,946],{"class":492},"*",[486,948,949],{"class":541},"Handler",[486,951,545],{"class":492},[486,953,954],{"class":496}," ListProducts",[486,956,500],{"class":492},[486,958,959],{"class":503},"ctx",[486,961,962],{"class":541}," context",[486,964,965],{"class":492},".",[486,967,968],{"class":541},"Context",[486,970,514],{"class":492},[486,972,973],{"class":503}," req",[486,975,976],{"class":541}," gen",[486,978,965],{"class":492},[486,980,981],{"class":541},"ListProductsRequest",[486,983,545],{"class":492},[486,985,548],{"class":492},[486,987,988],{"class":541},"gen",[486,990,965],{"class":492},[486,992,993],{"class":541},"ListProductsResponse",[486,995,514],{"class":492},[486,997,556],{"class":510},[486,999,545],{"class":492},[486,1001,817],{"class":492},[486,1003,1004,1007,1009,1012,1015,1018,1020,1023,1025,1028,1030,1032,1034,1036,1038,1041,1043,1046,1048,1050,1052,1054,1056,1059],{"class":488,"line":562},[486,1005,1006],{"class":581},"    page",[486,1008,514],{"class":492},[486,1010,1011],{"class":581}," err ",[486,1013,1014],{"class":492},":=",[486,1016,1017],{"class":581}," h",[486,1019,965],{"class":492},[486,1021,1022],{"class":581},"svc",[486,1024,965],{"class":492},[486,1026,1027],{"class":496},"ListProducts",[486,1029,500],{"class":492},[486,1031,959],{"class":581},[486,1033,514],{"class":492},[486,1035,973],{"class":581},[486,1037,965],{"class":492},[486,1039,1040],{"class":581},"Params",[486,1042,965],{"class":492},[486,1044,1045],{"class":581},"Cursor",[486,1047,514],{"class":492},[486,1049,973],{"class":581},[486,1051,965],{"class":492},[486,1053,1040],{"class":581},[486,1055,965],{"class":492},[486,1057,1058],{"class":581},"Limit",[486,1060,559],{"class":492},[486,1062,1063,1067,1069,1072,1075],{"class":488,"line":569},[486,1064,1066],{"class":1065},"s7zQu","    if",[486,1068,1011],{"class":581},[486,1070,1071],{"class":492},"!=",[486,1073,1074],{"class":492}," nil",[486,1076,817],{"class":492},[486,1078,1079,1082,1085,1088,1090,1093,1095,1098,1100,1103,1105,1108,1112,1115],{"class":488,"line":594},[486,1080,1081],{"class":1065},"        return",[486,1083,1084],{"class":492}," nil,",[486,1086,1087],{"class":581}," service",[486,1089,965],{"class":492},[486,1091,1092],{"class":581},"ErrListProducts",[486,1094,965],{"class":492},[486,1096,1097],{"class":496},"Wrap",[486,1099,500],{"class":492},[486,1101,1102],{"class":581},"err",[486,1104,514],{"class":492},[486,1106,1107],{"class":492}," \"",[486,1109,1111],{"class":1110},"sfazB","api: list products",[486,1113,1114],{"class":492},"\"",[486,1116,559],{"class":492},[486,1118,1119],{"class":488,"line":620},[486,1120,1121],{"class":492},"    }\n",[486,1123,1124,1127,1130,1132,1135,1137,1140,1143,1145],{"class":488,"line":653},[486,1125,1126],{"class":1065},"    return",[486,1128,1129],{"class":581}," mapper",[486,1131,965],{"class":492},[486,1133,1134],{"class":496},"ToProductList",[486,1136,500],{"class":492},[486,1138,1139],{"class":581},"page",[486,1141,1142],{"class":492},"),",[486,1144,1074],{"class":492},[486,1146,1147],{"class":587}," \u002F\u002F paginate.CursorPage[Product] → gen.CursorPageProduct\n",[486,1149,1150],{"class":488,"line":677},[486,1151,855],{"class":492},[477,1153,1156],{"className":479,"code":1154,"filename":1155,"language":481,"meta":482,"style":482},"const productSort = \"created_at DESC, id DESC\" \u002F\u002F must match the repository's ORDER BY\n\nfunc (s *Service) ListProducts(ctx context.Context, cursor *string, limit *int) (paginate.CursorPage[Product], error) {\n    p, err := paginate.NormalizeCursor(cursor, limit, productSort)\n    if err != nil {\n        return paginate.CursorPage[Product]{}, err \u002F\u002F ErrInvalidCursor → HTTP 400\n    }\n    return s.products.List(ctx, p)\n}\n","internal\u002Fmodules\u002Fshop\u002Fsurfaces\u002Fapi\u002Fservice\u002Fservice.go",[455,1157,1158,1179,1183,1251,1284,1296,1318,1322,1349],{"__ignoreMap":482},[486,1159,1160,1163,1166,1169,1171,1174,1176],{"class":488,"line":489},[486,1161,1162],{"class":492},"const",[486,1164,1165],{"class":581}," productSort ",[486,1167,1168],{"class":492},"=",[486,1170,1107],{"class":492},[486,1172,1173],{"class":1110},"created_at DESC, id DESC",[486,1175,1114],{"class":492},[486,1177,1178],{"class":587}," \u002F\u002F must match the repository's ORDER BY\n",[486,1180,1181],{"class":488,"line":562},[486,1182,566],{"emptyLinePlaceholder":565},[486,1184,1185,1187,1189,1192,1194,1197,1199,1201,1203,1205,1207,1209,1211,1213,1216,1218,1220,1222,1224,1226,1228,1230,1232,1234,1236,1238,1240,1243,1245,1247,1249],{"class":488,"line":569},[486,1186,493],{"class":492},[486,1188,548],{"class":492},[486,1190,1191],{"class":503},"s ",[486,1193,946],{"class":492},[486,1195,1196],{"class":541},"Service",[486,1198,545],{"class":492},[486,1200,954],{"class":496},[486,1202,500],{"class":492},[486,1204,959],{"class":503},[486,1206,962],{"class":541},[486,1208,965],{"class":492},[486,1210,968],{"class":541},[486,1212,514],{"class":492},[486,1214,1215],{"class":503}," cursor",[486,1217,507],{"class":492},[486,1219,511],{"class":510},[486,1221,514],{"class":492},[486,1223,517],{"class":503},[486,1225,507],{"class":492},[486,1227,522],{"class":510},[486,1229,545],{"class":492},[486,1231,548],{"class":492},[486,1233,430],{"class":541},[486,1235,965],{"class":492},[486,1237,778],{"class":541},[486,1239,727],{"class":492},[486,1241,1242],{"class":541},"Product",[486,1244,785],{"class":492},[486,1246,556],{"class":510},[486,1248,545],{"class":492},[486,1250,817],{"class":492},[486,1252,1253,1256,1258,1260,1262,1265,1267,1269,1271,1273,1275,1277,1279,1282],{"class":488,"line":594},[486,1254,1255],{"class":581},"    p",[486,1257,514],{"class":492},[486,1259,1011],{"class":581},[486,1261,1014],{"class":492},[486,1263,1264],{"class":581}," paginate",[486,1266,965],{"class":492},[486,1268,877],{"class":496},[486,1270,500],{"class":492},[486,1272,504],{"class":581},[486,1274,514],{"class":492},[486,1276,517],{"class":581},[486,1278,514],{"class":492},[486,1280,1281],{"class":581}," productSort",[486,1283,559],{"class":492},[486,1285,1286,1288,1290,1292,1294],{"class":488,"line":620},[486,1287,1066],{"class":1065},[486,1289,1011],{"class":581},[486,1291,1071],{"class":492},[486,1293,1074],{"class":492},[486,1295,817],{"class":492},[486,1297,1298,1300,1302,1304,1306,1308,1310,1313,1315],{"class":488,"line":653},[486,1299,1081],{"class":1065},[486,1301,1264],{"class":541},[486,1303,965],{"class":492},[486,1305,778],{"class":541},[486,1307,727],{"class":492},[486,1309,1242],{"class":541},[486,1311,1312],{"class":492},"]{},",[486,1314,1011],{"class":581},[486,1316,1317],{"class":587},"\u002F\u002F ErrInvalidCursor → HTTP 400\n",[486,1319,1320],{"class":488,"line":677},[486,1321,1121],{"class":492},[486,1323,1324,1326,1329,1331,1334,1336,1339,1341,1343,1345,1347],{"class":488,"line":714},[486,1325,1126],{"class":1065},[486,1327,1328],{"class":581}," s",[486,1330,965],{"class":492},[486,1332,1333],{"class":581},"products",[486,1335,965],{"class":492},[486,1337,1338],{"class":496},"List",[486,1340,500],{"class":492},[486,1342,959],{"class":581},[486,1344,514],{"class":492},[486,1346,749],{"class":581},[486,1348,559],{"class":492},[486,1350,1351],{"class":488,"line":719},[486,1352,855],{"class":492},[477,1354,1357],{"className":479,"code":1355,"filename":1356,"language":481,"meta":482,"style":482},"func (r *Products) List(ctx context.Context, p paginate.CursorParams) (paginate.CursorPage[Product], error) {\n    query := `SELECT id, name, price_cents, created_at FROM products`\n    args := []any{p.FetchLimit()}\n\n    if p.HasCursor() {\n        var createdAt time.Time\n        var id string\n        if err := p.DecodeKey(&createdAt, &id); err != nil {\n            return paginate.CursorPage[Product]{}, err\n        }\n        query += ` WHERE (created_at, id) \u003C ($2, $3)`\n        args = append(args, createdAt, id)\n    }\n    query += ` ORDER BY created_at DESC, id DESC LIMIT $1`\n\n    rows, err := r.db.Query(ctx, query, args...)\n    if err != nil {\n        return paginate.CursorPage[Product]{}, errs.Wrap(err, \"products: list\")\n    }\n    products, err := pgx.CollectRows(rows, pgx.RowToStructByName[Product])\n    if err != nil {\n        return paginate.CursorPage[Product]{}, errs.Wrap(err, \"products: scan\")\n    }\n\n    return paginate.NewCursorPage(products, p, func(last Product) []any {\n        return []any{last.CreatedAt, last.ID}\n    })\n}\n","internal\u002Fmodules\u002Fshop\u002Frepository\u002Fproducts.go",[455,1358,1359,1420,1436,1460,1464,1479,1495,1505,1546,1566,1571,1586,1613,1617,1630,1634,1675,1687,1726,1731,1771,1784,1822,1827,1832,1869,1899,1905],{"__ignoreMap":482},[486,1360,1361,1363,1365,1368,1370,1373,1375,1378,1380,1382,1384,1386,1388,1390,1392,1394,1396,1398,1400,1402,1404,1406,1408,1410,1412,1414,1416,1418],{"class":488,"line":489},[486,1362,493],{"class":492},[486,1364,548],{"class":492},[486,1366,1367],{"class":503},"r ",[486,1369,946],{"class":492},[486,1371,1372],{"class":541},"Products",[486,1374,545],{"class":492},[486,1376,1377],{"class":496}," List",[486,1379,500],{"class":492},[486,1381,959],{"class":503},[486,1383,962],{"class":541},[486,1385,965],{"class":492},[486,1387,968],{"class":541},[486,1389,514],{"class":492},[486,1391,749],{"class":503},[486,1393,1264],{"class":541},[486,1395,965],{"class":492},[486,1397,551],{"class":541},[486,1399,545],{"class":492},[486,1401,548],{"class":492},[486,1403,430],{"class":541},[486,1405,965],{"class":492},[486,1407,778],{"class":541},[486,1409,727],{"class":492},[486,1411,1242],{"class":541},[486,1413,785],{"class":492},[486,1415,556],{"class":510},[486,1417,545],{"class":492},[486,1419,817],{"class":492},[486,1421,1422,1425,1427,1430,1433],{"class":488,"line":562},[486,1423,1424],{"class":581},"    query ",[486,1426,1014],{"class":492},[486,1428,1429],{"class":492}," `",[486,1431,1432],{"class":1110},"SELECT id, name, price_cents, created_at FROM products",[486,1434,1435],{"class":492},"`\n",[486,1437,1438,1441,1443,1445,1447,1450,1452,1454,1457],{"class":488,"line":569},[486,1439,1440],{"class":581},"    args ",[486,1442,1014],{"class":492},[486,1444,742],{"class":492},[486,1446,643],{"class":541},[486,1448,1449],{"class":492},"{",[486,1451,447],{"class":581},[486,1453,965],{"class":492},[486,1455,1456],{"class":496},"FetchLimit",[486,1458,1459],{"class":492},"()}\n",[486,1461,1462],{"class":488,"line":594},[486,1463,566],{"emptyLinePlaceholder":565},[486,1465,1466,1468,1470,1472,1475,1477],{"class":488,"line":620},[486,1467,1066],{"class":1065},[486,1469,749],{"class":581},[486,1471,965],{"class":492},[486,1473,1474],{"class":496},"HasCursor",[486,1476,611],{"class":492},[486,1478,817],{"class":492},[486,1480,1481,1484,1487,1490,1492],{"class":488,"line":653},[486,1482,1483],{"class":492},"        var",[486,1485,1486],{"class":581}," createdAt ",[486,1488,1489],{"class":541},"time",[486,1491,965],{"class":492},[486,1493,1494],{"class":541},"Time\n",[486,1496,1497,1499,1502],{"class":488,"line":677},[486,1498,1483],{"class":492},[486,1500,1501],{"class":581}," id ",[486,1503,1504],{"class":510},"string\n",[486,1506,1507,1510,1512,1514,1516,1518,1521,1524,1527,1529,1532,1535,1538,1540,1542,1544],{"class":488,"line":714},[486,1508,1509],{"class":1065},"        if",[486,1511,1011],{"class":581},[486,1513,1014],{"class":492},[486,1515,749],{"class":581},[486,1517,965],{"class":492},[486,1519,1520],{"class":496},"DecodeKey",[486,1522,1523],{"class":492},"(&",[486,1525,1526],{"class":581},"createdAt",[486,1528,514],{"class":492},[486,1530,1531],{"class":492}," &",[486,1533,1534],{"class":581},"id",[486,1536,1537],{"class":492},");",[486,1539,1011],{"class":581},[486,1541,1071],{"class":492},[486,1543,1074],{"class":492},[486,1545,817],{"class":492},[486,1547,1548,1551,1553,1555,1557,1559,1561,1563],{"class":488,"line":719},[486,1549,1550],{"class":1065},"            return",[486,1552,1264],{"class":541},[486,1554,965],{"class":492},[486,1556,778],{"class":541},[486,1558,727],{"class":492},[486,1560,1242],{"class":541},[486,1562,1312],{"class":492},[486,1564,1565],{"class":581}," err\n",[486,1567,1568],{"class":488,"line":792},[486,1569,1570],{"class":492},"        }\n",[486,1572,1573,1576,1579,1581,1584],{"class":488,"line":797},[486,1574,1575],{"class":581},"        query ",[486,1577,1578],{"class":492},"+=",[486,1580,1429],{"class":492},[486,1582,1583],{"class":1110}," WHERE (created_at, id) \u003C ($2, $3)",[486,1585,1435],{"class":492},[486,1587,1588,1591,1593,1596,1598,1601,1603,1606,1608,1611],{"class":488,"line":820},[486,1589,1590],{"class":581},"        args ",[486,1592,1168],{"class":492},[486,1594,1595],{"class":496}," append",[486,1597,500],{"class":492},[486,1599,1600],{"class":581},"args",[486,1602,514],{"class":492},[486,1604,1605],{"class":581}," createdAt",[486,1607,514],{"class":492},[486,1609,1610],{"class":581}," id",[486,1612,559],{"class":492},[486,1614,1615],{"class":488,"line":832},[486,1616,1121],{"class":492},[486,1618,1619,1621,1623,1625,1628],{"class":488,"line":843},[486,1620,1424],{"class":581},[486,1622,1578],{"class":492},[486,1624,1429],{"class":492},[486,1626,1627],{"class":1110}," ORDER BY created_at DESC, id DESC LIMIT $1",[486,1629,1435],{"class":492},[486,1631,1632],{"class":488,"line":852},[486,1633,566],{"emptyLinePlaceholder":565},[486,1635,1636,1639,1641,1643,1645,1648,1650,1653,1655,1658,1660,1662,1664,1667,1669,1672],{"class":488,"line":858},[486,1637,1638],{"class":581},"    rows",[486,1640,514],{"class":492},[486,1642,1011],{"class":581},[486,1644,1014],{"class":492},[486,1646,1647],{"class":581}," r",[486,1649,965],{"class":492},[486,1651,1652],{"class":581},"db",[486,1654,965],{"class":492},[486,1656,1657],{"class":496},"Query",[486,1659,500],{"class":492},[486,1661,959],{"class":581},[486,1663,514],{"class":492},[486,1665,1666],{"class":581}," query",[486,1668,514],{"class":492},[486,1670,1671],{"class":581}," args",[486,1673,1674],{"class":492},"...)\n",[486,1676,1677,1679,1681,1683,1685],{"class":488,"line":863},[486,1678,1066],{"class":1065},[486,1680,1011],{"class":581},[486,1682,1071],{"class":492},[486,1684,1074],{"class":492},[486,1686,817],{"class":492},[486,1688,1690,1692,1694,1696,1698,1700,1702,1704,1707,1709,1711,1713,1715,1717,1719,1722,1724],{"class":488,"line":1689},18,[486,1691,1081],{"class":1065},[486,1693,1264],{"class":541},[486,1695,965],{"class":492},[486,1697,778],{"class":541},[486,1699,727],{"class":492},[486,1701,1242],{"class":541},[486,1703,1312],{"class":492},[486,1705,1706],{"class":581}," errs",[486,1708,965],{"class":492},[486,1710,1097],{"class":496},[486,1712,500],{"class":492},[486,1714,1102],{"class":581},[486,1716,514],{"class":492},[486,1718,1107],{"class":492},[486,1720,1721],{"class":1110},"products: list",[486,1723,1114],{"class":492},[486,1725,559],{"class":492},[486,1727,1729],{"class":488,"line":1728},19,[486,1730,1121],{"class":492},[486,1732,1734,1737,1739,1741,1743,1746,1748,1751,1753,1755,1757,1759,1761,1764,1766,1768],{"class":488,"line":1733},20,[486,1735,1736],{"class":581},"    products",[486,1738,514],{"class":492},[486,1740,1011],{"class":581},[486,1742,1014],{"class":492},[486,1744,1745],{"class":581}," pgx",[486,1747,965],{"class":492},[486,1749,1750],{"class":496},"CollectRows",[486,1752,500],{"class":492},[486,1754,739],{"class":581},[486,1756,514],{"class":492},[486,1758,1745],{"class":581},[486,1760,965],{"class":492},[486,1762,1763],{"class":581},"RowToStructByName",[486,1765,727],{"class":492},[486,1767,1242],{"class":581},[486,1769,1770],{"class":492},"])\n",[486,1772,1774,1776,1778,1780,1782],{"class":488,"line":1773},21,[486,1775,1066],{"class":1065},[486,1777,1011],{"class":581},[486,1779,1071],{"class":492},[486,1781,1074],{"class":492},[486,1783,817],{"class":492},[486,1785,1787,1789,1791,1793,1795,1797,1799,1801,1803,1805,1807,1809,1811,1813,1815,1818,1820],{"class":488,"line":1786},22,[486,1788,1081],{"class":1065},[486,1790,1264],{"class":541},[486,1792,965],{"class":492},[486,1794,778],{"class":541},[486,1796,727],{"class":492},[486,1798,1242],{"class":541},[486,1800,1312],{"class":492},[486,1802,1706],{"class":581},[486,1804,965],{"class":492},[486,1806,1097],{"class":496},[486,1808,500],{"class":492},[486,1810,1102],{"class":581},[486,1812,514],{"class":492},[486,1814,1107],{"class":492},[486,1816,1817],{"class":1110},"products: scan",[486,1819,1114],{"class":492},[486,1821,559],{"class":492},[486,1823,1825],{"class":488,"line":1824},23,[486,1826,1121],{"class":492},[486,1828,1830],{"class":488,"line":1829},24,[486,1831,566],{"emptyLinePlaceholder":565},[486,1833,1835,1837,1839,1841,1844,1846,1848,1850,1852,1854,1856,1858,1861,1863,1865,1867],{"class":488,"line":1834},25,[486,1836,1126],{"class":1065},[486,1838,1264],{"class":581},[486,1840,965],{"class":492},[486,1842,1843],{"class":496},"NewCursorPage",[486,1845,500],{"class":492},[486,1847,1333],{"class":581},[486,1849,514],{"class":492},[486,1851,749],{"class":581},[486,1853,514],{"class":492},[486,1855,759],{"class":492},[486,1857,762],{"class":503},[486,1859,1860],{"class":541}," Product",[486,1862,545],{"class":492},[486,1864,742],{"class":492},[486,1866,643],{"class":541},[486,1868,817],{"class":492},[486,1870,1872,1874,1876,1878,1880,1882,1884,1887,1889,1892,1894,1897],{"class":488,"line":1871},26,[486,1873,1081],{"class":1065},[486,1875,742],{"class":492},[486,1877,643],{"class":541},[486,1879,1449],{"class":492},[486,1881,762],{"class":581},[486,1883,965],{"class":492},[486,1885,1886],{"class":581},"CreatedAt",[486,1888,514],{"class":492},[486,1890,1891],{"class":581}," last",[486,1893,965],{"class":492},[486,1895,1896],{"class":581},"ID",[486,1898,855],{"class":492},[486,1900,1902],{"class":488,"line":1901},27,[486,1903,1904],{"class":492},"    })\n",[486,1906,1908],{"class":488,"line":1907},28,[486,1909,855],{"class":492},[447,1911,1912,1913,1916,1917,1920,1921,1924,1925,1927,1928,1930,1931,1934,1935,1938,1939,1942,1943,1946,1947,1950,1951,1953],{},"The key to the mechanics is the ",[460,1914,1915],{},"+1 row",": the query requests ",[455,1918,1919],{},"FetchLimit()"," (= ",[455,1922,1923],{},"Limit+1","), and ",[455,1926,1843],{}," cuts the page down to ",[455,1929,1058],{}," items. If the extra row was present, ",[455,1932,1933],{},"HasMore = true",", and ",[455,1936,1937],{},"NextCursor"," is built from the key of the ",[460,1940,1941],{},"last actually returned"," item: no separate ",[455,1944,1945],{},"COUNT"," query. The ",[455,1948,1949],{},"key"," function must return exactly the values, in exactly the order, used by the ",[455,1952,911],{}," \u002F seek predicate.",[914,1955,1957],{"id":1956},"requirements-of-the-seek-predicate","Requirements of the seek predicate",[447,1959,1960,1961,1964],{},"The Postgres row-value comparison (",[455,1962,1963],{},"(created_at, id) \u003C ($2, $3)",") is only correct and fast when:",[1966,1967,1968,1982,1998,2008],"ul",{},[1969,1970,1971,1974,1975,886,1978,1981],"li",{},[460,1972,1973],{},"the sort direction is uniform"," across all key columns: mixed ",[455,1976,1977],{},"ASC",[455,1979,1980],{},"DESC"," cannot be expressed with a tuple comparison;",[1969,1983,1984,1990,1991,1994,1995,1997],{},[460,1985,1986,1987],{},"every key column is ",[455,1988,1989],{},"NOT NULL",": a ",[455,1992,1993],{},"NULL"," tuple element makes the whole comparison ",[455,1996,1993],{},", silently dropping the row from every page;",[1969,1999,2000,2003,2004,2007],{},[460,2001,2002],{},"the last key is a unique tiebreaker",": typically the primary key, so a non-unique leading column (e.g. ",[455,2005,2006],{},"created_at",") cannot cause skipped or duplicated rows at page boundaries;",[1969,2009,2010,2015,2016,2019],{},[460,2011,2012,2013],{},"a composite index matching the ",[455,2014,911],{}," exists (in the shop: ",[455,2017,2018],{},"CREATE INDEX ON products (created_at DESC, id DESC)","), so the seek stays an index scan.",[914,2021,2023],{"id":2022},"the-opaque-cursor","The opaque cursor",[447,2025,2026,2027,2029,2030,2033,2034,2037,2038,2041],{},"Towards the client the cursor is an opaque string: a base64url-encoded, versioned JSON envelope carrying the key values and a fingerprint of the sort spec. ",[455,2028,877],{}," rejects with ",[455,2031,2032],{},"ErrInvalidCursor"," (HTTP 400, code ",[455,2035,2036],{},"paginate_invalid_cursor",") any cursor that fails to decode, was made for a different version, or ",[460,2039,2040],{},"was issued for a different sort order",". It never silently falls back to the first page, because that would hide the client's bug. Two consequences:",[1966,2043,2044,2047],{},[1969,2045,2046],{},"changing an endpoint's default sort invalidates outstanding cursors, by design;",[1969,2048,2049,2050,2052,2053,2056,2057,965],{},"the cursor is not encrypted or signed: its key values (timestamp, id) are plainly decodable, and nothing stops a client from hand-editing one. Treat it as an efficient continuation token, not a tamper-proof capability: don't put anything in it the client must not see, and don't use it as an authorization boundary. The sort fingerprint only guards against a cursor being replayed against the wrong ",[455,2051,911],{},", not against a client forging key values outright; if forged values point past a row the caller shouldn't see, your query's own authorization filter (a tenant or ownership ",[455,2054,2055],{},"WHERE"," clause) is what has to catch that, exactly as it would for a hand-crafted ",[455,2058,2059],{},"OFFSET",[2061,2062,2063,2064,2067,2068,2071,2072,2075,2076,2079],"note",{},"Cursor pagination in this version only moves forward: there is no ",[455,2065,2066],{},"before"," parameter for paging backwards. ",[455,2069,2070],{},"CursorPage[T]"," mirrors the ",[455,2073,2074],{},"shared\u002Fpaginator.tsp"," ",[455,2077,2078],{},"CursorPage\u003CT>"," model.",[914,2081,2083],{"id":2082},"which-one-when","Which one, when",[2085,2086,2087,2106],"table",{},[2088,2089,2090],"thead",{},[2091,2092,2093,2096,2101],"tr",{},[2094,2095],"th",{},[2094,2097,2098,2099,545],{},"Offset (",[455,2100,896],{},[2094,2102,2103,2104,545],{},"Cursor (",[455,2105,877],{},[2107,2108,2109,2124,2135,2146,2157],"tbody",{},[2091,2110,2111,2115,2121],{},[2112,2113,2114],"td",{},"page-numbered UI (\"page 3 of 12\")",[2112,2116,2117,2118,545],{},"✔ (provides ",[455,2119,2120],{},"Total",[2112,2122,2123],{},"✘ (no page numbers, no total count)",[2091,2125,2126,2129,2132],{},[2112,2127,2128],{},"infinite scroll \u002F \"load more\" button",[2112,2130,2131],{},"works, but drift-prone",[2112,2133,2134],{},"✔ (its native case)",[2091,2136,2137,2140,2143],{},[2112,2138,2139],{},"large table, deep paging",[2112,2141,2142],{},"O(n), degrades",[2112,2144,2145],{},"✔ (near-constant)",[2091,2147,2148,2151,2154],{},[2112,2149,2150],{},"stable pages under concurrent inserts",[2112,2152,2153],{},"✘ (drifts)",[2112,2155,2156],{},"✔",[2091,2158,2159,2162,2164],{},[2112,2160,2161],{},"jumping to an arbitrary page",[2112,2163,2156],{},[2112,2165,2166],{},"✘ (sequential only)",[447,2168,2169,2170,965],{},"Rule of thumb from the shop: the public product listing (large, growing, infinite scroll) uses cursors; an admin-side list of a few hundred rows is more comfortable with page numbers on ",[450,2171,2172],{"href":435},"offset",[914,2174,2176],{"id":2175},"patterns-used","Patterns used",[1966,2178,2179,2189],{},[1969,2180,2181,2184,2185,2188],{},[460,2182,2183],{},"Opaque, tamper-resistant cursor"," (base64url + sha256 sort fingerprint): ",[450,2186,238],{"href":2187},"\u002Fen\u002Fconcepts\u002Fdesign-patterns#opaque-tamper-resistant-cursor",". \"Tamper-resistant\" here refers specifically to the sort-fingerprint check above, not to the key values, which travel in the clear; see the integrity note above.",[1969,2190,2191,548,2194,2196,2197,2200,2201,965],{},[460,2192,2193],{},"Generic result wrapper",[455,2195,2070],{},"), the sibling of offset's ",[455,2198,2199],{},"Page[T]",": see ",[450,2202,52],{"href":893},[914,2204,2206],{"id":2205},"where-to-next","Where to next",[1966,2208,2209],{},[1969,2210,2211,2213,2214,2216,2217,2219],{},[450,2212,52],{"href":435},": offset pagination (",[455,2215,896],{},", ",[455,2218,2199],{},"), install, and the standalone example.",[2221,2222,2223],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":482,"searchDepth":562,"depth":562,"links":2225},[2226,2227,2228,2229,2230,2231],{"id":916,"depth":562,"text":917},{"id":1956,"depth":562,"text":1957},{"id":2022,"depth":562,"text":2023},{"id":2082,"depth":562,"text":2083},{"id":2175,"depth":562,"text":2176},{"id":2205,"depth":562,"text":2206},"Keyset pagination with an opaque cursor and a Postgres tuple-comparison seek, for large or fast-growing tables.","md",null,{},{"icon":19},{"title":438,"description":2232},"Gr6gyPgV7y44rtRY4_YL2hVX6YiafV5z0VP8q83pudQ",[2240,2234],{"title":52,"path":435,"stem":436,"description":2241,"icon":55,"children":-1},"Offset (page\u002Fper-page) pagination with the paginate module, normalized parameters and a typed page wrapper.",1785445904262]