diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command.ts new file mode 100644 index 0000000000..4c63a9d098 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command.ts @@ -0,0 +1,154 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { FieldMetadataType, ViewFilterOperand } from 'twenty-shared/types'; +import { Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type'; + +@Command({ + name: 'upgrade:1-17:migrate-date-time-is-filter-values', + description: 'Migrate DATE_TIME IS operand values from Instant to Plain Date', +}) +export class MigrateDateTimeIsFilterValuesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + protected readonly logger = new Logger( + MigrateDateTimeIsFilterValuesCommand.name, + {}, + ); + + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + @InjectRepository(ViewFilterEntity) + private readonly viewFilterRepository: Repository, + private readonly featureFlagService: FeatureFlagService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + ) { + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun || false; + + this.logger.log(`Processing workspace ${workspaceId}`); + + const viewFilters = await this.viewFilterRepository.find({ + where: { + workspaceId, + }, + relations: ['fieldMetadata'], + withDeleted: true, + }); + + const filtersToUpdate = viewFilters.filter((filter) => { + if ( + !filter.fieldMetadata || + filter.fieldMetadata.type !== FieldMetadataType.DATE_TIME || + filter.operand !== ViewFilterOperand.IS + ) { + return false; + } + + const value = filter.value; + + if (typeof value !== 'string') { + return false; + } + + return value.includes('T'); + }); + + if (filtersToUpdate.length === 0) { + return; + } + + this.logger.log( + `Found ${filtersToUpdate.length} filters to migrate in workspace ${workspaceId}`, + ); + + if (isDryRun) { + this.logger.log( + `Dry run: would update ${filtersToUpdate.length} filters`, + ); + + return; + } + + let updatedCount = 0; + + for (const filter of filtersToUpdate) { + try { + const newDate = (filter.value as string).split('T')[0]; + + if (!/^\d{4}-\d{2}-\d{2}$/.test(newDate)) { + this.logger.warn( + `Skipping invalid date extraction for filter ${filter.id}: ${filter.value} -> ${newDate}`, + ); + continue; + } + + await this.viewFilterRepository.update(filter.id, { + value: newDate, + }); + updatedCount++; + } catch (err) { + this.logger.error( + `Failed to migrate filter ${filter.id}: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + + this.logger.log( + `Migrated ${updatedCount} filters in workspace ${workspaceId}`, + ); + + await this.featureFlagService.enableFeatureFlags( + [FeatureFlagKey.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED], + workspaceId, + ); + + this.logger.log( + `Enabled IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED for workspace ${workspaceId}`, + ); + + const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [ + 'flatViewFilterMaps', + ]; + + this.logger.log(`Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`); + + await this.workspaceCacheService.invalidateAndRecompute( + workspaceId, + cacheKeysToInvalidate, + ); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + + this.logger.log(`Cache flushed`); + + this.logger.log(`Flush cache for workspace ${workspaceId}`); + + await this.workspaceCacheStorageService.flush(workspaceId); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts index 9ce14fb641..733c02b882 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts @@ -7,6 +7,7 @@ import { FixMorphRelationFieldNamesCommand } from 'src/database/commands/upgrade import { IdentifyWebhookMetadataCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-identify-webhook-metadata.command'; import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-make-webhook-universal-identifier-and-application-id-not-nullable-migration.command'; import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command'; +import { MigrateDateTimeIsFilterValuesCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-date-time-is-filter-values.command'; import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command'; import { MigrateSendEmailRecipientsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command'; import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command'; @@ -27,6 +28,7 @@ import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/ import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; +import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module'; @@ -49,6 +51,7 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas NoteTargetWorkspaceEntity, TaskTargetWorkspaceEntity, FileEntity, + ViewFilterEntity, LogicFunctionEntity, ]), DataSourceModule, @@ -75,6 +78,7 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, DeleteFileRecordsAndUpdateTableCommand, MigrateSendEmailRecipientsCommand, + MigrateDateTimeIsFilterValuesCommand, MigrateWorkflowCodeStepsCommand, SeedWorkflowV1_16Command, BackfillApplicationPackageFilesCommand, @@ -87,6 +91,7 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas IdentifyWebhookMetadataCommand, MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, MigrateSendEmailRecipientsCommand, + MigrateDateTimeIsFilterValuesCommand, DeleteFileRecordsAndUpdateTableCommand, MigrateWorkflowCodeStepsCommand, SeedWorkflowV1_16Command,