@@ -146,6 +146,7 @@ describe('Capabilities', () => {
146146 {
147147 name : 'id' ,
148148 type : 'Uuid' ,
149+ isGroupable : true ,
149150 operators : [
150151 'blank' ,
151152 'equal' ,
@@ -161,6 +162,7 @@ describe('Capabilities', () => {
161162 {
162163 name : 'name' ,
163164 type : 'String' ,
165+ isGroupable : true ,
164166 operators : [
165167 'blank' ,
166168 'equal' ,
@@ -188,6 +190,7 @@ describe('Capabilities', () => {
188190 {
189191 name : 'publishedAt' ,
190192 type : 'Date' ,
193+ isGroupable : true ,
191194 operators : [
192195 'blank' ,
193196 'equal' ,
@@ -217,6 +220,7 @@ describe('Capabilities', () => {
217220 {
218221 name : 'price' ,
219222 type : 'Number' ,
223+ isGroupable : true ,
220224 operators : [
221225 'blank' ,
222226 'equal' ,
@@ -239,6 +243,227 @@ describe('Capabilities', () => {
239243 } ) ;
240244 } ) ;
241245
246+ describe ( 'when collection has aggregationCapabilities' , ( ) => {
247+ test ( 'should include aggregationCapabilities with serialized supportedDateOperations' , async ( ) => {
248+ const collectionWithCaps = factories . collection . build ( {
249+ name : 'orders' ,
250+ schema : factories . collectionSchema . build ( {
251+ fields : {
252+ id : factories . columnSchema . uuidPrimaryKey ( ) . build ( ) ,
253+ author_id : factories . columnSchema . text ( ) . build ( { isGroupable : true } ) ,
254+ } ,
255+ aggregationCapabilities : {
256+ supportGroups : true ,
257+ supportedDateOperations : new Set ( [ 'Year' , 'Month' ] ) ,
258+ } ,
259+ } ) ,
260+ } ) ;
261+
262+ const dsWithCaps = factories . dataSource . buildWithCollection ( collectionWithCaps ) ;
263+ const routeWithCaps = new Capabilities ( services , options , dsWithCaps ) ;
264+
265+ const context = createMockContext ( {
266+ ...defaultContext ,
267+ requestBody : { collectionNames : [ 'orders' ] } ,
268+ } ) ;
269+
270+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
271+ // @ts -ignore
272+ await routeWithCaps . fetchCapabilities ( context ) ;
273+
274+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
275+ const { collections } = context . response . body as any ;
276+
277+ expect ( collections [ 0 ] . aggregationCapabilities ) . toEqual ( {
278+ supportGroups : true ,
279+ supportedDateOperations : [ 'Year' , 'Month' ] ,
280+ } ) ;
281+ } ) ;
282+
283+ test ( 'should derive supportGroups to false when no field is groupable' , async ( ) => {
284+ const collectionWithCaps = factories . collection . build ( {
285+ name : 'orders' ,
286+ schema : factories . collectionSchema . build ( {
287+ fields : {
288+ id : factories . columnSchema . uuidPrimaryKey ( ) . build ( { isGroupable : false } ) ,
289+ } ,
290+ aggregationCapabilities : {
291+ supportGroups : true ,
292+ supportedDateOperations : new Set ( ) ,
293+ } ,
294+ } ) ,
295+ } ) ;
296+
297+ const dsWithCaps = factories . dataSource . buildWithCollection ( collectionWithCaps ) ;
298+ const routeWithCaps = new Capabilities ( services , options , dsWithCaps ) ;
299+
300+ const context = createMockContext ( {
301+ ...defaultContext ,
302+ requestBody : { collectionNames : [ 'orders' ] } ,
303+ } ) ;
304+
305+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
306+ // @ts -ignore
307+ await routeWithCaps . fetchCapabilities ( context ) ;
308+
309+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310+ const { collections } = context . response . body as any ;
311+
312+ expect ( collections [ 0 ] . aggregationCapabilities . supportGroups ) . toBe ( false ) ;
313+ } ) ;
314+
315+ test ( 'should expose isGroupable per field' , async ( ) => {
316+ const collectionWithCaps = factories . collection . build ( {
317+ name : 'orders' ,
318+ schema : factories . collectionSchema . build ( {
319+ fields : {
320+ id : factories . columnSchema . uuidPrimaryKey ( ) . build ( { isGroupable : false } ) ,
321+ status : factories . columnSchema . text ( ) . build ( { isGroupable : true } ) ,
322+ } ,
323+ aggregationCapabilities : {
324+ supportGroups : true ,
325+ supportedDateOperations : new Set ( ) ,
326+ } ,
327+ } ) ,
328+ } ) ;
329+
330+ const dsWithCaps = factories . dataSource . buildWithCollection ( collectionWithCaps ) ;
331+ const routeWithCaps = new Capabilities ( services , options , dsWithCaps ) ;
332+
333+ const context = createMockContext ( {
334+ ...defaultContext ,
335+ requestBody : { collectionNames : [ 'orders' ] } ,
336+ } ) ;
337+
338+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
339+ // @ts -ignore
340+ await routeWithCaps . fetchCapabilities ( context ) ;
341+
342+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
343+ const { collections } = context . response . body as any ;
344+ const idField = collections [ 0 ] . fields . find ( f => f . name === 'id' ) ;
345+ const statusField = collections [ 0 ] . fields . find ( f => f . name === 'status' ) ;
346+
347+ expect ( idField . isGroupable ) . toBe ( false ) ;
348+ expect ( statusField . isGroupable ) . toBe ( true ) ;
349+ } ) ;
350+ } ) ;
351+
352+ describe ( 'when collection has ManyToOne relations' , ( ) => {
353+ test ( 'should return ManyToOne field with isGroupable from foreign key column' , async ( ) => {
354+ const collectionWithRelation = factories . collection . build ( {
355+ name : 'orders' ,
356+ schema : factories . collectionSchema . build ( {
357+ fields : {
358+ id : factories . columnSchema . uuidPrimaryKey ( ) . build ( ) ,
359+ authorId : factories . columnSchema . text ( ) . build ( { isGroupable : true } ) ,
360+ author : factories . manyToOneSchema . build ( {
361+ foreignKey : 'authorId' ,
362+ foreignCollection : 'author' ,
363+ } ) ,
364+ } ,
365+ } ) ,
366+ } ) ;
367+
368+ const dsWithRelation = factories . dataSource . buildWithCollection ( collectionWithRelation ) ;
369+ const routeWithRelation = new Capabilities ( services , options , dsWithRelation ) ;
370+
371+ const context = createMockContext ( {
372+ ...defaultContext ,
373+ requestBody : { collectionNames : [ 'orders' ] } ,
374+ } ) ;
375+
376+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
377+ // @ts -ignore
378+ await routeWithRelation . fetchCapabilities ( context ) ;
379+
380+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
381+ const { collections } = context . response . body as any ;
382+ const authorField = collections [ 0 ] . fields . find ( f => f . name === 'author' ) ;
383+
384+ expect ( authorField ) . toEqual ( {
385+ name : 'author' ,
386+ type : 'ManyToOne' ,
387+ isGroupable : true ,
388+ } ) ;
389+ } ) ;
390+
391+ test ( 'should set isGroupable to false on ManyToOne when foreign key is not groupable' , async ( ) => {
392+ const collectionWithRelation = factories . collection . build ( {
393+ name : 'orders' ,
394+ schema : factories . collectionSchema . build ( {
395+ fields : {
396+ id : factories . columnSchema . uuidPrimaryKey ( ) . build ( ) ,
397+ authorId : factories . columnSchema . text ( ) . build ( { isGroupable : false } ) ,
398+ author : factories . manyToOneSchema . build ( {
399+ foreignKey : 'authorId' ,
400+ foreignCollection : 'author' ,
401+ } ) ,
402+ } ,
403+ } ) ,
404+ } ) ;
405+
406+ const dsWithRelation = factories . dataSource . buildWithCollection ( collectionWithRelation ) ;
407+ const routeWithRelation = new Capabilities ( services , options , dsWithRelation ) ;
408+
409+ const context = createMockContext ( {
410+ ...defaultContext ,
411+ requestBody : { collectionNames : [ 'orders' ] } ,
412+ } ) ;
413+
414+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
415+ // @ts -ignore
416+ await routeWithRelation . fetchCapabilities ( context ) ;
417+
418+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
419+ const { collections } = context . response . body as any ;
420+ const authorField = collections [ 0 ] . fields . find ( f => f . name === 'author' ) ;
421+
422+ expect ( authorField ) . toEqual ( {
423+ name : 'author' ,
424+ type : 'ManyToOne' ,
425+ isGroupable : false ,
426+ } ) ;
427+ } ) ;
428+
429+ test ( 'should derive supportGroups to true when only ManyToOne field is groupable' , async ( ) => {
430+ const collectionWithRelation = factories . collection . build ( {
431+ name : 'orders' ,
432+ schema : factories . collectionSchema . build ( {
433+ fields : {
434+ id : factories . columnSchema . uuidPrimaryKey ( ) . build ( { isGroupable : false } ) ,
435+ authorId : factories . columnSchema . text ( ) . build ( { isGroupable : true } ) ,
436+ author : factories . manyToOneSchema . build ( {
437+ foreignKey : 'authorId' ,
438+ foreignCollection : 'author' ,
439+ } ) ,
440+ } ,
441+ aggregationCapabilities : {
442+ supportGroups : true ,
443+ supportedDateOperations : new Set ( ) ,
444+ } ,
445+ } ) ,
446+ } ) ;
447+
448+ const dsWithRelation = factories . dataSource . buildWithCollection ( collectionWithRelation ) ;
449+ const routeWithRelation = new Capabilities ( services , options , dsWithRelation ) ;
450+
451+ const context = createMockContext ( {
452+ ...defaultContext ,
453+ requestBody : { collectionNames : [ 'orders' ] } ,
454+ } ) ;
455+
456+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
457+ // @ts -ignore
458+ await routeWithRelation . fetchCapabilities ( context ) ;
459+
460+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
461+ const { collections } = context . response . body as any ;
462+
463+ expect ( collections [ 0 ] . aggregationCapabilities . supportGroups ) . toBe ( true ) ;
464+ } ) ;
465+ } ) ;
466+
242467 describe ( 'when field ColumnType is an object' , ( ) => {
243468 test ( 'should not return a field capabilities for that field' , async ( ) => {
244469 const context = createMockContext ( {
0 commit comments