From 2370326d6280c21e6ea0c5788baf610b1d44b52b Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Tue, 10 Mar 2026 09:14:54 +0000 Subject: [PATCH] optimize Cedar migration: replace N+1 queries with single query Fetch all groups needing migration in one query instead of iterating over every connection and querying groups per connection. Co-Authored-By: Claude Opus 4.6 --- .../scripts/migrate-permissions-to-cedar.ts | 86 +++++++++---------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts b/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts index f9cf0044b..e6a42df15 100644 --- a/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts +++ b/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts @@ -1,65 +1,59 @@ import { DataSource } from 'typeorm'; import { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; -import { ConnectionEntity } from '../../connection/connection.entity.js'; import { GroupEntity } from '../../group/group.entity.js'; -import { PermissionEntity } from '../../permission/permission.entity.js'; import { IComplexPermission, ITablePermissionData } from '../../permission/permission.interface.js'; import { generateCedarPolicyForGroup } from '../cedar-policy-generator.js'; export async function migratePermissionsToCedar(dataSource: DataSource): Promise { - const connectionRepository = dataSource.getRepository(ConnectionEntity); const groupRepository = dataSource.getRepository(GroupEntity); - const permissionRepository = dataSource.getRepository(PermissionEntity); - - const connections = await connectionRepository.find(); let migratedCount = 0; - for (const connection of connections) { - const groups = await groupRepository - .createQueryBuilder('group') - .leftJoinAndSelect('group.connection', 'connection') - .leftJoinAndSelect('group.permissions', 'permission') - .where('connection.id = :connectionId', { connectionId: connection.id }) - .andWhere('(group.cedarPolicy IS NULL OR group.cedarPolicy = :empty)', { empty: '' }) - .getMany(); + const groups = await groupRepository + .createQueryBuilder('group') + .leftJoinAndSelect('group.connection', 'connection') + .leftJoinAndSelect('group.permissions', 'permission') + .where('group.cedarPolicy IS NULL OR group.cedarPolicy = :empty', { empty: '' }) + .getMany(); - for (const group of groups) { - const permissions = group.permissions || []; + for (const group of groups) { + const connection = group.connection; + if (!connection) continue; - const connectionPermission = permissions.find((p) => p.type === PermissionTypeEnum.Connection); - const groupPermission = permissions.find((p) => p.type === PermissionTypeEnum.Group); - const tablePermissions = permissions.filter((p) => p.type === PermissionTypeEnum.Table); + const permissions = group.permissions || []; - const tableMap = new Map(); - for (const tp of tablePermissions) { - const existing = tableMap.get(tp.tableName) || { - tableName: tp.tableName, - accessLevel: { visibility: false, readonly: false, add: false, delete: false, edit: false }, - }; - const level = tp.accessLevel as keyof ITablePermissionData['accessLevel']; - if (level in existing.accessLevel) { - existing.accessLevel[level] = true; - } - tableMap.set(tp.tableName, existing); - } + const connectionPermission = permissions.find((p) => p.type === PermissionTypeEnum.Connection); + const groupPermission = permissions.find((p) => p.type === PermissionTypeEnum.Group); + const tablePermissions = permissions.filter((p) => p.type === PermissionTypeEnum.Table); - const complexPermission: IComplexPermission = { - connection: { - connectionId: connection.id, - accessLevel: (connectionPermission?.accessLevel as AccessLevelEnum) || AccessLevelEnum.none, - }, - group: { - groupId: group.id, - accessLevel: (groupPermission?.accessLevel as AccessLevelEnum) || AccessLevelEnum.none, - }, - tables: Array.from(tableMap.values()), + const tableMap = new Map(); + for (const tp of tablePermissions) { + const existing = tableMap.get(tp.tableName) || { + tableName: tp.tableName, + accessLevel: { visibility: false, readonly: false, add: false, delete: false, edit: false }, }; - - const cedarPolicy = generateCedarPolicyForGroup(group.id, connection.id, group.isMain, complexPermission); - group.cedarPolicy = cedarPolicy; - await groupRepository.save(group); - migratedCount++; + const level = tp.accessLevel as keyof ITablePermissionData['accessLevel']; + if (level in existing.accessLevel) { + existing.accessLevel[level] = true; + } + tableMap.set(tp.tableName, existing); } + + const complexPermission: IComplexPermission = { + connection: { + connectionId: connection.id, + accessLevel: (connectionPermission?.accessLevel as AccessLevelEnum) || AccessLevelEnum.none, + }, + group: { + groupId: group.id, + accessLevel: (groupPermission?.accessLevel as AccessLevelEnum) || AccessLevelEnum.none, + }, + tables: Array.from(tableMap.values()), + }; + + const cedarPolicy = generateCedarPolicyForGroup(group.id, connection.id, group.isMain, complexPermission); + group.cedarPolicy = cedarPolicy; + await groupRepository.save(group); + migratedCount++; } console.log(`Migrated Cedar policies for ${migratedCount} groups (skipped groups with existing policies)`);