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 { 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 { 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 {
public addSelectedAggregatedFieldsQueriesToQueryBuilder = ({
fieldMetadataMapByName,
selectedAggregatedFields,
queryBuilder,
}: {
fieldMetadataMapByName: Record<string, FieldMetadataInterface>;
selectedAggregatedFields: Record<string, AggregationField>;
queryBuilder: SelectQueryBuilder<any>;
}) => {
@ -19,17 +17,21 @@ export class ProcessAggregateHelper {
for (const [aggregatedFieldName, aggregatedField] of Object.entries(
selectedAggregatedFields,
)) {
const fieldMetadata = fieldMetadataMapByName[aggregatedField.fromField];
if (!fieldMetadata) {
if (
!isDefined(aggregatedField?.fromField) ||
!isDefined(aggregatedField?.aggregationOperation)
) {
continue;
}
const fieldName = fieldMetadata.name;
const columnName = formatColumnNameFromCompositeFieldAndSubfield(
aggregatedField.fromField,
aggregatedField.fromSubField,
);
const operation = aggregatedField.aggregationOperation;
queryBuilder.addSelect(
`${operation}("${fieldName}")`,
`${operation}("${columnName}")`,
`${aggregatedFieldName}`,
);
}

View File

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

View File

@ -159,7 +159,6 @@ export class GraphqlQueryFindManyResolverService
const processAggregateHelper = new ProcessAggregateHelper();
processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
fieldMetadataMapByName: objectMetadataItemWithFieldMaps.fieldsByName,
selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate,
queryBuilder: withDeletedAggregateQueryBuilder,
});
@ -214,7 +213,7 @@ export class GraphqlQueryFindManyResolverService
selectedAggregatedFields: graphqlQuerySelectedFieldsResult.aggregate,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: limit,
totalCount: parentObjectRecordsAggregatedValues.totalCount,
totalCount: parentObjectRecordsAggregatedValues?.totalCount,
order: orderByWithIdCondition,
hasNextPage,
hasPreviousPage,

View File

@ -19,6 +19,7 @@ export type AggregationField = {
type: GraphQLScalarType;
description: string;
fromField: string;
fromSubField?: string;
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;
}, {});
};

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