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:
Weiko 2024-11-18 12:02:57 +01:00 committed by GitHub
parent 2f5dc26545
commit 0f1cf0e4e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 54 additions and 12 deletions

View File

@ -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}`,
); );
} }

View File

@ -344,7 +344,6 @@ export class ProcessNestedRelationsHelper {
this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder( this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder(
{ {
fieldMetadataMapByName: referenceObjectMetadata.fieldsByName,
selectedAggregatedFields: aggregateForRelation, selectedAggregatedFields: aggregateForRelation,
queryBuilder: aggregateQueryBuilder, queryBuilder: aggregateQueryBuilder,
}, },

View File

@ -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,

View File

@ -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;
}, {}); }, {});
}; };

View File

@ -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');
});
});

View File

@ -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;
};