@@ -12,6 +12,7 @@ import {
1212 FieldEqualityCondition ,
1313 isSingleValueFieldEqualityCondition ,
1414 OrderByOrdering ,
15+ SearchStrategy ,
1516} from './BasePostgresEntityDatabaseAdapter' ;
1617import { BaseSQLQueryBuilder } from './BaseSQLQueryBuilder' ;
1718import { SQLFragment } from './SQLOperator' ;
@@ -76,6 +77,40 @@ export interface EntityLoaderQuerySelectionModifiersWithOrderByFragment<
7677 orderByFragment ?: SQLFragment ;
7778}
7879
80+ interface SearchSpecificationBase <
81+ TFields extends Record < string , any > ,
82+ TSelectedFields extends keyof TFields ,
83+ > {
84+ term : string ;
85+ fields : TSelectedFields [ ] ;
86+ }
87+
88+ interface ILikeSearchSpecification <
89+ TFields extends Record < string , any > ,
90+ TSelectedFields extends keyof TFields ,
91+ > extends SearchSpecificationBase < TFields , TSelectedFields > {
92+ strategy : SearchStrategy . ILIKE ;
93+ }
94+
95+ interface TrigramSearchSpecification <
96+ TFields extends Record < string , any > ,
97+ TSelectedFields extends keyof TFields ,
98+ > extends SearchSpecificationBase < TFields , TSelectedFields > {
99+ strategy : SearchStrategy . TRIGRAM ;
100+ threshold : number ;
101+ extraOrderByFields ?: TSelectedFields [ ] ;
102+ }
103+
104+ /**
105+ * Search specification for SQL-based pagination.
106+ */
107+ export type SearchSpecification <
108+ TFields extends Record < string , any > ,
109+ TSelectedFields extends keyof TFields ,
110+ > =
111+ | ILikeSearchSpecification < TFields , TSelectedFields >
112+ | TrigramSearchSpecification < TFields , TSelectedFields > ;
113+
79114/**
80115 * Base pagination arguments
81116 */
@@ -105,6 +140,31 @@ interface EntityLoaderBasePaginationArgs<
105140 includeTotal ?: boolean ;
106141}
107142
143+ /**
144+ * Base search pagination arguments
145+ */
146+ interface EntityLoaderBaseSearchPaginationArgs <
147+ TFields extends Record < string , any > ,
148+ TSelectedFields extends keyof TFields ,
149+ > {
150+ /**
151+ * SQLFragment representing the WHERE clause to filter the entities being paginated.
152+ */
153+ where ?: SQLFragment ;
154+
155+ /**
156+ * Whether to calculate and include the Connection totalCount field.
157+ */
158+ includeTotal ?: boolean ;
159+
160+ /**
161+ * Search specification for filtering and ordering entities by text search relevance.
162+ * For TRIGRAM strategy: Results are ordered by exact match priority, then similarity score.
163+ * For ILIKE strategy: Results use standard field ordering.
164+ */
165+ search : SearchSpecification < TFields , TSelectedFields > ;
166+ }
167+
108168/**
109169 * Forward pagination arguments
110170 */
@@ -141,6 +201,44 @@ export interface EntityLoaderBackwardPaginationArgs<
141201 before ?: string ;
142202}
143203
204+ /**
205+ * Forward search pagination arguments
206+ */
207+ export interface EntityLoaderForwardSearchPaginationArgs <
208+ TFields extends Record < string , any > ,
209+ TSelectedFields extends keyof TFields ,
210+ > extends EntityLoaderBaseSearchPaginationArgs < TFields , TSelectedFields > {
211+ /**
212+ * The number of entities to return starting from the entity after the cursor. Must be a positive integer.
213+ */
214+ first : number ;
215+
216+ /**
217+ * The cursor to paginate after for forward pagination.
218+ * Note: With search relevance ordering, cursor pagination may not maintain exact relevance order across pages.
219+ */
220+ after ?: string ;
221+ }
222+
223+ /**
224+ * Backward search pagination arguments
225+ */
226+ export interface EntityLoaderBackwardSearchPaginationArgs <
227+ TFields extends Record < string , any > ,
228+ TSelectedFields extends keyof TFields ,
229+ > extends EntityLoaderBaseSearchPaginationArgs < TFields , TSelectedFields > {
230+ /**
231+ * The number of entities to return starting from the entity before the cursor. Must be a positive integer.
232+ */
233+ last : number ;
234+
235+ /**
236+ * The cursor to paginate before for backward pagination.
237+ * Note: With search relevance ordering, cursor pagination may not maintain exact relevance order across pages.
238+ */
239+ before ?: string ;
240+ }
241+
144242/**
145243 * Load page pagination arguments, which can be either forward or backward pagination arguments.
146244 */
@@ -151,6 +249,16 @@ export type EntityLoaderLoadPageArgs<
151249 | EntityLoaderForwardPaginationArgs < TFields , TSelectedFields >
152250 | EntityLoaderBackwardPaginationArgs < TFields , TSelectedFields > ;
153251
252+ /**
253+ * Load page with search pagination arguments, which can be either forward or backward search pagination arguments.
254+ */
255+ export type EntityLoaderLoadPageWithSearchArgs <
256+ TFields extends Record < string , any > ,
257+ TSelectedFields extends keyof TFields ,
258+ > =
259+ | EntityLoaderForwardSearchPaginationArgs < TFields , TSelectedFields >
260+ | EntityLoaderBackwardSearchPaginationArgs < TFields , TSelectedFields > ;
261+
154262/**
155263 * Authorization-result-based knex entity loader for non-data-loader-based load methods.
156264 * All loads through this loader are results (or null for some loader methods), where an
@@ -318,6 +426,49 @@ export class AuthorizationResultBasedKnexEntityLoader<
318426 ...( pageResult . totalCount !== undefined && { totalCount : pageResult . totalCount } ) ,
319427 } ;
320428 }
429+
430+ /**
431+ * Load a page of entities with Relay-style cursor pagination and search.
432+ * Search determines the ordering - no explicit orderBy is allowed.
433+ * Only returns successfully authorized entities for cursor stability; failed authorization results are filtered out.
434+ *
435+ * @returns Connection with only successfully authorized entities
436+ */
437+ async loadPageBySQLAndSearchAsync (
438+ args : EntityLoaderLoadPageWithSearchArgs < TFields , TSelectedFields > ,
439+ ) : Promise < Connection < TEntity > > {
440+ const pageResult = await this . knexDataManager . loadPageBySQLFragmentAndSearchAsync (
441+ this . queryContext ,
442+ args ,
443+ ) ;
444+
445+ const edgeResults = await Promise . all (
446+ pageResult . edges . map ( async ( edge ) => {
447+ const entityResult = await this . constructionUtils . constructAndAuthorizeEntityAsync (
448+ edge . node ,
449+ ) ;
450+ if ( ! entityResult . ok ) {
451+ return null ;
452+ }
453+ return {
454+ ...edge ,
455+ node : entityResult . value ,
456+ } ;
457+ } ) ,
458+ ) ;
459+ const edges = edgeResults . filter ( ( edge ) => edge !== null ) ;
460+ const pageInfo : PageInfo = {
461+ ...pageResult . pageInfo ,
462+ startCursor : edges [ 0 ] ?. cursor ?? null ,
463+ endCursor : edges [ edges . length - 1 ] ?. cursor ?? null ,
464+ } ;
465+
466+ return {
467+ edges,
468+ pageInfo,
469+ ...( pageResult . totalCount !== undefined && { totalCount : pageResult . totalCount } ) ,
470+ } ;
471+ }
321472}
322473
323474/**
0 commit comments