From d6404434a386716509cf6bf6db8969e8136bddeb Mon Sep 17 00:00:00 2001 From: Bastian Masanek Date: Mon, 3 Nov 2025 10:54:58 +0100 Subject: [PATCH] Refactor database schema for roles and visibility management - Updated the roles table to use `code` as the primary key, enhancing readability and simplifying junction tables. - Modified `user_roles` and `product_role_visibility` tables to reference `role_code` instead of `role_id`, maintaining many-to-many relationships. - Added foreign key constraints and created new indexes to optimize queries. - Removed the previous migration file that was no longer needed, ensuring a cleaner migration history. - Updated related documentation to reflect the new schema changes and their benefits. --- .../migrations/0001_clammy_bulldozer.sql | 38 ++++---- .../0002_refactor_roles_primary_key.sql | 58 ------------ .../migrations/meta/0001_snapshot.json | 91 +++++++++++-------- server/database/migrations/meta/_journal.json | 7 -- tasks/00-PROGRESS.md | 28 +++++- 5 files changed, 99 insertions(+), 123 deletions(-) delete mode 100644 server/database/migrations/0002_refactor_roles_primary_key.sql diff --git a/server/database/migrations/0001_clammy_bulldozer.sql b/server/database/migrations/0001_clammy_bulldozer.sql index 6efbb14..cd2b217 100644 --- a/server/database/migrations/0001_clammy_bulldozer.sql +++ b/server/database/migrations/0001_clammy_bulldozer.sql @@ -1,29 +1,20 @@ CREATE TYPE "public"."role_code" AS ENUM('private', 'educator', 'company');--> statement-breakpoint CREATE TYPE "public"."role_request_status" AS ENUM('pending', 'approved', 'rejected');--> statement-breakpoint -CREATE TABLE "product_role_visibility" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "product_id" uuid NOT NULL, - "role_id" uuid NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint CREATE TABLE "roles" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "code" "role_code" NOT NULL, + "code" "role_code" PRIMARY KEY NOT NULL, "display_name" text NOT NULL, "description" text NOT NULL, "requires_approval" boolean DEFAULT false NOT NULL, "sort_order" integer DEFAULT 0 NOT NULL, "active" boolean DEFAULT true NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "roles_code_unique" UNIQUE("code") + "updated_at" timestamp DEFAULT now() NOT NULL ); --> statement-breakpoint CREATE TABLE "user_roles" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "user_id" uuid NOT NULL, - "role_id" uuid NOT NULL, + "role_code" "role_code" NOT NULL, "status" "role_request_status" DEFAULT 'pending' NOT NULL, "organization_name" text, "admin_notes" text, @@ -32,12 +23,21 @@ CREATE TABLE "user_roles" ( "updated_at" timestamp DEFAULT now() NOT NULL ); --> statement-breakpoint -ALTER TABLE "product_role_visibility" ADD CONSTRAINT "product_role_visibility_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "product_role_visibility" ADD CONSTRAINT "product_role_visibility_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE TABLE "product_role_visibility" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "product_id" uuid NOT NULL, + "role_code" "role_code" NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "product_role_visibility_product_id_role_id_unique" ON "product_role_visibility" USING btree ("product_id","role_id");--> statement-breakpoint -CREATE INDEX "product_role_visibility_product_id_idx" ON "product_role_visibility" USING btree ("product_id");--> statement-breakpoint -CREATE INDEX "user_roles_user_id_role_id_unique" ON "user_roles" USING btree ("user_id","role_id");--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_code_roles_code_fk" FOREIGN KEY ("role_code") REFERENCES "public"."roles"("code") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "product_role_visibility" ADD CONSTRAINT "product_role_visibility_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "product_role_visibility" ADD CONSTRAINT "product_role_visibility_role_code_roles_code_fk" FOREIGN KEY ("role_code") REFERENCES "public"."roles"("code") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "user_roles_user_id_role_code_unique" ON "user_roles" USING btree ("user_id","role_code");--> statement-breakpoint CREATE INDEX "user_roles_user_id_idx" ON "user_roles" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX "user_roles_status_idx" ON "user_roles" USING btree ("status"); \ No newline at end of file +CREATE INDEX "user_roles_role_code_idx" ON "user_roles" USING btree ("role_code");--> statement-breakpoint +CREATE INDEX "user_roles_status_idx" ON "user_roles" USING btree ("status");--> statement-breakpoint +CREATE INDEX "product_role_visibility_product_id_role_code_unique" ON "product_role_visibility" USING btree ("product_id","role_code");--> statement-breakpoint +CREATE INDEX "product_role_visibility_product_id_idx" ON "product_role_visibility" USING btree ("product_id");--> statement-breakpoint +CREATE INDEX "product_role_visibility_role_code_idx" ON "product_role_visibility" USING btree ("role_code"); \ No newline at end of file diff --git a/server/database/migrations/0002_refactor_roles_primary_key.sql b/server/database/migrations/0002_refactor_roles_primary_key.sql deleted file mode 100644 index ce8b9ed..0000000 --- a/server/database/migrations/0002_refactor_roles_primary_key.sql +++ /dev/null @@ -1,58 +0,0 @@ --- Migration: Refactor roles table to use code as primary key --- This migration drops and recreates the role-related tables with the new schema - --- Drop existing tables (CASCADE removes dependent foreign keys) -DROP TABLE IF EXISTS "product_role_visibility" CASCADE;--> statement-breakpoint -DROP TABLE IF EXISTS "user_roles" CASCADE;--> statement-breakpoint -DROP TABLE IF EXISTS "roles" CASCADE;--> statement-breakpoint - --- Recreate roles table with code as primary key -CREATE TABLE "roles" ( - "code" "role_code" PRIMARY KEY NOT NULL, - "display_name" text NOT NULL, - "description" text NOT NULL, - "requires_approval" boolean DEFAULT false NOT NULL, - "sort_order" integer DEFAULT 0 NOT NULL, - "active" boolean DEFAULT true NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint - --- Recreate user_roles table with roleCode instead of roleId -CREATE TABLE "user_roles" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" uuid NOT NULL, - "role_code" "role_code" NOT NULL, - "status" "role_request_status" DEFAULT 'pending' NOT NULL, - "organization_name" text, - "admin_notes" text, - "status_history" jsonb DEFAULT '[]' NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint - --- Recreate product_role_visibility table with roleCode instead of roleId -CREATE TABLE "product_role_visibility" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "product_id" uuid NOT NULL, - "role_code" "role_code" NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint - --- Add foreign key constraints -ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_code_roles_code_fk" FOREIGN KEY ("role_code") REFERENCES "public"."roles"("code") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "product_role_visibility" ADD CONSTRAINT "product_role_visibility_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "product_role_visibility" ADD CONSTRAINT "product_role_visibility_role_code_roles_code_fk" FOREIGN KEY ("role_code") REFERENCES "public"."roles"("code") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint - --- Create indexes -CREATE INDEX "user_roles_user_id_role_code_unique" ON "user_roles" USING btree ("user_id","role_code");--> statement-breakpoint -CREATE INDEX "user_roles_user_id_idx" ON "user_roles" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX "user_roles_role_code_idx" ON "user_roles" USING btree ("role_code");--> statement-breakpoint -CREATE INDEX "user_roles_status_idx" ON "user_roles" USING btree ("status");--> statement-breakpoint -CREATE INDEX "product_role_visibility_product_id_role_code_unique" ON "product_role_visibility" USING btree ("product_id","role_code");--> statement-breakpoint -CREATE INDEX "product_role_visibility_product_id_idx" ON "product_role_visibility" USING btree ("product_id");--> statement-breakpoint -CREATE INDEX "product_role_visibility_role_code_idx" ON "product_role_visibility" USING btree ("role_code"); diff --git a/server/database/migrations/meta/0001_snapshot.json b/server/database/migrations/meta/0001_snapshot.json index 67d8883..747eb8a 100644 --- a/server/database/migrations/meta/0001_snapshot.json +++ b/server/database/migrations/meta/0001_snapshot.json @@ -385,9 +385,10 @@ "primaryKey": false, "notNull": true }, - "role_id": { - "name": "role_id", - "type": "uuid", + "role_code": { + "name": "role_code", + "type": "role_code", + "typeSchema": "public", "primaryKey": false, "notNull": true }, @@ -400,8 +401,8 @@ } }, "indexes": { - "product_role_visibility_product_id_role_id_unique": { - "name": "product_role_visibility_product_id_role_id_unique", + "product_role_visibility_product_id_role_code_unique": { + "name": "product_role_visibility_product_id_role_code_unique", "columns": [ { "expression": "product_id", @@ -410,7 +411,7 @@ "nulls": "last" }, { - "expression": "role_id", + "expression": "role_code", "isExpression": false, "asc": true, "nulls": "last" @@ -435,6 +436,21 @@ "concurrently": false, "method": "btree", "with": {} + }, + "product_role_visibility_role_code_idx": { + "name": "product_role_visibility_role_code_idx", + "columns": [ + { + "expression": "role_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": { @@ -451,15 +467,15 @@ "onDelete": "cascade", "onUpdate": "no action" }, - "product_role_visibility_role_id_roles_id_fk": { - "name": "product_role_visibility_role_id_roles_id_fk", + "product_role_visibility_role_code_roles_code_fk": { + "name": "product_role_visibility_role_code_roles_code_fk", "tableFrom": "product_role_visibility", "tableTo": "roles", "columnsFrom": [ - "role_id" + "role_code" ], "columnsTo": [ - "id" + "code" ], "onDelete": "cascade", "onUpdate": "no action" @@ -607,18 +623,11 @@ "name": "roles", "schema": "", "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, "code": { "name": "code", "type": "role_code", "typeSchema": "public", - "primaryKey": false, + "primaryKey": true, "notNull": true }, "display_name": { @@ -672,15 +681,7 @@ "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "roles_code_unique": { - "name": "roles_code_unique", - "nullsNotDistinct": false, - "columns": [ - "code" - ] - } - }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false @@ -702,9 +703,10 @@ "primaryKey": false, "notNull": true }, - "role_id": { - "name": "role_id", - "type": "uuid", + "role_code": { + "name": "role_code", + "type": "role_code", + "typeSchema": "public", "primaryKey": false, "notNull": true }, @@ -751,8 +753,8 @@ } }, "indexes": { - "user_roles_user_id_role_id_unique": { - "name": "user_roles_user_id_role_id_unique", + "user_roles_user_id_role_code_unique": { + "name": "user_roles_user_id_role_code_unique", "columns": [ { "expression": "user_id", @@ -761,7 +763,7 @@ "nulls": "last" }, { - "expression": "role_id", + "expression": "role_code", "isExpression": false, "asc": true, "nulls": "last" @@ -787,6 +789,21 @@ "method": "btree", "with": {} }, + "user_roles_role_code_idx": { + "name": "user_roles_role_code_idx", + "columns": [ + { + "expression": "role_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, "user_roles_status_idx": { "name": "user_roles_status_idx", "columns": [ @@ -817,15 +834,15 @@ "onDelete": "cascade", "onUpdate": "no action" }, - "user_roles_role_id_roles_id_fk": { - "name": "user_roles_role_id_roles_id_fk", + "user_roles_role_code_roles_code_fk": { + "name": "user_roles_role_code_roles_code_fk", "tableFrom": "user_roles", "tableTo": "roles", "columnsFrom": [ - "role_id" + "role_code" ], "columnsTo": [ - "id" + "code" ], "onDelete": "cascade", "onUpdate": "no action" diff --git a/server/database/migrations/meta/_journal.json b/server/database/migrations/meta/_journal.json index 911a69d..ebc6f2c 100644 --- a/server/database/migrations/meta/_journal.json +++ b/server/database/migrations/meta/_journal.json @@ -15,13 +15,6 @@ "when": 1762074397305, "tag": "0001_clammy_bulldozer", "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1762157410000, - "tag": "0002_refactor_roles_primary_key", - "breakpoints": true } ] } \ No newline at end of file diff --git a/tasks/00-PROGRESS.md b/tasks/00-PROGRESS.md index 7dbf16d..6c4e678 100644 --- a/tasks/00-PROGRESS.md +++ b/tasks/00-PROGRESS.md @@ -2,9 +2,9 @@ ## my.experimenta.science -**Last Updated:** 2025-11-01 +**Last Updated:** 2025-11-03 **Overall Progress:** 39/137 tasks (28.5%) -**Current Phase:** ✅ Phase 3 - Authentication (Validated & Completed) +**Current Phase:** ✅ Phase 3 - Authentication (Completed) | Database Schema Refinement Completed --- @@ -30,6 +30,29 @@ ## 🚀 Current Work +**Phase:** Database Schema Refinement ✅ **COMPLETED** (2025-11-03) + +**Recent Work: Roles Table Refactoring** + +Completed a major database schema refinement to improve code readability and performance: + +- ✅ **Refactored `roles` table**: Changed Primary Key from `id` (UUID) to `code` (enum: 'private' | 'educator' | 'company') +- ✅ **Updated junction tables**: `user_roles.roleCode` and `product_role_visibility.roleCode` now reference `roles.code` directly +- ✅ **Simplified code**: Removed all UUID lookup queries for roles - direct enum usage throughout +- ✅ **Maintained functionality**: Many-to-Many relationships fully preserved +- ✅ **Migration**: Successfully applied, database reseeded with 3 roles, 3 products, 7 role assignments +- ✅ **Auto-assignment**: Confirmed that new users automatically receive `'private'` role on first login +- ✅ **Product visibility**: Verified role-based product filtering works correctly +- ✅ **Documentation**: Updated CLAUDE.md and ARCHITECTURE.md to reflect new schema + +**Benefits:** +- Better readability: `roleCode: 'private'` instead of `roleId: 'uuid...'` +- Simpler code: No role lookups needed +- Better performance: Fewer joins in queries +- Type safety: Direct enum type usage + +--- + **Phase:** Phase 3 - Authentication ✅ **VALIDATED & COMPLETED** (2025-11-01) **Validation Summary:** @@ -431,6 +454,7 @@ Tasks: | 2025-10-30 | 15.3% | Phase 2 - MVP | ✅ Database completed: Drizzle ORM, all tables defined, migrations applied, Studio working, schema documented | | 2025-10-30 | 28.5% | Phase 3 - MVP | ✅ Authentication completed: Password Grant Flow, JWT validation, auth endpoints, UI components, middleware | | 2025-11-01 | 28.5% | Phase 3 - Validation | ✅ Authentication validated: Login tested with Playwright, DB user creation verified, docs updated | +| 2025-11-03 | 28.5% | DB Refinement | ✅ Roles table refactored: `code` as PK, simplified junction tables, maintained Many-to-Many functionality | ---