ToolJet/server/migrations/1766500000000-AddFolderPermissionSystem.ts
vjaris42 0431098e2c
Feat: Folder permission system (#15028)
* feat: Folder permission system

* fix(group-permissions): resolve custom group validation, folder edit check, and UI inconsistencie

* edit folder container && no folder in custom resource

* fix the ui for custom in empty state

* fix: coercion logic for folder permissions

* feat: enhance folder permissions handling in app components

* feat: add folder granular permissions handling in user apps permissions

* feat: implement granular folder permissions in ability guard and service

* feat: improve error handling for folder permissions with specific messages

* feat: enhance EnvironmentSelect component to handle disabled state and improve display logic

* chore: bump ee submodules

* feat: Update permission prop to isEditable in BaseManageGranularAccess component

* chore: bump ee server submodule

* fix: refine folder visibility logic based on user permissions

* feat: enhance MultiValue rendering and styling for "All environments" option

* feat: implement folder ownership checks and enhance app permissions handling

* feat: enhance folder permissions handling for app ownership and actions

* chore: clarify folder creation and deletion permissions in workspace context

* fix: update folder permission labels

* fixed folder permission cases

* fixed css class issue

* minor fix

* feat: streamline folder permissions handling by removing redundant checks and simplifying access logic

* refactor: made error message consistent

* fix: add missing permission message for folder deletion action

* refactor: consolidate forbidden messages for folder actions and maintain consistency

* feat: streamline permission handling and improve app visibility logic

* fix: remove default access denial message in AbilityGuard

* fixed all user page functionality falky case

* Fixed profile flaky case

* fixed granular access flaky case

* chore: revert package-lock.json

* chore: revert frontend/package-lock.json to main

* refactor: revert folder operations

* fix: remove unused AppEnvironmentsModule and AppsUtilService exports

---------

Co-authored-by: gsmithun4 <gsmithun4@gmail.com>
Co-authored-by: Pratush <pratush@Pratushs-MBP.lan>
Co-authored-by: Shantanu Mane <maneshantanu.20@gmail.com>
Co-authored-by: Yukti Goyal <yuktigoyal02@gmail.com>
2026-03-19 22:00:30 +05:30

165 lines
5.9 KiB
TypeScript

import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey, TableIndex } from 'typeorm';
export class AddFolderPermissionSystem1766500000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Step 1: Add 'folder' to resource_type enum
// NOTE: New enum values cannot be used in the same transaction in PostgreSQL
// Data seeding is done in a separate migration (1766500000001-SeedFolderPermissions.ts)
await queryRunner.query(`
ALTER TYPE "resource_type" ADD VALUE IF NOT EXISTS 'folder';
`);
// Step 2: Add folder_create and folder_delete columns to permission_groups if they don't exist
const hasfolderCreate = await queryRunner.hasColumn('permission_groups', 'folder_create');
if (!hasfolderCreate) {
await queryRunner.addColumn(
'permission_groups',
new TableColumn({
name: 'folder_create',
type: 'boolean',
default: false,
isNullable: false,
})
);
}
const hasFolderDelete = await queryRunner.hasColumn('permission_groups', 'folder_delete');
if (!hasFolderDelete) {
await queryRunner.addColumn(
'permission_groups',
new TableColumn({
name: 'folder_delete',
type: 'boolean',
default: false,
isNullable: false,
})
);
}
// Step 2b: Migrate existing folder_crud values to folder_create and folder_delete
// This preserves permissions for existing users
const hasFolderCrud = await queryRunner.hasColumn('permission_groups', 'folder_crud');
if (hasFolderCrud) {
await queryRunner.query(`
UPDATE permission_groups
SET folder_create = folder_crud, folder_delete = folder_crud
WHERE folder_crud = true;
`);
// Drop the old folder_crud column after migration
await queryRunner.dropColumn('permission_groups', 'folder_crud');
}
// Step 3: Add created_by column to folders table
await queryRunner.addColumn(
'folders',
new TableColumn({
name: 'created_by',
type: 'uuid',
isNullable: true,
})
);
// Add foreign key for created_by -> users.id
await queryRunner.createForeignKey(
'folders',
new TableForeignKey({
name: 'fk_folders_created_by',
columnNames: ['created_by'],
referencedTableName: 'users',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
})
);
// Step 4: Create folders_group_permissions table
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS folders_group_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
granular_permission_id UUID UNIQUE NOT NULL,
can_edit_folder BOOLEAN DEFAULT false,
can_edit_apps BOOLEAN DEFAULT false,
can_view_apps BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_folders_granular_permission_id
FOREIGN KEY (granular_permission_id)
REFERENCES granular_permissions(id)
ON DELETE CASCADE
);
`);
// Create index on granular_permission_id
await queryRunner.createIndex(
'folders_group_permissions',
new TableIndex({
name: 'idx_folders_group_permissions_granular_permission_id',
columnNames: ['granular_permission_id'],
})
);
// Step 5: Create group_folders table (junction table)
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS group_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
folder_id UUID,
folders_group_permissions_id UUID,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_group_folders_folder_id
FOREIGN KEY (folder_id)
REFERENCES folders(id)
ON DELETE CASCADE,
CONSTRAINT fk_group_folders_permissions_id
FOREIGN KEY (folders_group_permissions_id)
REFERENCES folders_group_permissions(id)
ON DELETE CASCADE,
CONSTRAINT unique_folder_and_permission
UNIQUE (folder_id, folders_group_permissions_id)
);
`);
// Create indexes for group_folders
await queryRunner.createIndex(
'group_folders',
new TableIndex({
name: 'idx_group_folders_folder_id',
columnNames: ['folder_id'],
})
);
await queryRunner.createIndex(
'group_folders',
new TableIndex({
name: 'idx_group_folders_permissions_id',
columnNames: ['folders_group_permissions_id'],
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop group_folders indexes and table
await queryRunner.dropIndex('group_folders', 'idx_group_folders_permissions_id');
await queryRunner.dropIndex('group_folders', 'idx_group_folders_folder_id');
await queryRunner.query(`DROP TABLE IF EXISTS group_folders`);
// Drop folders_group_permissions index and table
await queryRunner.dropIndex('folders_group_permissions', 'idx_folders_group_permissions_granular_permission_id');
await queryRunner.query(`DROP TABLE IF EXISTS folders_group_permissions`);
// Drop created_by foreign key and column from folders
await queryRunner.dropForeignKey('folders', 'fk_folders_created_by');
await queryRunner.dropColumn('folders', 'created_by');
// Drop folder_create and folder_delete columns from permission_groups
const hasFolderCreate = await queryRunner.hasColumn('permission_groups', 'folder_create');
if (hasFolderCreate) {
await queryRunner.dropColumn('permission_groups', 'folder_create');
}
const hasFolderDelete = await queryRunner.hasColumn('permission_groups', 'folder_delete');
if (hasFolderDelete) {
await queryRunner.dropColumn('permission_groups', 'folder_delete');
}
}
}