Fix rolesPermissions cache query cartesian product (62k → 162 rows) (#19511)
## Summary - **Split the `rolesPermissions` cache query into parallel queries** to eliminate a 5-way LEFT JOIN cartesian product. A workspace with a role having 5 objectPermissions × 4 permissionFlags × 124 fieldPermissions × 5 RLP predicates × 5 RLP groups was returning 62,000 rows from just ~162 distinct records, causing 10s+ query timeouts on view updates. - **Added missing `roleId` index on `permissionFlag` table** — the only permission table without one, which was causing sequential scans on the JOIN.
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('1.22.0', 1775749486425)
|
||||
export class AutoGeneratedFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE INDEX "IDX_PERMISSION_FLAG_ROLE_ID" ON "core"."permissionFlag" ("roleId") ',
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "core"."IDX_PERMISSION_FLAG_ROLE_ID"');
|
||||
}
|
||||
}
|
||||
+2
@@ -3,9 +3,11 @@
|
||||
import { AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775129420309-add-view-field-group-id-index-on-view-field';
|
||||
import { MigrateMessagingCalendarToCoreFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775165049548-migrate-messaging-calendar-to-core';
|
||||
import { AddEmailThreadWidgetTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-instance-command-fast-1775200000000-add-email-thread-widget-type';
|
||||
import { AutoGeneratedFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-22/1-22-instance-command-fast-1775749486425-auto-generated';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
MigrateMessagingCalendarToCoreFastInstanceCommand,
|
||||
AddEmailThreadWidgetTypeFastInstanceCommand,
|
||||
AutoGeneratedFastInstanceCommand,
|
||||
];
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
@@ -16,6 +17,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
|
||||
@Entity('permissionFlag')
|
||||
@Unique('IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId'])
|
||||
@Index('IDX_PERMISSION_FLAG_ROLE_ID', ['roleId'])
|
||||
export class PermissionFlagEntity extends SyncableEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
+98
-29
@@ -2,20 +2,26 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
type ObjectsPermissions,
|
||||
type ObjectsPermissionsByRoleId,
|
||||
type RestrictedFieldsPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
|
||||
|
||||
const WORKFLOW_STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS = [
|
||||
STANDARD_OBJECTS.workflow.universalIdentifier,
|
||||
@@ -31,6 +37,16 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(RoleEntity)
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(ObjectPermissionEntity)
|
||||
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
|
||||
@InjectRepository(PermissionFlagEntity)
|
||||
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
|
||||
@InjectRepository(FieldPermissionEntity)
|
||||
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
|
||||
@InjectRepository(RowLevelPermissionPredicateEntity)
|
||||
private readonly rowLevelPermissionPredicateRepository: Repository<RowLevelPermissionPredicateEntity>,
|
||||
@InjectRepository(RowLevelPermissionPredicateGroupEntity)
|
||||
private readonly rowLevelPermissionPredicateGroupRepository: Repository<RowLevelPermissionPredicateGroupEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -38,25 +54,74 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<ObjectsPermissionsByRoleId> {
|
||||
const roles = await this.roleRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
relations: [
|
||||
'objectPermissions',
|
||||
'permissionFlags',
|
||||
'fieldPermissions',
|
||||
'rowLevelPermissionPredicates',
|
||||
'rowLevelPermissionPredicateGroups',
|
||||
],
|
||||
});
|
||||
const [
|
||||
roles,
|
||||
objectPermissions,
|
||||
permissionFlags,
|
||||
fieldPermissions,
|
||||
rowLevelPermissionPredicates,
|
||||
rowLevelPermissionPredicateGroups,
|
||||
workspaceObjectMetadataCollection,
|
||||
] = await Promise.all([
|
||||
this.roleRepository.find({
|
||||
where: { workspaceId },
|
||||
}),
|
||||
this.objectPermissionRepository.find({
|
||||
where: { workspaceId },
|
||||
}),
|
||||
this.permissionFlagRepository.find({
|
||||
where: { workspaceId },
|
||||
}),
|
||||
this.fieldPermissionRepository.find({
|
||||
where: { workspaceId },
|
||||
}),
|
||||
this.rowLevelPermissionPredicateRepository.find({
|
||||
where: { workspaceId, deletedAt: IsNull() },
|
||||
}),
|
||||
this.rowLevelPermissionPredicateGroupRepository.find({
|
||||
where: { workspaceId, deletedAt: IsNull() },
|
||||
}),
|
||||
this.getWorkspaceObjectMetadataCollection(workspaceId),
|
||||
]);
|
||||
|
||||
const workspaceObjectMetadataCollection =
|
||||
await this.getWorkspaceObjectMetadataCollection(workspaceId);
|
||||
const objectPermissionsByRoleId =
|
||||
regroupEntitiesByRelatedEntityId<'objectPermission'>({
|
||||
entities: objectPermissions,
|
||||
foreignKey: 'roleId',
|
||||
});
|
||||
const permissionFlagsByRoleId =
|
||||
regroupEntitiesByRelatedEntityId<'permissionFlag'>({
|
||||
entities: permissionFlags,
|
||||
foreignKey: 'roleId',
|
||||
});
|
||||
const fieldPermissionsByRoleId =
|
||||
regroupEntitiesByRelatedEntityId<'fieldPermission'>({
|
||||
entities: fieldPermissions,
|
||||
foreignKey: 'roleId',
|
||||
});
|
||||
const rowLevelPermissionPredicatesByRoleId =
|
||||
regroupEntitiesByRelatedEntityId<'rowLevelPermissionPredicate'>({
|
||||
entities: rowLevelPermissionPredicates,
|
||||
foreignKey: 'roleId',
|
||||
});
|
||||
const rowLevelPermissionPredicateGroupsByRoleId =
|
||||
regroupEntitiesByRelatedEntityId<'rowLevelPermissionPredicateGroup'>({
|
||||
entities: rowLevelPermissionPredicateGroups,
|
||||
foreignKey: 'roleId',
|
||||
});
|
||||
|
||||
const permissionsByRoleId: ObjectsPermissionsByRoleId = {};
|
||||
|
||||
for (const role of roles) {
|
||||
const roleObjectPermissions =
|
||||
objectPermissionsByRoleId.get(role.id) ?? [];
|
||||
const rolePermissionFlags = permissionFlagsByRoleId.get(role.id) ?? [];
|
||||
const roleFieldPermissions = fieldPermissionsByRoleId.get(role.id) ?? [];
|
||||
const roleRowLevelPermissionPredicates =
|
||||
rowLevelPermissionPredicatesByRoleId.get(role.id) ?? [];
|
||||
const roleRowLevelPermissionPredicateGroups =
|
||||
rowLevelPermissionPredicateGroupsByRoleId.get(role.id) ?? [];
|
||||
|
||||
const objectRecordsPermissions: ObjectsPermissions = {};
|
||||
|
||||
for (const objectMetadata of workspaceObjectMetadataCollection) {
|
||||
@@ -77,14 +142,17 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
universalIdentifier as (typeof WORKFLOW_STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS)[number],
|
||||
)
|
||||
) {
|
||||
const hasWorkflowsPermissions = this.hasWorkflowsPermissions(role);
|
||||
const hasWorkflowsPermissions = this.hasWorkflowsPermissions(
|
||||
role,
|
||||
rolePermissionFlags,
|
||||
);
|
||||
|
||||
canRead = hasWorkflowsPermissions;
|
||||
canUpdate = hasWorkflowsPermissions;
|
||||
canSoftDelete = hasWorkflowsPermissions;
|
||||
canDestroy = hasWorkflowsPermissions;
|
||||
} else {
|
||||
const objectRecordPermissionsOverride = role.objectPermissions.find(
|
||||
const objectRecordPermissionsOverride = roleObjectPermissions.find(
|
||||
(objectPermission) =>
|
||||
objectPermission.objectMetadataId === objectMetadataId,
|
||||
);
|
||||
@@ -111,12 +179,12 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
canDestroy,
|
||||
);
|
||||
|
||||
const fieldPermissions = role.fieldPermissions.filter(
|
||||
const fieldPermissionsForObject = roleFieldPermissions.filter(
|
||||
(fieldPermission) =>
|
||||
fieldPermission.objectMetadataId === objectMetadataId,
|
||||
);
|
||||
|
||||
for (const fieldPermission of fieldPermissions) {
|
||||
for (const fieldPermission of fieldPermissionsForObject) {
|
||||
const isFieldLabelIdentifier =
|
||||
fieldPermission.fieldMetadataId ===
|
||||
objectMetadata.labelIdentifierFieldMetadataId;
|
||||
@@ -141,14 +209,12 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
canSoftDeleteObjectRecords: canSoftDelete,
|
||||
canDestroyObjectRecords: canDestroy,
|
||||
restrictedFields,
|
||||
rowLevelPermissionPredicates:
|
||||
role.rowLevelPermissionPredicates.filter(
|
||||
(rowLevelPermissionPredicate) =>
|
||||
rowLevelPermissionPredicate.objectMetadataId ===
|
||||
objectMetadataId,
|
||||
),
|
||||
rowLevelPermissionPredicates: roleRowLevelPermissionPredicates.filter(
|
||||
(rowLevelPermissionPredicate) =>
|
||||
rowLevelPermissionPredicate.objectMetadataId === objectMetadataId,
|
||||
),
|
||||
rowLevelPermissionPredicateGroups:
|
||||
role.rowLevelPermissionPredicateGroups.filter(
|
||||
roleRowLevelPermissionPredicateGroups.filter(
|
||||
(rowLevelPermissionPredicateGroup) =>
|
||||
rowLevelPermissionPredicateGroup.objectMetadataId ===
|
||||
objectMetadataId,
|
||||
@@ -180,10 +246,13 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
return workspaceObjectMetadata;
|
||||
}
|
||||
|
||||
private hasWorkflowsPermissions(role: RoleEntity): boolean {
|
||||
private hasWorkflowsPermissions(
|
||||
role: RoleEntity,
|
||||
permissionFlags: PermissionFlagEntity[],
|
||||
): boolean {
|
||||
const hasWorkflowsPermissionFromRole = role.canUpdateAllSettings;
|
||||
const hasWorkflowsPermissionsFromSettingPermissions = isDefined(
|
||||
role.permissionFlags.find(
|
||||
permissionFlags.find(
|
||||
(permissionFlag) =>
|
||||
permissionFlag.flag === PermissionFlagType.WORKFLOWS,
|
||||
),
|
||||
|
||||
+2
-5
@@ -17,7 +17,7 @@ export const regroupEntitiesByRelatedEntityId = <T extends AllMetadataName>({
|
||||
entities,
|
||||
foreignKey,
|
||||
}: RegroupEntitiesByRelatedEntityIdArgs<T>) => {
|
||||
const entitiesByRelatedEntityId = new Map<string, RegroupedEntity[]>();
|
||||
const entitiesByRelatedEntityId = new Map<string, MetadataEntity<T>[]>();
|
||||
|
||||
for (const entity of entities) {
|
||||
const currentRelatedEntityId = entity[
|
||||
@@ -32,10 +32,7 @@ export const regroupEntitiesByRelatedEntityId = <T extends AllMetadataName>({
|
||||
entitiesByRelatedEntityId.set(currentRelatedEntityId, []);
|
||||
}
|
||||
|
||||
entitiesByRelatedEntityId.get(currentRelatedEntityId)!.push({
|
||||
id: entity.id,
|
||||
universalIdentifier: entity.universalIdentifier,
|
||||
});
|
||||
entitiesByRelatedEntityId.get(currentRelatedEntityId)!.push(entity);
|
||||
}
|
||||
|
||||
return entitiesByRelatedEntityId;
|
||||
|
||||
Reference in New Issue
Block a user