mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-29 20:02:29 +00:00 
			
		
		
		
	Add composite fields to aggregation (#8518)
## Context This PR introduces a first aggregation for a composite field ## Test <img width="1074" alt="Screenshot 2024-11-15 at 15 37 05" src="https://github.com/user-attachments/assets/db2563f9-26b7-421f-9431-48fc13bce49e">
This commit is contained in:
		| @@ -1,16 +1,14 @@ | |||||||
| import { SelectQueryBuilder } from 'typeorm'; | import { SelectQueryBuilder } from 'typeorm'; | ||||||
|  |  | ||||||
| import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; |  | ||||||
|  |  | ||||||
| import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; | import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; | ||||||
|  | import { formatColumnNameFromCompositeFieldAndSubfield } from 'src/engine/twenty-orm/utils/format-column-name-from-composite-field-and-subfield.util'; | ||||||
|  | import { isDefined } from 'src/utils/is-defined'; | ||||||
|  |  | ||||||
| export class ProcessAggregateHelper { | export class ProcessAggregateHelper { | ||||||
|   public addSelectedAggregatedFieldsQueriesToQueryBuilder = ({ |   public addSelectedAggregatedFieldsQueriesToQueryBuilder = ({ | ||||||
|     fieldMetadataMapByName, |  | ||||||
|     selectedAggregatedFields, |     selectedAggregatedFields, | ||||||
|     queryBuilder, |     queryBuilder, | ||||||
|   }: { |   }: { | ||||||
|     fieldMetadataMapByName: Record<string, FieldMetadataInterface>; |  | ||||||
|     selectedAggregatedFields: Record<string, AggregationField>; |     selectedAggregatedFields: Record<string, AggregationField>; | ||||||
|     queryBuilder: SelectQueryBuilder<any>; |     queryBuilder: SelectQueryBuilder<any>; | ||||||
|   }) => { |   }) => { | ||||||
| @@ -19,17 +17,21 @@ export class ProcessAggregateHelper { | |||||||
|     for (const [aggregatedFieldName, aggregatedField] of Object.entries( |     for (const [aggregatedFieldName, aggregatedField] of Object.entries( | ||||||
|       selectedAggregatedFields, |       selectedAggregatedFields, | ||||||
|     )) { |     )) { | ||||||
|       const fieldMetadata = fieldMetadataMapByName[aggregatedField.fromField]; |       if ( | ||||||
|  |         !isDefined(aggregatedField?.fromField) || | ||||||
|       if (!fieldMetadata) { |         !isDefined(aggregatedField?.aggregationOperation) | ||||||
|  |       ) { | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const fieldName = fieldMetadata.name; |       const columnName = formatColumnNameFromCompositeFieldAndSubfield( | ||||||
|  |         aggregatedField.fromField, | ||||||
|  |         aggregatedField.fromSubField, | ||||||
|  |       ); | ||||||
|       const operation = aggregatedField.aggregationOperation; |       const operation = aggregatedField.aggregationOperation; | ||||||
|  |  | ||||||
|       queryBuilder.addSelect( |       queryBuilder.addSelect( | ||||||
|         `${operation}("${fieldName}")`, |         `${operation}("${columnName}")`, | ||||||
|         `${aggregatedFieldName}`, |         `${aggregatedFieldName}`, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -344,7 +344,6 @@ export class ProcessNestedRelationsHelper { | |||||||
|  |  | ||||||
|       this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder( |       this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder( | ||||||
|         { |         { | ||||||
|           fieldMetadataMapByName: referenceObjectMetadata.fieldsByName, |  | ||||||
|           selectedAggregatedFields: aggregateForRelation, |           selectedAggregatedFields: aggregateForRelation, | ||||||
|           queryBuilder: aggregateQueryBuilder, |           queryBuilder: aggregateQueryBuilder, | ||||||
|         }, |         }, | ||||||
|   | |||||||
| @@ -159,7 +159,6 @@ export class GraphqlQueryFindManyResolverService | |||||||
|     const processAggregateHelper = new ProcessAggregateHelper(); |     const processAggregateHelper = new ProcessAggregateHelper(); | ||||||
|  |  | ||||||
|     processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ |     processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ | ||||||
|       fieldMetadataMapByName: objectMetadataItemWithFieldMaps.fieldsByName, |  | ||||||
|       selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate, |       selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate, | ||||||
|       queryBuilder: withDeletedAggregateQueryBuilder, |       queryBuilder: withDeletedAggregateQueryBuilder, | ||||||
|     }); |     }); | ||||||
| @@ -214,7 +213,7 @@ export class GraphqlQueryFindManyResolverService | |||||||
|       selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate, |       selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate, | ||||||
|       objectName: objectMetadataItemWithFieldMaps.nameSingular, |       objectName: objectMetadataItemWithFieldMaps.nameSingular, | ||||||
|       take: limit, |       take: limit, | ||||||
|       totalCount: parentObjectRecordsAggregatedValues.totalCount, |       totalCount: parentObjectRecordsAggregatedValues?.totalCount, | ||||||
|       order: orderByWithIdCondition, |       order: orderByWithIdCondition, | ||||||
|       hasNextPage, |       hasNextPage, | ||||||
|       hasPreviousPage, |       hasPreviousPage, | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ export type AggregationField = { | |||||||
|   type: GraphQLScalarType; |   type: GraphQLScalarType; | ||||||
|   description: string; |   description: string; | ||||||
|   fromField: string; |   fromField: string; | ||||||
|  |   fromSubField?: string; | ||||||
|   aggregationOperation: AGGREGATION_OPERATIONS; |   aggregationOperation: AGGREGATION_OPERATIONS; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -79,6 +80,16 @@ export const getAvailableAggregationsFromObjectFields = ( | |||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (field.type === FieldMetadataType.CURRENCY) { | ||||||
|  |       acc[`avg${capitalize(field.name)}AmountMicros`] = { | ||||||
|  |         type: GraphQLFloat, | ||||||
|  |         description: `Average amount contained in the field ${field.name}`, | ||||||
|  |         fromField: field.name, | ||||||
|  |         fromSubField: 'amountMicros', | ||||||
|  |         aggregationOperation: AGGREGATION_OPERATIONS.avg, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return acc; |     return acc; | ||||||
|   }, {}); |   }, {}); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | import { formatColumnNameFromCompositeFieldAndSubfield } from 'src/engine/twenty-orm/utils/format-column-name-from-composite-field-and-subfield.util'; | ||||||
|  |  | ||||||
|  | describe('formatColumnNameFromCompositeFieldAndSubfield', () => { | ||||||
|  |   it('should return fieldName when subFieldName is not defined', () => { | ||||||
|  |     const result = formatColumnNameFromCompositeFieldAndSubfield('firstName'); | ||||||
|  |  | ||||||
|  |     expect(result).toBe('firstName'); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should return concatenated fieldName and capitalized subFieldName when subFieldName is defined', () => { | ||||||
|  |     const result = formatColumnNameFromCompositeFieldAndSubfield( | ||||||
|  |       'user', | ||||||
|  |       'firstName', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     expect(result).toBe('userFirstName'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | import { capitalize } from 'src/utils/capitalize'; | ||||||
|  | import { isDefined } from 'src/utils/is-defined'; | ||||||
|  |  | ||||||
|  | export const formatColumnNameFromCompositeFieldAndSubfield = ( | ||||||
|  |   fieldName: string, | ||||||
|  |   subFieldName?: string, | ||||||
|  | ): string => { | ||||||
|  |   if (isDefined(subFieldName)) { | ||||||
|  |     return `${fieldName}${capitalize(subFieldName)}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return fieldName; | ||||||
|  | }; | ||||||
		Reference in New Issue
	
	Block a user
	 Weiko
					Weiko