@@ -114,6 +114,37 @@ public static JsonDocument CreatePaginationConnectionFromJsonDocument(JsonDocume
114114 return result ;
115115 }
116116
117+ /// <summary>
118+ /// Holds the information safe to expose in the response's pagination cursor,
119+ /// the NextLink. The NextLink column represents the safe to expose information
120+ /// that defines the entity, field, field value, and direction of sorting to
121+ /// continue to the next page. These can then be used to form the pagination
122+ /// columns that will be needed for the actual query.
123+ /// </summary>
124+ protected class NextLinkField
125+ {
126+ public string EntityName { get ; set ; }
127+ public string FieldName { get ; set ; }
128+ public object ? FieldValue { get ; }
129+ public string ? ParamName { get ; set ; }
130+ public OrderBy Direction { get ; set ; }
131+
132+ public NextLinkField (
133+ string entityName ,
134+ string fieldName ,
135+ object ? fieldValue ,
136+ string ? paramName = null ,
137+ // default sorting direction is ascending so we maintain that convention
138+ OrderBy direction = OrderBy . ASC )
139+ {
140+ EntityName = entityName ;
141+ FieldName = fieldName ;
142+ FieldValue = fieldValue ;
143+ ParamName = paramName ;
144+ Direction = direction ;
145+ }
146+ }
147+
117148 /// <summary>
118149 /// Extracts the columns from the JsonElement needed for pagination, represents them as a string in json format and base64 encodes.
119150 /// The JSON is encoded in base64 for opaqueness. The cursor should function as a token that the user copies and pastes
@@ -128,7 +159,7 @@ public static string MakeCursorFromJsonElement(
128159 string tableName = "" ,
129160 ISqlMetadataProvider ? sqlMetadataProvider = null )
130161 {
131- List < PaginationColumn > cursorJson = new ( ) ;
162+ List < NextLinkField > cursorJson = new ( ) ;
132163 JsonSerializerOptions options = new ( ) { DefaultIgnoreCondition = JsonIgnoreCondition . WhenWritingNull } ;
133164 // Hash set is used here to maintain linear runtime
134165 // in the worst case for this function. If list is used
@@ -148,12 +179,11 @@ public static string MakeCursorFromJsonElement(
148179 string ? exposedColumnName = GetExposedColumnName ( entityName , column . ColumnName , sqlMetadataProvider ) ;
149180 if ( TryResolveJsonElementToScalarVariable ( element . GetProperty ( exposedColumnName ) , out object ? value ) )
150181 {
151- cursorJson . Add ( new PaginationColumn ( tableSchema : schemaName ,
152- tableName : tableName ,
153- exposedColumnName ,
154- value ,
155- tableAlias : null ,
156- direction : column . Direction ) ) ;
182+ cursorJson . Add ( new NextLinkField (
183+ entityName : entityName ,
184+ fieldName : exposedColumnName ,
185+ fieldValue : value ,
186+ direction : column . Direction ) ) ;
157187 }
158188 else
159189 {
@@ -181,11 +211,10 @@ public static string MakeCursorFromJsonElement(
181211 string ? exposedColumnName = GetExposedColumnName ( entityName , column , sqlMetadataProvider ) ;
182212 if ( TryResolveJsonElementToScalarVariable ( element . GetProperty ( exposedColumnName ) , out object ? value ) )
183213 {
184- cursorJson . Add ( new PaginationColumn ( tableSchema : schemaName ,
185- tableName : tableName ,
186- exposedColumnName ,
187- value ,
188- direction : OrderBy . ASC ) ) ;
214+ cursorJson . Add ( new NextLinkField (
215+ entityName : entityName ,
216+ fieldName : exposedColumnName ,
217+ fieldValue : value ) ) ;
189218 }
190219 else
191220 {
@@ -233,45 +262,54 @@ public static IEnumerable<PaginationColumn> ParseAfterFromQueryParams(
233262 /// Validate the value associated with $after, and return list of orderby columns
234263 /// it represents.
235264 /// </summary>
236- public static IEnumerable < PaginationColumn > ParseAfterFromJsonString ( string afterJsonString ,
237- PaginationMetadata paginationMetadata ,
238- ISqlMetadataProvider sqlMetadataProvider ,
239- string entityName ,
240- RuntimeConfigProvider runtimeConfigProvider
241- )
265+ public static IEnumerable < PaginationColumn > ParseAfterFromJsonString (
266+ string afterJsonString ,
267+ PaginationMetadata paginationMetadata ,
268+ ISqlMetadataProvider sqlMetadataProvider ,
269+ string entityName ,
270+ RuntimeConfigProvider runtimeConfigProvider
271+ )
242272 {
243- IEnumerable < PaginationColumn > ? after ;
273+ List < PaginationColumn > ? paginationCursorColumnsForQuery = new ( ) ;
274+ IEnumerable < NextLinkField > ? paginationCursorFieldsFromRequest ;
244275 try
245276 {
246277 afterJsonString = Base64Decode ( afterJsonString ) ;
247- after = JsonSerializer . Deserialize < IEnumerable < PaginationColumn > > ( afterJsonString ) ;
278+ paginationCursorFieldsFromRequest = JsonSerializer . Deserialize < IEnumerable < NextLinkField > > ( afterJsonString ) ;
248279
249- if ( after is null )
280+ if ( paginationCursorFieldsFromRequest is null )
250281 {
251282 throw new ArgumentException ( "Failed to parse the pagination information from the provided token" ) ;
252283 }
253284
254- Dictionary < string , PaginationColumn > afterDict = new ( ) ;
255- foreach ( PaginationColumn column in after )
285+ Dictionary < string , PaginationColumn > exposedFieldNameToBackingColumn = new ( ) ;
286+ foreach ( NextLinkField field in paginationCursorFieldsFromRequest )
256287 {
257288 // REST calls this function with a non null sqlMetadataProvider
258289 // which will get the exposed name for safe messaging in the response.
259290 // Since we are looking for pagination columns from the $after query
260291 // param, we expect this column to exist as the $after query param
261292 // was formed from a previous response with a nextLink. If the nextLink
262293 // has been modified and backingColumn is null we throw exception.
263- string backingColumnName = GetBackingColumnName ( entityName , column . ColumnName , sqlMetadataProvider ) ;
294+ string backingColumnName = GetBackingColumnName ( entityName , field . FieldName , sqlMetadataProvider ) ;
264295 if ( backingColumnName is null )
265296 {
266- throw new DataApiBuilderException ( message : $ "Cursor for Pagination Predicates is not well formed, { column . ColumnName } is not valid.",
267- statusCode : HttpStatusCode . BadRequest ,
268- subStatusCode : DataApiBuilderException . SubStatusCodes . BadRequest ) ;
297+ throw new DataApiBuilderException (
298+ message : $ "Pagination token is not well formed because { field . FieldName } is not valid.",
299+ statusCode : HttpStatusCode . BadRequest ,
300+ subStatusCode : DataApiBuilderException . SubStatusCodes . BadRequest ) ;
269301 }
270302
303+ PaginationColumn pageColumn = new (
304+ tableName : "" ,
305+ tableSchema : "" ,
306+ columnName : backingColumnName ,
307+ value : field . FieldValue ,
308+ paramName : field . ParamName ,
309+ direction : field . Direction ) ;
310+ paginationCursorColumnsForQuery . Add ( pageColumn ) ;
271311 // holds exposed name mapped to exposed pagination column
272- afterDict . Add ( column . ColumnName , column ) ;
273- // overwrite with backing column's name for query generation
274- column . ColumnName = backingColumnName ;
312+ exposedFieldNameToBackingColumn . Add ( field . FieldName , pageColumn ) ;
275313 }
276314
277315 // verify that primary keys is a sub set of after's column names
@@ -284,12 +322,13 @@ RuntimeConfigProvider runtimeConfigProvider
284322 // which will get the exposed name for safe messaging in the response.
285323 // Since we are looking for primary keys we expect these columns to
286324 // exist.
287- string safePK = GetExposedColumnName ( entityName , pk , sqlMetadataProvider ) ;
288- if ( ! afterDict . ContainsKey ( safePK ) )
325+ string exposedFieldName = GetExposedColumnName ( entityName , pk , sqlMetadataProvider ) ;
326+ if ( ! exposedFieldNameToBackingColumn . ContainsKey ( exposedFieldName ) )
289327 {
290- throw new DataApiBuilderException ( message : $ "Cursor for Pagination Predicates is not well formed, missing primary key column: { safePK } ",
291- statusCode : HttpStatusCode . BadRequest ,
292- subStatusCode : DataApiBuilderException . SubStatusCodes . BadRequest ) ;
328+ throw new DataApiBuilderException (
329+ message : $ "Pagination token is not well formed because it is missing an expected field: { exposedFieldName } ",
330+ statusCode : HttpStatusCode . BadRequest ,
331+ subStatusCode : DataApiBuilderException . SubStatusCodes . BadRequest ) ;
293332 }
294333 }
295334
@@ -299,28 +338,28 @@ RuntimeConfigProvider runtimeConfigProvider
299338 SqlQueryStructure structure = paginationMetadata . Structure ! ;
300339 foreach ( OrderByColumn column in structure . OrderByColumns )
301340 {
302- string columnName = GetExposedColumnName ( entityName , column . ColumnName , sqlMetadataProvider ) ;
341+ string exposedFieldName = GetExposedColumnName ( entityName , column . ColumnName , sqlMetadataProvider ) ;
303342
304- if ( ! afterDict . ContainsKey ( columnName ) ||
305- afterDict [ columnName ] . Direction != column . Direction )
343+ if ( ! exposedFieldNameToBackingColumn . ContainsKey ( exposedFieldName ) ||
344+ exposedFieldNameToBackingColumn [ exposedFieldName ] . Direction != column . Direction )
306345 {
307346 // REST calls this function with a non null sqlMetadataProvider
308347 // which will get the exposed name for safe messaging in the response.
309348 // Since we are looking for valid orderby columns we expect
310349 // these columns to exist.
311- string safeColumnName = GetExposedColumnName ( entityName , columnName , sqlMetadataProvider ) ;
350+ string exposedOrderByFieldName = GetExposedColumnName ( entityName , column . ColumnName , sqlMetadataProvider ) ;
312351 throw new DataApiBuilderException (
313- message : $ "Could not match order by column { safeColumnName } with a column in the pagination token with the same name and direction.",
314- statusCode : HttpStatusCode . BadRequest ,
315- subStatusCode : DataApiBuilderException . SubStatusCodes . BadRequest ) ;
352+ message : $ "Could not match order by column { exposedOrderByFieldName } with a column in the pagination token with the same name and direction.",
353+ statusCode : HttpStatusCode . BadRequest ,
354+ subStatusCode : DataApiBuilderException . SubStatusCodes . BadRequest ) ;
316355 }
317356
318357 orderByColumnCount ++ ;
319358 }
320359
321360 // the check above validates that all orderby columns are matched with after columns
322361 // also validate that there are no extra after columns
323- if ( afterDict . Count != orderByColumnCount )
362+ if ( exposedFieldNameToBackingColumn . Count != orderByColumnCount )
324363 {
325364 throw new ArgumentException ( "After token contains extra columns not present in order by columns." ) ;
326365 }
@@ -351,7 +390,7 @@ e is NotSupportedException
351390 innerException : e ) ;
352391 }
353392
354- return after ;
393+ return paginationCursorColumnsForQuery ;
355394 }
356395
357396 /// <summary>
0 commit comments