diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts index aefb1379b..d3b5106b0 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts @@ -31,6 +31,10 @@ export const getAggregateOperationLabel = ( return t`Earliest date`; case DATE_AGGREGATE_OPERATIONS.latest: return t`Latest date`; + case AGGREGATE_OPERATIONS.countTrue: + return t`Count true`; + case AGGREGATE_OPERATIONS.countFalse: + return t`Count false`; default: throw new Error(`Unknown aggregate operation: ${operation}`); } diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationShortLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationShortLabel.ts index f9f1a9c81..a53476326 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationShortLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationShortLabel.ts @@ -29,6 +29,10 @@ export const getAggregateOperationShortLabel = ( return msg`Earliest`; case DATE_AGGREGATE_OPERATIONS.latest: return msg`Latest`; + case AGGREGATE_OPERATIONS.countTrue: + return msg`True`; + case AGGREGATE_OPERATIONS.countFalse: + return msg`False`; default: throw new Error(`Unknown aggregate operation: ${operation}`); } diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/AggregateOperations.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/AggregateOperations.ts index 74a9bc6d6..e0f6ce08f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/constants/AggregateOperations.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/AggregateOperations.ts @@ -9,4 +9,6 @@ export enum AGGREGATE_OPERATIONS { countUniqueValues = 'COUNT_UNIQUE_VALUES', percentageEmpty = 'PERCENTAGE_EMPTY', percentageNotEmpty = 'PERCENTAGE_NOT_EMPTY', + countTrue = 'COUNT_TRUE', + countFalse = 'COUNT_FALSE', } diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts index 3ebadc892..4adf44dd6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts @@ -19,6 +19,8 @@ export const FIELD_TYPES_AVAILABLE_FOR_NON_STANDARD_AGGREGATE_OPERATION = { FieldMetadataType.NUMBER, FieldMetadataType.CURRENCY, ], + [AGGREGATE_OPERATIONS.countFalse]: [FieldMetadataType.BOOLEAN], + [AGGREGATE_OPERATIONS.countTrue]: [FieldMetadataType.BOOLEAN], [DATE_AGGREGATE_OPERATIONS.earliest]: [ FieldMetadataType.DATE_TIME, FieldMetadataType.DATE, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions.tsx index c8c6bb530..0b474c880 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions.tsx @@ -5,4 +5,6 @@ export const COUNT_AGGREGATE_OPERATION_OPTIONS = [ AGGREGATE_OPERATIONS.countEmpty, AGGREGATE_OPERATIONS.countNotEmpty, AGGREGATE_OPERATIONS.countUniqueValues, + AGGREGATE_OPERATIONS.countTrue, + AGGREGATE_OPERATIONS.countFalse, ]; diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/getAvailableFieldsIdsForAggregationFromObjectFields.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/getAvailableFieldsIdsForAggregationFromObjectFields.test.ts index 130e83376..227e41d69 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/getAvailableFieldsIdsForAggregationFromObjectFields.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/getAvailableFieldsIdsForAggregationFromObjectFields.test.ts @@ -9,17 +9,23 @@ import { FieldMetadataType } from '~/generated/graphql'; const AMOUNT_FIELD_ID = '7d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0a'; const PRICE_FIELD_ID = '9d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0b'; const NAME_FIELD_ID = '5d2d7b5e-7b3e-4b4a-8b0a-7b3e4b4a8b0c'; +const ACTIVE_FIELD_ID = '0825d011-6006-49a2-99c5-8d67bed77e55'; const FIELDS_MOCKS = [ { id: AMOUNT_FIELD_ID, type: FieldMetadataType.NUMBER, name: 'amount' }, { id: PRICE_FIELD_ID, type: FieldMetadataType.CURRENCY, name: 'price' }, { id: NAME_FIELD_ID, type: FieldMetadataType.TEXT, name: 'name' }, + { id: ACTIVE_FIELD_ID, type: FieldMetadataType.BOOLEAN, name: 'active' }, ]; jest.mock( '@/object-record/utils/getAvailableAggregationsFromObjectFields', () => ({ getAvailableAggregationsFromObjectFields: jest.fn().mockReturnValue({ + active: { + [AGGREGATE_OPERATIONS.countTrue]: 'countTrueActive', + [AGGREGATE_OPERATIONS.countFalse]: 'CountFalseActive', + }, amount: { [AGGREGATE_OPERATIONS.sum]: 'sumAmount', [AGGREGATE_OPERATIONS.avg]: 'avgAmount', @@ -78,13 +84,16 @@ describe('getAvailableFieldsIdsForAggregationFromObjectFields', () => { COUNT_AGGREGATE_OPERATION_OPTIONS, ); - COUNT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => { - expect(result[operation]).toEqual([ + expect(result.COUNT).toEqual( + expect.arrayContaining([ AMOUNT_FIELD_ID, PRICE_FIELD_ID, NAME_FIELD_ID, - ]); - }); + ]), + ); + + expect(result.COUNT_TRUE).toContain(ACTIVE_FIELD_ID); + expect(result.COUNT_FALSE).toContain(ACTIVE_FIELD_ID); PERCENT_AGGREGATE_OPERATION_OPTIONS.forEach((operation) => { expect(result[operation]).toBeUndefined(); diff --git a/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts b/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts index ee50c8eb8..1eb554141 100644 --- a/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts +++ b/packages/twenty-front/src/modules/object-record/utils/getAvailableAggregationsFromObjectFields.ts @@ -62,6 +62,14 @@ export const getAvailableAggregationsFromObjectFields = ( }; } + if (field.type === FieldMetadataType.BOOLEAN) { + acc[field.name] = { + ...acc[field.name], + [AGGREGATE_OPERATIONS.countTrue]: `countTrue${capitalize(field.name)}`, + [AGGREGATE_OPERATIONS.countFalse]: `countFalse${capitalize(field.name)}`, + }; + } + if (isFieldMetadataDateKind(field.type) === true) { acc[field.name] = { ...acc[field.name], diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-50/0-50-update-view-aggregate-operations.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-50/0-50-update-view-aggregate-operations.command.ts new file mode 100644 index 000000000..e7b5a1576 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-50/0-50-update-view-aggregate-operations.command.ts @@ -0,0 +1,272 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; +import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; + +const AGGREGATE_OPERATION_OPTIONS = [ + { + value: AGGREGATE_OPERATIONS.avg, + label: 'Average', + position: 0, + color: 'red', + }, + { + value: AGGREGATE_OPERATIONS.count, + label: 'Count', + position: 1, + color: 'purple', + }, + { + value: AGGREGATE_OPERATIONS.max, + label: 'Maximum', + position: 2, + color: 'sky', + }, + { + value: AGGREGATE_OPERATIONS.min, + label: 'Minimum', + position: 3, + color: 'turquoise', + }, + { + value: AGGREGATE_OPERATIONS.sum, + label: 'Sum', + position: 4, + color: 'yellow', + }, + { + value: AGGREGATE_OPERATIONS.countEmpty, + label: 'Count empty', + position: 5, + color: 'red', + }, + { + value: AGGREGATE_OPERATIONS.countNotEmpty, + label: 'Count not empty', + position: 6, + color: 'purple', + }, + { + value: AGGREGATE_OPERATIONS.countUniqueValues, + label: 'Count unique values', + position: 7, + color: 'sky', + }, + { + value: AGGREGATE_OPERATIONS.percentageEmpty, + label: 'Percent empty', + position: 8, + color: 'turquoise', + }, + { + value: AGGREGATE_OPERATIONS.percentageNotEmpty, + label: 'Percent not empty', + position: 9, + color: 'yellow', + }, + { + value: AGGREGATE_OPERATIONS.countTrue, + label: 'Count true', + position: 10, + color: 'red', + }, + { + value: AGGREGATE_OPERATIONS.countFalse, + label: 'Count false', + position: 11, + color: 'purple', + }, +]; + +@Command({ + name: 'upgrade:0-50:update-view-aggregate-operations', + description: + 'Update View and ViewField entities with new aggregate operations (countTrue, countFalse)', +}) +export class UpdateViewAggregateOperationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + private readonly workspaceMigrationService: WorkspaceMigrationService, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + override async runOnWorkspace({ + index, + total, + workspaceId, + }: RunOnWorkspaceArgs): Promise { + this.logger.log( + `Running command for workspace ${workspaceId} ${index + 1}/${total}`, + ); + + await this.updateViewAggregateOperations(workspaceId); + await this.updateViewFieldAggregateOperations(workspaceId); + + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, + ); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + + this.logger.log( + chalk.green(`Command completed for workspace ${workspaceId}.`), + ); + } + + private async updateViewAggregateOperations( + workspaceId: string, + ): Promise { + const viewObjectMetadata = await this.objectMetadataRepository.findOne({ + where: { + workspaceId, + standardId: STANDARD_OBJECT_IDS.view, + }, + relations: ['fields'], + }); + + if (!viewObjectMetadata) { + this.logger.warn( + `View object metadata not found for workspace ${workspaceId}`, + ); + + return; + } + + const kanbanAggregateOperationField = viewObjectMetadata.fields.find( + (field) => field.name === 'kanbanAggregateOperation', + ); + + if (!kanbanAggregateOperationField) { + this.logger.warn( + `kanbanAggregateOperation field not found for workspace ${workspaceId}`, + ); + + return; + } + + await this.fieldMetadataRepository.update( + { id: kanbanAggregateOperationField.id }, + { options: AGGREGATE_OPERATION_OPTIONS }, + ); + + this.logger.log( + `Updated kanbanAggregateOperation options for workspace ${workspaceId}`, + ); + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`update-view-operations`), + workspaceId, + [ + { + name: computeObjectTargetTable(viewObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.ALTER, + { ...kanbanAggregateOperationField, options: undefined }, + { + ...kanbanAggregateOperationField, + options: AGGREGATE_OPERATION_OPTIONS, + }, + ), + } satisfies WorkspaceMigrationTableAction, + ], + ); + } + + private async updateViewFieldAggregateOperations( + workspaceId: string, + ): Promise { + const viewFieldObjectMetadata = await this.objectMetadataRepository.findOne( + { + where: { + workspaceId, + standardId: STANDARD_OBJECT_IDS.viewField, + }, + relations: ['fields'], + }, + ); + + if (!viewFieldObjectMetadata) { + this.logger.warn( + `ViewField object metadata not found for workspace ${workspaceId}`, + ); + + return; + } + + const aggregateOperationField = viewFieldObjectMetadata.fields.find( + (field) => field.name === 'aggregateOperation', + ); + + if (!aggregateOperationField) { + this.logger.warn( + `aggregateOperation field not found for workspace ${workspaceId}`, + ); + + return; + } + + await this.fieldMetadataRepository.update( + { id: aggregateOperationField.id }, + { options: AGGREGATE_OPERATION_OPTIONS }, + ); + + this.logger.log( + `Updated aggregateOperation options for workspace ${workspaceId}`, + ); + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`update-view-field-operations`), + workspaceId, + [ + { + name: computeObjectTargetTable(viewFieldObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.ALTER, + { ...aggregateOperationField, options: undefined }, + { + ...aggregateOperationField, + options: AGGREGATE_OPERATION_OPTIONS, + }, + ), + } satisfies WorkspaceMigrationTableAction, + ], + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-50/0-50-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-50/0-50-upgrade-version-command.module.ts index 9a0ab6eda..5b0e9c1fe 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-50/0-50-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-50/0-50-upgrade-version-command.module.ts @@ -2,13 +2,17 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-50/0-50-migrate-relations-to-field-metadata.command'; +import { UpdateViewAggregateOperationsCommand } from 'src/database/commands/upgrade-version-command/0-50/0-50-update-view-aggregate-operations.command'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; +import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; @Module({ imports: [ @@ -20,8 +24,17 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works WorkspaceDataSourceModule, RoleModule, UserRoleModule, + WorkspaceMigrationModule, + WorkspaceMigrationRunnerModule, + WorkspaceMetadataVersionModule, + ], + providers: [ + MigrateRelationsToFieldMetadataCommand, + UpdateViewAggregateOperationsCommand, + ], + exports: [ + MigrateRelationsToFieldMetadataCommand, + UpdateViewAggregateOperationsCommand, ], - providers: [MigrateRelationsToFieldMetadataCommand], - exports: [MigrateRelationsToFieldMetadataCommand], }) export class V0_50_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index b4e1aa433..a0965ac13 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -16,6 +16,7 @@ import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/co import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command'; import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-initialize-permissions.command'; import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-50/0-50-migrate-relations-to-field-metadata.command'; +import { UpdateViewAggregateOperationsCommand } from 'src/database/commands/upgrade-version-command/0-50/0-50-update-view-aggregate-operations.command'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -52,6 +53,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { // 0.50 Commands protected readonly migrateRelationsToFieldMetadataCommand: MigrateRelationsToFieldMetadataCommand, + protected readonly updateViewAggregateOperationsCommand: UpdateViewAggregateOperationsCommand, ) { super( workspaceRepository, @@ -77,7 +79,10 @@ export class UpgradeCommand extends UpgradeCommandRunner { afterSyncMetadata: [], }; const _commands_050: VersionCommands = { - beforeSyncMetadata: [this.migrateRelationsToFieldMetadataCommand], + beforeSyncMetadata: [ + this.migrateRelationsToFieldMetadataCommand, + this.updateViewAggregateOperationsCommand, + ], afterSyncMetadata: [], }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant.ts index 39b916caf..ad28365c3 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant.ts @@ -7,6 +7,8 @@ export enum AGGREGATE_OPERATIONS { countUniqueValues = 'COUNT_UNIQUE_VALUES', countEmpty = 'COUNT_EMPTY', countNotEmpty = 'COUNT_NOT_EMPTY', + countTrue = 'COUNT_TRUE', + countFalse = 'COUNT_FALSE', percentageEmpty = 'PERCENTAGE_EMPTY', percentageNotEmpty = 'PERCENTAGE_NOT_EMPTY', } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts index 6911040dc..686ea9e58 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { SelectQueryBuilder } from 'typeorm'; import { isDefined } from 'twenty-shared'; +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'; @@ -87,6 +87,19 @@ export class ProcessAggregateHelper { `${aggregatedFieldName}`, ); break; + case AGGREGATE_OPERATIONS.countTrue: + queryBuilder.addSelect( + `CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(CASE WHEN ${columnExpression}::boolean = TRUE THEN 1 ELSE NULL END) END`, + `${aggregatedFieldName}`, + ); + break; + + case AGGREGATE_OPERATIONS.countFalse: + queryBuilder.addSelect( + `CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(CASE WHEN ${columnExpression}::boolean = FALSE THEN 1 ELSE NULL END) END`, + `${aggregatedFieldName}`, + ); + break; default: { queryBuilder.addSelect( `${aggregatedField.aggregateOperation}("${columnNameForNumericOperation}")`, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts index f659720bf..71e3f4d60 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts @@ -98,6 +98,24 @@ export const getAvailableAggregationsFromObjectFields = ( } switch (field.type) { + case FieldMetadataType.BOOLEAN: + acc[`countTrue${capitalize(field.name)}`] = { + type: GraphQLInt, + description: `Count of true values in the field ${field.name}`, + fromField: field.name, + fromFieldType: field.type, + aggregateOperation: AGGREGATE_OPERATIONS.countTrue, + }; + + acc[`countFalse${capitalize(field.name)}`] = { + type: GraphQLInt, + description: `Count of false values in the field ${field.name}`, + fromField: field.name, + fromFieldType: field.type, + aggregateOperation: AGGREGATE_OPERATIONS.countFalse, + }; + break; + case FieldMetadataType.NUMBER: acc[`min${capitalize(field.name)}`] = { type: GraphQLFloat, diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts index 4aa376d97..e41d777a7 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts @@ -157,6 +157,18 @@ export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity { position: 9, color: 'yellow', }, + { + value: AGGREGATE_OPERATIONS.countTrue, + label: 'Count true', + position: 10, + color: 'red', + }, + { + value: AGGREGATE_OPERATIONS.countFalse, + label: 'Count false', + position: 11, + color: 'purple', + }, ], defaultValue: null, }) diff --git a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts index 3b56f606e..15a41e3e4 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts @@ -284,6 +284,18 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity { position: 9, color: 'yellow', }, + { + value: AGGREGATE_OPERATIONS.countTrue, + label: 'Count true', + position: 10, + color: 'red', + }, + { + value: AGGREGATE_OPERATIONS.countFalse, + label: 'Count false', + position: 11, + color: 'purple', + }, ], defaultValue: `'${AGGREGATE_OPERATIONS.count}'`, })