Code first roles sync (#13667)

This commit is contained in:
Abdul Rahman
2025-08-11 16:44:45 +05:30
committed by GitHub
parent 2a2a2faadf
commit 9b11735c57
26 changed files with 474 additions and 73 deletions
@@ -1,5 +1,6 @@
import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator';
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator';
import { WorkspaceFieldComparator } from './workspace-field.comparator';
import { WorkspaceObjectComparator } from './workspace-object.comparator';
@@ -9,4 +10,5 @@ export const workspaceSyncMetadataComparators = [
WorkspaceFieldRelationComparator,
WorkspaceObjectComparator,
WorkspaceIndexComparator,
WorkspaceRoleComparator,
];
@@ -0,0 +1,121 @@
import { Injectable } from '@nestjs/common';
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type RoleComparatorResult =
| {
action: ComparatorAction.CREATE;
fromFlatRole: null;
toFlatRole: FlatRole;
}
| {
action: ComparatorAction.UPDATE;
fromFlatRole: FlatRole;
toFlatRole: FlatRole;
}
| {
action: ComparatorAction.DELETE;
fromFlatRole: FlatRole;
toFlatRole: null;
};
type WorkspaceRoleComparatorArgs = FromTo<FlatRole[], 'FlatRoles'>;
const rolePropertiesToIgnore = [
'id',
'createdAt',
'updatedAt',
'workspaceId',
'roleTargets',
'permissionFlags',
'objectPermissions',
'fieldPermissions',
];
@Injectable()
export class WorkspaceRoleComparator {
compare({
fromFlatRoles,
toFlatRoles,
}: WorkspaceRoleComparatorArgs): RoleComparatorResult[] {
const results: RoleComparatorResult[] = [];
const keyFactory = (role: FlatRole | RoleEntity) =>
role.standardId || role.id;
const fromRoleMap = transformMetadataForComparison(fromFlatRoles, {
shouldIgnoreProperty: (property) =>
rolePropertiesToIgnore.includes(property),
keyFactory,
});
const toRoleMap = transformMetadataForComparison(toFlatRoles, {
shouldIgnoreProperty: (property) =>
rolePropertiesToIgnore.includes(property),
keyFactory,
});
const roleDifferences = diff(fromRoleMap, toRoleMap);
for (const difference of roleDifferences) {
const uniqueIdentifier = difference.path[0] as string;
switch (difference.type) {
case 'CREATE': {
const toRole = toFlatRoles.find(
(role) => keyFactory(role) === uniqueIdentifier,
);
if (toRole) {
results.push({
action: ComparatorAction.CREATE,
fromFlatRole: null,
toFlatRole: toRole,
});
}
break;
}
case 'CHANGE': {
const fromRole = fromFlatRoles.find(
(role) => keyFactory(role) === uniqueIdentifier,
);
const toRole = toFlatRoles.find(
(role) => keyFactory(role) === uniqueIdentifier,
);
if (fromRole && toRole) {
results.push({
action: ComparatorAction.UPDATE,
fromFlatRole: fromRole,
toFlatRole: toRole,
});
}
break;
}
case 'REMOVE': {
const fromRole = fromFlatRoles.find(
(role) => keyFactory(role) === uniqueIdentifier,
);
if (fromRole && difference.path.length === 1) {
results.push({
action: ComparatorAction.DELETE,
fromFlatRole: fromRole,
toFlatRole: null,
});
}
break;
}
}
}
return results;
}
}
@@ -1,4 +1,5 @@
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory';
import { StandardFieldRelationFactory } from './standard-field-relation.factory';
import { StandardFieldFactory } from './standard-field.factory';
@@ -9,4 +10,5 @@ export const workspaceSyncMetadataFactories = [
StandardObjectFactory,
StandardFieldRelationFactory,
StandardIndexFactory,
StandardRoleFactory,
];
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { fromStandardRoleDefinitionToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface';
@Injectable()
export class StandardRoleFactory {
create(
roleDefinitions: StandardRoleDefinition[],
context: WorkspaceSyncContext,
existingRoles: RoleEntity[],
): FlatRole[] {
const computedRoles: FlatRole[] = [];
for (const roleDefinition of roleDefinitions) {
const existingRole = existingRoles.find(
(role) => role.standardId === roleDefinition.standardId,
);
const flatRole = fromStandardRoleDefinitionToFlatRole(
roleDefinition,
context.workspaceId,
);
if (existingRole) {
computedRoles.push({
...flatRole,
id: existingRole.id,
uniqueIdentifier: roleDefinition.standardId || existingRole.id,
});
} else {
computedRoles.push({
...flatRole,
uniqueIdentifier: roleDefinition.standardId,
});
}
}
return computedRoles;
}
}
@@ -0,0 +1,89 @@
import { Injectable, Logger } from '@nestjs/common';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { IsNull, Not, type EntityManager } from 'typeorm';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator';
import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory';
import { standardRoleDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles';
@Injectable()
export class WorkspaceSyncRoleService {
private readonly logger = new Logger(WorkspaceSyncRoleService.name);
constructor(
private readonly standardRoleFactory: StandardRoleFactory,
private readonly workspaceRoleComparator: WorkspaceRoleComparator,
) {}
async synchronize(
context: WorkspaceSyncContext,
manager: EntityManager,
): Promise<void> {
this.logger.log('Syncing standard role metadata');
const roleRepository = manager.getRepository(RoleEntity);
const existingStandardRoleEntities = await roleRepository.find({
where: {
workspaceId: context.workspaceId,
standardId: Not(IsNull()),
},
});
const targetStandardRoles = this.standardRoleFactory.create(
standardRoleDefinitions,
context,
existingStandardRoleEntities,
);
const roleComparatorResults = this.workspaceRoleComparator.compare({
fromFlatRoles: existingStandardRoleEntities.map(fromRoleEntityToFlatRole),
toFlatRoles: targetStandardRoles,
});
for (const roleComparatorResult of roleComparatorResults) {
switch (roleComparatorResult.action) {
case ComparatorAction.CREATE: {
const roleToCreate = roleComparatorResult.toFlatRole;
const flatRoleData = removePropertiesFromRecord(roleToCreate, [
'uniqueIdentifier',
'id',
]);
await roleRepository.save({
...flatRoleData,
workspaceId: context.workspaceId,
});
break;
}
case ComparatorAction.UPDATE: {
const roleToUpdate = roleComparatorResult.toFlatRole;
const flatRoleData = removePropertiesFromRecord(roleToUpdate, [
'id',
'uniqueIdentifier',
'workspaceId',
]);
await roleRepository.update({ id: roleToUpdate.id }, flatRoleData);
break;
}
case ComparatorAction.DELETE: {
const roleToDelete = roleComparatorResult.fromFlatRole;
await roleRepository.delete({ id: roleToDelete.id });
break;
}
}
}
}
}
@@ -0,0 +1,6 @@
import { ADMIN_ROLE } from './roles/admin-role';
import { type StandardRoleDefinition } from './types/standard-role-definition.interface';
export const standardRoleDefinitions = [
ADMIN_ROLE,
] as const satisfies StandardRoleDefinition[];
@@ -0,0 +1,15 @@
import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface';
export const ADMIN_ROLE: StandardRoleDefinition = {
standardId: '20202020-0001-0001-0001-000000000001',
label: 'Admin',
description: 'Admin role',
icon: 'IconUserCog',
isEditable: false,
canUpdateAllSettings: true,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
};
@@ -0,0 +1,8 @@
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
export type StandardRoleDefinition = Omit<
FlatRole,
'id' | 'workspaceId' | 'uniqueIdentifier' | 'standardId'
> & {
standardId: string;
};
@@ -21,6 +21,7 @@ import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service';
import { WorkspaceSyncObjectMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service';
import { WorkspaceSyncRoleService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
@Module({
@@ -46,6 +47,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
WorkspaceSyncFieldMetadataRelationService,
WorkspaceSyncMetadataService,
WorkspaceSyncIndexMetadataService,
WorkspaceSyncRoleService,
SyncWorkspaceLoggerService,
SyncWorkspaceMetadataCommand,
],
@@ -16,6 +16,7 @@ import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service';
import { WorkspaceSyncObjectMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service';
import { WorkspaceSyncRoleService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service';
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
interface SynchronizeOptions {
@@ -36,6 +37,7 @@ export class WorkspaceSyncMetadataService {
private readonly workspaceSyncIndexMetadataService: WorkspaceSyncIndexMetadataService,
private readonly workspaceSyncObjectMetadataIdentifiersService: WorkspaceSyncObjectMetadataIdentifiersService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly workspaceSyncRoleService: WorkspaceSyncRoleService,
) {}
/**
@@ -159,6 +161,17 @@ export class WorkspaceSyncMetadataService {
`Workspace object metadata identifiers took ${workspaceObjectMetadataIdentifiersEnd - workspaceObjectMetadataIdentifiersStart}ms`,
);
// 6 - Sync standard roles
const workspaceRoleMigrationsStart = performance.now();
await this.workspaceSyncRoleService.synchronize(context, manager);
const workspaceRoleMigrationsEnd = performance.now();
this.logger.log(
`Workspace role migrations took ${workspaceRoleMigrationsEnd - workspaceRoleMigrationsStart}ms`,
);
const workspaceMigrationsSaveStart = performance.now();
// Save workspace migrations into the database