Invalidate flat cache command (#17442)

# Introduction
A command allowing to invalidate flat cache entries. Extends the active
or suspended workspace coverage.

## Args
### --metadataName
If provided will invalidate metadata and related metadata cache entry (
can be repeated see usage )

### --all-metadata
Will invalidate all metadata entries

## Usage example
```
npx nx command twenty-server cache:flat-cache-invalidate --metadataName viewFilter --metadataName objectMetadata
```

```
npx nx command twenty-server cache:flat-cache-invalidate --all-metadata -w 0000-0000-0000-0000
```
This commit is contained in:
Paul Rastoin
2026-01-26 14:08:31 +01:00
committed by GitHub
parent 90a496bbe8
commit 406e269cf1
2 changed files with 171 additions and 0 deletions
@@ -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<FlatCacheFlushCommandOptions> {
private metadataNames: string[] = [];
private flatMapsKeysToFlush: ReturnType<
typeof getMetadataFlatEntityMapsKey
>[] = [];
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
@Option({
flags: '--metadataName <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<void> {
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<void> {
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<typeof getMetadataFlatEntityMapsKey>[] {
const allMetadataNamesToFlush = [
...new Set([
...metadataNames,
...metadataNames.flatMap(getMetadataRelatedMetadataNames),
]),
];
const allFlatMapsKeys = allMetadataNamesToFlush.map(
getMetadataFlatEntityMapsKey,
);
return allFlatMapsKeys;
}
}