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:
-2
@@ -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,
|
||||
|
||||
+55
-29
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user