1111
1212import type { FtAggregateOptions } from '@redis/search/dist/lib/commands/AGGREGATE.js' ;
1313import { QueryValidationError } from '../errors.js' ;
14- import { renderFilter , type FilterInput } from './base.js' ;
14+ import { BaseQuery , renderFilter , type FilterInput , type SortByOptions } from './base.js' ;
1515
1616/** Output of {@link AggregationQuery.toCommand}. */
1717export interface AggregateCommand {
@@ -224,8 +224,7 @@ function assertPositiveInteger(value: number | undefined, label: string): void {
224224 * const { total, results } = await index.aggregate(q);
225225 * ```
226226 */
227- export class AggregationQuery {
228- private readonly _query : string ;
227+ export class AggregationQuery extends BaseQuery {
229228 private readonly steps : Step [ ] = [ ] ;
230229 private _load ?: LoadField [ ] ;
231230 private _params ?: Record < string , string | number > ;
@@ -240,7 +239,7 @@ export class AggregationQuery {
240239 * the rest of the query DSL uses.
241240 */
242241 constructor ( query ?: FilterInput ) {
243- this . _query = renderFilter ( query ) ;
242+ super ( { filter : query } ) ;
244243 }
245244
246245 /**
@@ -279,8 +278,16 @@ export class AggregationQuery {
279278 }
280279
281280 /** SORTBY one or more fields. Bare strings sort ASC. */
282- sortBy ( by : SortSpec | SortSpec [ ] , max ?: number ) : this {
283- const list = Array . isArray ( by ) ? by : [ by ] ;
281+ sortBy ( field : string , options ?: SortByOptions ) : this;
282+ sortBy ( by : SortSpec | SortSpec [ ] , max ?: number ) : this;
283+ sortBy ( by : SortSpec | SortSpec [ ] , maxOrOptions ?: number | SortByOptions ) : this {
284+ const list =
285+ typeof maxOrOptions === 'object' && ! Array . isArray ( by ) && typeof by === 'string'
286+ ? [ { field : by , direction : maxOrOptions . direction } ]
287+ : Array . isArray ( by )
288+ ? by
289+ : [ by ] ;
290+ const max = typeof maxOrOptions === 'number' ? maxOrOptions : undefined ;
284291 if ( list . length === 0 ) {
285292 throw new QueryValidationError ( 'sortBy requires at least one field' ) ;
286293 }
@@ -378,7 +385,12 @@ export class AggregationQuery {
378385
379386 /** The rendered query string this aggregation will use. */
380387 get query ( ) : string {
381- return this . _query ;
388+ return this . buildQuery ( ) ;
389+ }
390+
391+ /** Build the FT.AGGREGATE query string. */
392+ buildQuery ( ) : string {
393+ return renderFilter ( this . queryFilter ) ;
382394 }
383395
384396 /** Build the structured options for `client.ft.aggregate(indexName, query, options)`. */
@@ -395,12 +407,13 @@ export class AggregationQuery {
395407 options . PARAMS = this . _params ;
396408 }
397409
398- if ( this . _load && this . _load . length > 0 ) {
410+ const loadFields = [ ...( this . returnFields ?? [ ] ) , ...( this . _load ?? [ ] ) ] ;
411+ if ( loadFields . length > 0 ) {
399412 // The Redis client types LOAD entries with template-literal types
400413 // (`@${string}` / `$.${string}`). We've already enforced that
401414 // contract via prefixFieldRef, but TS can't see through the
402415 // string return — cast at the boundary.
403- options . LOAD = this . _load . map ( ( f ) =>
416+ options . LOAD = loadFields . map ( ( f ) =>
404417 typeof f === 'string'
405418 ? prefixFieldRef ( f )
406419 : f . as !== undefined
@@ -415,7 +428,7 @@ export class AggregationQuery {
415428 ) as FtAggregateOptions [ 'STEPS' ] ;
416429 }
417430
418- return { query : this . _query , options } ;
431+ return { query : this . buildQuery ( ) , options } ;
419432 }
420433
421434 private renderStep ( step : Step ) : unknown {
0 commit comments