Fix role synchronisation (#19469)

As title
solves
https://discord.com/channels/1130383047699738754/1491167098398052503
This commit is contained in:
martmull
2026-04-09 11:36:16 +02:00
committed by GitHub
parent 5edc034f8b
commit 5eaabe95e7
18 changed files with 282 additions and 170 deletions
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { type Manifest, type RoleManifest } from 'twenty-shared/application';
import { type Manifest } from 'twenty-shared/application';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
@@ -15,9 +15,6 @@ import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
@@ -34,9 +31,6 @@ export class ApplicationManifestMigrationService {
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
private readonly objectPermissionService: ObjectPermissionService,
private readonly fieldPermissionService: FieldPermissionService,
private readonly permissionFlagService: PermissionFlagService,
) {}
async syncMetadataFromManifest({
@@ -119,7 +113,7 @@ export class ApplicationManifestMigrationService {
`Metadata migration completed for application ${ownerFlatApplication.universalIdentifier}`,
);
await this.syncRolePermissionsAndDefaultRole({
await this.syncDefaultRoleAndSettingsCustomTab({
manifest,
workspaceId,
ownerFlatApplication,
@@ -131,13 +125,7 @@ export class ApplicationManifestMigrationService {
};
}
/**
* @deprecated should be remove once below issues are resolved:
* - [objectPermission](https://github.com/twentyhq/core-team-issues/issues/2223)
* - [fieldPermission](https://github.com/twentyhq/core-team-issues/issues/2224)
* - [permissionFlag](https://github.com/twentyhq/core-team-issues/issues/2225)
*/
private async syncRolePermissionsAndDefaultRole({
private async syncDefaultRoleAndSettingsCustomTab({
manifest,
workspaceId,
ownerFlatApplication,
@@ -148,13 +136,9 @@ export class ApplicationManifestMigrationService {
}) {
const {
flatRoleMaps: refreshedFlatRoleMaps,
flatObjectMetadataMaps: refreshedFlatObjectMetadataMaps,
flatFieldMetadataMaps: refreshedFlatFieldMetadataMaps,
flatFrontComponentMaps: refreshedFlatFrontComponentMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatRoleMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatFrontComponentMaps',
]);
@@ -173,14 +157,6 @@ export class ApplicationManifestMigrationService {
);
}
await this.syncApplicationRolePermissions({
role,
workspaceId,
roleId: flatRole.id,
refreshedFlatObjectMetadataMaps,
refreshedFlatFieldMetadataMaps,
});
if (
role.universalIdentifier ===
manifest.application.defaultRoleUniversalIdentifier
@@ -216,126 +192,4 @@ export class ApplicationManifestMigrationService {
...(isDefined(defaultRoleId) ? { defaultRoleId } : {}),
});
}
private async syncApplicationRolePermissions({
role,
workspaceId,
roleId,
refreshedFlatObjectMetadataMaps,
refreshedFlatFieldMetadataMaps,
}: {
role: RoleManifest;
workspaceId: string;
roleId: string;
refreshedFlatObjectMetadataMaps: Awaited<
ReturnType<WorkspaceCacheService['getOrRecompute']>
>['flatObjectMetadataMaps'];
refreshedFlatFieldMetadataMaps: Awaited<
ReturnType<WorkspaceCacheService['getOrRecompute']>
>['flatFieldMetadataMaps'];
}) {
if (
(role.objectPermissions ?? []).length > 0 ||
(role.fieldPermissions ?? []).length > 0
) {
const formattedObjectPermissions = role.objectPermissions
?.map((permission) => {
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
flatEntityMaps: refreshedFlatObjectMetadataMaps,
universalIdentifier: permission.objectUniversalIdentifier,
});
if (!isDefined(flatObjectMetadata)) {
throw new ApplicationException(
`Failed to find object with universalIdentifier ${permission.objectUniversalIdentifier}`,
ApplicationExceptionCode.OBJECT_NOT_FOUND,
);
}
return {
...permission,
objectMetadataId: flatObjectMetadata.id,
};
})
.filter(
(
permission,
): permission is typeof permission & { objectMetadataId: string } =>
isDefined(permission.objectMetadataId),
);
if (isDefined(formattedObjectPermissions)) {
await this.objectPermissionService.upsertObjectPermissions({
workspaceId,
input: {
roleId,
objectPermissions: formattedObjectPermissions,
},
});
}
const formattedFieldPermissions = role.fieldPermissions
?.map((permission) => {
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
flatEntityMaps: refreshedFlatObjectMetadataMaps,
universalIdentifier: permission.objectUniversalIdentifier,
});
if (!isDefined(flatObjectMetadata)) {
throw new ApplicationException(
`Failed to find object with universalIdentifier ${permission.objectUniversalIdentifier}`,
ApplicationExceptionCode.OBJECT_NOT_FOUND,
);
}
const flatFieldMetadata = findFlatEntityByUniversalIdentifier({
flatEntityMaps: refreshedFlatFieldMetadataMaps,
universalIdentifier: permission.fieldUniversalIdentifier,
});
if (!isDefined(flatFieldMetadata)) {
throw new ApplicationException(
`Failed to find field with universalIdentifier ${permission.fieldUniversalIdentifier}`,
ApplicationExceptionCode.FIELD_NOT_FOUND,
);
}
return {
...permission,
objectMetadataId: flatObjectMetadata.id,
fieldMetadataId: flatFieldMetadata.id,
};
})
.filter(
(
permission,
): permission is typeof permission & {
objectMetadataId: string;
fieldMetadataId: string;
} =>
isDefined(permission.objectMetadataId) &&
isDefined(permission.fieldMetadataId),
);
if (isDefined(formattedFieldPermissions)) {
await this.fieldPermissionService.upsertFieldPermissions({
workspaceId,
input: {
roleId,
fieldPermissions: formattedFieldPermissions,
},
});
}
}
if (isDefined(role.permissionFlags) && role.permissionFlags.length > 0) {
await this.permissionFlagService.upsertPermissionFlags({
workspaceId,
input: {
roleId,
permissionFlagKeys: role.permissionFlags,
},
});
}
}
}
@@ -7,8 +7,6 @@ import { ApplicationSyncService } from 'src/engine/core-modules/application/appl
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/application/application-variable/application-variable.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@@ -21,8 +19,6 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
ApplicationVariableEntityModule,
FeatureFlagModule,
FileStorageModule,
ObjectPermissionModule,
PermissionFlagModule,
PermissionsModule,
WorkspaceCacheModule,
WorkspaceMigrationModule,
@@ -0,0 +1,28 @@
import { type FieldPermissionManifest } from 'twenty-shared/application';
import { type UniversalFlatFieldPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-permission.type';
export const fromFieldPermissionManifestToUniversalFlatFieldPermission = ({
fieldPermissionManifest,
roleUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
fieldPermissionManifest: FieldPermissionManifest;
roleUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatFieldPermission => {
return {
universalIdentifier: fieldPermissionManifest.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
objectMetadataUniversalIdentifier:
fieldPermissionManifest.objectUniversalIdentifier,
fieldMetadataUniversalIdentifier:
fieldPermissionManifest.fieldUniversalIdentifier,
canReadFieldValue: fieldPermissionManifest.canReadFieldValue ?? null,
canUpdateFieldValue: fieldPermissionManifest.canUpdateFieldValue ?? null,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,29 @@
import { type ObjectPermissionManifest } from 'twenty-shared/application';
import { type UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type';
export const fromObjectPermissionManifestToUniversalFlatObjectPermission = ({
objectPermissionManifest,
roleUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
objectPermissionManifest: ObjectPermissionManifest;
roleUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatObjectPermission => {
return {
universalIdentifier: objectPermissionManifest.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
objectMetadataUniversalIdentifier:
objectPermissionManifest.objectUniversalIdentifier,
canReadObjectRecords: objectPermissionManifest.canReadObjectRecords,
canUpdateObjectRecords: objectPermissionManifest.canUpdateObjectRecords,
canSoftDeleteObjectRecords:
objectPermissionManifest.canSoftDeleteObjectRecords,
canDestroyObjectRecords: objectPermissionManifest.canDestroyObjectRecords,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,23 @@
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
import { PermissionFlagManifest } from 'twenty-shared/application';
export const fromPermissionFlagToUniversalFlatPermissionFlag = ({
permissionFlag,
roleUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
permissionFlag: PermissionFlagManifest;
roleUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatPermissionFlag => {
return {
universalIdentifier: permissionFlag.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
flag: permissionFlag.flag,
createdAt: now,
updatedAt: now,
};
};
@@ -4,13 +4,16 @@ import { isDefined } from 'twenty-shared/utils';
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util';
import { fromFieldPermissionManifestToUniversalFlatFieldPermission } from 'src/engine/core-modules/application/application-manifest/converters/from-field-permission-manifest-to-universal-flat-field-permission.util';
import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util';
import { fromLogicFunctionManifestToUniversalFlatLogicFunction } from 'src/engine/core-modules/application/application-manifest/converters/from-logic-function-manifest-to-universal-flat-logic-function.util';
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
import { fromObjectManifestToUniversalFlatObjectMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-object-manifest-to-universal-flat-object-metadata.util';
import { fromObjectPermissionManifestToUniversalFlatObjectPermission } from 'src/engine/core-modules/application/application-manifest/converters/from-object-permission-manifest-to-universal-flat-object-permission.util';
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromPermissionFlagToUniversalFlatPermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-permission-flag.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util';
@@ -144,6 +147,47 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
}),
universalFlatEntityMapsToMutate: allUniversalFlatEntityMaps.flatRoleMaps,
});
for (const objectPermissionManifest of roleManifest.objectPermissions ??
[]) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromObjectPermissionManifestToUniversalFlatObjectPermission({
objectPermissionManifest,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatObjectPermissionMaps,
});
}
for (const fieldPermissionManifest of roleManifest.fieldPermissions ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromFieldPermissionManifestToUniversalFlatFieldPermission({
fieldPermissionManifest,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatFieldPermissionMaps,
});
}
for (const permissionFlag of roleManifest.permissionFlags ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromPermissionFlagToUniversalFlatPermissionFlag({
permissionFlag,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPermissionFlagMaps,
});
}
}
for (const skillManifest of manifest.skills ?? []) {