feat(cohorts): rename Programs to Cohorts with data migration (#694)

* feat(cohorts): rename Programs to Cohorts with data migration

- Add cohort database tables and Drizzle migration alongside legacy program tables
- Introduce migrate-programs-to-cohorts script to copy existing program data safely
- Rename API routes (/cohort), services, queries, validation, and error codes
- Rename dashboard feature module, routes (/cohorts), navigation, and translations
- Update docs, emails, and internal compliance cron endpoints for cohort terminology
- Keep legacy program tables in place for rollback until explicitly removed

Co-authored-by: Rotimi Best <[email protected]>

* fix(db): restore migrate:answer-data script alongside cohort migration

Co-authored-by: Rotimi Best <[email protected]>

* fix(db): cast program enums when migrating data to cohort tables

Co-authored-by: Rotimi Best <[email protected]>

* fix(db): truncate cohort tables before program-to-cohort copy

Co-authored-by: Rotimi Best <[email protected]>

* fix(db): report accurate row counts in programs-to-cohorts migration

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-02 12:10:31 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent fe9f8c441f
commit ec61bfe7d9
115 changed files with 19703 additions and 6046 deletions
@@ -0,0 +1,114 @@
CREATE TYPE "public"."COHORT_GOAL_ASSIGNMENT_STATUS" AS ENUM('not_started', 'in_progress', 'completed', 'at_risk', 'overdue', 'waived');--> statement-breakpoint
CREATE TYPE "public"."COHORT_GOAL_DEADLINE_KIND" AS ENUM('absolute', 'relative_to_join', 'recurring', 'none');--> statement-breakpoint
CREATE TYPE "public"."COHORT_GOAL_STATUS" AS ENUM('active', 'archived');--> statement-breakpoint
CREATE TYPE "public"."COHORT_GOAL_TYPE" AS ENUM('complete_all', 'n_of_m', 'score', 'pass_rate', 'readiness');--> statement-breakpoint
CREATE TYPE "public"."COHORT_STATUS" AS ENUM('ACTIVE', 'INACTIVE', 'ARCHIVED');--> statement-breakpoint
CREATE TABLE "cohort" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now(),
"organization_id" uuid NOT NULL,
"name" varchar NOT NULL,
"description" text,
"cover_image" text,
"status" "COHORT_STATUS" DEFAULT 'ACTIVE' NOT NULL,
"created_by_profile_id" uuid
);
--> statement-breakpoint
CREATE TABLE "cohort_course" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cohort_id" uuid NOT NULL,
"course_id" uuid NOT NULL,
"added_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "cohort_course_cohort_id_course_id_unique" UNIQUE("cohort_id","course_id")
);
--> statement-breakpoint
CREATE TABLE "cohort_goal" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cohort_id" uuid NOT NULL,
"title" varchar(255) NOT NULL,
"description" text,
"type" "COHORT_GOAL_TYPE" NOT NULL,
"course_ids" jsonb DEFAULT '[]'::jsonb NOT NULL,
"required_count" integer,
"score_threshold" integer,
"team_pass_rate_threshold" integer,
"deadline_kind" "COHORT_GOAL_DEADLINE_KIND" DEFAULT 'none' NOT NULL,
"deadline_date" timestamp with time zone,
"relative_days" integer,
"recurring_months" integer,
"reminder_days_before" jsonb DEFAULT '[7,1]'::jsonb NOT NULL,
"status" "COHORT_GOAL_STATUS" DEFAULT 'active' NOT NULL,
"created_by_profile_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "cohort_goal_assignment" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"goal_id" uuid NOT NULL,
"cohort_member_id" uuid NOT NULL,
"due_date" timestamp with time zone,
"status" "COHORT_GOAL_ASSIGNMENT_STATUS" DEFAULT 'not_started' NOT NULL,
"completed_count" integer DEFAULT 0 NOT NULL,
"required_count" integer DEFAULT 0 NOT NULL,
"completed_at" timestamp with time zone,
"last_evaluated_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "cohort_goal_assignment_goal_id_cohort_member_id_unique" UNIQUE("goal_id","cohort_member_id")
);
--> statement-breakpoint
CREATE TABLE "cohort_member" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"cohort_id" uuid NOT NULL,
"profile_id" uuid,
"role_id" integer NOT NULL,
"email" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "cohort_member_cohort_id_profile_id_unique" UNIQUE("cohort_id","profile_id")
);
--> statement-breakpoint
CREATE TABLE "cohort_newsfeed" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"author_id" uuid,
"cohort_id" uuid,
"content" text,
"reaction" jsonb DEFAULT '{"clap":[],"smile":[],"thumbsup":[],"thumbsdown":[]}'::jsonb,
"is_pinned" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
CREATE TABLE "cohort_newsfeed_comment" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "cohort_newsfeed_comment_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"author_id" uuid,
"content" text,
"cohort_newsfeed_id" uuid
);
--> statement-breakpoint
ALTER TABLE "cohort" ADD CONSTRAINT "cohort_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort" ADD CONSTRAINT "cohort_created_by_profile_id_fkey" FOREIGN KEY ("created_by_profile_id") REFERENCES "public"."profile"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_course" ADD CONSTRAINT "cohort_course_cohort_id_fkey" FOREIGN KEY ("cohort_id") REFERENCES "public"."cohort"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_course" ADD CONSTRAINT "cohort_course_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "public"."course"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_goal" ADD CONSTRAINT "cohort_goal_cohort_id_fkey" FOREIGN KEY ("cohort_id") REFERENCES "public"."cohort"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_goal" ADD CONSTRAINT "cohort_goal_created_by_profile_id_fkey" FOREIGN KEY ("created_by_profile_id") REFERENCES "public"."profile"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_goal_assignment" ADD CONSTRAINT "cohort_goal_assignment_goal_id_fkey" FOREIGN KEY ("goal_id") REFERENCES "public"."cohort_goal"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_goal_assignment" ADD CONSTRAINT "cohort_goal_assignment_cohort_member_id_fkey" FOREIGN KEY ("cohort_member_id") REFERENCES "public"."cohort_member"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_member" ADD CONSTRAINT "cohort_member_cohort_id_fkey" FOREIGN KEY ("cohort_id") REFERENCES "public"."cohort"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_member" ADD CONSTRAINT "cohort_member_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "public"."profile"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_member" ADD CONSTRAINT "cohort_member_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "public"."role"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_newsfeed" ADD CONSTRAINT "cohort_newsfeed_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "public"."cohort_member"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_newsfeed" ADD CONSTRAINT "cohort_newsfeed_cohort_id_fkey" FOREIGN KEY ("cohort_id") REFERENCES "public"."cohort"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_newsfeed_comment" ADD CONSTRAINT "cohort_newsfeed_comment_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "public"."cohort_member"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "cohort_newsfeed_comment" ADD CONSTRAINT "cohort_newsfeed_comment_cohort_newsfeed_id_fkey" FOREIGN KEY ("cohort_newsfeed_id") REFERENCES "public"."cohort_newsfeed"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "idx_cohort_organization_id" ON "cohort" USING btree ("organization_id");--> statement-breakpoint
CREATE INDEX "idx_cohort_course_cohort_id" ON "cohort_course" USING btree ("cohort_id");--> statement-breakpoint
CREATE INDEX "idx_cohort_goal_cohort_id" ON "cohort_goal" USING btree ("cohort_id");--> statement-breakpoint
CREATE INDEX "idx_cohort_goal_status" ON "cohort_goal" USING btree ("status");--> statement-breakpoint
CREATE INDEX "idx_cohort_goal_assignment_goal_id" ON "cohort_goal_assignment" USING btree ("goal_id");--> statement-breakpoint
CREATE INDEX "idx_cohort_goal_assignment_cohort_member_id" ON "cohort_goal_assignment" USING btree ("cohort_member_id");--> statement-breakpoint
CREATE INDEX "idx_cohort_goal_assignment_due_date" ON "cohort_goal_assignment" USING btree ("due_date");--> statement-breakpoint
CREATE INDEX "idx_cohort_member_cohort_id" ON "cohort_member" USING btree ("cohort_id");--> statement-breakpoint
CREATE INDEX "idx_cohort_member_profile_id" ON "cohort_member" USING btree ("profile_id");--> statement-breakpoint
CREATE INDEX "idx_cohort_newsfeed_cohort_id" ON "cohort_newsfeed" USING btree ("cohort_id");
File diff suppressed because it is too large Load Diff
@@ -15,6 +15,13 @@
"when": 1782684436805,
"tag": "0001_slow_lila_cheney",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1782660458258,
"tag": "0002_majestic_masked_marvel",
"breakpoints": true
}
]
}