Compare commits

...

6 Commits

Author SHA1 Message Date
Etienne 715eed85be UpdateTaskOnDeleteActionCommand - Add logs (#17479) 2026-01-27 17:26:12 +01:00
Paul Rastoin 03651cab6c Invalidate and flush cache post 1.16 upgrade (#17465)
Breaking change requires cache flush
Centralized invalidation cache logic
```
[Nest] 42871  - 01/27/2026, 11:39:33 AM     LOG [FlushV2CacheAndIncrementMetadataVersionCommand] Flushing v2 cache and incrementing metadata version for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Runner] Cache invalidation flatFieldMetadataMaps,flatObjectMetadataMaps,flatViewMaps,flatViewFieldMaps,flatViewGroupMaps,flatRowLevelPermissionPredicateMaps,flatRowLevelPermissionPredicateGroupMaps,flatViewFilterGroupMaps,flatIndexMaps,flatServerlessFunctionMaps,flatCronTriggerMaps,flatDatabaseEventTriggerMaps,flatRouteTriggerMaps,flatViewFilterMaps,flatRoleMaps,flatRoleTargetMaps,flatAgentMaps,flatSkillMaps,flatPageLayoutMaps,flatPageLayoutWidgetMaps,flatPageLayoutTabMaps,flatCommandMenuItemMaps,flatNavigationMenuItemMaps,flatFrontComponentMaps: 81.41ms
```
2026-01-27 17:25:16 +01:00
Paul Rastoin 493e6a7f8f 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
```
2026-01-27 17:24:51 +01:00
Paul Rastoin c4775450f5 Backfill owner standard field check colliding joinColumnName (#17449)
# Introduction
Previously the command would have been `old` renaming only any `owner`
field on `opportunity` object
Now we also search for any colliding `joinColumnName` with `ownerId`
which is also introduced by the new standard owner field

In a nutshell: previously gracefully handling existing custom `owner`
field collision but not for RELATION types that has an additional
collision surface: `joinColumnName`

Related
https://github.com/twentyhq/twenty/issues/17413#issuecomment-3799872079
2026-01-26 16:50:19 +01:00
Paul Rastoin cb6957fa51 Fix upgrade command order backfillStandardPageLayoutsCommand (#17430)
# Introduction
`backfillStandardPageLayoutsCommand` expects field and object metadata
to have been identified in prior to be run
https://github.com/twentyhq/twenty/issues/17413#issuecomment-3796933563
2026-01-25 21:53:29 +01:00
Paul Rastoin 9633740132 [Debug log level] Print validation build result failure (#17423)
# Introduction
As per title, in order to ease debug
Motivation
https://github.com/twentyhq/twenty/issues/17413#issuecomment-3796168946
2026-01-25 12:38:16 +01:00
9 changed files with 396 additions and 71 deletions
@@ -15,7 +15,11 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
import { MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
@@ -115,32 +119,22 @@ export class BackfillOpportunityOwnerFieldCommand extends ActiveOrSuspendedWorks
return;
}
const customOwnerField = flatFieldMetadatas.find(
(field) =>
field.name === 'owner' &&
field.universalIdentifier !== ownerUniversalIdentifier,
);
const customOwnerField =
flatFieldMetadatas.find(
(field) =>
field.name === 'owner' &&
field.universalIdentifier !== ownerUniversalIdentifier,
// Some self hoster might have run the name renaming successfully but not the joinColumnName
// Rather than asking them to restore db also grabbing them here
) ?? flatFieldMetadatas.find((field) => field.name === 'ownerOld');
if (isDefined(customOwnerField)) {
if (options.dryRun) {
this.logger.log(
`[DRY RUN] Would rename custom owner field to 'ownerOld' in workspace ${workspaceId}`,
);
} else {
await this.fieldMetadataService.updateOneField({
updateFieldInput: {
id: customOwnerField.id,
name: 'ownerOld',
label: 'Owner (Old)',
},
workspaceId,
isSystemBuild: true,
});
this.logger.log(
`Renamed custom owner field to 'ownerOld' in workspace ${workspaceId}`,
);
}
await this.renameCustomOwnerFieldToOwnerOld({
customOwnerField,
flatFieldMetadataMaps,
workspaceId,
dryRun: options.dryRun,
});
}
if (options.dryRun) {
@@ -232,4 +226,96 @@ export class BackfillOpportunityOwnerFieldCommand extends ActiveOrSuspendedWorks
throw error;
}
}
private async renameCustomOwnerFieldToOwnerOld({
customOwnerField,
flatFieldMetadataMaps,
workspaceId,
dryRun,
}: {
customOwnerField: FlatFieldMetadata;
flatFieldMetadataMaps: MetadataFlatEntityMaps<'fieldMetadata'>;
workspaceId: string;
dryRun?: boolean;
}): Promise<void> {
if (dryRun) {
this.logger.log(
`[DRY RUN] Would rename custom owner field to 'ownerOld' in workspace and search for colliding foreignKey ${workspaceId}`,
);
return;
}
const isMorphOrRelationField =
isMorphOrRelationFlatFieldMetadata(customOwnerField);
const hasCollidingJoinColumnName =
isMorphOrRelationField &&
isDefined(customOwnerField.settings.joinColumnName) &&
customOwnerField.settings.joinColumnName === 'ownerId';
await this.fieldMetadataService.updateOneField({
updateFieldInput: {
id: customOwnerField.id,
name: 'ownerOld',
label: 'Owner (Old)',
...(hasCollidingJoinColumnName
? {
settings: {
...customOwnerField.settings,
joinColumnName: `ownerOldId`,
},
}
: {}),
},
workspaceId,
isSystemBuild: true,
});
this.logger.log(
`Renamed custom owner field to 'ownerOld' in workspace ${workspaceId}`,
);
if (!isMorphOrRelationField || hasCollidingJoinColumnName) {
return;
}
const relatedFlatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: customOwnerField.relationTargetFieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (
!isDefined(relatedFlatFieldMetadata) ||
!isMorphOrRelationFlatFieldMetadata(relatedFlatFieldMetadata)
) {
this.logger.error(
`Could not find custom owner relation target field ${customOwnerField.relationTargetFieldMetadataId}`,
);
return;
}
const targetHasCollidingJoinColumnName =
isDefined(relatedFlatFieldMetadata.settings.joinColumnName) &&
relatedFlatFieldMetadata.settings.joinColumnName === 'ownerId';
if (!targetHasCollidingJoinColumnName) {
return;
}
await this.fieldMetadataService.updateOneField({
updateFieldInput: {
id: relatedFlatFieldMetadata.id,
settings: {
...relatedFlatFieldMetadata.settings,
joinColumnName: `ownerOldId`,
},
},
workspaceId,
isSystemBuild: true,
});
this.logger.log(
`Renamed custom owner field target related field join column name to 'ownerOldId' in workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,54 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { 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 { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
@Command({
name: 'upgrade:1-16:flush-v2-cache-and-increment-metadata-version',
description: 'Flush the whole v2 cache and increment the metadata version',
})
export class FlushV2CacheAndIncrementMetadataVersionCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
) {
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Flushing v2 cache and incrementing metadata version for workspace ${workspaceId}`,
);
if (options.dryRun) {
this.logger.log(
`DRY RUN: Would flush v2 cache and increment metadata version for workspace ${workspaceId}`,
);
return;
}
await this.workspaceMigrationRunnerService.invalidateCache({
allFlatEntityMapsKeys: ALL_FLAT_ENTITY_MAPS_PROPERTIES,
workspaceId,
});
this.logger.log(
`Successfully flushed v2 cache and incremented metadata version for workspace ${workspaceId}`,
);
}
}
@@ -126,14 +126,20 @@ export class UpdateTaskOnDeleteActionCommand extends ActiveOrSuspendedWorkspaces
onDelete: RelationOnDeleteAction.CASCADE,
};
await this.fieldMetadataService.updateOneField({
updateFieldInput: {
id: taskField.id,
settings: updatedSettings,
},
workspaceId,
isSystemBuild: true,
});
try {
await this.fieldMetadataService.updateOneField({
updateFieldInput: {
id: taskField.id,
settings: updatedSettings,
},
workspaceId,
isSystemBuild: true,
});
} catch (error) {
this.logger.debug(`Error details: ${JSON.stringify(error)}`);
throw error;
}
this.logger.log(
`Successfully updated task relation onDelete to CASCADE in workspace ${workspaceId}`,
@@ -3,10 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { BackfillOpportunityOwnerFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-opportunity-owner-field.command';
import { BackfillStandardPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-standard-page-layouts.command';
import { FlushV2CacheAndIncrementMetadataVersionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-flush-v2-cache-and-increment-metadata-version.command';
import { IdentifyAgentMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-agent-metadata.command';
import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command';
import { IdentifyIndexMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-index-metadata.command';
import { IdentifyObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-object-metadata.command';
import { IdentifyRemainingEntitiesMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-remaining-entities-metadata.command';
import { IdentifyRoleMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-role-metadata.command';
import { IdentifyViewFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-field-metadata.command';
import { IdentifyViewFilterMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-filter-metadata.command';
@@ -16,7 +18,6 @@ import { MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand
import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-index-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeObjectMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-object-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
import { IdentifyRemainingEntitiesMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-remaining-entities-metadata.command';
import { MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-remaining-entities-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeRoleUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-role-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeViewFieldUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-view-field-universal-identifier-and-application-id-not-nullable-migration.command';
@@ -41,6 +42,7 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit
import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
@@ -64,6 +66,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
GlobalWorkspaceDataSourceModule,
TwentyStandardApplicationModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [
@@ -90,6 +93,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyRemainingEntitiesMetadataCommand,
FlushV2CacheAndIncrementMetadataVersionCommand,
],
exports: [
UpdateTaskOnDeleteActionCommand,
@@ -115,6 +119,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyRemainingEntitiesMetadataCommand,
FlushV2CacheAndIncrementMetadataVersionCommand,
],
})
export class V1_16_UpgradeVersionCommandModule {}
@@ -24,14 +24,15 @@ import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrad
import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command';
import { BackfillOpportunityOwnerFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-opportunity-owner-field.command';
import { BackfillStandardPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-standard-page-layouts.command';
import { FlushV2CacheAndIncrementMetadataVersionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-flush-v2-cache-and-increment-metadata-version.command';
import { IdentifyAgentMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-agent-metadata.command';
import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command';
import { IdentifyIndexMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-index-metadata.command';
import { IdentifyObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-object-metadata.command';
import { IdentifyRemainingEntitiesMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-remaining-entities-metadata.command';
import { IdentifyRoleMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-role-metadata.command';
import { IdentifyViewFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-field-metadata.command';
import { IdentifyViewFilterMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-filter-metadata.command';
import { IdentifyRemainingEntitiesMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-remaining-entities-metadata.command';
import { IdentifyViewGroupMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-group-metadata.command';
import { IdentifyViewMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-view-metadata.command';
import { MakeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-agent-universal-identifier-and-application-id-not-nullable-migration.command';
@@ -107,6 +108,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly makeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeIndexMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly identifyRemainingEntitiesMetadataCommand: IdentifyRemainingEntitiesMetadataCommand,
protected readonly makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly flushV2CacheAndIncrementMetadataVersionCommand: FlushV2CacheAndIncrementMetadataVersionCommand,
) {
super(
workspaceRepository,
@@ -142,8 +144,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
const commands_1160: VersionCommands = [
this.updateTaskOnDeleteActionCommand,
this.backfillOpportunityOwnerFieldCommand,
this.backfillStandardPageLayoutsCommand,
this.identifyAgentMetadataCommand,
this.identifyFieldMetadataCommand,
this.identifyObjectMetadataCommand,
@@ -153,6 +153,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.identifyViewFilterMetadataCommand,
this.identifyViewGroupMetadataCommand,
this.identifyIndexMetadataCommand,
this.backfillOpportunityOwnerFieldCommand,
this.backfillStandardPageLayoutsCommand,
this
.makeAgentUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this
@@ -174,6 +176,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.identifyRemainingEntitiesMetadataCommand,
this
.makeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this.flushV2CacheAndIncrementMetadataVersionCommand,
];
this.allCommands = {
@@ -6,6 +6,7 @@ import {
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
@@ -38,12 +39,18 @@ export class WorkspaceMigrationValidateBuildAndRunService {
private readonly logger = new Logger(
WorkspaceMigrationValidateBuildAndRunService.name,
);
private readonly isDebugEnabled: boolean;
constructor(
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMigrationBuildOrchestratorService: WorkspaceMigrationBuildOrchestratorService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
twentyConfigService: TwentyConfigService,
) {
const logLevels = twentyConfigService.get('LOG_LEVELS');
this.isDebugEnabled = logLevels.includes('debug');
}
private async computeAllRelatedFlatEntityMaps({
allFlatEntityOperationByMetadataName,
@@ -186,6 +193,10 @@ export class WorkspaceMigrationValidateBuildAndRunService {
});
if (validateAndBuildResult.status === 'fail') {
if (this.isDebugEnabled) {
this.logger.debug(JSON.stringify(validateAndBuildResult, null, 2));
}
return validateAndBuildResult;
}
@@ -0,0 +1,165 @@
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 { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
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';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
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: (keyof AllFlatEntityMaps)[] = [];
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
) {
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.workspaceMigrationRunnerService.invalidateCache({
allFlatEntityMapsKeys: this.flatMapsKeysToFlush,
workspaceId,
});
this.logger.log(
`Successfully invalidated 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;
}
}
@@ -14,7 +14,6 @@ import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/wor
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration';
import { WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
import {
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
@@ -35,19 +34,18 @@ export class WorkspaceMigrationRunnerService {
) {}
private getLegacyCacheInvalidationPromises({
workspaceMigration: { actions, workspaceId },
allFlatEntityMapsKeys,
workspaceId,
}: {
workspaceMigration: Omit<WorkspaceMigration, 'relatedFlatEntityMapsKeys'>;
allFlatEntityMapsKeys: (keyof AllFlatEntityMaps)[];
workspaceId: string;
}): Promise<void>[] {
const asyncOperations: Promise<void>[] = [];
const shouldIncrementMetadataGraphqlSchemaVersion = actions.some(
(action) => {
return (
action.metadataName === 'objectMetadata' ||
action.metadataName === 'fieldMetadata'
);
},
);
const flatMapsKeysSet = new Set(allFlatEntityMapsKeys);
const shouldIncrementMetadataGraphqlSchemaVersion =
flatMapsKeysSet.has('flatObjectMetadataMaps') ||
flatMapsKeysSet.has('flatFieldMetadataMaps');
if (shouldIncrementMetadataGraphqlSchemaVersion) {
asyncOperations.push(
@@ -57,16 +55,15 @@ export class WorkspaceMigrationRunnerService {
);
}
const viewRelatedMetadataNames = [
'view',
'viewFilter',
'viewGroup',
'viewField',
'viewFilterGroup',
const viewRelatedFlatMapsKeys: (keyof AllFlatEntityMaps)[] = [
'flatViewMaps',
'flatViewFilterMaps',
'flatViewGroupMaps',
'flatViewFieldMaps',
'flatViewFilterGroupMaps',
];
const shouldInvalidFindCoreViewsGraphqlCacheOperation = actions.some(
(action) => viewRelatedMetadataNames.includes(action.metadataName),
);
const shouldInvalidFindCoreViewsGraphqlCacheOperation =
viewRelatedFlatMapsKeys.some((key) => flatMapsKeysSet.has(key));
if (
shouldInvalidFindCoreViewsGraphqlCacheOperation ||
@@ -80,11 +77,9 @@ export class WorkspaceMigrationRunnerService {
);
}
const shouldInvalidateRoleMapCache = actions.some((action) => {
return (
action.metadataName === 'role' || action.metadataName === 'roleTarget'
);
});
const shouldInvalidateRoleMapCache =
flatMapsKeysSet.has('flatRoleMaps') ||
flatMapsKeysSet.has('flatRoleTargetMaps');
if (
shouldIncrementMetadataGraphqlSchemaVersion ||
@@ -105,14 +100,12 @@ export class WorkspaceMigrationRunnerService {
return asyncOperations;
}
private async invalidateCachePostExecution({
async invalidateCache({
allFlatEntityMapsKeys,
workspaceId,
actions,
}: {
allFlatEntityMapsKeys: (keyof AllFlatEntityMaps)[];
workspaceId: string;
actions: WorkspaceMigrationAction[];
}): Promise<void> {
this.logger.time(
'Runner',
@@ -126,10 +119,8 @@ export class WorkspaceMigrationRunnerService {
const invalidationResults = await Promise.allSettled(
this.getLegacyCacheInvalidationPromises({
workspaceMigration: {
actions,
workspaceId,
},
allFlatEntityMapsKeys,
workspaceId,
}),
);
@@ -214,10 +205,9 @@ export class WorkspaceMigrationRunnerService {
this.logger.timeEnd('Runner', 'Transaction execution');
await this.invalidateCachePostExecution({
await this.invalidateCache({
allFlatEntityMapsKeys,
workspaceId,
actions,
});
this.logger.timeEnd('Runner', 'Total execution');
@@ -1,14 +1,17 @@
import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceSchemaMigrationRunnerActionHandlersModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module';
import { FlatCacheInvalidateCommand } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/commands/flat-cache-invalidate.command';
import { WorkspaceMigrationRunnerActionHandlerRegistryService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/registry/workspace-migration-runner-action-handler-registry.service';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
@@ -23,10 +26,12 @@ import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/wo
DiscoveryModule,
WorkspaceCacheStorageModule,
WorkspaceCacheModule,
TypeOrmModule.forFeature([WorkspaceEntity]),
],
providers: [
WorkspaceMigrationRunnerService,
WorkspaceMigrationRunnerActionHandlerRegistryService,
FlatCacheInvalidateCommand,
],
exports: [WorkspaceMigrationRunnerService],
})