diff --git a/migrations/20260330120000_add_rbac_tables.ts b/migrations/20260330120000_add_rbac_tables.ts index 6e00e9c9..6f6ff935 100644 --- a/migrations/20260330120000_add_rbac_tables.ts +++ b/migrations/20260330120000_add_rbac_tables.ts @@ -45,6 +45,7 @@ export async function up(knex: Knex): Promise { table.timestamp("updated_at").defaultTo(knex.fn.now()); table.primary(["roles_id", "users_id"]); + table.index("users_id", "idx_users_roles_users_id"); }); } } diff --git a/migrations/20260331120000_remove_role_from_users.ts b/migrations/20260331120000_remove_role_from_users.ts index 95ab6d42..0f174f13 100644 --- a/migrations/20260331120000_remove_role_from_users.ts +++ b/migrations/20260331120000_remove_role_from_users.ts @@ -34,9 +34,17 @@ export async function up(knex: Knex): Promise { } } - // 2. Migrate users.role → users_roles + // 2. Read users.role into memory BEFORE dropping the column. + // On SQLite, dropColumn recreates the table (create → copy → drop → rename), + // which can discard DML inserts to tables with FKs pointing at users. const users: Array<{ id: number; role: string }> = await knex("users").select("id", "role"); + // 3. Drop the column first. + await knex.schema.alterTable("users", (table) => { + table.dropColumn("role"); + }); + + // 4. Now populate users_roles from the in-memory snapshot. for (const user of users) { const newRoleId = ROLE_MAP[user.role] ?? "member"; @@ -51,11 +59,6 @@ export async function up(knex: Knex): Promise { }); } } - - // 3. Now safe to drop the column - await knex.schema.alterTable("users", (table) => { - table.dropColumn("role"); - }); } // Reverse map: pick the highest-precedence role when backfilling. diff --git a/seeds/roles.ts b/seeds/roles.ts index 0d50ac64..21ccf193 100644 --- a/seeds/roles.ts +++ b/seeds/roles.ts @@ -45,14 +45,21 @@ export async function seed(knex: Knex): Promise { } // 2. Seed roles_permissions for readonly roles + // Only insert permissions that actually exist in the permissions table + // to avoid FK constraint errors if permissions seed hasn't run yet. + const existingPermRows: Array<{ id: string }> = await knex("permissions").select("id"); + const existingPermIds = new Set(existingPermRows.map((p) => p.id)); + for (const [roleId, permissionIds] of Object.entries(rolePermissions)) { + const validPermissionIds = permissionIds.filter((id) => existingPermIds.has(id)); + const existingPerms: Array<{ permissions_id: string }> = await knex("roles_permissions") .where("roles_id", roleId) .select("permissions_id"); const existingSet = new Set(existingPerms.map((e) => e.permissions_id)); // Insert missing permissions - for (const permId of permissionIds) { + for (const permId of validPermissionIds) { if (!existingSet.has(permId)) { await knex("roles_permissions").insert({ roles_id: roleId, @@ -65,7 +72,7 @@ export async function seed(knex: Knex): Promise { } // Remove permissions no longer assigned to this role - const desiredSet = new Set(permissionIds); + const desiredSet = new Set(validPermissionIds); const toRemove = existingPerms.filter((e) => !desiredSet.has(e.permissions_id)).map((e) => e.permissions_id); if (toRemove.length > 0) { await knex("roles_permissions").where("roles_id", roleId).whereIn("permissions_id", toRemove).del();