fixed migration
This commit is contained in:
parent
799eba7306
commit
040300f3d1
@ -1,5 +1,9 @@
|
|||||||
import { DataTypes, type QueryInterface } from 'sequelize';
|
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
|
* 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;
|
* and a nullable `campusId` to `users` (campus scope for campus-bound roles;
|
||||||
* null for system/organization scopes, so legitimately optional).
|
* null for system/organization scopes, so legitimately optional).
|
||||||
*
|
*
|
||||||
* Pre-launch with no production data, the `scope` column is added NOT NULL
|
* Handles existing data: adds column as nullable, backfills scope based on
|
||||||
* against the (empty, freshly-migrated) `roles` table; seeders populate it.
|
* role name using ROLE_DEFINITIONS mapping, then makes it NOT NULL.
|
||||||
|
*
|
||||||
* `campusId` is a plain UUID (no DB-level FK), matching how
|
* `campusId` is a plain UUID (no DB-level FK), matching how
|
||||||
* `users.organizationId` is modeled — associations use `constraints: false`.
|
* `users.organizationId` is modeled — associations use `constraints: false`.
|
||||||
*
|
*
|
||||||
@ -26,17 +31,54 @@ async function columnExists(
|
|||||||
return (results as unknown[]).length > 0;
|
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 {
|
export default {
|
||||||
up: async (queryInterface: QueryInterface) => {
|
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(`
|
await queryInterface.sequelize.query(`
|
||||||
DO 'BEGIN
|
DO 'BEGIN
|
||||||
CREATE TYPE "public"."enum_roles_scope" AS ENUM(${ROLE_SCOPE_VALUES.map((v) => `''${v}''`).join(', ')});
|
CREATE TYPE "public"."enum_roles_scope" AS ENUM(${ROLE_SCOPE_VALUES.map((v) => `''${v}''`).join(', ')});
|
||||||
EXCEPTION WHEN duplicate_object THEN null; END';
|
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', {
|
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),
|
type: DataTypes.ENUM(...ROLE_SCOPE_VALUES),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user