1- import { OrmAdapter } from "../orm/orm"
1+ import { Buffer } from "node:buffer"
2+
3+ import { OrmAdapter , SortOrder } from "../orm/orm"
24import { PaginateContext , Paginator } from "./base"
35
46export function defineCursorPaginator (
@@ -8,8 +10,8 @@ export function defineCursorPaginator(
810}
911
1012export interface CursorPaginatorOptions {
11- fields : string [ ]
12- take : number
13+ fields ? : string [ ]
14+ take ? : number
1315}
1416
1517export interface CursorPaginatorArgs {
@@ -26,20 +28,17 @@ class CursorPaginator<Orm extends OrmAdapter, Context>
2628 implements Paginator < Orm , Context >
2729{
2830 readonly path = [ "nodes" ]
29- readonly options : CursorPaginatorOptions
3031
31- readonly fields : Array < {
32+ readonly pageSize : number
33+
34+ readonly fields ?: Array < {
3235 name : string
3336 desc : boolean
3437 } >
3538
36- constructor ( options : Partial < CursorPaginatorOptions > = { } ) {
37- this . options = {
38- fields : [ "id" ] ,
39- take : 10 ,
40- ...options ,
41- }
42- this . fields = this . options . fields . map ( ( field ) => {
39+ constructor ( options : CursorPaginatorOptions = { } ) {
40+ this . pageSize = options . take ?? 10
41+ this . fields = options . fields ?. map ( ( field ) => {
4342 if ( field . startsWith ( "-" ) ) {
4443 return { name : field . slice ( 1 ) , desc : true }
4544 } else {
@@ -52,63 +51,92 @@ class CursorPaginator<Orm extends OrmAdapter, Context>
5251 const { orm } = context . graph
5352 const { args } = context . tree
5453
55- const take = ( args . take as number | undefined ) ?? this . options . take
54+ const pageSize = ( args . take as number | undefined ) ?? this . pageSize
5655 const cursor = args . cursor as string | undefined
5756
58- // Set query order
59- query = orm . reset_query_order ( query )
60- for ( const field of this . fields ) {
61- // TODO: prevent potential name clash with aliases like .as(`_${table_ref}_order_key_0`)
62- query = orm . select_field ( query , { field : field . name , as : field . name } )
63- query = orm . add_query_order ( query , field . name , field . desc )
57+ const table = orm . get_query_table ( query )
58+
59+ const orderFields = (
60+ this . fields
61+ ? this . fields . map < SortOrder > ( ( f ) => ( {
62+ field : f . name ,
63+ dir : f . desc ? "DESC" : "ASC" ,
64+ } ) )
65+ : orm . get_query_order ( query )
66+ ) . map ( ( o ) => ( { ...o , alias : "_order_" + o . field } ) )
67+
68+ if ( this . fields ) {
69+ query = orm . reset_query_order ( query )
70+ for ( const { alias, dir } of orderFields ) {
71+ query = orm . add_query_order ( query , { field : alias , dir } )
72+ }
73+ }
74+
75+ if ( ! orderFields . length ) {
76+ throw new Error ( "Query must be ordered." )
77+ }
78+
79+ for ( const { field, alias } of orderFields ) {
80+ query = orm . select_field ( query , { field, as : alias } )
6481 }
6582
6683 if ( cursor ) {
67- const { expression, bindings } = this . _parse_cursor ( cursor )
68- query = orm . where_raw ( query , expression , bindings )
84+ const parsedCursor = parseCursor ( cursor )
85+ // Prepare raw SQL.
86+ // For example, for (amount asc, id asc) order, that would be:
87+ // (amount, $id) >= ($amount, id)
88+ const left : string [ ] = [ ]
89+ const right : string [ ] = [ ]
90+ for ( let i = 0 ; i < orderFields . length ; ++ i ) {
91+ const { field, alias, dir } = orderFields [ i ]
92+ const [ expressions , placeholders ] =
93+ dir === "ASC" ? [ left , right ] : [ right , left ]
94+ expressions . push ( `"${ table } "."${ field } "` )
95+ placeholders . push ( "$" + alias )
96+ }
97+ const sqlExpr = `(${ left . join ( "," ) } ) > (${ right . join ( "," ) } )`
98+ const bindings = Object . fromEntries (
99+ orderFields . map ( ( { alias } , i ) => [ alias , parsedCursor [ i ] ] ) ,
100+ )
101+ query = orm . where_raw ( query , sqlExpr , bindings )
102+ }
103+ query = orm . set_query_limit ( query , pageSize + 1 )
104+
105+ // TODO add support for reverse cursor, borrow implementation from orchid-pagination.
106+ function createNodeCursor ( node : any ) {
107+ return createCursor (
108+ orderFields . map ( ( { field, alias } ) => {
109+ const value = node [ alias ]
110+ // TODO add support for custom serializer(s).
111+ if ( value === undefined ) {
112+ throw new Error (
113+ `Unable to create cursor: undefined field ${ field } (${ alias } )` ,
114+ )
115+ }
116+ return String ( value )
117+ } ) ,
118+ )
69119 }
70- query = orm . set_query_limit ( query , take + 1 )
71120
72121 return orm . set_query_page_result ( query , ( nodes ) => {
73122 let cursor : string | undefined
74- if ( nodes . length > take ) {
75- cursor = this . _create_cursor ( nodes [ take - 1 ] )
76- nodes = nodes . slice ( 0 , take )
123+ if ( nodes . length > pageSize ) {
124+ cursor = createNodeCursor ( nodes [ pageSize - 1 ] )
125+ nodes = nodes . slice ( 0 , pageSize )
77126 }
78127 return { nodes, cursor }
79128 } )
80129 }
130+ }
81131
82- _create_cursor ( instance : any ) {
83- return JSON . stringify (
84- this . fields . map ( ( field ) => {
85- const value = instance [ field . name ]
86- if ( value === undefined ) {
87- throw new Error (
88- `Unable to create cursor: undefined field ${ field . name } ` ,
89- )
90- }
91- return String ( value )
92- } ) ,
93- )
94- }
132+ function createCursor ( parts : string [ ] ) {
133+ return Buffer . from ( parts . map ( String ) . join ( String . fromCharCode ( 0 ) ) ) . toString (
134+ "base64url" ,
135+ )
136+ }
95137
96- _parse_cursor ( cursor : string ) {
97- const values = JSON . parse ( cursor )
98- const left : string [ ] = [ ]
99- const right : string [ ] = [ ]
100- const bindings : Record < string , any > = { }
101- for ( let i = 0 ; i < this . fields . length ; ++ i ) {
102- const field = this . fields [ i ]
103- const expressions = field . desc ? right : left
104- const placeholders = field . desc ? left : right
105- expressions . push ( `"${ field . name } "` )
106- placeholders . push ( "$" + field . name )
107- bindings [ field . name ] = values [ i ]
108- }
109- return {
110- expression : `(${ left . join ( "," ) } ) > (${ right . join ( "," ) } )` ,
111- bindings,
112- }
113- }
138+ function parseCursor ( cursor : string ) : string [ ] {
139+ return Buffer . from ( cursor , "base64url" )
140+ . toString ( )
141+ . split ( String . fromCharCode ( 0 ) )
114142}
0 commit comments