Compare commits

...
Author SHA1 Message Date
sonarly-bot 3625218da9 fix: return validation error for missing manifest references
https://sonarly.com/issue/41051?type=bug

A metadata sync request hit a hard throw in migration building when resolving a universal-identifier reference that did not exist in flat entity maps. The API returned a Builder Internal Server Error instead of a user-correctable validation failure.

Fix: Implemented a migration-build error translation path so dangling manifest references are treated as validation failures instead of internal server errors.

What changed:
- In `WorkspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo`, I now intercept `FlatEntityMapsException` with:
  - `ENTITY_NOT_FOUND`
  - `RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND`
- For those expected unresolved-reference cases, the service now returns a failed orchestrator result (status=`fail`) built as a metadata validation payload, instead of throwing `WorkspaceMigrationV2Exception(BUILDER_INTERNAL_SERVER_ERROR)`.
- Added a new utility `buildFailedWorkspaceMigrationResultFromFlatEntityMapsException` to consistently build that failure payload.
  - It maps to the first involved metadata map key when available.
  - Falls back to `objectMetadata` if no key is available.

Outcome:
- `syncApplication`/manifest sync flows now surface user-correctable validation errors for dangling universal identifiers rather than hard internal errors.

Authored by Sonarly by autonomous analysis (run 46791).
2026-05-27 13:22:19 +00:00
3 changed files with 125 additions and 0 deletions
@@ -0,0 +1,56 @@
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { buildFailedWorkspaceMigrationResultFromFlatEntityMapsException } from 'src/engine/workspace-manager/workspace-migration/services/utils/build-failed-workspace-migration-result-from-flat-entity-maps-exception.util';
describe('buildFailedWorkspaceMigrationResultFromFlatEntityMapsException', () => {
it('should use the first metadata map key as the failed metadata name', () => {
const result =
buildFailedWorkspaceMigrationResultFromFlatEntityMapsException({
error: new FlatEntityMapsException(
'Could not find flat entity with universal identifier 123',
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
),
fromToAllFlatEntityMaps: {
flatViewMaps: {
from: {
byId: {},
idByUniversalIdentifier: {},
byUniversalIdentifier: {},
},
to: {
byId: {},
idByUniversalIdentifier: {},
byUniversalIdentifier: {},
},
},
},
});
expect(result.status).toBe('fail');
expect(result.report.view).toHaveLength(1);
expect(result.report.view[0].errors[0].code).toBe(
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
expect(result.report.view[0].errors[0].message).toBe(
'Could not find flat entity with universal identifier 123',
);
expect(result.report.view[0].errors[0].userFriendlyMessage).toBeDefined();
});
it('should fallback to objectMetadata when no map keys are provided', () => {
const result =
buildFailedWorkspaceMigrationResultFromFlatEntityMapsException({
error: new FlatEntityMapsException(
'Missing relation',
FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
),
fromToAllFlatEntityMaps: {},
});
expect(result.report[ALL_METADATA_NAME.objectMetadata]).toHaveLength(1);
});
});
@@ -0,0 +1,47 @@
import { ALL_METADATA_NAME, type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMapsException } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { getMetadataNameFromFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-name-from-flat-entity-maps-key.util';
import { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-failure-report.constant';
import {
type FromToAllUniversalFlatEntityMaps,
type WorkspaceMigrationOrchestratorFailedResult,
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
const DEFAULT_METADATA_NAME: AllMetadataName = ALL_METADATA_NAME.objectMetadata;
export const buildFailedWorkspaceMigrationResultFromFlatEntityMapsException = ({
error,
fromToAllFlatEntityMaps,
}: {
error: FlatEntityMapsException;
fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps;
}): WorkspaceMigrationOrchestratorFailedResult => {
const firstInvolvedFlatEntityMapsKey = Object.keys(fromToAllFlatEntityMaps)[0];
const metadataName = isDefined(firstInvolvedFlatEntityMapsKey)
? getMetadataNameFromFlatEntityMapsKey(firstInvolvedFlatEntityMapsKey)
: DEFAULT_METADATA_NAME;
return {
status: 'fail',
report: {
...EMPTY_ORCHESTRATOR_FAILURE_REPORT(),
[metadataName]: [
{
type: 'update',
metadataName,
errors: [
{
code: error.code,
message: error.message,
userFriendlyMessage: error.userFriendlyMessage,
},
],
flatEntityMinimalInformation: {},
},
],
},
};
};
@@ -30,6 +30,7 @@ import {
enrichCreateWorkspaceMigrationActionsWithIds,
IdByUniversalIdentifierByMetadataName,
} from 'src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util';
import { buildFailedWorkspaceMigrationResultFromFlatEntityMapsException } from 'src/engine/workspace-manager/workspace-migration/services/utils/build-failed-workspace-migration-result-from-flat-entity-maps-exception.util';
import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service';
import { WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type';
import {
@@ -371,7 +372,28 @@ export class WorkspaceMigrationValidateBuildAndRunService {
await this.workspaceMigrationBuildOrchestratorService
.buildWorkspaceMigration(buildArgs)
.catch((error) => {
if (
error instanceof FlatEntityMapsException &&
[
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
FlatEntityMapsExceptionCode
.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
].includes(error.code)
) {
this.logger.warn(
`Workspace migration build failed due to unresolved metadata reference on ${args.workspaceId}: ${error.message}`,
);
return buildFailedWorkspaceMigrationResultFromFlatEntityMapsException(
{
error,
fromToAllFlatEntityMaps: buildArgs.fromToAllFlatEntityMaps,
},
);
}
this.logger.error(error);
throw new WorkspaceMigrationV2Exception(
error.message,
WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR,