Add upgrade commands to backfill updatedBy field and view fields (#16845)

---
prastoin edit
## Introduction
As we're deprecating the sync metadata we cannot rely on it anymore in
order to create the new standard updatedBy field
In this PR we introduce a command that will backfill the dynamic field
on both workspace standard and custom objects, for standard object it
will create a standard fields ( twenty-standard app scoped ) and for
custom one it will create a custom field ( worksapce-custom-app scoped )

## Testing method
Checkout `1.14.0` `yarn` and `npx nx database:reset twenty-server`
checkout PR and `yarn` `npx nx build twenty-server` and `yarn
database:migrate:prod` finally running the command

#### Before
<img width="2244" height="1464" alt="image"
src="https://github.com/user-attachments/assets/2fb866cd-13b8-4152-99b8-1fcc813b1d46"
/>

#### After
<img width="2244" height="1464" alt="image"
src="https://github.com/user-attachments/assets/b836ac06-c7b8-4add-bee1-bc1963418f29"
/>


#### Logs
```ts
[Nest] 20678  - 12/30/2025, 1:47:35 PM     LOG [BackfillUpdatedByFieldCommand] Found 12 objects that need updatedBy field
[EntityBuilder fieldMetadata] matrix computation: 3.994ms
[EntityBuilder fieldMetadata] creation validation: 1.822ms
[EntityBuilder fieldMetadata] deletion validation: 0.016ms
[EntityBuilder fieldMetadata] update validation: 0.009ms
[EntityBuilder fieldMetadata] entity processing: 6.098ms
[EntityBuilder fieldMetadata] validateAndBuild: 10.217ms
[EntityBuilder index] matrix computation: 0.857ms
[EntityBuilder index] creation validation: 0.003ms
[EntityBuilder index] deletion validation: 0.017ms
[EntityBuilder index] update validation: 0.008ms
[EntityBuilder index] entity processing: 4.721ms
[EntityBuilder index] validateAndBuild: 5.632ms
[Runner] Initial cache retrieval: 0.07ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 7.69ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 10.531ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 5.354ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 6.134ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.507ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.745ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.113ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.585ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.223ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.221ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 5.375ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 5.943ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.363ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.997ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.03ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.982ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.88ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.977ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 9.638ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 11.297ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.581ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.248ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.847ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.732ms
[Runner] Transaction execution: 82.555ms
[Runner] Cache invalidation flatFieldMetadataMaps,flatIndexMaps,flatObjectMetadataMaps: 89.247ms
[Runner] Total execution: 171.963ms
[Nest] 20678  - 12/30/2025, 1:47:35 PM     LOG [BackfillUpdatedByFieldCommand] Successfully backfilled updatedBy field for 12 objects in workspace 20202020-1c25-4d02-bf25-6aeccf7ea419
[Nest] 20678  - 12/30/2025, 1:47:35 PM     LOG [BackfillUpdatedByFieldCommand] Running command on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2
[Nest] 20678  - 12/30/2025, 1:47:35 PM     LOG [BackfillUpdatedByFieldCommand] Starting backfill of updatedBy field for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db
[Nest] 20678  - 12/30/2025, 1:47:35 PM     LOG [BackfillUpdatedByFieldCommand] Found 10 objects that need updatedBy field
[EntityBuilder fieldMetadata] matrix computation: 3.224ms
[EntityBuilder fieldMetadata] creation validation: 0.969ms
[EntityBuilder fieldMetadata] deletion validation: 0.014ms
[EntityBuilder fieldMetadata] update validation: 0.002ms
[EntityBuilder fieldMetadata] entity processing: 5.149ms
[EntityBuilder fieldMetadata] validateAndBuild: 8.472ms
[EntityBuilder index] matrix computation: 0.785ms
[EntityBuilder index] creation validation: 0.002ms
[EntityBuilder index] deletion validation: 0.014ms
[EntityBuilder index] update validation: 0.002ms
[EntityBuilder index] entity processing: 3.818ms
[EntityBuilder index] validateAndBuild: 4.637ms
[Runner] Initial cache retrieval: 0.05ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.964ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.902ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.502ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.039ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.673ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.403ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.143ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.123ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.033ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.954ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.826ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.903ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.495ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.106ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 14.014ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 14.373ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.669ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.48ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.62ms
[BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.048ms
[Runner] Transaction execution: 73.222ms
[Runner] Cache invalidation flatFieldMetadataMaps,flatIndexMaps,flatObjectMetadataMaps: 37.703ms
[Runner] Total execution: 111.071ms
```
---

---------

Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
Abdul Rahman
2025-12-30 18:35:26 +05:30
committed by GitHub
parent a0491e4755
commit 720348c583
6 changed files with 238 additions and 3 deletions
@@ -0,0 +1,191 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
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 { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-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 {
ATTACHMENT_STANDARD_FIELD_IDS,
COMPANY_STANDARD_FIELD_IDS,
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
DASHBOARD_STANDARD_FIELD_IDS,
NOTE_STANDARD_FIELD_IDS,
OPPORTUNITY_STANDARD_FIELD_IDS,
PERSON_STANDARD_FIELD_IDS,
TASK_STANDARD_FIELD_IDS,
WORKFLOW_RUN_STANDARD_FIELD_IDS,
WORKFLOW_STANDARD_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
@Command({
name: 'upgrade:1-16:backfill-updated-by-field',
description: 'Backfill updatedBy field for all objects',
})
export class BackfillUpdatedByFieldCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
protected readonly logger = new Logger(BackfillUpdatedByFieldCommand.name);
private readonly objectToUpdatedByStandardIdMap: Record<string, string> = {
[STANDARD_OBJECT_IDS.attachment]: ATTACHMENT_STANDARD_FIELD_IDS.updatedBy,
[STANDARD_OBJECT_IDS.company]: COMPANY_STANDARD_FIELD_IDS.updatedBy,
[STANDARD_OBJECT_IDS.person]: PERSON_STANDARD_FIELD_IDS.updatedBy,
[STANDARD_OBJECT_IDS.note]: NOTE_STANDARD_FIELD_IDS.updatedBy,
[STANDARD_OBJECT_IDS.task]: TASK_STANDARD_FIELD_IDS.updatedBy,
[STANDARD_OBJECT_IDS.opportunity]: OPPORTUNITY_STANDARD_FIELD_IDS.updatedBy,
[STANDARD_OBJECT_IDS.workflow]: WORKFLOW_STANDARD_FIELD_IDS.updatedBy,
[STANDARD_OBJECT_IDS.workflowRun]:
WORKFLOW_RUN_STANDARD_FIELD_IDS.updatedBy,
[STANDARD_OBJECT_IDS.dashboard]: DASHBOARD_STANDARD_FIELD_IDS.updatedBy,
};
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationService: ApplicationService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Starting backfill of updatedBy field for workspace ${workspaceId}`,
);
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
]);
const objectMetadataList = Object.values(
flatObjectMetadataMaps.byId,
).filter(isDefined);
if (objectMetadataList.length === 0) {
this.logger.log(
`No objects found that need updatedBy field for workspace ${workspaceId}`,
);
return;
}
const createFieldInputs: CreateFieldInput[] = [];
for (const flatObjectMetadata of objectMetadataList) {
const isStandardObject = isStandardMetadata(flatObjectMetadata);
const updatedByStandardId = !isStandardObject
? CUSTOM_OBJECT_STANDARD_FIELD_IDS.updatedBy
: isDefined(flatObjectMetadata.standardId)
? this.objectToUpdatedByStandardIdMap[flatObjectMetadata.standardId]
: undefined;
if (!isDefined(updatedByStandardId)) {
continue;
}
const flatFieldMetadatas = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
flatEntityMaps: flatFieldMetadataMaps,
});
const updatedByField = flatFieldMetadatas.find(
(field) => field.standardId === updatedByStandardId,
);
if (isDefined(updatedByField)) {
this.logger.log(
`Object ${flatObjectMetadata.nameSingular} (${flatObjectMetadata.standardId || 'custom'}) already has updatedBy field, skipping`,
);
continue;
}
const applicationId = isStandardObject
? twentyStandardFlatApplication.id
: workspaceCustomFlatApplication.id;
const universalIdentifier = isStandardObject ? updatedByStandardId : v4();
createFieldInputs.push({
workspaceId,
objectMetadataId: flatObjectMetadata.id,
name: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The user who last updated the record',
icon: 'IconUserCircle',
isNullable: true,
isUIReadOnly: true,
isCustom: false,
isSystem: false,
isActive: true,
standardId: updatedByStandardId,
universalIdentifier,
applicationId,
});
}
if (createFieldInputs.length === 0) {
this.logger.log(
`All objects already have updatedBy field for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Found ${createFieldInputs.length} objects that need updatedBy field`,
);
if (options.dryRun) {
this.logger.log(
`[DRY RUN] Would create updatedBy field for ${createFieldInputs.length} objects`,
);
return;
}
try {
await this.fieldMetadataService.createManyFields({
createFieldInputs,
workspaceId,
isSystemBuild: true,
});
} catch (error) {
this.logger.error(
`Failed to create many fields \n ${JSON.stringify(error, null, 2)}`,
);
throw error;
}
this.logger.log(
`Successfully backfilled updatedBy field for ${createFieldInputs.length} objects in workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,32 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BackfillUpdatedByFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-updated-by-field.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [
TypeOrmModule.forFeature([
WorkspaceEntity,
ObjectMetadataEntity,
ViewEntity,
ViewFieldEntity,
]),
DataSourceModule,
FieldMetadataModule,
ViewFieldModule,
WorkspaceCacheModule,
ApplicationModule,
],
providers: [BackfillUpdatedByFieldCommand],
exports: [BackfillUpdatedByFieldCommand],
})
export class V1_16_UpgradeVersionCommandModule {}
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { V1_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module';
import { V1_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-14/1-14-upgrade-version-command.module';
import { V1_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-15/1-15-upgrade-version-command.module';
import { V1_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module';
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@@ -14,6 +15,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
V1_13_UpgradeVersionCommandModule,
V1_14_UpgradeVersionCommandModule,
V1_15_UpgradeVersionCommandModule,
V1_16_UpgradeVersionCommandModule,
DataSourceModule,
],
providers: [UpgradeCommand],
@@ -1,7 +1,7 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { type Repository } from 'typeorm';
import { type ActiveOrSuspendedWorkspacesMigrationCommandOptions } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import {
@@ -20,6 +20,7 @@ import { DeleteRemovedAgentsCommand } from 'src/database/commands/upgrade-versio
import { UpdateCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/1-14/1-14-update-created-by-enum.command';
import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-fix-nan-position-values-in-notes.command';
import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command';
import { BackfillUpdatedByFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-updated-by-field.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -55,6 +56,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
// 1.15 Commands
protected readonly migratePageLayoutWidgetConfigurationCommand: MigratePageLayoutWidgetConfigurationCommand,
protected readonly fixNanPositionValuesInNotesCommand: FixNanPositionValuesInNotesCommand,
// 1.16 Commands
protected readonly backfillUpdatedByFieldCommand: BackfillUpdatedByFieldCommand,
) {
super(
workspaceRepository,
@@ -86,11 +90,14 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.fixNanPositionValuesInNotesCommand,
];
const commands_1160: VersionCommands = [this.backfillUpdatedByFieldCommand];
this.allCommands = {
'1.12.0': commands_1120,
'1.13.0': commands_1130,
'1.14.0': commands_1140,
'1.15.0': commands_1150,
'1.16.0': commands_1160,
};
}
@@ -284,6 +284,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
createFieldInputs,
workspaceId,
applicationId,
isSystemBuild = false,
}: {
createFieldInputs: Omit<CreateFieldInput, 'workspaceId'>[];
workspaceId: string;
@@ -292,6 +293,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
* when interacting with another application than workspace custom one
* */
applicationId?: string;
isSystemBuild?: boolean;
}): Promise<FlatFieldMetadata[]> {
if (createFieldInputs.length === 0) {
return [];
@@ -363,7 +365,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
},
},
workspaceId,
isSystemBuild: false,
isSystemBuild,
},
);
@@ -58,7 +58,8 @@ export const getDefaultFlatFieldMetadata = ({
updatedAt: createdAt,
isUIReadOnly: createFieldInput.isUIReadOnly ?? false,
morphId: null,
applicationId: workspaceCustomApplicationId,
applicationId:
createFieldInput.applicationId ?? workspaceCustomApplicationId,
viewFilterIds: [],
kanbanAggregateOperationViewIds: [],
} as const satisfies FlatFieldMetadata;