fix: deployment errors
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
"plugins": {
|
||||
"svelte": {
|
||||
"enabled": true
|
||||
},
|
||||
"supabase": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BaseApi, classroomio } from '$lib/utils/services/api';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { currentOrg, mergeAccountOrgFromServer, orgs } from '$lib/utils/store/org';
|
||||
import { defaultProfileState, defaultUserState, profile, user } from '$lib/utils/store/user';
|
||||
|
||||
@@ -10,6 +9,7 @@ import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { handleLocaleChange } from '$lib/utils/functions/translations';
|
||||
import { identifyPosthogUser } from '$lib/utils/services/posthog';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { isPublicRoute } from '$lib/utils/functions/routes/isPublicRoute';
|
||||
import { licenseApi } from '$features/license/api/license.svelte';
|
||||
import { logout } from '$lib/utils/functions/logout';
|
||||
|
||||
@@ -56,7 +56,10 @@
|
||||
onMount(async () => {
|
||||
if (!data.isOrgSite || !data.org) {
|
||||
if (!appInitApi.loading) {
|
||||
appInitApi.routeUserToNextPage({ isOrgSite: false, orgSiteName: '' });
|
||||
appInitApi.setupApp(data.locals, {
|
||||
isOrgSite: data.isOrgSite,
|
||||
orgSiteName: data.orgSiteName
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"postinstall": "test -d .git && lefthook install || true",
|
||||
"release": "standard-version",
|
||||
"db:reset": "pnpm --filter @cio/db db:reset",
|
||||
"dashboard:dev": "pnpm --filter=@cio/dashboard --filter=@cio/ui run dev",
|
||||
"dashboard:dev": "pnpm --filter=@cio/dashboard run dev",
|
||||
"api:dev": "pnpm --filter=@cio/api --filter=@cio/jobs-worker run dev",
|
||||
"jobs:dev": "pnpm --filter=@cio/jobs-worker run dev",
|
||||
"dev:all": "pnpm concurrently -n dash,api,jobs -c green,blue,yellow \"pnpm dashboard:dev\" \"pnpm api:dev\" \"pnpm jobs:dev\"",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"rootDir": "./src",
|
||||
"module": "ES2022",
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
backups/
|
||||
|
||||
dist
|
||||
node_modules
|
||||
*.log
|
||||
|
||||
@@ -24,7 +24,12 @@
|
||||
"generate-types": "tsx scripts/generate-types.ts && pnpm format",
|
||||
"seed": "tsx src/scripts/seed.ts",
|
||||
"format": "prettier . --write",
|
||||
"db:supabase-to-betterauth": "tsx src/scripts/supabase-to-betterauth.ts"
|
||||
"db:supabase-to-betterauth": "tsx src/scripts/supabase-to-betterauth.ts",
|
||||
"db:dump-backup": "tsx src/scripts/pg-dump-backup.ts",
|
||||
"db:push-dump-to-new-db": "tsx src/scripts/push-dump-to-new-db.ts",
|
||||
"db:restore-lesson-section-from-dump": "tsx src/scripts/restore-lesson-section-from-dump.ts",
|
||||
"db:restore-organizationmember-from-dump": "tsx src/scripts/restore-organizationmember-from-dump.ts",
|
||||
"db:disable-v1-content-grouping-from-dump": "tsx src/scripts/disable-v1-content-grouping-from-dump.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/sso": "^1.4.18",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CREATE TABLE "account" (
|
||||
CREATE TABLE "public"."account" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"account_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
@@ -14,7 +14,7 @@ CREATE TABLE "account" (
|
||||
"updated_at" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "session" (
|
||||
CREATE TABLE "public"."session" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
@@ -27,7 +27,7 @@ CREATE TABLE "session" (
|
||||
CONSTRAINT "session_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user" (
|
||||
CREATE TABLE "public"."user" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
@@ -43,5 +43,5 @@ CREATE TABLE "user" (
|
||||
CONSTRAINT "user_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
ALTER TABLE "public"."account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "public"."session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS "verification" (
|
||||
CREATE TABLE IF NOT EXISTS "public"."verification" (
|
||||
"id" uuid PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
@@ -7,16 +7,14 @@ CREATE TABLE IF NOT EXISTS "verification" (
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "account" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
||||
ALTER TABLE "account" ALTER COLUMN "user_id" SET DATA TYPE uuid USING user_id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
|
||||
ALTER TABLE "user" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "user" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
||||
|
||||
ALTER TABLE "session" DROP CONSTRAINT "session_user_id_user_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "session" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "session" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
||||
ALTER TABLE "session" ALTER COLUMN "user_id" SET DATA TYPE uuid USING user_id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "public"."account" DROP CONSTRAINT IF EXISTS "account_user_id_user_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "public"."session" DROP CONSTRAINT IF EXISTS "session_user_id_user_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "public"."user" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "public"."user" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
||||
ALTER TABLE "public"."account" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
||||
ALTER TABLE "public"."account" ALTER COLUMN "user_id" SET DATA TYPE uuid USING user_id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "public"."account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "public"."session" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "public"."session" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
||||
ALTER TABLE "public"."session" ALTER COLUMN "user_id" SET DATA TYPE uuid USING user_id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "public"."session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS "exercise_template" (
|
||||
CREATE TABLE IF NOT EXISTS "public"."exercise_template" (
|
||||
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "exercise_template_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"title" text,
|
||||
@@ -7,237 +7,300 @@ CREATE TABLE IF NOT EXISTS "exercise_template" (
|
||||
"questionnaire" jsonb DEFAULT '{}'::jsonb
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "analytics_login_events" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "apps_poll" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "apps_poll_option" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "apps_poll_submission" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "community_answer" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "community_question" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "course" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "course_newsfeed" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "course_newsfeed_comment" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "lesson_section" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "exercise" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "group" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "group_attendance" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "groupmember" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "lesson" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "lesson_comment" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "lesson_completion" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "lesson_language" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "lesson_language_history" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "option" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "organization" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "organization_contacts" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "organization_emaillist" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "organization_plan" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "organizationmember" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "profile" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "question" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "question_answer" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "question_type" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "quiz" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "quiz_play" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "role" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "submission" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "submissionstatus" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "lesson_section" RENAME TO "course_section";--> statement-breakpoint
|
||||
ALTER TABLE "course_newsfeed_comment" DROP CONSTRAINT "course_newsfeed_comment_id_key";--> statement-breakpoint
|
||||
ALTER TABLE "lesson" DROP CONSTRAINT "public_lesson_section_id_fkey";
|
||||
ALTER TABLE IF EXISTS "public"."analytics_login_events" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."apps_poll" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."apps_poll_option" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."apps_poll_submission" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."community_answer" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."community_question" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."course" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."course_newsfeed" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."course_newsfeed_comment" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."lesson_section" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."course_section" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."exercise" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."group" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."group_attendance" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."groupmember" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."lesson" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."lesson_comment" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."lesson_completion" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."lesson_language" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."lesson_language_history" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."option" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."organization" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."organization_contacts" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."organization_emaillist" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."organization_plan" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."organizationmember" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."profile" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."question" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."question_answer" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."question_type" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."quiz" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."quiz_play" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."role" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."submission" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."submissionstatus" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
ALTER TABLE "public"."lesson" DROP CONSTRAINT IF EXISTS "public_lesson_section_id_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "course_section" DROP CONSTRAINT "public_lesson_section_course_id_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "account" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "analytics_login_events" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "community_answer" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "course" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "exercise" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "group" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "groupmember" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "lesson" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "option" ALTER COLUMN "value" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "organization" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "question" ALTER COLUMN "name" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "quiz" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "session" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "submission" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "user" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "verification" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "verification" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "exercise" ADD COLUMN "course_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "exercise" ADD COLUMN "section_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "exercise" ADD COLUMN "order" bigint;--> statement-breakpoint
|
||||
ALTER TABLE "exercise" ADD COLUMN "is_unlocked" boolean DEFAULT true;--> statement-breakpoint
|
||||
ALTER TABLE "exercise" ADD CONSTRAINT "exercise_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "public"."course"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "exercise" ADD CONSTRAINT "exercise_section_id_fkey" FOREIGN KEY ("section_id") REFERENCES "public"."course_section"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "lesson" ADD CONSTRAINT "public_course_section_id_fkey" FOREIGN KEY ("section_id") REFERENCES "public"."course_section"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "course_section" ADD CONSTRAINT "public_course_section_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "public"."course"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "course" DROP COLUMN "version";--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable delete for users based on user_id" ON "analytics_login_events" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Users can insert their own login events" ON "analytics_login_events" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete only your own poll" ON "apps_poll" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update only your own" ON "apps_poll" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "apps_poll" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be course member to SELECT" ON "apps_poll" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "apps_poll_option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to UPDATE" ON "apps_poll_option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a teacher to DELETE" ON "apps_poll_option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be course member to SELECT" ON "apps_poll_option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can read" ON "apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own submission" ON "apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update your own submission" ON "apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "community_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own answer" ON "community_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "community_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update your own answer" ON "community_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "community_question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own question" ON "community_question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "community_question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update your own question" ON "community_question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable access to all users if PUBLIC or to course members when " ON "course" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "course" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "course" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "course" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own comment" ON "course_newsfeed" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update only your own" ON "course_newsfeed" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "course_newsfeed" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "course_newsfeed" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own" ON "course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update only your own" ON "course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "exercise" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "exercise" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "exercise" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "exercise" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "group" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "group" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only org admins can delete" ON "group" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only org admins can update" ON "group" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "group_attendance" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "group_attendance" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to UPDATE" ON "group_attendance" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "group_attendance" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "groupmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "groupmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "groupmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "groupmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "lesson" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "lesson" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "lesson" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "lesson" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete only your own comment" ON "lesson_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "lesson_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update only your own" ON "lesson_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be in course group to INSERT" ON "lesson_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "lesson_completion" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an course member or Admin to perform ALL operation" ON "lesson_completion" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "lesson_completion" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "lesson_completion" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "lesson_language" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "lesson_language" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "lesson_language" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "lesson_language" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "lesson_language_history" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "lesson_language_history" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "lesson_language_history" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "lesson_language_history" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "course_section" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "course_section" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "course_section" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "course_section" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Allow authenticated users to SELECT" ON "option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can delete" ON "organization" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "organization" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "organization" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be admin to UPDATE" ON "organization" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "organization_contacts" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "organization_emaillist" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "organization_plan" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "organization_plan" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "organization_plan" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "organization_plan" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Allow authenticated users to read." ON "organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only admin can delete" ON "organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only admin can update" ON "organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only user can update their account via email" ON "organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an admin to INSERT or allow if no existing member" ON "organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only auth users can read profile" ON "profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Public profiles are viewable by everyone." ON "profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User can only delete their profiles" ON "profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Users can insert their own profile." ON "profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Users can update own profile." ON "profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Allow authenticated users to SELECT" ON "question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only authenticated users can select." ON "question_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an course member to DELETE" ON "question_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an course member to INSERT" ON "question_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an course member to UPDATE" ON "question_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "question_type" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "quiz" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "quiz_play" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "role" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only authenticated users can SELECT" ON "submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to DELETE" ON "submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to UPDATE" ON "submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "submissionstatus" CASCADE;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."lesson_section" DROP CONSTRAINT IF EXISTS "public_lesson_section_course_id_fkey";--> statement-breakpoint
|
||||
DO $migrate_rename_lesson_section_to_course_section$ BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'public' AND c.relkind = 'r' AND c.relname = 'lesson_section'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_class c2
|
||||
JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
|
||||
WHERE n2.nspname = 'public' AND c2.relkind = 'r' AND c2.relname = 'course_section'
|
||||
) THEN
|
||||
ALTER TABLE public.lesson_section RENAME TO course_section;
|
||||
END IF;
|
||||
END;
|
||||
$migrate_rename_lesson_section_to_course_section$;--> statement-breakpoint
|
||||
ALTER TABLE IF EXISTS "public"."course_newsfeed_comment" DROP CONSTRAINT IF EXISTS "course_newsfeed_comment_id_key";--> statement-breakpoint
|
||||
ALTER TABLE "public"."account" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "public"."account" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."analytics_login_events" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."community_answer" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."course" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."exercise" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."group" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."groupmember" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."lesson" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."option" ALTER COLUMN "value" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."organization" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."question" ALTER COLUMN "name" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."quiz" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."session" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."submission" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."user" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."verification" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||
ALTER TABLE "public"."verification" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "public"."exercise" ADD COLUMN "course_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "public"."exercise" ADD COLUMN "section_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "public"."exercise" ADD COLUMN "order" bigint;--> statement-breakpoint
|
||||
ALTER TABLE "public"."exercise" ADD COLUMN "is_unlocked" boolean DEFAULT true;--> statement-breakpoint
|
||||
DO $add_exercise_course_fk$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'exercise' AND schemaname = 'public')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint co
|
||||
JOIN pg_class rel ON rel.oid = co.conrelid
|
||||
JOIN pg_namespace n ON n.oid = rel.relnamespace
|
||||
WHERE n.nspname = 'public' AND rel.relname = 'exercise' AND co.contype = 'f' AND co.conname = 'exercise_course_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.exercise ADD CONSTRAINT exercise_course_id_fkey FOREIGN KEY ("course_id") REFERENCES public.course("id") ON DELETE no action ON UPDATE no action;
|
||||
END IF;
|
||||
END;
|
||||
$add_exercise_course_fk$;--> statement-breakpoint
|
||||
DO $add_exercise_section_fk$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'exercise' AND schemaname = 'public')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint co
|
||||
JOIN pg_class rel ON rel.oid = co.conrelid
|
||||
JOIN pg_namespace n ON n.oid = rel.relnamespace
|
||||
WHERE n.nspname = 'public' AND rel.relname = 'exercise' AND co.contype = 'f' AND co.conname = 'exercise_section_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.exercise ADD CONSTRAINT exercise_section_id_fkey FOREIGN KEY ("section_id") REFERENCES public.course_section("id") ON DELETE no action ON UPDATE no action;
|
||||
END IF;
|
||||
END;
|
||||
$add_exercise_section_fk$;--> statement-breakpoint
|
||||
DO $add_lesson_course_section_fk$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'lesson' AND schemaname = 'public')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint co
|
||||
JOIN pg_class rel ON rel.oid = co.conrelid
|
||||
JOIN pg_namespace n ON n.oid = rel.relnamespace
|
||||
WHERE n.nspname = 'public' AND rel.relname = 'lesson' AND co.contype = 'f' AND co.conname = 'public_course_section_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.lesson ADD CONSTRAINT public_course_section_id_fkey FOREIGN KEY ("section_id") REFERENCES public.course_section("id") ON DELETE cascade ON UPDATE cascade;
|
||||
END IF;
|
||||
END;
|
||||
$add_lesson_course_section_fk$;--> statement-breakpoint
|
||||
DO $add_course_section_course_fk$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'course_section' AND schemaname = 'public')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint co
|
||||
JOIN pg_class rel ON rel.oid = co.conrelid
|
||||
JOIN pg_namespace n ON n.oid = rel.relnamespace
|
||||
WHERE n.nspname = 'public' AND rel.relname = 'course_section' AND co.contype = 'f' AND co.conname = 'public_course_section_course_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE public.course_section ADD CONSTRAINT public_course_section_course_id_fkey FOREIGN KEY ("course_id") REFERENCES public.course("id") ON DELETE cascade ON UPDATE cascade;
|
||||
END IF;
|
||||
END;
|
||||
$add_course_section_course_fk$;--> statement-breakpoint
|
||||
ALTER TABLE "public"."course" DROP COLUMN "version";--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable delete for users based on user_id" ON "public"."analytics_login_events" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Users can insert their own login events" ON "public"."analytics_login_events" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete only your own poll" ON "public"."apps_poll" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update only your own" ON "public"."apps_poll" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "public"."apps_poll" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be course member to SELECT" ON "public"."apps_poll" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "public"."apps_poll_option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to UPDATE" ON "public"."apps_poll_option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a teacher to DELETE" ON "public"."apps_poll_option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be course member to SELECT" ON "public"."apps_poll_option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can read" ON "public"."apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own submission" ON "public"."apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update your own submission" ON "public"."apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "public"."community_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own answer" ON "public"."community_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."community_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update your own answer" ON "public"."community_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "public"."community_question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own question" ON "public"."community_question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."community_question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update your own question" ON "public"."community_question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable access to all users if PUBLIC or to course members when " ON "public"."course" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."course" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."course" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."course" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own comment" ON "public"."course_newsfeed" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update only your own" ON "public"."course_newsfeed" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "public"."course_newsfeed" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "public"."course_newsfeed" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete your own" ON "public"."course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update only your own" ON "public"."course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "public"."course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "public"."course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."exercise" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."exercise" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."exercise" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."exercise" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."group" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."group" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only org admins can delete" ON "public"."group" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only org admins can update" ON "public"."group" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "public"."group_attendance" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "public"."group_attendance" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to UPDATE" ON "public"."group_attendance" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."group_attendance" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."groupmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."groupmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."groupmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."groupmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."lesson" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."lesson" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."lesson" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."lesson" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Delete only your own comment" ON "public"."lesson_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."lesson_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Update only your own" ON "public"."lesson_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be in course group to INSERT" ON "public"."lesson_comment" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."lesson_completion" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an course member or Admin to perform ALL operation" ON "public"."lesson_completion" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."lesson_completion" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."lesson_completion" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."lesson_language" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."lesson_language" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."lesson_language" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."lesson_language" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "public"."lesson_language_history" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."lesson_language_history" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."lesson_language_history" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."lesson_language_history" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."course_section" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."course_section" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."course_section" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."course_section" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Allow authenticated users to SELECT" ON "public"."option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."option" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can delete" ON "public"."organization" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."organization" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."organization" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be admin to UPDATE" ON "public"."organization" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."organization_contacts" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."organization_emaillist" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."organization_plan" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."organization_plan" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."organization_plan" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."organization_plan" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Allow authenticated users to read." ON "public"."organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only admin can delete" ON "public"."organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only admin can update" ON "public"."organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only user can update their account via email" ON "public"."organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an admin to INSERT or allow if no existing member" ON "public"."organizationmember" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only auth users can read profile" ON "public"."profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Public profiles are viewable by everyone." ON "public"."profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User can only delete their profiles" ON "public"."profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Users can insert their own profile." ON "public"."profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Users can update own profile." ON "public"."profile" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Allow authenticated users to SELECT" ON "public"."question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "public"."question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "public"."question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "public"."question" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only authenticated users can select." ON "public"."question_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an course member to DELETE" ON "public"."question_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an course member to INSERT" ON "public"."question_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be an course member to UPDATE" ON "public"."question_answer" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."question_type" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "public"."quiz" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "public"."quiz_play" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."role" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Only authenticated users can SELECT" ON "public"."submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to DELETE" ON "public"."submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "public"."submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "User must be a course member to UPDATE" ON "public"."submission" CASCADE;--> statement-breakpoint
|
||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "public"."submissionstatus" CASCADE;--> statement-breakpoint
|
||||
DROP TYPE "public"."COURSE_VERSION";
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('group_attendance', 'id'), COALESCE((SELECT MAX(id) FROM group_attendance), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.group_attendance', 'id'), COALESCE((SELECT MAX(id) FROM public.group_attendance), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('currency', 'id'), COALESCE((SELECT MAX(id) FROM currency), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.currency', 'id'), COALESCE((SELECT MAX(id) FROM public.currency), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('apps_poll_option', 'id'), COALESCE((SELECT MAX(id) FROM apps_poll_option), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.apps_poll_option', 'id'), COALESCE((SELECT MAX(id) FROM public.apps_poll_option), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('option', 'id'), COALESCE((SELECT MAX(id) FROM option), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.option', 'id'), COALESCE((SELECT MAX(id) FROM public.option), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('quiz_play', 'id'), COALESCE((SELECT MAX(id) FROM quiz_play), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.quiz_play', 'id'), COALESCE((SELECT MAX(id) FROM public.quiz_play), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('organization_contacts', 'id'), COALESCE((SELECT MAX(id) FROM organization_contacts), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.organization_contacts', 'id'), COALESCE((SELECT MAX(id) FROM public.organization_contacts), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('lesson_comment', 'id'), COALESCE((SELECT MAX(id) FROM lesson_comment), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.lesson_comment', 'id'), COALESCE((SELECT MAX(id) FROM public.lesson_comment), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('submissionstatus', 'id'), COALESCE((SELECT MAX(id) FROM submissionstatus), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.submissionstatus', 'id'), COALESCE((SELECT MAX(id) FROM public.submissionstatus), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('video_transcripts', 'id'), COALESCE((SELECT MAX(id) FROM video_transcripts), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.video_transcripts', 'id'), COALESCE((SELECT MAX(id) FROM public.video_transcripts), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('organization_emaillist', 'id'), COALESCE((SELECT MAX(id) FROM organization_emaillist), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.organization_emaillist', 'id'), COALESCE((SELECT MAX(id) FROM public.organization_emaillist), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('apps_poll_submission', 'id'), COALESCE((SELECT MAX(id) FROM apps_poll_submission), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.apps_poll_submission', 'id'), COALESCE((SELECT MAX(id) FROM public.apps_poll_submission), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('lesson_completion', 'id'), COALESCE((SELECT MAX(id) FROM lesson_completion), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.lesson_completion', 'id'), COALESCE((SELECT MAX(id) FROM public.lesson_completion), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('community_question', 'id'), COALESCE((SELECT MAX(id) FROM community_question), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.community_question', 'id'), COALESCE((SELECT MAX(id) FROM public.community_question), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('course_newsfeed_comment', 'id'), COALESCE((SELECT MAX(id) FROM course_newsfeed_comment), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.course_newsfeed_comment', 'id'), COALESCE((SELECT MAX(id) FROM public.course_newsfeed_comment), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('organization_plan', 'id'), COALESCE((SELECT MAX(id) FROM organization_plan), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.organization_plan', 'id'), COALESCE((SELECT MAX(id) FROM public.organization_plan), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('lesson_language', 'id'), COALESCE((SELECT MAX(id) FROM lesson_language), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.lesson_language', 'id'), COALESCE((SELECT MAX(id) FROM public.lesson_language), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('lesson_language_history', 'id'), COALESCE((SELECT MAX(id) FROM lesson_language_history), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.lesson_language_history', 'id'), COALESCE((SELECT MAX(id) FROM public.lesson_language_history), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('organizationmember', 'id'), COALESCE((SELECT MAX(id) FROM organizationmember), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.organizationmember', 'id'), COALESCE((SELECT MAX(id) FROM public.organizationmember), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('question', 'id'), COALESCE((SELECT MAX(id) FROM question), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.question', 'id'), COALESCE((SELECT MAX(id) FROM public.question), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('question_answer', 'id'), COALESCE((SELECT MAX(id) FROM question_answer), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.question_answer', 'id'), COALESCE((SELECT MAX(id) FROM public.question_answer), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('question_type', 'id'), COALESCE((SELECT MAX(id) FROM question_type), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.question_type', 'id'), COALESCE((SELECT MAX(id) FROM public.question_type), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('role', 'id'), COALESCE((SELECT MAX(id) FROM role), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.role', 'id'), COALESCE((SELECT MAX(id) FROM public.role), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('waitinglist', 'id'), COALESCE((SELECT MAX(id) FROM waitinglist), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.waitinglist', 'id'), COALESCE((SELECT MAX(id) FROM public.waitinglist), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('test_tenant', 'id'), COALESCE((SELECT MAX(id) FROM test_tenant), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.test_tenant', 'id'), COALESCE((SELECT MAX(id) FROM public.test_tenant), 1), true);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(pg_get_serial_sequence('exercise_template', 'id'), COALESCE((SELECT MAX(id) FROM exercise_template), 1), true);
|
||||
SELECT setval(pg_get_serial_sequence('public.exercise_template', 'id'), COALESCE((SELECT MAX(id) FROM public.exercise_template), 1), true);
|
||||
@@ -272,27 +272,18 @@ export const analyticsCountryDaily = pgTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const courseSection = pgTable(
|
||||
'course_section',
|
||||
{
|
||||
export const courseSection = pgTable('course_section', {
|
||||
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
|
||||
title: varchar(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
order: bigint({ mode: 'number' }).default(sql`'0'`),
|
||||
courseId: uuid('course_id')
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.courseId],
|
||||
foreignColumns: [course.id],
|
||||
name: 'public_course_section_course_id_fkey'
|
||||
courseId: uuid('course_id').references(() => course.id, {
|
||||
onDelete: 'cascade',
|
||||
onUpdate: 'cascade'
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade')
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
export const group = pgTable(
|
||||
'group',
|
||||
@@ -1001,7 +992,10 @@ export const lesson = pgTable(
|
||||
assetId?: string;
|
||||
}[]
|
||||
>(),
|
||||
sectionId: uuid('section_id'),
|
||||
sectionId: uuid('section_id').references(() => courseSection.id, {
|
||||
onDelete: 'cascade',
|
||||
onUpdate: 'cascade'
|
||||
}),
|
||||
slug: varchar()
|
||||
},
|
||||
(table) => [
|
||||
@@ -1015,13 +1009,6 @@ export const lesson = pgTable(
|
||||
foreignColumns: [profile.id],
|
||||
name: 'lesson_teacher_id_fkey'
|
||||
}),
|
||||
foreignKey({
|
||||
columns: [table.sectionId],
|
||||
foreignColumns: [courseSection.id],
|
||||
name: 'public_course_section_id_fkey'
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
index('idx_lesson_course_slug').on(table.courseId, table.slug)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1,203 +1,98 @@
|
||||
import { eq, isNotNull } from 'drizzle-orm';
|
||||
import { and, eq, inArray, isNotNull, or, sql } from 'drizzle-orm';
|
||||
import { exercise, lesson } from '../schema';
|
||||
|
||||
import { db } from '../drizzle';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
async function backfillExerciseCourse() {
|
||||
try {
|
||||
const result = await setExerciseCourseAndSectionId();
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const backupDir = path.resolve(scriptDir, '../../backups');
|
||||
|
||||
// TypeScript implementation of exercise ordering logic
|
||||
// Algorithm:
|
||||
// 1. Fetch each lesson (grouped by course_id, section_id, ordered by lesson order or created_at)
|
||||
// 2. For each lesson, fetch its related exercises (ordered by createdAt)
|
||||
// 3. Create a linear sequence: lesson, then its exercises, then next lesson, etc.
|
||||
// 4. Assign sequential order numbers
|
||||
// 5. Update exercises with their new order values
|
||||
const orderBatchSize = Number.parseInt(process.env.EXERCISE_BACKFILL_BATCH_SIZE ?? '200', 10) || 200;
|
||||
|
||||
// Step 1: Fetch all lessons with their order (or calculate it from created_at)
|
||||
const allLessons = await db
|
||||
.select({
|
||||
id: lesson.id,
|
||||
courseId: lesson.courseId,
|
||||
sectionId: lesson.sectionId,
|
||||
order: lesson.order,
|
||||
createdAt: lesson.createdAt
|
||||
})
|
||||
.from(lesson);
|
||||
/** Optional course scope: `pnpm db:exercise-backfill [courseId]` */
|
||||
const scopedCourseId = process.argv[2]?.trim() || null;
|
||||
|
||||
// Group lessons by course_id and section_id
|
||||
type LessonWithOrder = (typeof allLessons)[0] & { calculatedOrder: number };
|
||||
const lessonsByGroup = new Map<string, LessonWithOrder[]>();
|
||||
|
||||
for (const lessonItem of allLessons) {
|
||||
const groupKey = `${lessonItem.courseId || 'null'}-${lessonItem.sectionId || 'null'}`;
|
||||
|
||||
if (!lessonsByGroup.has(groupKey)) {
|
||||
lessonsByGroup.set(groupKey, []);
|
||||
}
|
||||
// Add calculatedOrder property when pushing
|
||||
lessonsByGroup.get(groupKey)!.push({
|
||||
...lessonItem,
|
||||
calculatedOrder: lessonItem.order ?? 0 // Temporary, will be recalculated
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate order for lessons that don't have one, within each group
|
||||
for (const [groupKey, groupLessons] of lessonsByGroup.entries()) {
|
||||
// Sort by existing order (nulls last), then by created_at
|
||||
groupLessons.sort((a, b) => {
|
||||
if (a.order !== null && b.order !== null) {
|
||||
return a.order - b.order;
|
||||
}
|
||||
if (a.order !== null) return -1;
|
||||
if (b.order !== null) return 1;
|
||||
return new Date(a.createdAt || '').getTime() - new Date(b.createdAt || '').getTime();
|
||||
});
|
||||
|
||||
// Assign calculated order
|
||||
groupLessons.forEach((lessonItem, index) => {
|
||||
lessonItem.calculatedOrder = lessonItem.order ?? index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: Fetch all exercises that have a lesson_id
|
||||
const allExercises = await db
|
||||
.select({
|
||||
id: exercise.id,
|
||||
lessonId: exercise.lessonId,
|
||||
courseId: exercise.courseId,
|
||||
sectionId: exercise.sectionId,
|
||||
createdAt: exercise.createdAt
|
||||
})
|
||||
.from(exercise)
|
||||
.where(isNotNull(exercise.lessonId));
|
||||
|
||||
// Group exercises by lesson_id
|
||||
const exercisesByLesson = new Map<string, typeof allExercises>();
|
||||
for (const exerciseItem of allExercises) {
|
||||
if (!exerciseItem.lessonId) continue;
|
||||
|
||||
if (!exercisesByLesson.has(exerciseItem.lessonId)) {
|
||||
exercisesByLesson.set(exerciseItem.lessonId, []);
|
||||
}
|
||||
exercisesByLesson.get(exerciseItem.lessonId)!.push(exerciseItem);
|
||||
}
|
||||
|
||||
// Sort exercises within each lesson by createdAt
|
||||
for (const exercises of exercisesByLesson.values()) {
|
||||
exercises.sort((a, b) => {
|
||||
return new Date(a.createdAt || '').getTime() - new Date(b.createdAt || '').getTime();
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: Create linear sequence - lesson, then its exercises, then next lesson, etc.
|
||||
interface ContentItem {
|
||||
type: 'lesson' | 'exercise';
|
||||
lessonId: string | null;
|
||||
exerciseId: string | null;
|
||||
type LessonRow = {
|
||||
id: string;
|
||||
courseId: string;
|
||||
sectionId: string | null;
|
||||
lessonOrder: number;
|
||||
sortOffset: number;
|
||||
order: number | null;
|
||||
createdAt: string | null;
|
||||
};
|
||||
|
||||
type ExerciseRow = {
|
||||
id: string;
|
||||
lessonId: string | null;
|
||||
courseId: string | null;
|
||||
sectionId: string | null;
|
||||
order: number | null;
|
||||
createdAt: string | null;
|
||||
};
|
||||
|
||||
type OrderUpdate = { id: string; order: number };
|
||||
|
||||
type FailedOrderBatch = {
|
||||
table: 'lesson' | 'exercise';
|
||||
batchIndex: number;
|
||||
rowCount: number;
|
||||
ids: string[];
|
||||
error: string;
|
||||
};
|
||||
|
||||
type OrderBackfillReport = {
|
||||
startedAt: string;
|
||||
completedAt: string;
|
||||
courseId: string | null;
|
||||
batchSize: number;
|
||||
summary: {
|
||||
lessonsAttempted: number;
|
||||
lessonsUpdated: number;
|
||||
lessonsFailed: number;
|
||||
exercisesAttempted: number;
|
||||
exercisesUpdated: number;
|
||||
exercisesFailed: number;
|
||||
};
|
||||
failures: FailedOrderBatch[];
|
||||
reportPath: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates lesson-attached exercises from the V1 model to the V2 linear content model.
|
||||
*
|
||||
* **V1:** exercises nested under a lesson (`exercise.lesson_id`); lesson `order` was the
|
||||
* only sequence that mattered for navigation.
|
||||
*
|
||||
* **V2:** lessons and exercises share one `order` column per course (and per section when
|
||||
* `section_id` is set). The UI sorts both tables by that single sequence.
|
||||
*
|
||||
* Runs two phases in order:
|
||||
* 1. {@link setExerciseCourseAndSectionId} — copy `course_id` / `section_id` from the parent lesson.
|
||||
* 2. {@link assignLinearContentOrder} — renumber `lesson.order` and `exercise.order` in one shared sequence.
|
||||
*
|
||||
* @example
|
||||
* // Single course (dry-run scope while validating):
|
||||
* pnpm --filter @cio/db db:exercise-backfill 73432009-4c40-457d-9e90-6b48b18b0e23
|
||||
*
|
||||
* @example
|
||||
* // All courses:
|
||||
* pnpm --filter @cio/db db:exercise-backfill
|
||||
*/
|
||||
async function backfillExerciseCourse() {
|
||||
try {
|
||||
const courseSectionResult = await setExerciseCourseAndSectionId(scopedCourseId);
|
||||
const orderResult = await assignLinearContentOrder(scopedCourseId);
|
||||
|
||||
console.log('Exercise course/section backfill completed', courseSectionResult);
|
||||
console.log('Linear content order backfill completed', orderResult.summary);
|
||||
console.log('Report written to', orderResult.reportPath);
|
||||
|
||||
if (orderResult.failures.length > 0) {
|
||||
console.error(`${orderResult.failures.length} batch(es) failed — see report for ids and errors.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const allItems: ContentItem[] = [];
|
||||
|
||||
// Process each group (course_id + section_id combination)
|
||||
for (const [groupKey, groupLessons] of lessonsByGroup.entries()) {
|
||||
// Sort lessons by their calculated order
|
||||
const sortedLessons = [...groupLessons].sort((a, b) => a.calculatedOrder - b.calculatedOrder);
|
||||
|
||||
for (const lessonItem of sortedLessons) {
|
||||
// Add the lesson itself
|
||||
allItems.push({
|
||||
type: 'lesson',
|
||||
lessonId: lessonItem.id,
|
||||
exerciseId: null,
|
||||
courseId: lessonItem.courseId,
|
||||
sectionId: lessonItem.sectionId,
|
||||
lessonOrder: lessonItem.calculatedOrder,
|
||||
sortOffset: 0 // Lessons always have offset 0
|
||||
});
|
||||
|
||||
// Add exercises for this lesson (sorted by createdAt)
|
||||
const lessonExercises = exercisesByLesson.get(lessonItem.id) || [];
|
||||
for (let i = 0; i < lessonExercises.length; i++) {
|
||||
const exerciseItem = lessonExercises[i];
|
||||
allItems.push({
|
||||
type: 'exercise',
|
||||
lessonId: exerciseItem.lessonId,
|
||||
exerciseId: exerciseItem.id,
|
||||
courseId: exerciseItem.courseId || lessonItem.courseId,
|
||||
sectionId: exerciseItem.sectionId || lessonItem.sectionId,
|
||||
lessonOrder: lessonItem.calculatedOrder,
|
||||
sortOffset: i + 1 // Exercises have offset 1, 2, 3, etc.
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Sort all items by lesson_order, then sort_offset (within each course_id + section_id group)
|
||||
allItems.sort((a, b) => {
|
||||
const groupKeyA = `${a.courseId}-${a.sectionId || 'null'}`;
|
||||
const groupKeyB = `${b.courseId}-${b.sectionId || 'null'}`;
|
||||
|
||||
if (groupKeyA !== groupKeyB) {
|
||||
return groupKeyA.localeCompare(groupKeyB);
|
||||
}
|
||||
|
||||
if (a.lessonOrder !== b.lessonOrder) {
|
||||
return a.lessonOrder - b.lessonOrder;
|
||||
}
|
||||
|
||||
return a.sortOffset - b.sortOffset;
|
||||
});
|
||||
|
||||
// Step 5: Assign sequential order numbers within each group and update exercises
|
||||
const orderUpdates: Array<{ exerciseId: string; order: number }> = [];
|
||||
let currentOrder = 1;
|
||||
let lastGroupKey = '';
|
||||
|
||||
for (const item of allItems) {
|
||||
const groupKey = `${item.courseId}-${item.sectionId || 'null'}`;
|
||||
|
||||
// Reset order counter when we move to a new group
|
||||
if (groupKey !== lastGroupKey) {
|
||||
currentOrder = 1;
|
||||
lastGroupKey = groupKey;
|
||||
}
|
||||
|
||||
// Only update exercises (not lessons)
|
||||
if (item.type === 'exercise' && item.exerciseId) {
|
||||
orderUpdates.push({
|
||||
exerciseId: item.exerciseId,
|
||||
order: currentOrder
|
||||
});
|
||||
}
|
||||
|
||||
currentOrder++;
|
||||
}
|
||||
|
||||
// Batch update exercises
|
||||
let exerciseOrderResult;
|
||||
if (orderUpdates.length > 0) {
|
||||
// Use a transaction to update all exercises
|
||||
exerciseOrderResult = await db.transaction(async (tx) => {
|
||||
const updatePromises = orderUpdates.map((update) =>
|
||||
tx.update(exercise).set({ order: update.order }).where(eq(exercise.id, update.exerciseId))
|
||||
);
|
||||
await Promise.all(updatePromises);
|
||||
return { rowCount: orderUpdates.length };
|
||||
});
|
||||
} else {
|
||||
exerciseOrderResult = { rowCount: 0 };
|
||||
}
|
||||
|
||||
console.log('Exercise backfill completed', result);
|
||||
console.log('Exercise order backfill completed', exerciseOrderResult);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Failed to backfill exercise course data', error);
|
||||
@@ -205,7 +100,154 @@ async function backfillExerciseCourse() {
|
||||
}
|
||||
}
|
||||
|
||||
async function setExerciseCourseAndSectionId() {
|
||||
/**
|
||||
* Assigns a single 1-based `order` sequence to lessons and their child exercises.
|
||||
*
|
||||
* **Grouping** — Items are processed independently per `(course_id, section_id)` bucket.
|
||||
* Unsectioned content uses `section_id = null`. Section order is not changed here.
|
||||
*
|
||||
* **Lesson ordering** — Within each bucket, lessons are sorted by existing `order`
|
||||
* (ascending), with `null` orders last; ties break on `created_at`.
|
||||
*
|
||||
* **Exercise ordering** — Exercises with a `lesson_id` are grouped under that lesson.
|
||||
* Within a lesson, exercises sort by `order` then `created_at` (same comparator as lessons).
|
||||
*
|
||||
* **Linear sequence** — For each lesson in sorted order, emit one slot for the lesson,
|
||||
* then one slot per child exercise (in exercise sort order). Assign `order = 1..n` across
|
||||
* that combined list and persist both tables in batched `CASE` updates
|
||||
* (`EXERCISE_BACKFILL_BATCH_SIZE`, default 200). Failed batches are logged and skipped;
|
||||
* the run continues until all batches are attempted. See `packages/db/backups/exercise-backfill-report-*.json`.
|
||||
*
|
||||
* Example (one section bucket):
|
||||
*
|
||||
* ```
|
||||
* Before (lesson order only): After (shared order):
|
||||
* L1 order=1 L1 order=1
|
||||
* └ Ea (nested) Ea order=2
|
||||
* └ Eb Eb order=3
|
||||
* L2 order=2 L2 order=4
|
||||
* └ Ec Ec order=5
|
||||
* ```
|
||||
*
|
||||
* Only exercises that still have `lesson_id` are included. Course-level exercises
|
||||
* (`lesson_id IS NULL`) are left unchanged.
|
||||
*
|
||||
* @param courseId — When set, only lessons/exercises for this course are loaded and updated.
|
||||
*/
|
||||
async function assignLinearContentOrder(courseId: string | null) {
|
||||
const lessonWhere = courseId ? eq(lesson.courseId, courseId) : undefined;
|
||||
|
||||
const allLessons = await db
|
||||
.select({
|
||||
id: lesson.id,
|
||||
courseId: lesson.courseId,
|
||||
sectionId: lesson.sectionId,
|
||||
order: lesson.order,
|
||||
createdAt: lesson.createdAt
|
||||
})
|
||||
.from(lesson)
|
||||
.where(lessonWhere);
|
||||
|
||||
const allExercises = await db
|
||||
.select({
|
||||
id: exercise.id,
|
||||
lessonId: exercise.lessonId,
|
||||
courseId: exercise.courseId,
|
||||
sectionId: exercise.sectionId,
|
||||
order: exercise.order,
|
||||
createdAt: exercise.createdAt
|
||||
})
|
||||
.from(exercise)
|
||||
.leftJoin(lesson, eq(exercise.lessonId, lesson.id))
|
||||
.where(
|
||||
courseId
|
||||
? and(or(eq(exercise.courseId, courseId), eq(lesson.courseId, courseId)), isNotNull(exercise.lessonId))
|
||||
: isNotNull(exercise.lessonId)
|
||||
);
|
||||
|
||||
const lessonsByGroup = new Map<string, LessonRow[]>();
|
||||
for (const lessonRow of allLessons) {
|
||||
const key = groupKey(lessonRow.courseId, lessonRow.sectionId);
|
||||
const group = lessonsByGroup.get(key) ?? [];
|
||||
group.push(lessonRow);
|
||||
lessonsByGroup.set(key, group);
|
||||
}
|
||||
|
||||
const exercisesByLesson = new Map<string, ExerciseRow[]>();
|
||||
for (const exerciseRow of allExercises) {
|
||||
if (!exerciseRow.lessonId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lessonExercises = exercisesByLesson.get(exerciseRow.lessonId) ?? [];
|
||||
lessonExercises.push(exerciseRow);
|
||||
exercisesByLesson.set(exerciseRow.lessonId, lessonExercises);
|
||||
}
|
||||
|
||||
for (const lessonExercises of exercisesByLesson.values()) {
|
||||
sortExercises(lessonExercises);
|
||||
}
|
||||
|
||||
const lessonOrderUpdates: Array<{ id: string; order: number }> = [];
|
||||
const exerciseOrderUpdates: Array<{ id: string; order: number }> = [];
|
||||
|
||||
for (const [, groupLessons] of lessonsByGroup) {
|
||||
const sortedLessons = sortLessons(groupLessons);
|
||||
let nextOrder = 1;
|
||||
|
||||
for (const lessonRow of sortedLessons) {
|
||||
lessonOrderUpdates.push({ id: lessonRow.id, order: nextOrder });
|
||||
nextOrder += 1;
|
||||
|
||||
const lessonExercises = exercisesByLesson.get(lessonRow.id) ?? [];
|
||||
for (const exerciseRow of lessonExercises) {
|
||||
exerciseOrderUpdates.push({ id: exerciseRow.id, order: nextOrder });
|
||||
nextOrder += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startedAt = new Date().toISOString();
|
||||
const failures: FailedOrderBatch[] = [];
|
||||
|
||||
const lessonsUpdated = await applyBatchedOrderUpdates('lesson', lessonOrderUpdates, failures);
|
||||
const exercisesUpdated = await applyBatchedOrderUpdates('exercise', exerciseOrderUpdates, failures);
|
||||
|
||||
const report = buildOrderBackfillReport({
|
||||
startedAt,
|
||||
courseId,
|
||||
lessonOrderUpdates,
|
||||
exerciseOrderUpdates,
|
||||
lessonsUpdated,
|
||||
exercisesUpdated,
|
||||
failures
|
||||
});
|
||||
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
writeFileSync(report.reportPath, JSON.stringify(report, null, 2));
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalizes `course_id` and `section_id` onto exercises from their parent lesson.
|
||||
*
|
||||
* Updates rows where `lesson_id` is set and either `course_id` or `section_id` is still null,
|
||||
* so later queries can filter exercises by course without joining `lesson`.
|
||||
*/
|
||||
async function setExerciseCourseAndSectionId(courseId: string | null) {
|
||||
if (courseId) {
|
||||
return db.execute(sql`
|
||||
UPDATE exercise AS e
|
||||
SET course_id = l.course_id,
|
||||
section_id = l.section_id
|
||||
FROM lesson AS l
|
||||
WHERE e.lesson_id = l.id
|
||||
AND l.course_id = ${courseId}
|
||||
AND (e.course_id IS NULL OR e.section_id IS NULL)
|
||||
`);
|
||||
}
|
||||
|
||||
return db.execute(sql`
|
||||
UPDATE exercise AS e
|
||||
SET course_id = l.course_id,
|
||||
@@ -216,4 +258,131 @@ async function setExerciseCourseAndSectionId() {
|
||||
`);
|
||||
}
|
||||
|
||||
async function applyBatchedOrderUpdates(
|
||||
table: 'lesson' | 'exercise',
|
||||
updates: OrderUpdate[],
|
||||
failures: FailedOrderBatch[]
|
||||
) {
|
||||
if (updates.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let updatedCount = 0;
|
||||
const totalBatches = Math.ceil(updates.length / orderBatchSize);
|
||||
|
||||
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex += 1) {
|
||||
const batchStart = batchIndex * orderBatchSize;
|
||||
const batch = updates.slice(batchStart, batchStart + orderBatchSize);
|
||||
const batchIds = batch.map((update) => update.id);
|
||||
|
||||
try {
|
||||
if (table === 'lesson') {
|
||||
await db
|
||||
.update(lesson)
|
||||
.set({
|
||||
order: sql`case ${lesson.id} ${sql.join(
|
||||
batch.map((update) => sql`when ${update.id} then ${update.order}`),
|
||||
sql` `
|
||||
)} else ${lesson.order} end`
|
||||
})
|
||||
.where(inArray(lesson.id, batchIds));
|
||||
} else {
|
||||
await db
|
||||
.update(exercise)
|
||||
.set({
|
||||
order: sql`case ${exercise.id} ${sql.join(
|
||||
batch.map((update) => sql`when ${update.id} then ${update.order}`),
|
||||
sql` `
|
||||
)} else ${exercise.order} end`
|
||||
})
|
||||
.where(inArray(exercise.id, batchIds));
|
||||
}
|
||||
|
||||
updatedCount += batch.length;
|
||||
console.log(`Updated ${table} batch ${batchIndex + 1}/${totalBatches} (${batch.length} rows)`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
failures.push({
|
||||
table,
|
||||
batchIndex,
|
||||
rowCount: batch.length,
|
||||
ids: batchIds,
|
||||
error: message
|
||||
});
|
||||
|
||||
console.error(`Failed ${table} batch ${batchIndex + 1}/${totalBatches} (${batch.length} rows):`, message);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
function buildOrderBackfillReport(input: {
|
||||
startedAt: string;
|
||||
courseId: string | null;
|
||||
lessonOrderUpdates: OrderUpdate[];
|
||||
exerciseOrderUpdates: OrderUpdate[];
|
||||
lessonsUpdated: number;
|
||||
exercisesUpdated: number;
|
||||
failures: FailedOrderBatch[];
|
||||
}): OrderBackfillReport {
|
||||
const stamp = input.startedAt.replace(/[:.]/g, '-');
|
||||
const reportPath = path.join(backupDir, `exercise-backfill-report-${stamp}.json`);
|
||||
|
||||
const lessonsAttempted = input.lessonOrderUpdates.length;
|
||||
const exercisesAttempted = input.exerciseOrderUpdates.length;
|
||||
const lessonsFailed = input.failures
|
||||
.filter((failure) => failure.table === 'lesson')
|
||||
.reduce((count, failure) => count + failure.rowCount, 0);
|
||||
const exercisesFailed = input.failures
|
||||
.filter((failure) => failure.table === 'exercise')
|
||||
.reduce((count, failure) => count + failure.rowCount, 0);
|
||||
|
||||
return {
|
||||
startedAt: input.startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
courseId: input.courseId,
|
||||
batchSize: orderBatchSize,
|
||||
summary: {
|
||||
lessonsAttempted,
|
||||
lessonsUpdated: input.lessonsUpdated,
|
||||
lessonsFailed,
|
||||
exercisesAttempted,
|
||||
exercisesUpdated: input.exercisesUpdated,
|
||||
exercisesFailed
|
||||
},
|
||||
failures: input.failures,
|
||||
reportPath
|
||||
};
|
||||
}
|
||||
|
||||
function groupKey(courseId: string, sectionId: string | null) {
|
||||
return `${courseId}-${sectionId ?? 'null'}`;
|
||||
}
|
||||
|
||||
function compareByOrderThenCreatedAt<T extends { order: number | null; createdAt: string | null }>(left: T, right: T) {
|
||||
if (left.order !== null && right.order !== null) {
|
||||
return left.order - right.order;
|
||||
}
|
||||
|
||||
if (left.order !== null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (right.order !== null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return new Date(left.createdAt || '').getTime() - new Date(right.createdAt || '').getTime();
|
||||
}
|
||||
|
||||
function sortLessons(lessons: LessonRow[]) {
|
||||
return [...lessons].sort(compareByOrderThenCreatedAt);
|
||||
}
|
||||
|
||||
function sortExercises(exercises: ExerciseRow[]) {
|
||||
return [...exercises].sort(compareByOrderThenCreatedAt);
|
||||
}
|
||||
|
||||
backfillExerciseCourse();
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Reads **`course`** table data from a **legacy** Postgres dump (before `course.version` was dropped)
|
||||
* and disables content grouping for former **V1** courses on the live DB (`DATABASE_URL`).
|
||||
*
|
||||
* Legacy model (see commented `0000_stale_storm.sql` and `DROP COLUMN "version"` in
|
||||
* `0003_fancy_harry_osborn.sql`):
|
||||
* - Column **`version`** had type **`"COURSE_VERSION"`** enum with values **`V1`** and **`V2`**.
|
||||
*
|
||||
* App stores grouping in **`course.metadata` → `isContentGroupingEnabled`**. This script sets
|
||||
* **`metadata.isContentGroupingEnabled = false`** for every course id whose dump row has **`version = V1`**.
|
||||
* Rows with **`V2`** or other values are unchanged.
|
||||
*
|
||||
* **Your dump must still contain the `version` column** (pg_dump from before that migration, or a
|
||||
* full schema+data backup from that era). Dumps taken after `version` was dropped cannot be used—
|
||||
* the script exits with a clear error if `COPY ... course (...)` has no `version` column.
|
||||
*
|
||||
* **Plain SQL** (`.sql`): finds `COPY public.course` / `COPY course` with column list, parses tab rows.
|
||||
* **Custom** (`.dump`): `pg_restore --data-only -t course`.
|
||||
*
|
||||
* Limitation: COPY rows are split on **tab** boundaries. If a text/json column contains a literal tab,
|
||||
* parsing may mis-align; use a plain-format dump from pg_dump defaults (usually fine).
|
||||
*
|
||||
* Usage:
|
||||
* DATABASE_URL="..." pnpm exec tsx src/scripts/disable-v1-content-grouping-from-dump.ts path/to/backup.sql
|
||||
* pnpm exec tsx src/scripts/disable-v1-content-grouping-from-dump.ts --dry-run path/to/backup.dump
|
||||
*
|
||||
* Env: `DATABASE_URL`, `PG_RESTORE_BIN`, `PSQL_BIN` (same as other restore scripts).
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const backupDir = path.resolve(scriptDir, '../../backups');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const dumpPathRaw = args.find((argument) => !argument.startsWith('-'));
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const pgRestoreExecutable = process.env.PG_RESTORE_BIN ?? 'pg_restore';
|
||||
const psqlExecutable = process.env.PSQL_BIN ?? 'psql';
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
function slicePlainCopyCourse(fileText: string) {
|
||||
const copyPublic = /\bCOPY\s+public\.course\b/i.exec(fileText);
|
||||
const copyBare = /\bCOPY\s+course\b/i.exec(fileText);
|
||||
|
||||
const matchIndex = copyPublic?.index ?? copyBare?.index;
|
||||
if (matchIndex === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fromStdinIdx = fileText.indexOf('FROM stdin', matchIndex);
|
||||
if (fromStdinIdx === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let lineEnd = fileText.indexOf('\n', fromStdinIdx);
|
||||
if (lineEnd === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const terminator = /\n\\\.\s*(?:\n|\r?\n|$)/;
|
||||
const dataRegion = fileText.slice(lineEnd);
|
||||
const terminatorMatch = terminator.exec(dataRegion);
|
||||
if (!terminatorMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const terminatorEndAbs = lineEnd + terminatorMatch.index + terminatorMatch[0].length;
|
||||
|
||||
return fileText.slice(matchIndex, terminatorEndAbs);
|
||||
}
|
||||
|
||||
function pgRestoreCourseExtract(dumpFilePath: string) {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), 'v1-course-copy-'));
|
||||
const extractedPath = path.join(tempDir, 'course.sql');
|
||||
|
||||
try {
|
||||
const flags = spawnSync(
|
||||
pgRestoreExecutable,
|
||||
[
|
||||
'--data-only',
|
||||
'--no-owner',
|
||||
'--no-privileges',
|
||||
'-n',
|
||||
'public',
|
||||
'-t',
|
||||
'course',
|
||||
'-f',
|
||||
extractedPath,
|
||||
dumpFilePath
|
||||
],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
|
||||
if (flags.status !== 0) {
|
||||
console.error(pgRestoreExecutable, '--data-only extraction failed:', (flags.stderr ?? '').trim() || flags.status);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return readFileSync(extractedPath, 'utf8');
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse `COPY ... course ( a, b, "c" ) FROM stdin;` */
|
||||
function parseCopyColumnList(copyBlock: string): string[] | null {
|
||||
const lineEnd = copyBlock.indexOf('\n');
|
||||
const firstLine = lineEnd === -1 ? copyBlock : copyBlock.slice(0, lineEnd);
|
||||
|
||||
const withCols = /COPY\s+(?:public\.)?course\s*\(\s*([^)]+)\s*\)\s*FROM\s+stdin\s*;?\s*$/im.exec(firstLine.trim());
|
||||
|
||||
if (!withCols) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = withCols[1];
|
||||
|
||||
const columns: string[] = [];
|
||||
let current = '';
|
||||
let inQuote = false;
|
||||
|
||||
for (let index = 0; index < raw.length; index++) {
|
||||
const character = raw[index];
|
||||
|
||||
if (character === '"') {
|
||||
inQuote = !inQuote;
|
||||
current += character;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === ',' && !inQuote) {
|
||||
columns.push(stripIdent(current.trim()));
|
||||
current = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
current += character;
|
||||
}
|
||||
|
||||
if (current.trim()) {
|
||||
columns.push(stripIdent(current.trim()));
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
function stripIdent(identifier: string) {
|
||||
return identifier.replace(/^"|"$/g, '');
|
||||
}
|
||||
|
||||
function extractV1CourseIdsFromCopyBlock(copyBlock: string): string[] | null {
|
||||
const columns = parseCopyColumnList(copyBlock);
|
||||
|
||||
if (!columns?.length) {
|
||||
console.error(
|
||||
'Could not parse COPY column list for course. Use a dump where pg_dump included explicit columns, e.g. `COPY public.course (id, …, version, …) FROM stdin;`'
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const idIndex = columns.findIndex((columnName) => columnName === 'id');
|
||||
const versionIndex = columns.findIndex((columnName) => columnName === 'version');
|
||||
|
||||
if (idIndex === -1) {
|
||||
console.error('COPY column list for course has no `id` column.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (versionIndex === -1) {
|
||||
console.error(
|
||||
'COPY column list for course has no `version` column. This script needs a **legacy** dump from before `course.version` was dropped (migration 0003).'
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const newlineAfterHeader = copyBlock.indexOf('\n');
|
||||
const dataPart = newlineAfterHeader === -1 ? '' : copyBlock.slice(newlineAfterHeader + 1);
|
||||
const lines = dataPart.split(/\r?\n/);
|
||||
|
||||
const v1Ids = new Set<string>();
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === '\\.') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** Postgres COPY text format: tab-separated; `\N` is null */
|
||||
const fields = line.split('\t');
|
||||
if (fields.length < Math.max(idIndex, versionIndex) + 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawId = fields[idIndex]?.trim() ?? '';
|
||||
const rawVersion = fields[versionIndex]?.trim() ?? '';
|
||||
|
||||
if (rawVersion === '\\N' || rawVersion === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const versionNormalized = rawVersion.replace(/^"|"$/g, '').toUpperCase();
|
||||
|
||||
if (versionNormalized !== 'V1') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!UUID_RE.test(rawId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
v1Ids.add(rawId);
|
||||
}
|
||||
|
||||
return [...v1Ids];
|
||||
}
|
||||
|
||||
function buildUpdateSql(courseIds: string[]) {
|
||||
const list = courseIds.map((courseId) => `'${courseId.replace(/'/g, "''")}'`).join(',\n ');
|
||||
|
||||
return `-- Former COURSE_VERSION = V1 → metadata.isContentGroupingEnabled = false
|
||||
BEGIN;
|
||||
|
||||
UPDATE public.course
|
||||
SET metadata = jsonb_set(
|
||||
COALESCE(metadata, '{}'::jsonb),
|
||||
'{isContentGroupingEnabled}',
|
||||
'false'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE id IN (
|
||||
${list}
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
`;
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.error(`Usage:
|
||||
DATABASE_URL="postgres://..." pnpm exec tsx src/scripts/disable-v1-content-grouping-from-dump.ts <dump.dump|.sql>
|
||||
|
||||
Flags:
|
||||
--dry-run Write SQL to packages/db/backups/disable-v1-content-grouping-preview.sql only`);
|
||||
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!dumpPathRaw) {
|
||||
console.error('Missing dump file path.');
|
||||
printUsage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dryRun && !databaseUrl) {
|
||||
console.error('Set DATABASE_URL (or use --dry-run).');
|
||||
printUsage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedDump = path.isAbsolute(dumpPathRaw) ? dumpPathRaw : path.resolve(process.cwd(), dumpPathRaw);
|
||||
|
||||
let copyBlock: string | null;
|
||||
|
||||
if (resolvedDump.endsWith('.sql')) {
|
||||
const text = readFileSync(resolvedDump, 'utf8');
|
||||
copyBlock = slicePlainCopyCourse(text);
|
||||
} else {
|
||||
const extracted = pgRestoreCourseExtract(resolvedDump);
|
||||
copyBlock = extracted ? slicePlainCopyCourse(extracted) : null;
|
||||
}
|
||||
|
||||
if (!copyBlock) {
|
||||
console.error('Could not find COPY block for public.course. Try: pg_restore -l your.dump | grep course');
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const v1Ids = extractV1CourseIdsFromCopyBlock(copyBlock);
|
||||
|
||||
if (v1Ids === null) {
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (v1Ids.length === 0) {
|
||||
console.log('No courses with version V1 found in dump (or no valid rows). Nothing to update.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${v1Ids.length} course id(s) with legacy version V1 in dump.`);
|
||||
|
||||
const sql = buildUpdateSql(v1Ids);
|
||||
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
const previewFile = path.join(backupDir, 'disable-v1-content-grouping-preview.sql');
|
||||
writeFileSync(previewFile, sql, 'utf8');
|
||||
console.log('Wrote SQL:', previewFile);
|
||||
|
||||
if (dryRun) {
|
||||
console.log('Dry run: not executing psql.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const invoke = spawnSync(psqlExecutable, [databaseUrl!, '-v', 'ON_ERROR_STOP=1', '-f', previewFile], {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
if (invoke.error) {
|
||||
console.error('psql spawn failed:', invoke.error.message);
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (invoke.status !== 0) {
|
||||
process.exitCode = invoke.status ?? 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Done: metadata.isContentGroupingEnabled set to false for V1 courses (where ids exist).');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Full logical backup of the database into `packages/db/backups/` using `pg_dump`.
|
||||
*
|
||||
* Requires PostgreSQL client tools (`pg_dump`) on PATH unless `PG_DUMP_BIN` is set.
|
||||
*
|
||||
* **Server/client major version:** `pg_dump` must match the server's major version or be newer (e.g.
|
||||
* PostgreSQL **15** server needs `pg_dump` **15+**, not 14). Postgres.app 14 ships `pg_dump` 14 —
|
||||
* upgrade Postgres.app, install `postgresql@15` / `libpq` from Homebrew, or set `PG_DUMP_BIN`.
|
||||
*
|
||||
* Env:
|
||||
* - `DATABASE_URL` — connection string (default source).
|
||||
* - `PGDUMP_URL` — overrides `DATABASE_URL` (use Supabase **direct** Postgres `5432` if pooler rejects dump).
|
||||
* - `PG_DUMP_BIN` — path to `pg_dump` executable (defaults to `pg_dump` on `PATH`).
|
||||
* - `PG_DUMP_FORMAT=plain` — SQL text `.sql`; default is custom `-Fc` `.dump` (`pg_restore` compatible).
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const connectionUrl = process.env.PGDUMP_URL ?? process.env.DATABASE_URL;
|
||||
|
||||
if (!connectionUrl) {
|
||||
console.error('Set DATABASE_URL (or PGDUMP_URL) to run a backup.');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
const usePlainSql = process.env.PG_DUMP_FORMAT === 'plain';
|
||||
const verbose = process.env.PG_DUMP_VERBOSE !== '0';
|
||||
const pgDumpExecutable = process.env.PG_DUMP_BIN ?? 'pg_dump';
|
||||
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const backupDir = path.resolve(scriptDir, '../../backups');
|
||||
|
||||
function versionMismatchHint() {
|
||||
return [
|
||||
'',
|
||||
'If the error mentions server/client version mismatch, use a newer pg_dump (major >= server), e.g.:',
|
||||
' brew install postgresql@15',
|
||||
` PG_DUMP_BIN=$(brew --prefix postgresql@15)/bin/pg_dump pnpm --filter @cio/db db:dump-backup`,
|
||||
'(Or upgrade Postgres.app to a bundle that ships PostgreSQL 15+ client tools.)',
|
||||
''
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!connectionUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
const outfile = path.join(backupDir, usePlainSql ? `full-${stamp}.sql` : `full-${stamp}.dump`);
|
||||
|
||||
const args = usePlainSql
|
||||
? [...(verbose ? ['-v'] : []), '--dbname', connectionUrl, '-F', 'p', '-f', outfile]
|
||||
: [...(verbose ? ['-v'] : []), '--dbname', connectionUrl, '-F', 'c', '-Z', '9', '--file', outfile];
|
||||
|
||||
console.log('Using:', pgDumpExecutable);
|
||||
console.log('pg_dump →', outfile);
|
||||
|
||||
const result = spawnSync(pgDumpExecutable, args, { stdio: 'inherit' });
|
||||
|
||||
if (result.error) {
|
||||
console.error('pg_dump spawn failed:', result.error.message);
|
||||
console.error(versionMismatchHint());
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exitCode = result.status ?? 1;
|
||||
console.error(versionMismatchHint());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Backup finished:', outfile);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* Pushes the **`public` schema DDL** from a `pg_dump` backup into a fresh Postgres database
|
||||
* (e.g. [PlanetScale Postgres](https://planetscale.com/docs/postgres/imports/postgres-migrate-dumprestore)).
|
||||
* Does **not** load table data (`COPY` / `--data-only` rows).
|
||||
*
|
||||
* **Custom format** (`.dump` / `-Fc`, default from `db:dump-backup`): `pg_restore --schema-only -n public`,
|
||||
* with `--no-publications` / `--no-subscriptions` (Supabase `supabase_realtime` is not on PlanetScale).
|
||||
*
|
||||
* **Plain SQL** (`.sql`): runs the file with `psql` when it looks like a schema-only dump (no `COPY public.`
|
||||
* data blocks). Full plain dumps with data are rejected — re-dump with `-Fc` or `pg_dump --schema-only`.
|
||||
*
|
||||
* Before push, creates `authenticated` / `anon` roles and a minimal `extensions` schema
|
||||
* (`moddatetime`, `gen_random_uuid`) so Supabase-sourced dumps restore on PlanetScale.
|
||||
*
|
||||
* @example
|
||||
* DATABASE_URL="postgres://...@aws.connect.psdb.cloud/..." \
|
||||
* pnpm --filter @cio/db db:push-dump-to-new-db -- packages/db/backups/full-….dump
|
||||
*
|
||||
* @example
|
||||
* pnpm --filter @cio/db db:push-dump-to-new-db -- --dry-run path/to/backup.dump
|
||||
*
|
||||
* Env:
|
||||
* - `DATABASE_URL` — target DB (required unless `--dry-run`).
|
||||
* - `PG_RESTORE_BIN` — default `pg_restore`.
|
||||
* - `PSQL_BIN` — default `psql`.
|
||||
* - `PG_RESTORE_JOBS` — parallel workers for custom-format restore (e.g. `4`).
|
||||
* - `PUSH_DUMP_SSLMODE` — override `sslmode` for `pg_restore` / `psql` only (e.g. `require`).
|
||||
* - `PGSSLROOTCERT` — path to CA bundle for libpq (alternative to `sslrootcert=system` in the URL).
|
||||
*
|
||||
* PlanetScale URLs often use `sslmode=verify-full`. Node `postgres` handles that; libpq often does not
|
||||
* unless `PGSSLROOTCERT` points at a CA file. This script downgrades to `sslmode=require` for
|
||||
* `pg_restore` / `psql` when verify modes are set without a cert path (encrypted, no CA verify).
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import postgres from 'postgres';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const backupDir = path.resolve(scriptDir, '../../backups');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const force = args.includes('--force');
|
||||
const cleanFirst = args.includes('--clean');
|
||||
const skipRoles = args.includes('--skip-roles');
|
||||
const skipSupabaseExtensions = args.includes('--skip-supabase-extensions');
|
||||
const sslRequireOnly = args.includes('--ssl-require');
|
||||
const jobsRaw = args.find((argument) => argument.startsWith('--jobs='));
|
||||
const parallelJobs = jobsRaw ? Number(jobsRaw.slice('--jobs='.length)) : Number(process.env.PG_RESTORE_JOBS ?? 0);
|
||||
|
||||
const dumpPathRaw = args.find((argument) => !argument.startsWith('-'));
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const pgRestoreExecutable = process.env.PG_RESTORE_BIN ?? 'pg_restore';
|
||||
const psqlExecutable = process.env.PSQL_BIN ?? 'psql';
|
||||
|
||||
async function main() {
|
||||
if (!dumpPathRaw) {
|
||||
printUsage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dryRun && !databaseUrl) {
|
||||
console.error('Set DATABASE_URL (or pass --dry-run to only generate a SQL preview).');
|
||||
printUsage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedDump = path.isAbsolute(dumpPathRaw) ? dumpPathRaw : path.resolve(process.cwd(), dumpPathRaw);
|
||||
|
||||
let dumpKind: DumpKind;
|
||||
try {
|
||||
dumpKind = detectDumpKind(resolvedDump);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Dump:', resolvedDump);
|
||||
console.log('Format:', dumpKind === 'custom' ? 'custom (pg_restore)' : 'plain SQL (psql)');
|
||||
|
||||
try {
|
||||
if (dryRun) {
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
const previewFile = path.join(backupDir, 'push-dump-to-new-db-preview.sql');
|
||||
|
||||
if (dumpKind === 'custom') {
|
||||
extractCustomFormatSchemaPreview(resolvedDump, previewFile);
|
||||
} else {
|
||||
writeFileSync(previewFile, readFileSync(resolvedDump, 'utf8'));
|
||||
}
|
||||
|
||||
console.log('Dry run: wrote preview SQL to', previewFile);
|
||||
console.log('Apply manually with psql, or re-run without --dry-run and DATABASE_URL set.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureTargetReady(databaseUrl!, force);
|
||||
|
||||
if (!skipRoles) {
|
||||
await ensureRlsRoles(databaseUrl!);
|
||||
}
|
||||
|
||||
if (!skipSupabaseExtensions) {
|
||||
await ensureSupabaseExtensionCompat(databaseUrl!);
|
||||
}
|
||||
|
||||
const pgCliDatabaseUrl = connectionStringForPgCli(databaseUrl!, sslRequireOnly);
|
||||
|
||||
if (dumpKind === 'custom') {
|
||||
pushCustomFormatSchema(resolvedDump, pgCliDatabaseUrl);
|
||||
} else {
|
||||
pushPlainSchemaSql(resolvedDump, pgCliDatabaseUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Push dump to new DB failed:', error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
type DumpKind = 'custom' | 'plain';
|
||||
|
||||
function printUsage() {
|
||||
console.error(`Usage:
|
||||
DATABASE_URL="postgres://..." pnpm --filter @cio/db db:push-dump-to-new-db -- <dump.dump|.sql>
|
||||
|
||||
Flags:
|
||||
--dry-run Write SQL preview to packages/db/backups/push-dump-to-new-db-preview.sql
|
||||
--force Push even when public already has tables
|
||||
--clean Drop existing public objects before push (pg_restore --clean --if-exists)
|
||||
--skip-roles Do not create authenticated / anon roles before push
|
||||
--skip-supabase-extensions Skip creating extensions.moddatetime / gen_random_uuid stubs
|
||||
--jobs=N Parallel pg_restore workers (custom .dump only)
|
||||
--ssl-require Force sslmode=require for pg_restore/psql (same as the default when no CA file)`);
|
||||
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
function detectDumpKind(dumpFilePath: string): DumpKind {
|
||||
const header = readFileSync(dumpFilePath).subarray(0, 5).toString();
|
||||
|
||||
if (header === 'PGDMP') {
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
const plainText = readFileSync(dumpFilePath, 'utf8');
|
||||
|
||||
if (/\bCOPY\s+public\./i.test(plainText)) {
|
||||
throw new Error(
|
||||
'Plain SQL dump contains public COPY data blocks. Use a custom-format .dump from db:dump-backup, or pg_dump --schema-only -n public.'
|
||||
);
|
||||
}
|
||||
|
||||
return 'plain';
|
||||
}
|
||||
|
||||
async function ensureTargetReady(connectionString: string, allowExistingTables: boolean) {
|
||||
const sql = postgres(connectionString, { max: 1 });
|
||||
|
||||
try {
|
||||
const existingTables = await sql<{ tablename: string }[]>`
|
||||
SELECT tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename
|
||||
`;
|
||||
|
||||
if (existingTables.length === 0) {
|
||||
console.log('Target public schema is empty.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowExistingTables) {
|
||||
throw new Error(
|
||||
`Target already has ${existingTables.length} table(s) in public (e.g. ${existingTables[0]?.tablename}). ` +
|
||||
'Use --force to push anyway, or provision a fresh database branch.'
|
||||
);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`--force: continuing despite ${existingTables.length} existing public table(s).`,
|
||||
cleanFirst ? ' (--clean will drop objects first)' : ''
|
||||
);
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supabase dumps reference `extensions.moddatetime` triggers and sometimes `extensions.gen_random_uuid`.
|
||||
* PlanetScale has no `extensions` schema; create minimal compatible stubs before pg_restore.
|
||||
*/
|
||||
async function ensureSupabaseExtensionCompat(connectionString: string) {
|
||||
const sql = postgres(connectionString, { max: 1 });
|
||||
|
||||
try {
|
||||
await sql.unsafe(`
|
||||
CREATE SCHEMA IF NOT EXISTS extensions;
|
||||
|
||||
CREATE OR REPLACE FUNCTION extensions.gen_random_uuid()
|
||||
RETURNS uuid
|
||||
LANGUAGE sql
|
||||
PARALLEL SAFE
|
||||
AS $$ SELECT gen_random_uuid(); $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION extensions.moddatetime()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION extensions.moddatetime(column_name text)
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF column_name = 'updated_at' THEN
|
||||
NEW.updated_at = NOW();
|
||||
ELSE
|
||||
NEW := jsonb_populate_record(NEW, jsonb_build_object(column_name, NOW()));
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
`);
|
||||
|
||||
console.log('Ensured extensions schema (moddatetime, gen_random_uuid) for Supabase-compatible dumps');
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureRlsRoles(connectionString: string) {
|
||||
const sql = postgres(connectionString, { max: 1 });
|
||||
|
||||
try {
|
||||
const authenticatedExists = await sql`SELECT 1 FROM pg_roles WHERE rolname = 'authenticated'`;
|
||||
if (authenticatedExists.length === 0) {
|
||||
await sql`CREATE ROLE authenticated`;
|
||||
console.log('Created role: authenticated');
|
||||
} else {
|
||||
console.log('Role already exists: authenticated');
|
||||
}
|
||||
|
||||
const anonExists = await sql`SELECT 1 FROM pg_roles WHERE rolname = 'anon'`;
|
||||
if (anonExists.length === 0) {
|
||||
await sql`CREATE ROLE anon`;
|
||||
console.log('Created role: anon');
|
||||
} else {
|
||||
console.log('Role already exists: anon');
|
||||
}
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
}
|
||||
|
||||
function pushCustomFormatSchema(dumpFilePath: string, connectionString: string) {
|
||||
const restoreArgs = [
|
||||
'--dbname',
|
||||
connectionString,
|
||||
'--schema-only',
|
||||
'--no-owner',
|
||||
'--no-privileges',
|
||||
'--no-publications',
|
||||
'--no-subscriptions',
|
||||
'--exit-on-error',
|
||||
'-n',
|
||||
'public',
|
||||
...(cleanFirst ? ['--clean', '--if-exists'] : []),
|
||||
...(parallelJobs > 0 ? ['--jobs', String(parallelJobs)] : []),
|
||||
'--verbose',
|
||||
dumpFilePath
|
||||
];
|
||||
|
||||
console.log('Running:', pgRestoreExecutable, restoreArgs.filter((argument) => !argument.includes('://')).join(' '));
|
||||
|
||||
const result = spawnSync(pgRestoreExecutable, restoreArgs, { stdio: 'inherit' });
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`pg_restore spawn failed: ${result.error.message}`);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`pg_restore exited with status ${result.status ?? 'unknown'}`);
|
||||
}
|
||||
|
||||
console.log('Public schema push finished.');
|
||||
console.log(
|
||||
'Next: run pnpm --filter @cio/db db:setup --skip-schema-sync (roles + reference seed), then data restores as needed.'
|
||||
);
|
||||
}
|
||||
|
||||
function pushPlainSchemaSql(dumpFilePath: string, connectionString: string) {
|
||||
console.log('Running:', psqlExecutable, '<DATABASE_URL> -v ON_ERROR_STOP=1 -f', dumpFilePath);
|
||||
|
||||
const result = spawnSync(psqlExecutable, [connectionString, '-v', 'ON_ERROR_STOP=1', '-f', dumpFilePath], {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`psql spawn failed: ${result.error.message}`);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`psql exited with status ${result.status ?? 'unknown'}`);
|
||||
}
|
||||
|
||||
console.log('Public schema SQL applied.');
|
||||
}
|
||||
|
||||
function extractCustomFormatSchemaPreview(dumpFilePath: string, previewFilePath: string) {
|
||||
const extractArgs = [
|
||||
'--schema-only',
|
||||
'--no-owner',
|
||||
'--no-privileges',
|
||||
'-n',
|
||||
'public',
|
||||
'-f',
|
||||
previewFilePath,
|
||||
dumpFilePath
|
||||
];
|
||||
|
||||
const result = spawnSync(pgRestoreExecutable, extractArgs, { encoding: 'utf8' });
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`pg_restore preview spawn failed: ${result.error.message}`);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`pg_restore preview failed: ${(result.stderr ?? '').trim() || `exit ${result.status}`}. ` +
|
||||
'Ensure pg_restore major version matches the dump.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* libpq (`pg_restore`, `psql`) does not use Node's TLS stack. `verify-full` without a CA file makes
|
||||
* libpq look for `~/.postgresql/root.crt`. Older libpq also treats `sslrootcert=system` as a path.
|
||||
* Use `sslmode=require` unless the URL or `PGSSLROOTCERT` supplies a real certificate file.
|
||||
*/
|
||||
function connectionStringForPgCli(rawUrl: string, useSslRequireOnly: boolean): string {
|
||||
const url = new URL(rawUrl.replace(/^postgres:/, 'postgresql:'));
|
||||
|
||||
const envSslMode = process.env.PUSH_DUMP_SSLMODE?.trim();
|
||||
if (envSslMode) {
|
||||
applyPgCliSslMode(url, envSslMode);
|
||||
console.log(`pg_restore/psql: using PUSH_DUMP_SSLMODE=${envSslMode}`);
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
if (useSslRequireOnly) {
|
||||
applyPgCliSslMode(url, 'require');
|
||||
console.log('pg_restore/psql: using sslmode=require (--ssl-require)');
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const sslmode = url.searchParams.get('sslmode') ?? '';
|
||||
const rootCertPath = resolvePgCliRootCertPath(url);
|
||||
|
||||
if (rootCertPath) {
|
||||
url.searchParams.set('sslrootcert', rootCertPath);
|
||||
if (!sslmode) {
|
||||
url.searchParams.set('sslmode', 'verify-full');
|
||||
}
|
||||
|
||||
console.log(`pg_restore/psql: using sslrootcert=${rootCertPath}`);
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const needsEncryptedConnection =
|
||||
sslmode === 'verify-full' ||
|
||||
sslmode === 'verify-ca' ||
|
||||
sslmode === 'require' ||
|
||||
url.hostname.endsWith('.psdb.cloud');
|
||||
|
||||
if (needsEncryptedConnection && sslmode !== 'require') {
|
||||
applyPgCliSslMode(url, 'require');
|
||||
console.log(
|
||||
'pg_restore/psql: using sslmode=require (libpq has no CA file; connection is still TLS-encrypted). ' +
|
||||
'Set PGSSLROOTCERT or PUSH_DUMP_SSLMODE=verify-full to verify the server certificate.'
|
||||
);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function applyPgCliSslMode(url: URL, sslmode: string) {
|
||||
url.searchParams.set('sslmode', sslmode);
|
||||
|
||||
if (sslmode === 'require' || sslmode === 'prefer' || sslmode === 'disable') {
|
||||
url.searchParams.delete('sslrootcert');
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePgCliRootCertPath(url: URL): string | null {
|
||||
const envPath = process.env.PGSSLROOTCERT?.trim();
|
||||
if (envPath) {
|
||||
return envPath;
|
||||
}
|
||||
|
||||
const paramPath = url.searchParams.get('sslrootcert')?.trim();
|
||||
if (!paramPath || paramPath === 'system') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return paramPath;
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copy `public.lesson_section` **data** from a `pg_dump` backup into **live** `public.course_section`
|
||||
* on `DATABASE_URL` (UPSERT on `id`).
|
||||
*
|
||||
* **Custom format** (`.dump` / `-Fc`): uses `pg_restore --data-only -t lesson_section` (needs version
|
||||
* compatible with the dump, same as `pg_dump`).
|
||||
*
|
||||
* **Plain SQL** (`-Fp` `.sql`): scans for a `COPY ... lesson_section ... FROM stdin` block.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm --filter @cio/db db:restore-lesson-section-from-dump -- path/to/backup.dump
|
||||
* pnpm exec tsx src/scripts/restore-lesson-section-from-dump.ts --dry-run path/to/backup.sql
|
||||
*
|
||||
* Workflow: If `pnpm db migrate` halted on FK errors because `course_section` had no rows yet, restore
|
||||
* sections from your dump with this script, then run `pnpm db migrate` again—the migration uses conditional
|
||||
* `DO` blocks that add missing FKs when referenced ids exist.
|
||||
*
|
||||
* Env:
|
||||
* - `DATABASE_URL` — target DB (required unless `--dry-run`).
|
||||
* - `PG_RESTORE_BIN` — `pg_restore` binary (default `pg_restore`).
|
||||
* - `PSQL_BIN` — `psql` binary (default `psql`).
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const backupDir = path.resolve(scriptDir, '../../backups');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const dumpPathRaw = args.find((argument) => !argument.startsWith('-'));
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const pgRestoreExecutable = process.env.PG_RESTORE_BIN ?? 'pg_restore';
|
||||
const psqlExecutable = process.env.PSQL_BIN ?? 'psql';
|
||||
|
||||
const prelude = `
|
||||
BEGIN;
|
||||
CREATE TEMP TABLE _lesson_section_import (
|
||||
id uuid NOT NULL PRIMARY KEY,
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz,
|
||||
title varchar,
|
||||
"order" bigint,
|
||||
course_id uuid
|
||||
);
|
||||
|
||||
`;
|
||||
|
||||
const mergeSql = `
|
||||
INSERT INTO public.course_section (id, created_at, updated_at, title, "order", course_id)
|
||||
SELECT id, created_at, updated_at, title, "order", course_id
|
||||
FROM _lesson_section_import
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
created_at = EXCLUDED.created_at,
|
||||
updated_at = EXCLUDED.updated_at,
|
||||
title = EXCLUDED.title,
|
||||
"order" = EXCLUDED."order",
|
||||
course_id = EXCLUDED.course_id;
|
||||
|
||||
COMMIT;
|
||||
`;
|
||||
|
||||
function renameCopyTargets(body: string) {
|
||||
let out = body;
|
||||
out = out.replace(/\bCOPY\s+public\.lesson_section\b/i, 'COPY _lesson_section_import');
|
||||
out = out.replace(/\bCOPY\s+lesson_section\b/i, 'COPY _lesson_section_import');
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Plain-format dump: one COPY ... lesson_section ... FROM stdin; … \\. block */
|
||||
function slicePlainCopyLessonSection(fileText: string) {
|
||||
const copyPublic = /\bCOPY\s+public\.lesson_section\b/i.exec(fileText);
|
||||
const copyBare = /\bCOPY\s+lesson_section\b/i.exec(fileText);
|
||||
|
||||
const matchIndex = copyPublic?.index ?? copyBare?.index;
|
||||
if (matchIndex === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fromStdinIdx = fileText.indexOf('FROM stdin;', matchIndex);
|
||||
|
||||
if (fromStdinIdx === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let lineEnd = fileText.indexOf('\n', fromStdinIdx + 'FROM stdin;'.length);
|
||||
|
||||
if (lineEnd === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** First line after DATA is first row; terminator is a line `\`. */
|
||||
const terminator = /\n\\\.\s*(?:\n|\r?\n|$)/;
|
||||
|
||||
const dataRegion = fileText.slice(lineEnd);
|
||||
const terminatorMatch = terminator.exec(dataRegion);
|
||||
|
||||
if (!terminatorMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const terminatorEndAbs = lineEnd + terminatorMatch.index + terminatorMatch[0].length;
|
||||
|
||||
return fileText.slice(matchIndex, terminatorEndAbs);
|
||||
}
|
||||
|
||||
function pgRestoreLessonSectionExtract(dumpFilePath: string) {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), 'lesson-section-restore-'));
|
||||
const extractedPath = path.join(tempDir, 'lesson_section.sql');
|
||||
|
||||
try {
|
||||
const flags = spawnSync(
|
||||
pgRestoreExecutable,
|
||||
[
|
||||
'--data-only',
|
||||
'--no-owner',
|
||||
'--no-privileges',
|
||||
'-n',
|
||||
'public',
|
||||
'-t',
|
||||
'lesson_section',
|
||||
'-f',
|
||||
extractedPath,
|
||||
dumpFilePath
|
||||
],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
|
||||
if (flags.status !== 0) {
|
||||
console.error(pgRestoreExecutable, '--data-only extraction failed:', (flags.stderr ?? '').trim() || flags.status);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return renameCopyTargets(readFileSync(extractedPath, 'utf8'));
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.error(`Usage:
|
||||
DATABASE_URL="postgres://..." pnpm exec tsx src/scripts/restore-lesson-section-from-dump.ts <dump.dump|.sql>
|
||||
|
||||
Flags:
|
||||
--dry-run Write merged SQL preview to packages/db/backups/restore-lesson-section-preview.sql`);
|
||||
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!dumpPathRaw) {
|
||||
console.error('Missing dump file path.');
|
||||
printUsage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dryRun && !databaseUrl) {
|
||||
console.error('Set DATABASE_URL (or pass --dry-run to only generate SQL preview).');
|
||||
|
||||
printUsage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedDump = path.isAbsolute(dumpPathRaw) ? dumpPathRaw : path.resolve(process.cwd(), dumpPathRaw);
|
||||
|
||||
let lessonSectionChunk: string | null;
|
||||
|
||||
if (resolvedDump.endsWith('.sql')) {
|
||||
const text = readFileSync(resolvedDump, 'utf8');
|
||||
lessonSectionChunk = slicePlainCopyLessonSection(text);
|
||||
if (lessonSectionChunk) {
|
||||
lessonSectionChunk = renameCopyTargets(lessonSectionChunk);
|
||||
}
|
||||
} else {
|
||||
lessonSectionChunk = pgRestoreLessonSectionExtract(resolvedDump);
|
||||
}
|
||||
|
||||
if (!lessonSectionChunk) {
|
||||
console.error(
|
||||
'Could not extract lesson_section data. Check:',
|
||||
`\n - Table exists in backup as public.lesson_section`,
|
||||
`\n - For custom dumps: pg_restore -l "${resolvedDump}" | grep lesson_section`,
|
||||
`\n - pg_restore major version matches the dump`
|
||||
);
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fullScript = prelude + lessonSectionChunk + mergeSql;
|
||||
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
const previewFile = path.join(backupDir, 'restore-lesson-section-preview.sql');
|
||||
writeFileSync(previewFile, fullScript, 'utf8');
|
||||
console.log('Wrote SQL preview:', previewFile);
|
||||
|
||||
if (dryRun) {
|
||||
console.log('Dry run: skipping psql execution.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const invoke = spawnSync(psqlExecutable, [databaseUrl!, '-v', 'ON_ERROR_STOP=1', '-f', previewFile], {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
if (invoke.error) {
|
||||
console.error('psql spawn failed:', invoke.error.message);
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (invoke.status !== 0) {
|
||||
process.exitCode = invoke.status ?? 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Done: lesson_section rows from dump were UPSERT-ed into course_section via', previewFile);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Insert `public.organizationmember` **rows from a dump** into the DB on `DATABASE_URL` **only when
|
||||
* missing** relative to existing data:
|
||||
* - skip if **`id`** already exists;
|
||||
* - skip if **`(organization_id, profile_id)`** already exists (**`profile_id IS NOT NULL`**, aligned with
|
||||
* partial unique `organizationmember_org_profile_unique`);
|
||||
* - skip if **`(organization_id, email)`** already exists (**`email IS NOT NULL`**, aligned with
|
||||
* `organizationmember_org_email_unique`).
|
||||
*
|
||||
* **Custom format** (`.dump` / `-Fc`): `pg_restore --data-only -t organizationmember`.
|
||||
* **Plain SQL** (`-Fp` `.sql`): finds `COPY ... organizationmember ... FROM stdin` block.
|
||||
*
|
||||
* Usage:
|
||||
* DATABASE_URL="postgres://..." pnpm exec tsx src/scripts/restore-organizationmember-from-dump.ts path/to/backup.dump
|
||||
* pnpm exec tsx src/scripts/restore-organizationmember-from-dump.ts --dry-run path/to/backup.sql
|
||||
*
|
||||
* Requires existing `organization`, `profile` (when `profile_id` set), and `role` rows for FKs.
|
||||
*
|
||||
* Env:
|
||||
* - `DATABASE_URL` — target DB (required unless `--dry-run`).
|
||||
* - `PG_RESTORE_BIN` — default `pg_restore`.
|
||||
* - `PSQL_BIN` — default `psql`.
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const backupDir = path.resolve(scriptDir, '../../backups');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const dumpPathRaw = args.find((argument) => !argument.startsWith('-'));
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const pgRestoreExecutable = process.env.PG_RESTORE_BIN ?? 'pg_restore';
|
||||
const psqlExecutable = process.env.PSQL_BIN ?? 'psql';
|
||||
|
||||
/** Column order matches `organizationmember` in schema / typical pg_dump. */
|
||||
const prelude = `
|
||||
BEGIN;
|
||||
|
||||
CREATE TEMP TABLE _organizationmember_import (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
organization_id uuid NOT NULL,
|
||||
role_id bigint NOT NULL,
|
||||
profile_id uuid,
|
||||
email text,
|
||||
verified boolean,
|
||||
created_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
`;
|
||||
|
||||
const mergeSql = `
|
||||
INSERT INTO public.organizationmember (
|
||||
id,
|
||||
organization_id,
|
||||
role_id,
|
||||
profile_id,
|
||||
email,
|
||||
verified,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
i.id,
|
||||
i.organization_id,
|
||||
i.role_id,
|
||||
i.profile_id,
|
||||
i.email,
|
||||
COALESCE(i.verified, false),
|
||||
i.created_at
|
||||
FROM _organizationmember_import i
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.organizationmember om WHERE om.id = i.id)
|
||||
AND (
|
||||
i.profile_id IS NULL
|
||||
OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.organizationmember om
|
||||
WHERE om.organization_id = i.organization_id
|
||||
AND om.profile_id IS NOT NULL
|
||||
AND om.profile_id = i.profile_id
|
||||
)
|
||||
)
|
||||
AND (
|
||||
i.email IS NULL
|
||||
OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.organizationmember om
|
||||
WHERE om.organization_id = i.organization_id
|
||||
AND om.email IS NOT NULL
|
||||
AND om.email = i.email
|
||||
)
|
||||
);
|
||||
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('public.organizationmember', 'id'),
|
||||
COALESCE((SELECT MAX(id)::bigint FROM public.organizationmember), 1),
|
||||
true
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
`;
|
||||
|
||||
function renameCopyTargets(body: string) {
|
||||
let out = body;
|
||||
out = out.replace(/\bCOPY\s+public\.organizationmember\b/i, 'COPY _organizationmember_import');
|
||||
out = out.replace(/\bCOPY\s+organizationmember\b/i, 'COPY _organizationmember_import');
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function slicePlainCopyOrganizationMember(fileText: string) {
|
||||
const copyPublic = /\bCOPY\s+public\.organizationmember\b/i.exec(fileText);
|
||||
const copyBare = /\bCOPY\s+organizationmember\b/i.exec(fileText);
|
||||
|
||||
const matchIndex = copyPublic?.index ?? copyBare?.index;
|
||||
if (matchIndex === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fromStdinIdx = fileText.indexOf('FROM stdin;', matchIndex);
|
||||
|
||||
if (fromStdinIdx === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let lineEnd = fileText.indexOf('\n', fromStdinIdx + 'FROM stdin;'.length);
|
||||
|
||||
if (lineEnd === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const terminator = /\n\\\.\s*(?:\n|\r?\n|$)/;
|
||||
|
||||
const dataRegion = fileText.slice(lineEnd);
|
||||
const terminatorMatch = terminator.exec(dataRegion);
|
||||
|
||||
if (!terminatorMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const terminatorEndAbs = lineEnd + terminatorMatch.index + terminatorMatch[0].length;
|
||||
|
||||
return fileText.slice(matchIndex, terminatorEndAbs);
|
||||
}
|
||||
|
||||
function pgRestoreOrganizationMemberExtract(dumpFilePath: string) {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), 'organizationmember-restore-'));
|
||||
const extractedPath = path.join(tempDir, 'organizationmember.sql');
|
||||
|
||||
try {
|
||||
const flags = spawnSync(
|
||||
pgRestoreExecutable,
|
||||
[
|
||||
'--data-only',
|
||||
'--no-owner',
|
||||
'--no-privileges',
|
||||
'-n',
|
||||
'public',
|
||||
'-t',
|
||||
'organizationmember',
|
||||
'-f',
|
||||
extractedPath,
|
||||
dumpFilePath
|
||||
],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
|
||||
if (flags.status !== 0) {
|
||||
console.error(pgRestoreExecutable, '--data-only extraction failed:', (flags.stderr ?? '').trim() || flags.status);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return renameCopyTargets(readFileSync(extractedPath, 'utf8'));
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.error(`Usage:
|
||||
DATABASE_URL="postgres://..." pnpm exec tsx src/scripts/restore-organizationmember-from-dump.ts <dump.dump|.sql>
|
||||
|
||||
Flags:
|
||||
--dry-run Write merged SQL preview to packages/db/backups/restore-organizationmember-preview.sql`);
|
||||
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!dumpPathRaw) {
|
||||
console.error('Missing dump file path.');
|
||||
printUsage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dryRun && !databaseUrl) {
|
||||
console.error('Set DATABASE_URL (or pass --dry-run to only generate SQL preview).');
|
||||
printUsage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedDump = path.isAbsolute(dumpPathRaw) ? dumpPathRaw : path.resolve(process.cwd(), dumpPathRaw);
|
||||
|
||||
let organizationMemberChunk: string | null;
|
||||
|
||||
if (resolvedDump.endsWith('.sql')) {
|
||||
const text = readFileSync(resolvedDump, 'utf8');
|
||||
organizationMemberChunk = slicePlainCopyOrganizationMember(text);
|
||||
if (organizationMemberChunk) {
|
||||
organizationMemberChunk = renameCopyTargets(organizationMemberChunk);
|
||||
}
|
||||
} else {
|
||||
organizationMemberChunk = pgRestoreOrganizationMemberExtract(resolvedDump);
|
||||
}
|
||||
|
||||
if (!organizationMemberChunk) {
|
||||
console.error(
|
||||
'Could not extract organizationmember data. Check:',
|
||||
`\n - Table exists in backup as public.organizationmember`,
|
||||
`\n - For custom dumps: pg_restore -l "${resolvedDump}" | grep organizationmember`,
|
||||
`\n - pg_restore major version matches the dump`
|
||||
);
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fullScript = prelude + organizationMemberChunk + mergeSql;
|
||||
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
const previewFile = path.join(backupDir, 'restore-organizationmember-preview.sql');
|
||||
writeFileSync(previewFile, fullScript, 'utf8');
|
||||
console.log('Wrote SQL preview:', previewFile);
|
||||
|
||||
if (dryRun) {
|
||||
console.log('Dry run: skipping psql execution.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const invoke = spawnSync(psqlExecutable, [databaseUrl!, '-v', 'ON_ERROR_STOP=1', '-f', previewFile], {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
if (invoke.error) {
|
||||
console.error('psql spawn failed:', invoke.error.message);
|
||||
process.exitCode = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (invoke.status !== 0) {
|
||||
process.exitCode = invoke.status ?? 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Done: organizationmember rows from dump were merged (missing-only) via', previewFile);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,5 +1,13 @@
|
||||
/**
|
||||
* Commenting this script out.
|
||||
* One-off migration: copies `auth.users` + identities into Better Auth `"user"` and `account`.
|
||||
*
|
||||
* Optional reset: when `RESET_BETTER_AUTH_BEFORE_SUPABASE_MIGRATE=1`, clears Better Auth targets
|
||||
* in a transaction (see `resetBetterAuthTables`) before re-import.
|
||||
*
|
||||
* Optional backfill: when `BACKFILL_AUTH_ACCOUNTS_FROM_SUPABASE=1`, only processes Supabase users
|
||||
* that already exist in `"user"` but have no rows in `account`, and creates missing account rows
|
||||
* from `auth.identities` (does not create or update `"user"` rows). Incompatible with reset;
|
||||
* reset is skipped if both flags are set (with a warning).
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
@@ -9,6 +17,87 @@ import { admin, anonymous } from 'better-auth/plugins';
|
||||
import { Pool } from 'pg';
|
||||
import { betterAuth } from 'better-auth';
|
||||
|
||||
const RESET_FLAG = process.env.RESET_BETTER_AUTH_BEFORE_SUPABASE_MIGRATE === '1';
|
||||
const BACKFILL_ACCOUNTS_ONLY = process.env.BACKFILL_AUTH_ACCOUNTS_FROM_SUPABASE === '1';
|
||||
|
||||
console.log('RESET_FLAG', RESET_FLAG);
|
||||
console.log('BACKFILL_AUTH_ACCOUNTS_FROM_SUPABASE', BACKFILL_ACCOUNTS_ONLY);
|
||||
|
||||
if (RESET_FLAG && BACKFILL_ACCOUNTS_ONLY) {
|
||||
console.warn(
|
||||
'Both RESET_BETTER_AUTH_BEFORE_SUPABASE_MIGRATE and BACKFILL_AUTH_ACCOUNTS_FROM_SUPABASE are set; skipping reset and running account backfill only.'
|
||||
);
|
||||
}
|
||||
|
||||
async function resetBetterAuthTables(pool: Pool) {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query('TRUNCATE TABLE analytics_login_events');
|
||||
await client.query('ALTER TABLE profile DROP CONSTRAINT IF EXISTS profile_id_fkey');
|
||||
await client.query('TRUNCATE TABLE verification');
|
||||
await client.query('DELETE FROM "user"');
|
||||
await client.query('COMMIT');
|
||||
console.log(
|
||||
'Reset complete: truncated analytics_login_events and verification; dropped profile_id_fkey; deleted all \"user\" rows (account/session cascade).'
|
||||
);
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
|
||||
console.error('resetBetterAuthTables error:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreProfileUserFkey(pool: Pool) {
|
||||
console.log('restoreProfileUserFkey');
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const orphanResult = await client.query(`
|
||||
SELECT COUNT(*)::bigint AS cnt
|
||||
FROM profile p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "user" u
|
||||
WHERE u.id = p.id
|
||||
)
|
||||
`);
|
||||
const orphanCount = Number(orphanResult.rows[0]?.cnt ?? 0);
|
||||
|
||||
if (orphanCount > 0) {
|
||||
throw new Error(
|
||||
`Cannot re-add profile_id_fkey: ${orphanCount} profile row(s) have no matching \"user\". Remove orphan profiles or create missing users before re-running.`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await client.query(`
|
||||
ALTER TABLE profile
|
||||
ADD CONSTRAINT profile_id_fkey FOREIGN KEY (id) REFERENCES "user"(id)
|
||||
`);
|
||||
console.log('Restored constraint profile_id_fkey on profile(id) -> \"user\"(id).');
|
||||
} catch (addConstraintError: unknown) {
|
||||
const pgError = addConstraintError as { code?: string; message?: string };
|
||||
|
||||
// Avoid failing if FK already exists from a retry or interrupted run
|
||||
|
||||
if (pgError.code === '42710' || pgError.message?.includes('already exists')) {
|
||||
console.warn('profile_id_fkey already present; skipping ADD CONSTRAINT.', pgError.message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw addConstraintError;
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// We create a custom betterAuth here because this code works well with pg
|
||||
const auth = betterAuth({
|
||||
database: new Pool({
|
||||
@@ -25,7 +114,32 @@ const auth = betterAuth({
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
|
||||
}
|
||||
},
|
||||
plugins: [admin(), anonymous()],
|
||||
plugins: [
|
||||
admin({
|
||||
schema: {
|
||||
user: {
|
||||
fields: {
|
||||
banReason: 'ban_reason',
|
||||
banExpires: 'ban_expires'
|
||||
}
|
||||
},
|
||||
session: {
|
||||
fields: {
|
||||
impersonatedBy: 'impersonated_by'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
anonymous({
|
||||
schema: {
|
||||
user: {
|
||||
fields: {
|
||||
isAnonymous: 'is_anonymous'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
|
||||
user: {
|
||||
modelName: 'user',
|
||||
@@ -99,7 +213,8 @@ type User = SupabaseUser & {
|
||||
updated_at: string;
|
||||
is_anonymous: boolean;
|
||||
profile_fullname: string;
|
||||
identities: {
|
||||
identities:
|
||||
| {
|
||||
id: string; // auth.identities.id (UUID) - use as account.id so DB accepts it
|
||||
provider: string;
|
||||
identity_data: {
|
||||
@@ -108,15 +223,52 @@ type User = SupabaseUser & {
|
||||
};
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}[]
|
||||
| string;
|
||||
};
|
||||
};
|
||||
|
||||
function parseIdentities(value: User['identities']) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value as string);
|
||||
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const migrateFromSupabase = async () => {
|
||||
const ctx = await auth.$context;
|
||||
const db = ctx.options.database;
|
||||
const users = await db
|
||||
.query(
|
||||
`
|
||||
const db = ctx.options.database as Pool;
|
||||
|
||||
const effectiveReset = RESET_FLAG && !BACKFILL_ACCOUNTS_ONLY;
|
||||
|
||||
if (effectiveReset) {
|
||||
await resetBetterAuthTables(db);
|
||||
}
|
||||
|
||||
const migrationWhere = `
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "user" existing_user
|
||||
WHERE existing_user.id = u.id
|
||||
)
|
||||
`;
|
||||
|
||||
const backfillAccountsWhere = `
|
||||
INNER JOIN "user" existing_ba_user ON existing_ba_user.id = u.id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM account a
|
||||
WHERE a.user_id = u.id
|
||||
)
|
||||
`;
|
||||
|
||||
const usersQuery = `
|
||||
SELECT
|
||||
u.*,
|
||||
COALESCE(
|
||||
@@ -129,23 +281,29 @@ const migrateFromSupabase = async () => {
|
||||
FROM auth.users u
|
||||
LEFT JOIN auth.identities i ON u.id = i.user_id
|
||||
LEFT JOIN public.profile p ON u.id = p.id -- LEFT JOIN with public.profile
|
||||
${BACKFILL_ACCOUNTS_ONLY ? backfillAccountsWhere : migrationWhere}
|
||||
GROUP BY u.id, p.fullname
|
||||
`
|
||||
)
|
||||
.then((res: { rows: User[] }) => res.rows);
|
||||
`;
|
||||
|
||||
console.log('users length', users.length);
|
||||
const users = await db.query(usersQuery).then((res: { rows: User[] }) => res.rows);
|
||||
|
||||
console.log(
|
||||
BACKFILL_ACCOUNTS_ONLY
|
||||
? 'users needing account backfill (have "user", no account rows)'
|
||||
: 'users to migrate (not yet in "user")',
|
||||
users.length
|
||||
);
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
continue;
|
||||
}
|
||||
if (!BACKFILL_ACCOUNTS_ONLY) {
|
||||
const userName = user.profile_fullname || user.email;
|
||||
console.log('inserting user', user.email);
|
||||
|
||||
let shouldContinue = false;
|
||||
await ctx.adapter
|
||||
.create({
|
||||
try {
|
||||
await ctx.adapter.create({
|
||||
model: 'user',
|
||||
forceAllowId: true,
|
||||
data: {
|
||||
@@ -158,23 +316,23 @@ const migrateFromSupabase = async () => {
|
||||
createdAt: new Date(user.created_at),
|
||||
updatedAt: new Date(user.updated_at)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('catch');
|
||||
shouldContinue = true;
|
||||
});
|
||||
|
||||
if (shouldContinue) {
|
||||
console.log('Continue');
|
||||
// continue;
|
||||
} catch (createUserError) {
|
||||
console.error('create user failed, skipping identities', user.email, createUserError);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.log('backfilling accounts only for', user.email);
|
||||
}
|
||||
|
||||
for (const identity of user.identities) {
|
||||
const identities = parseIdentities(user.identities);
|
||||
|
||||
for (const identity of identities) {
|
||||
const existingAccounts = await ctx.internalAdapter.findAccounts(user.id);
|
||||
|
||||
if (identity.provider === 'email') {
|
||||
console.log('email provider');
|
||||
const hasCredential = existingAccounts.find((account) => account.providerId === 'credential');
|
||||
const hasCredential = existingAccounts.find((accountRow) => accountRow.providerId === 'credential');
|
||||
console.log('hasCredential', hasCredential);
|
||||
if (!hasCredential) {
|
||||
await ctx.adapter
|
||||
@@ -198,7 +356,7 @@ const migrateFromSupabase = async () => {
|
||||
}
|
||||
const supportedProviders = Object.keys(ctx.options.socialProviders || {});
|
||||
if (supportedProviders.includes(identity.provider)) {
|
||||
const hasAccount = existingAccounts.find((account) => account.providerId === identity.provider);
|
||||
const hasAccount = existingAccounts.find((accountRow) => accountRow.providerId === identity.provider);
|
||||
if (!hasAccount) {
|
||||
await ctx.adapter.create({
|
||||
model: 'account',
|
||||
@@ -216,6 +374,13 @@ const migrateFromSupabase = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveReset) {
|
||||
await restoreProfileUserFkey(db);
|
||||
}
|
||||
};
|
||||
|
||||
migrateFromSupabase();
|
||||
migrateFromSupabase().catch((migrationError) => {
|
||||
console.error('migrateFromSupabase failed:', migrationError);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user