Skip to content

Commit 6aa0f80

Browse files
authored
Merge pull request #191 from refactor-group/185-migrate-users-to-new-roles
Migrate Users to use User Roles
2 parents d3851f1 + 7fa7910 commit 6aa0f80

File tree

5 files changed

+143
-3
lines changed

5 files changed

+143
-3
lines changed

migration/src/base_refactor_platform_rs.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-- SQL dump generated using DBML (dbml.dbdiagram.io)
22
-- Database: PostgreSQL
3-
-- Generated at: 2025-10-10T11:37:44.202Z
3+
-- Generated at: 2025-10-12T20:02:02.149Z
44

55
CREATE TYPE "refactor_platform"."status" AS ENUM (
66
'not_started',

migration/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ mod m20250705_200000_add_timezone_to_users;
99
mod m20250730_000000_add_coaching_sessions_sorting_indexes;
1010
mod m20250801_000000_add_sorting_indexes;
1111
mod m20251007_093603_add_user_roles_table_and_super_admin;
12-
12+
mod m20251008_000000_migrate_admin_users_to_super_admin_role;
13+
mod m20251009_000000_migrate_regular_users_to_user_roles;
1314
pub struct Migrator;
1415

1516
#[async_trait::async_trait]
@@ -25,6 +26,8 @@ impl MigratorTrait for Migrator {
2526
Box::new(m20250730_000000_add_coaching_sessions_sorting_indexes::Migration),
2627
Box::new(m20250801_000000_add_sorting_indexes::Migration),
2728
Box::new(m20251007_093603_add_user_roles_table_and_super_admin::Migration),
29+
Box::new(m20251008_000000_migrate_admin_users_to_super_admin_role::Migration),
30+
Box::new(m20251009_000000_migrate_regular_users_to_user_roles::Migration),
2831
]
2932
}
3033
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use sea_orm_migration::prelude::*;
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
impl MigrationTrait for Migration {
8+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9+
// For each user with role = 'admin', create a user_roles record with:
10+
// - role = 'super_admin'
11+
// - organization_id = NULL (global/super admin has no org association)
12+
// - user_id = the admin user's id
13+
//
14+
// We use ON CONFLICT DO NOTHING to handle cases where this migration
15+
// might be run multiple times (idempotent operation)
16+
//
17+
// Note: We explicitly cast 'super_admin' to the enum type to handle
18+
// PostgreSQL's transaction safety restrictions when using newly added enum values
19+
let insert_super_admin_roles_sql = r#"
20+
INSERT INTO refactor_platform.user_roles (user_id, role, organization_id)
21+
SELECT id, 'super_admin'::refactor_platform.role, NULL
22+
FROM refactor_platform.users
23+
WHERE role = 'admin'
24+
ON CONFLICT DO NOTHING
25+
"#;
26+
27+
manager
28+
.get_connection()
29+
.execute_unprepared(insert_super_admin_roles_sql)
30+
.await?;
31+
32+
Ok(())
33+
}
34+
35+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
36+
// Remove the super_admin user_roles records that were created for admin users
37+
// This only removes super_admin roles with NULL organization_id for users
38+
// who currently have role = 'admin'
39+
let delete_super_admin_roles_sql = r#"
40+
DELETE FROM refactor_platform.user_roles
41+
WHERE role = 'super_admin'::refactor_platform.role
42+
AND organization_id IS NULL
43+
AND user_id IN (
44+
SELECT id
45+
FROM refactor_platform.users
46+
WHERE role = 'admin'
47+
)
48+
"#;
49+
50+
manager
51+
.get_connection()
52+
.execute_unprepared(delete_super_admin_roles_sql)
53+
.await?;
54+
55+
Ok(())
56+
}
57+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use sea_orm_migration::prelude::*;
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
impl MigrationTrait for Migration {
8+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9+
// For each user with role = 'user', find their associated organizations
10+
// and create a user_roles record for each organization with:
11+
// - role = 'user'
12+
// - organization_id = the organization's id from organizations_users
13+
// - user_id = the user's id
14+
//
15+
// We use ON CONFLICT DO NOTHING to handle cases where this migration
16+
// might be run multiple times (idempotent operation)
17+
//
18+
// Note: We explicitly cast 'user' to the enum type to handle
19+
// PostgreSQL's transaction safety restrictions when using enum values
20+
let insert_user_roles_sql = r#"
21+
INSERT INTO refactor_platform.user_roles (user_id, role, organization_id)
22+
SELECT
23+
u.id,
24+
'user'::refactor_platform.role,
25+
ou.organization_id
26+
FROM refactor_platform.users u
27+
INNER JOIN refactor_platform.organizations_users ou ON u.id = ou.user_id
28+
WHERE u.role = 'user'
29+
ON CONFLICT DO NOTHING
30+
"#;
31+
32+
manager
33+
.get_connection()
34+
.execute_unprepared(insert_user_roles_sql)
35+
.await?;
36+
37+
Ok(())
38+
}
39+
40+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
41+
// Remove the user role user_roles records that were created for regular users
42+
// This only removes 'user' roles for users who currently have role = 'user'
43+
// and only for organizations they're actually associated with
44+
let delete_user_roles_sql = r#"
45+
DELETE FROM refactor_platform.user_roles
46+
WHERE role = 'user'::refactor_platform.role
47+
AND user_id IN (
48+
SELECT id
49+
FROM refactor_platform.users
50+
WHERE role = 'user'
51+
)
52+
AND organization_id IN (
53+
SELECT organization_id
54+
FROM refactor_platform.organizations_users
55+
WHERE user_id = refactor_platform.user_roles.user_id
56+
)
57+
"#;
58+
59+
manager
60+
.get_connection()
61+
.execute_unprepared(delete_user_roles_sql)
62+
.await?;
63+
64+
Ok(())
65+
}
66+
}

scripts/rebuild_db.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ echo "Modifying the generated SQL file..."
7070
sed -i '' '/CREATE SCHEMA/d' migration/src/base_refactor_platform_rs.sql
7171

7272
echo "Running the migrations..."
73-
DATABASE_URL=postgres://$DB_USER:password@localhost:5432/$DB_NAME sea-orm-cli migrate up -s $SCHEMA_NAME || { echo "Failed to run migrations"; exit 1; }
73+
# PostgreSQL Enum Transaction Restriction Workaround:
74+
# When ALTER TYPE ... ADD VALUE adds a new enum value (e.g., 'super_admin'), PostgreSQL
75+
# requires that value to be committed before it can be used in subsequent operations.
76+
# Even though SeaORM commits each migration separately, running all migrations in rapid
77+
# succession on a fresh database can cause "unsafe use of new value" errors.
78+
# Solution: Split migration execution into two batches to ensure the enum value is fully
79+
# committed before any migration attempts to use it.
80+
81+
# Step 1: Apply migrations up to and including the user_roles table creation (9 migrations)
82+
echo "Step 1: Applying migrations up to user_roles table creation..."
83+
DATABASE_URL=postgres://$DB_USER:password@localhost:5432/$DB_NAME sea-orm-cli migrate up -n 9 -s $SCHEMA_NAME || { echo "Failed to run initial migrations"; exit 1; }
84+
85+
# Step 2: Apply the remaining migration that uses super_admin enum value
86+
echo "Step 2: Applying remaining migrations that use the super_admin enum..."
87+
DATABASE_URL=postgres://$DB_USER:password@localhost:5432/$DB_NAME sea-orm-cli migrate up -s $SCHEMA_NAME || { echo "Failed to run remaining migrations"; exit 1; }
7488

7589
echo "Database setup and migrations completed successfully"

0 commit comments

Comments
 (0)