fix(server): allow relabelling onto a field introduced in the same manifest sync (#22727)
## Context Closes twentyhq/core-team-issues#2655. `computeOrderedMigrationActions` runs `objectMetadata.update` **before** `fieldMetadata.create`. In a single manifest sync that both introduces a new field and relabels the object's `labelIdentifierFieldMetadataUniversalIdentifier` onto that field, the object update handler resolved the label identifier's universal identifier against the persisted `flatFieldMetadataMaps` only. Since the field's `fieldMetadata.create` runs later in the same migration, the field isn't in the maps yet and the sync failed with `ENTITY_NOT_FOUND`. The API metadata path is unaffected because create-field and update-object are separate requests (separate transactions), so the field is already persisted by the time the object update resolves. ## What this does `update-object-action-handler.service.ts` now resolves `labelIdentifierFieldMetadataId` and `imageIdentifierFieldMetadataId` against the deterministically preallocated field ids first, then falls back to the persisted flat maps for fields that already exist. The preallocated ids (`preallocatedIdByUniversalIdentifierByMetadataName`) are built from every create action before the migration loop starts (`buildPreallocatedIdByUniversalIdentifierFromActions`) and are the same ids `create-field-action-handler` persists the fields with. This is the same "preallocated-first, then flat maps" resolution that `resolveUniversalRelationIdentifiersToIds` already uses for modeled many-to-one relations, so no ordering change or new machinery is needed, and the single sync stays one atomic transaction. This does not touch the underlying action ordering or the hand-rolled label/image identifier handling flagged by the `#2172` TODO; generalizing those into the relation config remains the follow-up. ## Test Adds `relabel-onto-new-field-manifest-sync.integration-spec.ts` (used as the TDD reproduction, now green): - introducing a field and relabelling onto it in a single sync succeeds, and the object's `labelIdentifierFieldMetadataId` points at the new field; - the split path (introduce in one sync, relabel in the next) still succeeds and exposes the enriched `labelIdentifierFieldMetadataId` through the metadata API. Both cases pass against the fix. `oxlint`, `oxfmt`, and `typecheck` are clean. --- _Generated by [Claude Code](https://claude.ai/code/session_01KJewXMuWYUyrX3YJh2JBDE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22727?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:
+50
-26
@@ -67,35 +67,21 @@ export class UpdateObjectActionHandlerService extends WorkspaceMigrationRunnerAc
|
||||
};
|
||||
|
||||
if (isDefined(labelIdentifierFieldMetadataUniversalIdentifier)) {
|
||||
const flatFieldMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: allFlatEntityMaps.flatFieldMetadataMaps,
|
||||
universalIdentifier: labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Could not resolve labelIdentifierFieldMetadataUniversalIdentifier to labelIdentifierFieldMetadataId: no fieldMetadata found for universal identifier ${labelIdentifierFieldMetadataUniversalIdentifier}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
transpiledUpdate.labelIdentifierFieldMetadataId = flatFieldMetadata.id;
|
||||
transpiledUpdate.labelIdentifierFieldMetadataId =
|
||||
this.resolveFieldMetadataIdOrThrow({
|
||||
context,
|
||||
universalIdentifier: labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
foreignKeyName: 'labelIdentifierFieldMetadataId',
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(imageIdentifierFieldMetadataUniversalIdentifier)) {
|
||||
const flatFieldMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: allFlatEntityMaps.flatFieldMetadataMaps,
|
||||
universalIdentifier: imageIdentifierFieldMetadataUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Could not resolve imageIdentifierFieldMetadataUniversalIdentifier to imageIdentifierFieldMetadataId: no fieldMetadata found for universal identifier ${imageIdentifierFieldMetadataUniversalIdentifier}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
transpiledUpdate.imageIdentifierFieldMetadataId = flatFieldMetadata.id;
|
||||
transpiledUpdate.imageIdentifierFieldMetadataId =
|
||||
this.resolveFieldMetadataIdOrThrow({
|
||||
context,
|
||||
universalIdentifier: imageIdentifierFieldMetadataUniversalIdentifier,
|
||||
foreignKeyName: 'imageIdentifierFieldMetadataId',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -106,6 +92,44 @@ export class UpdateObjectActionHandlerService extends WorkspaceMigrationRunnerAc
|
||||
};
|
||||
}
|
||||
|
||||
private resolveFieldMetadataIdOrThrow({
|
||||
context,
|
||||
universalIdentifier,
|
||||
foreignKeyName,
|
||||
}: {
|
||||
context: WorkspaceMigrationActionRunnerArgs<UniversalUpdateObjectAction>;
|
||||
universalIdentifier: string;
|
||||
foreignKeyName: string;
|
||||
}): string {
|
||||
const {
|
||||
allFlatEntityMaps,
|
||||
preallocatedIdByUniversalIdentifierByMetadataName,
|
||||
} = context;
|
||||
|
||||
const preallocatedFieldMetadataId =
|
||||
preallocatedIdByUniversalIdentifierByMetadataName?.fieldMetadata?.[
|
||||
universalIdentifier
|
||||
];
|
||||
|
||||
if (isDefined(preallocatedFieldMetadataId)) {
|
||||
return preallocatedFieldMetadataId;
|
||||
}
|
||||
|
||||
const flatFieldMetadata = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: allFlatEntityMaps.flatFieldMetadataMaps,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Could not resolve ${foreignKeyName}: no fieldMetadata found for universal identifier ${universalIdentifier}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return flatFieldMetadata.id;
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatUpdateObjectAction>,
|
||||
): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user