fix(server): add workspaceMember jobTitle field without view-field side effects (#22306)

## Problem

The `2.17` `AddWorkspaceMemberJobTitleField` upgrade command failed for
every workspace that has a **custom field on `workspaceMember`** (it
aborted the per-workspace upgrade sequence). Root cause:

The command used `FieldMetadataService.createManyFields`, which —
besides creating the field metadata — also creates the new field's
**view fields** across the object's existing views. Validating a
view-field creation enumerates the target view's full
`viewFieldUniversalIdentifiers`, which includes view fields owned by
**another application** (a custom field a user added to
`workspaceMember`). Those cross-application view fields are filtered out
of the build's application-scoped dependency maps, so the build throws
`Could not find flat entity with universal identifier ...`.

## Fix (command-scoped)

Create the field metadata **only — no view fields** — sourced from the
standard-application definition, mirroring
`AddInactiveGenericStandardFieldsCommand` (which adds a standard field
the same way and is unaffected by this bug). The standard view-field
side effects are reconciled as code, so they should not be produced at
runtime here.

Concretely: source the standard `jobTitle` `FlatFieldMetadata` from
`computeTwentyStandardApplicationAllFlatEntityMaps`, blank its
`viewFieldIds` / `viewFieldUniversalIdentifiers`, and run a
`fieldMetadata`-only `validateBuildAndRunWorkspaceMigration` instead of
`createManyFields`. Also drops the now-unused `FieldMetadataModule`
import from the 2-17 command module.

## Trade-off

Existing workspaces get the `jobTitle` field + column but **no view
field**, so it won't appear as a default column in `workspaceMember`
views until the standard-application reconciliation adds view fields.
`jobTitle` is `isSystem` / `isUIReadOnly`, so this is acceptable as the
immediate unblock.

## Scope

This is the small, immediate unblock for the failed `2.17` upgrade. The
general framework fix (load cross-application children of involved
parents into the build scope) is tracked separately in #22294.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22306?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Félix Malfait
2026-06-29 17:18:06 +02:00
committed by GitHub
parent be8102c4a4
commit 107245e56d
2 changed files with 55 additions and 31 deletions
@@ -5,14 +5,12 @@ import { AddReplyToMessageParticipantRoleOptionCommand } from 'src/database/comm
import { AddWorkspaceMemberJobTitleFieldCommand } from 'src/database/commands/upgrade-version-command/2-17/2-17-workspace-command-1801000010000-add-workspace-member-job-title-field.command';
import { SyncCallRecordingNavigationCommandMenuItemAvailabilityExpressionCommand } from 'src/database/commands/upgrade-version-command/2-17/2-17-workspace-command-1801000000000-sync-call-recording-navigation-command-menu-item-availability-expression.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
ApplicationModule,
FieldMetadataModule,
WorkspaceCacheModule,
WorkspaceIteratorModule,
WorkspaceMigrationModule,
@@ -1,6 +1,5 @@
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
@@ -8,12 +7,12 @@ import { WorkspaceIteratorService } from 'src/database/commands/command-runners/
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { type 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 { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const WORKSPACE_MEMBER_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.workspaceMember.universalIdentifier;
@@ -32,7 +31,7 @@ export class AddWorkspaceMemberJobTitleFieldCommand extends ActiveOrSuspendedWor
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {
super(workspaceIteratorService);
}
@@ -77,20 +76,6 @@ export class AddWorkspaceMemberJobTitleFieldCommand extends ActiveOrSuspendedWor
return;
}
const createFieldInput: Omit<CreateFieldInput, 'workspaceId'> = {
objectMetadataId: workspaceMemberObject.id,
name: 'jobTitle',
type: FieldMetadataType.TEXT,
label: 'Job Title',
description: 'Workspace member job title',
icon: 'IconBriefcase',
isNullable: true,
isUIReadOnly: true,
isSystem: true,
isActive: true,
universalIdentifier: JOB_TITLE_FIELD_UNIVERSAL_IDENTIFIER,
};
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create jobTitle field on workspaceMember for workspace ${workspaceId}`,
@@ -104,20 +89,61 @@ export class AddWorkspaceMemberJobTitleFieldCommand extends ActiveOrSuspendedWor
{ workspaceId },
);
try {
await this.fieldMetadataService.createManyFields({
createFieldInputs: [createFieldInput],
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
now: new Date().toISOString(),
workspaceId,
ownerFlatApplication: twentyStandardFlatApplication,
isSystemBuild: true,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
} catch (error) {
this.logger.error(
`Failed to add jobTitle field on workspaceMember for workspace ${workspaceId}:\n${
error instanceof Error ? error.stack : JSON.stringify(error, null, 2)
}`,
const standardJobTitleFlatFieldMetadata =
standardAllFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
JOB_TITLE_FIELD_UNIVERSAL_IDENTIFIER
];
if (!isDefined(standardJobTitleFlatFieldMetadata)) {
this.logger.log(
`jobTitle standard field definition not found, skipping workspace ${workspaceId}`,
);
return;
}
const flatFieldMetadataToCreate: FlatFieldMetadata = {
...standardJobTitleFlatFieldMetadata,
viewFieldIds: [],
viewFieldUniversalIdentifiers: [],
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
flatEntityToCreate: [flatFieldMetadataToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: true,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to add jobTitle field on workspaceMember for workspace ${workspaceId}:\n${JSON.stringify(
validateAndBuildResult,
null,
2,
)}`,
);
throw new Error(
`Failed to add jobTitle field on workspaceMember for workspace ${workspaceId}`,
);
throw error;
}
this.logger.log(