Add count and percent aggregations to kanban headers (#9348)
Closes https://github.com/twentyhq/private-issues/issues/226 https://github.com/user-attachments/assets/cee78080-6dda-4102-9595-d32971cf9104
This commit is contained in:
@ -3,7 +3,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 { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
export class ProcessAggregateHelper {
|
||||
@ -26,11 +26,20 @@ export class ProcessAggregateHelper {
|
||||
continue;
|
||||
}
|
||||
|
||||
const columnName = formatColumnNameFromCompositeFieldAndSubfield(
|
||||
const columnNames = formatColumnNamesFromCompositeFieldAndSubfields(
|
||||
aggregatedField.fromField,
|
||||
aggregatedField.fromSubField,
|
||||
aggregatedField.fromSubFields,
|
||||
);
|
||||
|
||||
const columnNameForNumericOperation = isDefined(
|
||||
aggregatedField.subFieldForNumericOperation,
|
||||
)
|
||||
? formatColumnNamesFromCompositeFieldAndSubfields(
|
||||
aggregatedField.fromField,
|
||||
[aggregatedField.subFieldForNumericOperation],
|
||||
)[0]
|
||||
: columnNames[0];
|
||||
|
||||
if (
|
||||
!Object.values(AGGREGATE_OPERATIONS).includes(
|
||||
aggregatedField.aggregateOperation,
|
||||
@ -39,49 +48,54 @@ export class ProcessAggregateHelper {
|
||||
continue;
|
||||
}
|
||||
|
||||
const columnEmptyValueExpression =
|
||||
const concatenatedColumns = columnNames
|
||||
.map((col) => `"${col}"`)
|
||||
.join(", ' ', ");
|
||||
|
||||
const columnExpression =
|
||||
FIELD_METADATA_TYPES_TO_TEXT_COLUMN_TYPE.includes(
|
||||
aggregatedField.fromFieldType,
|
||||
)
|
||||
? `NULLIF("${columnName}", '')`
|
||||
: `"${columnName}"`;
|
||||
? `NULLIF(CONCAT(${concatenatedColumns}), '')`
|
||||
: `CONCAT(${concatenatedColumns})`;
|
||||
|
||||
switch (aggregatedField.aggregateOperation) {
|
||||
case AGGREGATE_OPERATIONS.countEmpty:
|
||||
queryBuilder.addSelect(
|
||||
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(*) - COUNT(${columnEmptyValueExpression}) END`,
|
||||
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(*) - COUNT(${columnExpression}) END`,
|
||||
`${aggregatedFieldName}`,
|
||||
);
|
||||
break;
|
||||
case AGGREGATE_OPERATIONS.countNotEmpty:
|
||||
queryBuilder.addSelect(
|
||||
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(${columnEmptyValueExpression}) END`,
|
||||
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(${columnExpression}) END`,
|
||||
`${aggregatedFieldName}`,
|
||||
);
|
||||
break;
|
||||
case AGGREGATE_OPERATIONS.countUniqueValues:
|
||||
queryBuilder.addSelect(
|
||||
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(DISTINCT "${columnName}") END`,
|
||||
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(DISTINCT ${columnExpression}) 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`,
|
||||
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE CAST(((COUNT(*) - COUNT(${columnExpression})::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`,
|
||||
`CASE WHEN COUNT(*) = 0 THEN NULL ELSE CAST((COUNT(${columnExpression})::decimal / COUNT(*)) AS DECIMAL) END`,
|
||||
`${aggregatedFieldName}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
default: {
|
||||
queryBuilder.addSelect(
|
||||
`${aggregatedField.aggregateOperation}("${columnName}")`,
|
||||
`${aggregatedField.aggregateOperation}("${columnNameForNumericOperation}")`,
|
||||
`${aggregatedFieldName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { GraphQLISODateTime } from '@nestjs/graphql';
|
||||
|
||||
import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql';
|
||||
import {
|
||||
getColumnNameForAggregateOperation,
|
||||
getSubfieldForAggregateOperation,
|
||||
} from 'twenty-shared';
|
||||
import { getSubfieldsForAggregateOperation } from 'twenty-shared';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
@ -17,7 +14,8 @@ export type AggregationField = {
|
||||
description: string;
|
||||
fromField: string;
|
||||
fromFieldType: FieldMetadataType;
|
||||
fromSubField?: string;
|
||||
fromSubFields?: string[];
|
||||
subFieldForNumericOperation?: string;
|
||||
aggregateOperation: AGGREGATE_OPERATIONS;
|
||||
};
|
||||
|
||||
@ -30,55 +28,50 @@ export const getAvailableAggregationsFromObjectFields = (
|
||||
return acc;
|
||||
}
|
||||
|
||||
const columnName = getColumnNameForAggregateOperation(
|
||||
field.name,
|
||||
field.type,
|
||||
);
|
||||
const fromSubFields = getSubfieldsForAggregateOperation(field.type);
|
||||
|
||||
const fromSubField = getSubfieldForAggregateOperation(field.type);
|
||||
|
||||
acc[`countUniqueValues${capitalize(columnName)}`] = {
|
||||
acc[`countUniqueValues${capitalize(field.name)}`] = {
|
||||
type: GraphQLInt,
|
||||
description: `Number of unique values for ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromFieldType: field.type,
|
||||
fromSubField,
|
||||
fromSubFields,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.countUniqueValues,
|
||||
};
|
||||
|
||||
acc[`countEmpty${capitalize(columnName)}`] = {
|
||||
acc[`countEmpty${capitalize(field.name)}`] = {
|
||||
type: GraphQLInt,
|
||||
description: `Number of empty values for ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromFieldType: field.type,
|
||||
fromSubField,
|
||||
fromSubFields,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.countEmpty,
|
||||
};
|
||||
|
||||
acc[`countNotEmpty${capitalize(columnName)}`] = {
|
||||
acc[`countNotEmpty${capitalize(field.name)}`] = {
|
||||
type: GraphQLInt,
|
||||
description: `Number of non-empty values for ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromFieldType: field.type,
|
||||
fromSubField,
|
||||
fromSubFields,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.countNotEmpty,
|
||||
};
|
||||
|
||||
acc[`percentageEmpty${capitalize(columnName)}`] = {
|
||||
acc[`percentageEmpty${capitalize(field.name)}`] = {
|
||||
type: GraphQLFloat,
|
||||
description: `Percentage of empty values for ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromFieldType: field.type,
|
||||
fromSubField,
|
||||
fromSubFields,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.percentageEmpty,
|
||||
};
|
||||
|
||||
acc[`percentageNotEmpty${capitalize(columnName)}`] = {
|
||||
acc[`percentageNotEmpty${capitalize(field.name)}`] = {
|
||||
type: GraphQLFloat,
|
||||
description: `Percentage of non-empty values for ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromFieldType: field.type,
|
||||
fromSubField,
|
||||
fromSubFields,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.percentageNotEmpty,
|
||||
};
|
||||
|
||||
@ -138,7 +131,8 @@ export const getAvailableAggregationsFromObjectFields = (
|
||||
type: GraphQLFloat,
|
||||
description: `Minimum amount contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromSubField: 'amountMicros',
|
||||
fromSubFields: getSubfieldsForAggregateOperation(field.type),
|
||||
subFieldForNumericOperation: 'amountMicros',
|
||||
fromFieldType: field.type,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.min,
|
||||
};
|
||||
@ -147,7 +141,7 @@ export const getAvailableAggregationsFromObjectFields = (
|
||||
type: GraphQLFloat,
|
||||
description: `Maximal amount contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromSubField: 'amountMicros',
|
||||
fromSubFields: getSubfieldsForAggregateOperation(field.type),
|
||||
fromFieldType: field.type,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.max,
|
||||
};
|
||||
@ -156,7 +150,7 @@ export const getAvailableAggregationsFromObjectFields = (
|
||||
type: GraphQLFloat,
|
||||
description: `Sum of amounts contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromSubField: 'amountMicros',
|
||||
fromSubFields: getSubfieldsForAggregateOperation(field.type),
|
||||
fromFieldType: field.type,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
||||
};
|
||||
@ -165,7 +159,7 @@ export const getAvailableAggregationsFromObjectFields = (
|
||||
type: GraphQLFloat,
|
||||
description: `Average amount contained in the field ${field.name}`,
|
||||
fromField: field.name,
|
||||
fromSubField: 'amountMicros',
|
||||
fromSubFields: getSubfieldsForAggregateOperation(field.type),
|
||||
fromFieldType: field.type,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.avg,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user