fixed migration

This commit is contained in:
Dmitri 2026-06-12 11:13:33 +02:00
parent 799eba7306
commit 040300f3d1

View File

@ -1,5 +1,9 @@
import { DataTypes, type QueryInterface } from 'sequelize';
import { ROLE_SCOPE_VALUES } from '@/shared/constants/roles';
import {
ROLE_SCOPE_VALUES,
ROLE_DEFINITIONS,
ROLE_SCOPES,
} from '@/shared/constants/roles';
/**
* Workstream 3 §3.1 foundation: add the authorization `scope` to `roles` (NOT
@ -7,8 +11,9 @@ import { ROLE_SCOPE_VALUES } from '@/shared/constants/roles';
* and a nullable `campusId` to `users` (campus scope for campus-bound roles;
* null for system/organization scopes, so legitimately optional).
*
* Pre-launch with no production data, the `scope` column is added NOT NULL
* against the (empty, freshly-migrated) `roles` table; seeders populate it.
* Handles existing data: adds column as nullable, backfills scope based on
* role name using ROLE_DEFINITIONS mapping, then makes it NOT NULL.
*
* `campusId` is a plain UUID (no DB-level FK), matching how
* `users.organizationId` is modeled associations use `constraints: false`.
*
@ -26,17 +31,54 @@ async function columnExists(
return (results as unknown[]).length > 0;
}
async function columnIsNullable(
queryInterface: QueryInterface,
table: string,
column: string,
): Promise<boolean> {
const [results] = await queryInterface.sequelize.query(`
SELECT is_nullable FROM information_schema.columns
WHERE table_name = '${table}' AND column_name = '${column}'
`);
const row = (results as { is_nullable: string }[])[0];
return row?.is_nullable === 'YES';
}
export default {
up: async (queryInterface: QueryInterface) => {
// Create enum type if not exists, then add column if not exists
// Create enum type if not exists
await queryInterface.sequelize.query(`
DO 'BEGIN
CREATE TYPE "public"."enum_roles_scope" AS ENUM(${ROLE_SCOPE_VALUES.map((v) => `''${v}''`).join(', ')});
EXCEPTION WHEN duplicate_object THEN null; END';
`);
if (!(await columnExists(queryInterface, 'roles', 'scope'))) {
const scopeExists = await columnExists(queryInterface, 'roles', 'scope');
if (!scopeExists) {
// Step 1: Add column as nullable first
await queryInterface.addColumn('roles', 'scope', {
type: DataTypes.ENUM(...ROLE_SCOPE_VALUES),
allowNull: true,
});
}
// Step 2: Backfill existing rows with correct scope based on role name
for (const def of ROLE_DEFINITIONS) {
await queryInterface.sequelize.query(
`UPDATE roles SET scope = :scope WHERE name = :name AND scope IS NULL`,
{ replacements: { scope: def.scope, name: def.name } },
);
}
// Fallback for any roles not in ROLE_DEFINITIONS (e.g., custom roles)
await queryInterface.sequelize.query(
`UPDATE roles SET scope = :scope WHERE scope IS NULL`,
{ replacements: { scope: ROLE_SCOPES.CAMPUS } },
);
// Step 3: Make column NOT NULL if still nullable
if (!scopeExists || (await columnIsNullable(queryInterface, 'roles', 'scope'))) {
await queryInterface.changeColumn('roles', 'scope', {
type: DataTypes.ENUM(...ROLE_SCOPE_VALUES),
allowNull: false,
});