mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 03:17:40 +03: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:
parent
2f5dc26545
commit
0f1cf0e4e9
@ -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;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user