diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flat-cache-invalidate.command.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flat-cache-invalidate.command.ts new file mode 100644 index 0000000000..9dd56f81f1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flat-cache-invalidate.command.ts @@ -0,0 +1,164 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command, Option } from 'nest-commander'; +import { + ALL_METADATA_NAME, + type AllMetadataName, +} from 'twenty-shared/metadata'; +import { Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + type ActiveOrSuspendedWorkspacesMigrationCommandOptions, +} 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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; + +type FlatCacheFlushCommandOptions = + ActiveOrSuspendedWorkspacesMigrationCommandOptions & { + allMetadata?: boolean; + }; + +@Command({ + name: 'cache:flat-cache-invalidate', + description: + 'Flush flat entity cache for specific metadata names and workspaces', +}) +export class FlatCacheInvalidateCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + private metadataNames: string[] = []; + private flatMapsKeysToFlush: ReturnType< + typeof getMetadataFlatEntityMapsKey + >[] = []; + + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + ) { + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); + } + + @Option({ + flags: '--metadataName ', + description: + 'Metadata name(s) to flush cache for. Can be specified multiple times.', + required: false, + }) + parseMetadataName(val: string): string[] { + this.metadataNames.push(val); + + return this.metadataNames; + } + + @Option({ + flags: '--all-metadata', + description: + 'Flush cache for all metadata names. Takes precedence over --metadataName.', + required: false, + }) + parseAllMetadata(): boolean { + return true; + } + + override async runMigrationCommand( + passedParams: string[], + options: FlatCacheFlushCommandOptions, + ): Promise { + if (!options.allMetadata && this.metadataNames.length === 0) { + this.logger.error( + 'Either --all-metadata or at least one --metadataName must be provided.', + ); + + return; + } + + const validatedMetadataNames = this.validateAndExpandMetadataNames({ + inputMetadataNames: this.metadataNames, + allMetadata: options.allMetadata, + }); + + if (validatedMetadataNames === null) { + return; + } + + this.flatMapsKeysToFlush = this.computeFlatMapsKeysWithRelated( + validatedMetadataNames, + ); + + this.logger.log( + `Will flush cache for the following flat maps keys: ${this.flatMapsKeysToFlush.join(', ')}`, + ); + + await super.runMigrationCommand(passedParams, options); + } + + override async runOnWorkspace({ + workspaceId, + }: RunOnWorkspaceArgs): Promise { + await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ + workspaceId, + flatMapsKeys: this.flatMapsKeysToFlush, + }); + + this.logger.log(`Successfully flushed cache for workspace: ${workspaceId}`); + } + + private validateAndExpandMetadataNames({ + inputMetadataNames, + allMetadata, + }: { + inputMetadataNames: string[]; + allMetadata?: boolean; + }): AllMetadataName[] | null { + const validMetadataNames = Object.keys( + ALL_METADATA_NAME, + ) as AllMetadataName[]; + + if (allMetadata) { + this.logger.log('Using all metadata names'); + + return validMetadataNames; + } + + const invalidNames = inputMetadataNames.filter( + (name) => !validMetadataNames.includes(name as AllMetadataName), + ); + + if (invalidNames.length > 0) { + this.logger.error( + `Invalid metadata name(s) provided: ${invalidNames.join(', ')}`, + ); + this.logger.error( + `Valid metadata names are: ${validMetadataNames.join(', ')}, or use --all-metadata`, + ); + + return null; + } + + return inputMetadataNames as AllMetadataName[]; + } + + private computeFlatMapsKeysWithRelated( + metadataNames: AllMetadataName[], + ): ReturnType[] { + const allMetadataNamesToFlush = [ + ...new Set([ + ...metadataNames, + ...metadataNames.flatMap(getMetadataRelatedMetadataNames), + ]), + ]; + + const allFlatMapsKeys = allMetadataNamesToFlush.map( + getMetadataFlatEntityMapsKey, + ); + + return allFlatMapsKeys; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts index be2fbc436c..99882a02b5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts @@ -1,6 +1,9 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { FlatCacheInvalidateCommand } from 'src/engine/core-modules/cache-storage/commands/flat-cache-invalidate.command'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { WorkspaceFlatFieldMetadataMapCacheService } from 'src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service'; @@ -34,7 +37,9 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache @Module({ imports: [ WorkspaceCacheModule, + DataSourceModule, TypeOrmModule.forFeature([ + WorkspaceEntity, ViewEntity, ViewFieldEntity, ViewFilterEntity, @@ -66,6 +71,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache WorkspaceFlatPageLayoutWidgetMapCacheService, WorkspaceFlatRowLevelPermissionPredicateMapCacheService, WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService, + FlatCacheInvalidateCommand, ], exports: [ WorkspaceManyOrAllFlatEntityMapsCacheService, @@ -82,6 +88,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache WorkspaceFlatPageLayoutWidgetMapCacheService, WorkspaceFlatRowLevelPermissionPredicateMapCacheService, WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService, + FlatCacheInvalidateCommand, ], }) export class WorkspaceManyOrAllFlatEntityMapsCacheModule {}