Aggregate count variations (#9304)

Closes https://github.com/twentyhq/private-issues/issues/222

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Marie
2025-01-02 17:35:05 +01:00
committed by GitHub
parent 0f1458cbe9
commit 5d857fbfb5
43 changed files with 650 additions and 203 deletions

View File

@ -4,4 +4,9 @@ export enum AGGREGATE_OPERATIONS {
avg = 'AVG',
sum = 'SUM',
count = 'COUNT',
countUniqueValues = 'COUNT_UNIQUE_VALUES',
countEmpty = 'COUNT_EMPTY',
countNotEmpty = 'COUNT_NOT_EMPTY',
percentageEmpty = 'PERCENTAGE_EMPTY',
percentageNotEmpty = 'PERCENTAGE_NOT_EMPTY',
}

View File

@ -2,6 +2,7 @@ import { SelectQueryBuilder } from 'typeorm';
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
import { FIELD_METADATA_TYPES_TO_TEXT_COLUMN_TYPE } from 'src/engine/metadata-modules/workspace-migration/constants/fieldMetadataTypesToTextColumnType';
import { formatColumnNameFromCompositeFieldAndSubfield } from 'src/engine/twenty-orm/utils/format-column-name-from-composite-field-and-subfield.util';
import { isDefined } from 'src/utils/is-defined';
@ -38,10 +39,50 @@ export class ProcessAggregateHelper {
continue;
}
queryBuilder.addSelect(
`${aggregatedField.aggregateOperation}("${columnName}")`,
`${aggregatedFieldName}`,
);
const columnEmptyValueExpression =
FIELD_METADATA_TYPES_TO_TEXT_COLUMN_TYPE.includes(
aggregatedField.fromFieldType,
)
? `NULLIF("${columnName}", '')`
: `"${columnName}"`;
switch (aggregatedField.aggregateOperation) {
case AGGREGATE_OPERATIONS.countEmpty:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(*) - COUNT(${columnEmptyValueExpression}) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.countNotEmpty:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(${columnEmptyValueExpression}) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.countUniqueValues:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(DISTINCT "${columnName}") END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.percentageEmpty:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE CAST(((COUNT(*) - COUNT(${columnEmptyValueExpression})::decimal) / COUNT(*)) AS DECIMAL) END`,
`${aggregatedFieldName}`,
);
break;
case AGGREGATE_OPERATIONS.percentageNotEmpty:
queryBuilder.addSelect(
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE CAST((COUNT(${columnEmptyValueExpression})::decimal / COUNT(*)) AS DECIMAL) END`,
`${aggregatedFieldName}`,
);
break;
default:
queryBuilder.addSelect(
`${aggregatedField.aggregateOperation}("${columnName}")`,
`${aggregatedFieldName}`,
);
}
}
};
}

View File

@ -1,6 +1,10 @@
import { GraphQLISODateTime } from '@nestjs/graphql';
import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql';
import {
getColumnNameForAggregateOperation,
getSubfieldForAggregateOperation,
} from 'twenty-shared';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@ -12,6 +16,7 @@ export type AggregationField = {
type: GraphQLScalarType;
description: string;
fromField: string;
fromFieldType: FieldMetadataType;
fromSubField?: string;
aggregateOperation: AGGREGATE_OPERATIONS;
};
@ -19,94 +24,164 @@ export type AggregationField = {
export const getAvailableAggregationsFromObjectFields = (
fields: FieldMetadataInterface[],
): Record<string, AggregationField> => {
return fields.reduce<Record<string, AggregationField>>((acc, field) => {
acc['totalCount'] = {
type: GraphQLInt,
description: `Total number of records in the connection`,
fromField: 'id',
aggregateOperation: AGGREGATE_OPERATIONS.count,
};
return fields.reduce<Record<string, AggregationField>>(
(acc, field) => {
if (field.type === FieldMetadataType.RELATION) {
return acc;
}
if (field.type === FieldMetadataType.DATE_TIME) {
acc[`min${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Oldest date contained in the field ${field.name}`,
const columnName = getColumnNameForAggregateOperation(
field.name,
field.type,
);
const fromSubField = getSubfieldForAggregateOperation(field.type);
acc[`countUniqueValues${capitalize(columnName)}`] = {
type: GraphQLInt,
description: `Number of unique values for ${field.name}`,
fromField: field.name,
aggregateOperation: AGGREGATE_OPERATIONS.min,
fromFieldType: field.type,
fromSubField,
aggregateOperation: AGGREGATE_OPERATIONS.countUniqueValues,
};
acc[`max${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Most recent date contained in the field ${field.name}`,
acc[`countEmpty${capitalize(columnName)}`] = {
type: GraphQLInt,
description: `Number of empty values for ${field.name}`,
fromField: field.name,
aggregateOperation: AGGREGATE_OPERATIONS.max,
fromFieldType: field.type,
fromSubField,
aggregateOperation: AGGREGATE_OPERATIONS.countEmpty,
};
}
if (field.type === FieldMetadataType.NUMBER) {
acc[`min${capitalize(field.name)}`] = {
acc[`countNotEmpty${capitalize(columnName)}`] = {
type: GraphQLInt,
description: `Number of non-empty values for ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
fromSubField,
aggregateOperation: AGGREGATE_OPERATIONS.countNotEmpty,
};
acc[`percentageEmpty${capitalize(columnName)}`] = {
type: GraphQLFloat,
description: `Minimum amount contained in the field ${field.name}`,
description: `Percentage of empty values for ${field.name}`,
fromField: field.name,
aggregateOperation: AGGREGATE_OPERATIONS.min,
fromFieldType: field.type,
fromSubField,
aggregateOperation: AGGREGATE_OPERATIONS.percentageEmpty,
};
acc[`max${capitalize(field.name)}`] = {
acc[`percentageNotEmpty${capitalize(columnName)}`] = {
type: GraphQLFloat,
description: `Maximum amount contained in the field ${field.name}`,
description: `Percentage of non-empty values for ${field.name}`,
fromField: field.name,
aggregateOperation: AGGREGATE_OPERATIONS.max,
fromFieldType: field.type,
fromSubField,
aggregateOperation: AGGREGATE_OPERATIONS.percentageNotEmpty,
};
acc[`avg${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Average amount contained in the field ${field.name}`,
fromField: field.name,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
};
switch (field.type) {
case FieldMetadataType.DATE_TIME:
acc[`min${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Oldest date contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
acc[`sum${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Sum of amounts contained in the field ${field.name}`,
fromField: field.name,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
};
}
acc[`max${capitalize(field.name)}`] = {
type: GraphQLISODateTime,
description: `Most recent date contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
break;
case FieldMetadataType.NUMBER:
acc[`min${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Minimum amount contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
if (field.type === FieldMetadataType.CURRENCY) {
acc[`min${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Minimum amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
acc[`max${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Maximum amount contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
acc[`max${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Maximal amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
acc[`avg${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Average amount contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
};
acc[`sum${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Sum of amounts contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
aggregateOperation: AGGREGATE_OPERATIONS.sum,
};
acc[`sum${capitalize(field.name)}`] = {
type: GraphQLFloat,
description: `Sum of amounts contained in the field ${field.name}`,
fromField: field.name,
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
};
break;
case FieldMetadataType.CURRENCY:
acc[`min${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Minimum amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.min,
};
acc[`avg${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Average amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
aggregateOperation: AGGREGATE_OPERATIONS.avg,
};
}
acc[`max${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Maximal amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.max,
};
return acc;
}, {});
acc[`sum${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Sum of amounts contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.sum,
};
acc[`avg${capitalize(field.name)}AmountMicros`] = {
type: GraphQLFloat,
description: `Average amount contained in the field ${field.name}`,
fromField: field.name,
fromSubField: 'amountMicros',
fromFieldType: field.type,
aggregateOperation: AGGREGATE_OPERATIONS.avg,
};
break;
}
return acc;
},
{
totalCount: {
type: GraphQLInt,
description: `Total number of records in the connection`,
fromField: 'id',
fromFieldType: FieldMetadataType.UUID,
aggregateOperation: AGGREGATE_OPERATIONS.count,
},
},
);
};