Resolve involved application ids from API metadata (#18221)

# Introduction
Resolving each create|updated|deleted entities related entities
application through their universal foreingKey ( silently failing if not
found leaving the validator handling that )
In order to compute all the required application in the dependency flat
entity maps

## Tested
Creating a field on a custom local twenty-apps installed on the
workspace + view field

## Out of scope
- updated snapshot
- update graphql generated front
This commit is contained in:
Paul Rastoin
2026-02-25 13:58:38 +01:00
committed by GitHub
parent f3857c41b8
commit 98e73791a4
11 changed files with 154 additions and 47 deletions
@@ -1544,6 +1544,7 @@ export type Field = {
settings?: Maybe<Scalars['JSON']>;
standardOverrides?: Maybe<StandardOverrides>;
type: FieldMetadataType;
universalIdentifier: Scalars['UUID'];
updatedAt: Scalars['DateTime'];
};
@@ -1576,6 +1577,7 @@ export type FieldFilter = {
isSystem?: InputMaybe<BooleanFieldComparison>;
isUIReadOnly?: InputMaybe<BooleanFieldComparison>;
or?: InputMaybe<Array<FieldFilter>>;
universalIdentifier?: InputMaybe<UuidFilterComparison>;
};
/** Type of the field */
@@ -3351,6 +3353,7 @@ export type Object = {
nameSingular: Scalars['String'];
shortcut?: Maybe<Scalars['String']>;
standardOverrides?: Maybe<ObjectStandardOverrides>;
universalIdentifier: Scalars['UUID'];
updatedAt: Scalars['DateTime'];
};
@@ -3400,6 +3403,7 @@ export type ObjectFilter = {
isSystem?: InputMaybe<BooleanFieldComparison>;
isUIReadOnly?: InputMaybe<BooleanFieldComparison>;
or?: InputMaybe<Array<ObjectFilter>>;
universalIdentifier?: InputMaybe<UuidFilterComparison>;
};
export type ObjectIndexMetadatasConnection = {
@@ -4597,6 +4601,7 @@ export type UpdateFieldInput = {
name?: InputMaybe<Scalars['String']>;
options?: InputMaybe<Scalars['JSON']>;
settings?: InputMaybe<Scalars['JSON']>;
universalIdentifier?: InputMaybe<Scalars['UUID']>;
};
export type UpdateFrontComponentInput = {
@@ -6,8 +6,10 @@ import {
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { MetadataEventEmitter } from 'src/engine/metadata-event-emitter/metadata-event-emitter';
import { ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import {
FlatEntityMapsException,
@@ -68,6 +70,122 @@ export class WorkspaceMigrationValidateBuildAndRunService {
this.isDebugEnabled = logLevels.includes('debug');
}
private computeAllInvolvedApplicationIds({
allFlatEntityOperationByMetadataName,
flatApplicationMaps,
applicationUniversalIdentifier,
allRelatedFlatEntityMaps,
}: Pick<
ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs,
'allFlatEntityOperationByMetadataName' | 'applicationUniversalIdentifier'
> & {
flatApplicationMaps: FlatApplicationCacheMaps;
allRelatedFlatEntityMaps: Partial<AllFlatEntityMaps>;
}): string[] {
const applicationIds = new Set<string>();
const applicationId =
flatApplicationMaps.idByUniversalIdentifier[
applicationUniversalIdentifier
];
const twentyStandardApplicationId =
flatApplicationMaps.idByUniversalIdentifier[
TWENTY_STANDARD_APPLICATION.universalIdentifier
];
if (!isDefined(twentyStandardApplicationId) || !isDefined(applicationId)) {
throw new FlatEntityMapsException(
'Application to build and its dependent application not found',
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
applicationIds.add(applicationId);
const isBuildingTwentyStandardApplication =
applicationUniversalIdentifier ===
TWENTY_STANDARD_APPLICATION.universalIdentifier;
if (!isBuildingTwentyStandardApplication) {
applicationIds.add(twentyStandardApplicationId);
}
for (const metadataName of Object.keys(
allFlatEntityOperationByMetadataName,
) as AllMetadataName[]) {
const flatEntityOperations =
allFlatEntityOperationByMetadataName[metadataName];
if (!isDefined(flatEntityOperations)) {
continue;
}
const { flatEntityToCreate, flatEntityToUpdate, flatEntityToDelete } =
flatEntityOperations;
const relations = ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName];
for (const flatEntity of [
...flatEntityToCreate,
...flatEntityToUpdate,
...flatEntityToDelete,
]) {
const entityApplicationId =
flatApplicationMaps.idByUniversalIdentifier[
flatEntity.applicationUniversalIdentifier
];
if (isDefined(entityApplicationId)) {
applicationIds.add(entityApplicationId);
}
for (const relation of Object.values(relations) as ({
foreignKey: string;
metadataName: AllMetadataName;
isNullable: boolean;
universalForeignKey: string;
} | null)[]) {
if (!isDefined(relation)) {
continue;
}
const { universalForeignKey, metadataName: targetMetadataName } =
relation;
const referencedUniversalIdentifier =
flatEntity[universalForeignKey as keyof typeof flatEntity];
if (!isDefined(referencedUniversalIdentifier)) {
continue;
}
const targetFlatEntityMaps =
allRelatedFlatEntityMaps[
getMetadataFlatEntityMapsKey(
targetMetadataName as AllMetadataName,
)
];
if (!isDefined(targetFlatEntityMaps)) {
continue;
}
const referencedEntity =
targetFlatEntityMaps.byUniversalIdentifier[
referencedUniversalIdentifier
];
if (isDefined(referencedEntity)) {
applicationIds.add(referencedEntity.applicationId);
}
}
}
}
return [...applicationIds];
}
private async computeAllRelatedFlatEntityMaps({
allFlatEntityOperationByMetadataName,
workspaceId,
@@ -105,26 +223,12 @@ export class WorkspaceMigrationValidateBuildAndRunService {
{},
);
const twentyStandardApplicationId =
flatApplicationMaps.idByUniversalIdentifier[
TWENTY_STANDARD_APPLICATION.universalIdentifier
];
const applicationId =
flatApplicationMaps.idByUniversalIdentifier[
applicationUniversalIdentifier
];
if (!isDefined(twentyStandardApplicationId) || !isDefined(applicationId)) {
throw new FlatEntityMapsException(
'Application to build and its dependent application not found',
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const isBuildingTwentyStandardApplication =
applicationUniversalIdentifier ===
TWENTY_STANDARD_APPLICATION.universalIdentifier;
const applicationIds = this.computeAllInvolvedApplicationIds({
allFlatEntityOperationByMetadataName,
flatApplicationMaps,
applicationUniversalIdentifier,
allRelatedFlatEntityMaps,
});
const dependencyAllFlatEntityMaps = allMetadataNameCacheToCompute.reduce(
(allFlatEntityMaps, metadataName) => {
@@ -137,9 +241,7 @@ export class WorkspaceMigrationValidateBuildAndRunService {
getSubFlatEntityMapsByApplicationIdsOrThrow<
MetadataFlatEntity<typeof metadataName>
>({
applicationIds: isBuildingTwentyStandardApplication
? [applicationId]
: [applicationId, twentyStandardApplicationId],
applicationIds,
flatEntityMaps:
allRelatedFlatEntityMaps[metadataFlatEntityMapsKey],
}),
@@ -57,7 +57,7 @@ exports[`CommandMenuItem creation should fail when creating with empty workflowV
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -72,7 +72,7 @@ exports[`CommandMenuItem creation should fail when creating with invalid workflo
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -36,7 +36,7 @@ exports[`CommandMenuItem deletion should fail when deleting with empty id 1`] =
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -51,7 +51,7 @@ exports[`CommandMenuItem deletion should fail when deleting with invalid id (not
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -22,7 +22,7 @@ exports[`CommandMenuItem update should fail when updating with empty id 1`] = `
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -72,7 +72,7 @@ exports[`CommandMenuItem update should fail when updating with invalid id (not a
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -10,7 +10,7 @@ exports[`NavigationMenuItem creation should fail when creating with empty target
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -25,7 +25,7 @@ exports[`NavigationMenuItem creation should fail when creating with empty target
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -40,7 +40,7 @@ exports[`NavigationMenuItem creation should fail when creating with invalid fold
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -55,7 +55,7 @@ exports[`NavigationMenuItem creation should fail when creating with invalid targ
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -70,7 +70,7 @@ exports[`NavigationMenuItem creation should fail when creating with invalid targ
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -85,7 +85,7 @@ exports[`NavigationMenuItem creation should fail when creating with invalid user
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -22,7 +22,7 @@ exports[`NavigationMenuItem deletion should fail when deleting with empty id 1`]
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -37,7 +37,7 @@ exports[`NavigationMenuItem deletion should fail when deleting with invalid id (
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -22,7 +22,7 @@ exports[`NavigationMenuItem update should fail when updating with empty id 1`] =
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -37,7 +37,7 @@ exports[`NavigationMenuItem update should fail when updating with invalid folder
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -52,7 +52,7 @@ exports[`NavigationMenuItem update should fail when updating with invalid id (no
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -67,7 +67,7 @@ exports[`NavigationMenuItem update should fail when updating with missing id 1`]
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -65,7 +65,7 @@ exports[`Object metadata update should fail when labelIdentifier is not a uuid 1
"userFriendlyMessage": "An error occurred.",
"value": "not-a-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
},
]
@@ -22,7 +22,7 @@ exports[`Row Level Permission Predicate upsert should fail when fieldMetadataId
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -49,7 +49,7 @@ exports[`Row Level Permission Predicate upsert should fail when objectMetadataId
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -64,7 +64,7 @@ exports[`Row Level Permission Predicate upsert should fail when objectMetadataId
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -91,7 +91,7 @@ exports[`Row Level Permission Predicate upsert should fail when roleId is not a
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;
@@ -46,7 +46,7 @@ exports[`View Filter Group creation should fail when viewId is not a valid UUID
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID",
"message": "Invalid UUID: 'not-a-valid-uuid'",
"name": "ValidationError",
}
`;