fix: deployment errors
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"svelte": {
|
"svelte": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
},
|
||||||
|
"supabase": {
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { BaseApi, classroomio } from '$lib/utils/services/api';
|
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 { currentOrg, mergeAccountOrgFromServer, orgs } from '$lib/utils/store/org';
|
||||||
import { defaultProfileState, defaultUserState, profile, user } from '$lib/utils/store/user';
|
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 { goto } from '$app/navigation';
|
||||||
import { handleLocaleChange } from '$lib/utils/functions/translations';
|
import { handleLocaleChange } from '$lib/utils/functions/translations';
|
||||||
import { identifyPosthogUser } from '$lib/utils/services/posthog';
|
import { identifyPosthogUser } from '$lib/utils/services/posthog';
|
||||||
|
import { isOrgStudent } from '$lib/utils/store/app';
|
||||||
import { isPublicRoute } from '$lib/utils/functions/routes/isPublicRoute';
|
import { isPublicRoute } from '$lib/utils/functions/routes/isPublicRoute';
|
||||||
import { licenseApi } from '$features/license/api/license.svelte';
|
import { licenseApi } from '$features/license/api/license.svelte';
|
||||||
import { logout } from '$lib/utils/functions/logout';
|
import { logout } from '$lib/utils/functions/logout';
|
||||||
|
|||||||
@@ -56,7 +56,10 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!data.isOrgSite || !data.org) {
|
if (!data.isOrgSite || !data.org) {
|
||||||
if (!appInitApi.loading) {
|
if (!appInitApi.loading) {
|
||||||
appInitApi.routeUserToNextPage({ isOrgSite: false, orgSiteName: '' });
|
appInitApi.setupApp(data.locals, {
|
||||||
|
isOrgSite: data.isOrgSite,
|
||||||
|
orgSiteName: data.orgSiteName
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
"postinstall": "test -d .git && lefthook install || true",
|
"postinstall": "test -d .git && lefthook install || true",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"db:reset": "pnpm --filter @cio/db db:reset",
|
"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",
|
"api:dev": "pnpm --filter=@cio/api --filter=@cio/jobs-worker run dev",
|
||||||
"jobs:dev": "pnpm --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\"",
|
"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",
|
"rootDir": "./src",
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true
|
"declarationMap": true
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
backups/
|
||||||
|
|
||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -24,7 +24,12 @@
|
|||||||
"generate-types": "tsx scripts/generate-types.ts && pnpm format",
|
"generate-types": "tsx scripts/generate-types.ts && pnpm format",
|
||||||
"seed": "tsx src/scripts/seed.ts",
|
"seed": "tsx src/scripts/seed.ts",
|
||||||
"format": "prettier . --write",
|
"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": {
|
"dependencies": {
|
||||||
"@better-auth/sso": "^1.4.18",
|
"@better-auth/sso": "^1.4.18",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
CREATE TABLE "account" (
|
CREATE TABLE "public"."account" (
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
"account_id" text NOT NULL,
|
"account_id" text NOT NULL,
|
||||||
"provider_id" text NOT NULL,
|
"provider_id" text NOT NULL,
|
||||||
@@ -14,7 +14,7 @@ CREATE TABLE "account" (
|
|||||||
"updated_at" timestamp NOT NULL
|
"updated_at" timestamp NOT NULL
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "session" (
|
CREATE TABLE "public"."session" (
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
"expires_at" timestamp NOT NULL,
|
"expires_at" timestamp NOT NULL,
|
||||||
"token" text NOT NULL,
|
"token" text NOT NULL,
|
||||||
@@ -27,7 +27,7 @@ CREATE TABLE "session" (
|
|||||||
CONSTRAINT "session_token_unique" UNIQUE("token")
|
CONSTRAINT "session_token_unique" UNIQUE("token")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "user" (
|
CREATE TABLE "public"."user" (
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
"name" text NOT NULL,
|
"name" text NOT NULL,
|
||||||
"email" text NOT NULL,
|
"email" text NOT NULL,
|
||||||
@@ -43,5 +43,5 @@ CREATE TABLE "user" (
|
|||||||
CONSTRAINT "user_email_unique" UNIQUE("email")
|
CONSTRAINT "user_email_unique" UNIQUE("email")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> 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 "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 "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"."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,
|
"id" uuid PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
|
||||||
"identifier" text NOT NULL,
|
"identifier" text NOT NULL,
|
||||||
"value" text NOT NULL,
|
"value" text NOT NULL,
|
||||||
@@ -7,16 +7,14 @@ CREATE TABLE IF NOT EXISTS "verification" (
|
|||||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_id_fk";--> statement-breakpoint
|
ALTER TABLE "public"."account" DROP CONSTRAINT IF EXISTS "account_user_id_user_id_fk";--> statement-breakpoint
|
||||||
ALTER TABLE "account" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
ALTER TABLE "public"."session" DROP CONSTRAINT IF EXISTS "session_user_id_user_id_fk";--> statement-breakpoint
|
||||||
ALTER TABLE "account" ALTER COLUMN "user_id" SET DATA TYPE uuid USING user_id::uuid;--> statement-breakpoint
|
ALTER TABLE "public"."user" ALTER COLUMN "id" SET DATA TYPE uuid USING 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 "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 "user" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
ALTER TABLE "public"."account" ALTER COLUMN "user_id" SET DATA TYPE uuid USING user_id::uuid;--> statement-breakpoint
|
||||||
ALTER TABLE "user" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> 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 "session" DROP CONSTRAINT "session_user_id_user_id_fk";--> statement-breakpoint
|
ALTER TABLE "public"."session" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> statement-breakpoint
|
||||||
ALTER TABLE "session" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
ALTER TABLE "public"."session" ALTER COLUMN "user_id" SET DATA TYPE uuid USING user_id::uuid;--> statement-breakpoint
|
||||||
ALTER TABLE "session" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4();--> 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
|
||||||
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
|
|
||||||
|
|||||||
@@ -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),
|
"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,
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
"title" text,
|
"title" text,
|
||||||
@@ -7,237 +7,300 @@ CREATE TABLE IF NOT EXISTS "exercise_template" (
|
|||||||
"questionnaire" jsonb DEFAULT '{}'::jsonb
|
"questionnaire" jsonb DEFAULT '{}'::jsonb
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
ALTER TABLE "analytics_login_events" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."analytics_login_events" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "apps_poll" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."apps_poll" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "apps_poll_option" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."apps_poll_option" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "apps_poll_submission" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."apps_poll_submission" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "community_answer" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."community_answer" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "community_question" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."community_question" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "course" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."course" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "course_newsfeed" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."course_newsfeed" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "course_newsfeed_comment" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."course_newsfeed_comment" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "lesson_section" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."lesson_section" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "exercise" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."course_section" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "group" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."exercise" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "group_attendance" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."group" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "groupmember" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."group_attendance" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "lesson" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."groupmember" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "lesson_comment" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."lesson" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "lesson_completion" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."lesson_comment" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "lesson_language" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."lesson_completion" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "lesson_language_history" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."lesson_language" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "option" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."lesson_language_history" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "organization" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."option" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "organization_contacts" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."organization" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "organization_emaillist" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."organization_contacts" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "organization_plan" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."organization_emaillist" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "organizationmember" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."organization_plan" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "profile" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."organizationmember" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "question" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."profile" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "question_answer" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."question" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "question_type" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."question_answer" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "quiz" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."question_type" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "quiz_play" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."quiz" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "role" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."quiz_play" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "submission" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."role" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "submissionstatus" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."submission" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "lesson_section" RENAME TO "course_section";--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."submissionstatus" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
ALTER TABLE "course_newsfeed_comment" DROP CONSTRAINT "course_newsfeed_comment_id_key";--> statement-breakpoint
|
ALTER TABLE "public"."lesson" DROP CONSTRAINT IF EXISTS "public_lesson_section_id_fkey";
|
||||||
ALTER TABLE "lesson" DROP CONSTRAINT "public_lesson_section_id_fkey";
|
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
ALTER TABLE "course_section" DROP CONSTRAINT "public_lesson_section_course_id_fkey";
|
ALTER TABLE IF EXISTS "public"."lesson_section" DROP CONSTRAINT IF EXISTS "public_lesson_section_course_id_fkey";--> statement-breakpoint
|
||||||
--> statement-breakpoint
|
DO $migrate_rename_lesson_section_to_course_section$ BEGIN
|
||||||
ALTER TABLE "account" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
IF EXISTS (
|
||||||
ALTER TABLE "account" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
SELECT 1
|
||||||
ALTER TABLE "analytics_login_events" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
FROM pg_class c
|
||||||
ALTER TABLE "community_answer" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
ALTER TABLE "course" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
WHERE n.nspname = 'public' AND c.relkind = 'r' AND c.relname = 'lesson_section'
|
||||||
ALTER TABLE "exercise" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
) AND NOT EXISTS (
|
||||||
ALTER TABLE "group" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
SELECT 1
|
||||||
ALTER TABLE "groupmember" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
FROM pg_class c2
|
||||||
ALTER TABLE "lesson" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
|
||||||
ALTER TABLE "option" ALTER COLUMN "value" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
WHERE n2.nspname = 'public' AND c2.relkind = 'r' AND c2.relname = 'course_section'
|
||||||
ALTER TABLE "organization" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
) THEN
|
||||||
ALTER TABLE "question" ALTER COLUMN "name" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
ALTER TABLE public.lesson_section RENAME TO course_section;
|
||||||
ALTER TABLE "quiz" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
END IF;
|
||||||
ALTER TABLE "session" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
END;
|
||||||
ALTER TABLE "submission" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
$migrate_rename_lesson_section_to_course_section$;--> statement-breakpoint
|
||||||
ALTER TABLE "user" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
ALTER TABLE IF EXISTS "public"."course_newsfeed_comment" DROP CONSTRAINT IF EXISTS "course_newsfeed_comment_id_key";--> statement-breakpoint
|
||||||
ALTER TABLE "verification" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
ALTER TABLE "public"."account" 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 "public"."account" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
ALTER TABLE "exercise" ADD COLUMN "course_id" uuid;--> statement-breakpoint
|
ALTER TABLE "public"."analytics_login_events" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
ALTER TABLE "exercise" ADD COLUMN "section_id" uuid;--> statement-breakpoint
|
ALTER TABLE "public"."community_answer" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
ALTER TABLE "exercise" ADD COLUMN "order" bigint;--> statement-breakpoint
|
ALTER TABLE "public"."course" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
ALTER TABLE "exercise" ADD COLUMN "is_unlocked" boolean DEFAULT true;--> statement-breakpoint
|
ALTER TABLE "public"."exercise" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> 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 "public"."group" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> 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 "public"."groupmember" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> 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 "public"."lesson" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> 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 "public"."option" ALTER COLUMN "value" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
ALTER TABLE "course" DROP COLUMN "version";--> statement-breakpoint
|
ALTER TABLE "public"."organization" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Enable delete for users based on user_id" ON "analytics_login_events" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."question" ALTER COLUMN "name" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Users can insert their own login events" ON "analytics_login_events" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."quiz" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Delete only your own poll" ON "apps_poll" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."session" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Update only your own" ON "apps_poll" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."submission" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "apps_poll" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."user" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be course member to SELECT" ON "apps_poll" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."verification" ALTER COLUMN "id" SET DATA TYPE uuid USING id::uuid;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "apps_poll_option" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."verification" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be a course member to UPDATE" ON "apps_poll_option" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."exercise" ADD COLUMN "course_id" uuid;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be a teacher to DELETE" ON "apps_poll_option" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."exercise" ADD COLUMN "section_id" uuid;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be course member to SELECT" ON "apps_poll_option" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."exercise" ADD COLUMN "order" bigint;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Authenticated users can read" ON "apps_poll_submission" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."exercise" ADD COLUMN "is_unlocked" boolean DEFAULT true;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Delete your own submission" ON "apps_poll_submission" CASCADE;--> statement-breakpoint
|
DO $add_exercise_course_fk$
|
||||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "apps_poll_submission" CASCADE;--> statement-breakpoint
|
BEGIN
|
||||||
DROP POLICY IF EXISTS "Update your own submission" ON "apps_poll_submission" CASCADE;--> statement-breakpoint
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'exercise' AND schemaname = 'public')
|
||||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "community_answer" CASCADE;--> statement-breakpoint
|
AND NOT EXISTS (
|
||||||
DROP POLICY IF EXISTS "Delete your own answer" ON "community_answer" CASCADE;--> statement-breakpoint
|
SELECT 1 FROM pg_constraint co
|
||||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "community_answer" CASCADE;--> statement-breakpoint
|
JOIN pg_class rel ON rel.oid = co.conrelid
|
||||||
DROP POLICY IF EXISTS "Update your own answer" ON "community_answer" CASCADE;--> statement-breakpoint
|
JOIN pg_namespace n ON n.oid = rel.relnamespace
|
||||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "community_question" CASCADE;--> statement-breakpoint
|
WHERE n.nspname = 'public' AND rel.relname = 'exercise' AND co.contype = 'f' AND co.conname = 'exercise_course_id_fkey'
|
||||||
DROP POLICY IF EXISTS "Delete your own question" ON "community_question" CASCADE;--> statement-breakpoint
|
) THEN
|
||||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "community_question" CASCADE;--> statement-breakpoint
|
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;
|
||||||
DROP POLICY IF EXISTS "Update your own question" ON "community_question" CASCADE;--> statement-breakpoint
|
END IF;
|
||||||
DROP POLICY IF EXISTS "Enable access to all users if PUBLIC or to course members when " ON "course" CASCADE;--> statement-breakpoint
|
END;
|
||||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "course" CASCADE;--> statement-breakpoint
|
$add_exercise_course_fk$;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "course" CASCADE;--> statement-breakpoint
|
DO $add_exercise_section_fk$
|
||||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "course" CASCADE;--> statement-breakpoint
|
BEGIN
|
||||||
DROP POLICY IF EXISTS "Delete your own comment" ON "course_newsfeed" CASCADE;--> statement-breakpoint
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'exercise' AND schemaname = 'public')
|
||||||
DROP POLICY IF EXISTS "Update only your own" ON "course_newsfeed" CASCADE;--> statement-breakpoint
|
AND NOT EXISTS (
|
||||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "course_newsfeed" CASCADE;--> statement-breakpoint
|
SELECT 1 FROM pg_constraint co
|
||||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "course_newsfeed" CASCADE;--> statement-breakpoint
|
JOIN pg_class rel ON rel.oid = co.conrelid
|
||||||
DROP POLICY IF EXISTS "Delete your own" ON "course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
JOIN pg_namespace n ON n.oid = rel.relnamespace
|
||||||
DROP POLICY IF EXISTS "Update only your own" ON "course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
WHERE n.nspname = 'public' AND rel.relname = 'exercise' AND co.contype = 'f' AND co.conname = 'exercise_section_id_fkey'
|
||||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
) THEN
|
||||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
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;
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "exercise" CASCADE;--> statement-breakpoint
|
END IF;
|
||||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "exercise" CASCADE;--> statement-breakpoint
|
END;
|
||||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "exercise" CASCADE;--> statement-breakpoint
|
$add_exercise_section_fk$;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "exercise" CASCADE;--> statement-breakpoint
|
DO $add_lesson_course_section_fk$
|
||||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "group" CASCADE;--> statement-breakpoint
|
BEGIN
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "group" CASCADE;--> statement-breakpoint
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'lesson' AND schemaname = 'public')
|
||||||
DROP POLICY IF EXISTS "Only org admins can delete" ON "group" CASCADE;--> statement-breakpoint
|
AND NOT EXISTS (
|
||||||
DROP POLICY IF EXISTS "Only org admins can update" ON "group" CASCADE;--> statement-breakpoint
|
SELECT 1 FROM pg_constraint co
|
||||||
DROP POLICY IF EXISTS "User must be a course member to INSERT" ON "group_attendance" CASCADE;--> statement-breakpoint
|
JOIN pg_class rel ON rel.oid = co.conrelid
|
||||||
DROP POLICY IF EXISTS "User must be a course member to SELECT" ON "group_attendance" CASCADE;--> statement-breakpoint
|
JOIN pg_namespace n ON n.oid = rel.relnamespace
|
||||||
DROP POLICY IF EXISTS "User must be a course member to UPDATE" ON "group_attendance" CASCADE;--> statement-breakpoint
|
WHERE n.nspname = 'public' AND rel.relname = 'lesson' AND co.contype = 'f' AND co.conname = 'public_course_section_id_fkey'
|
||||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "group_attendance" CASCADE;--> statement-breakpoint
|
) THEN
|
||||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "groupmember" CASCADE;--> statement-breakpoint
|
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;
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "groupmember" CASCADE;--> statement-breakpoint
|
END IF;
|
||||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "groupmember" CASCADE;--> statement-breakpoint
|
END;
|
||||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "groupmember" CASCADE;--> statement-breakpoint
|
$add_lesson_course_section_fk$;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "lesson" CASCADE;--> statement-breakpoint
|
DO $add_course_section_course_fk$
|
||||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "lesson" CASCADE;--> statement-breakpoint
|
BEGIN
|
||||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "lesson" CASCADE;--> statement-breakpoint
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'course_section' AND schemaname = 'public')
|
||||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "lesson" CASCADE;--> statement-breakpoint
|
AND NOT EXISTS (
|
||||||
DROP POLICY IF EXISTS "Delete only your own comment" ON "lesson_comment" CASCADE;--> statement-breakpoint
|
SELECT 1 FROM pg_constraint co
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "lesson_comment" CASCADE;--> statement-breakpoint
|
JOIN pg_class rel ON rel.oid = co.conrelid
|
||||||
DROP POLICY IF EXISTS "Update only your own" ON "lesson_comment" CASCADE;--> statement-breakpoint
|
JOIN pg_namespace n ON n.oid = rel.relnamespace
|
||||||
DROP POLICY IF EXISTS "User must be in course group to INSERT" ON "lesson_comment" CASCADE;--> statement-breakpoint
|
WHERE n.nspname = 'public' AND rel.relname = 'course_section' AND co.contype = 'f' AND co.conname = 'public_course_section_course_id_fkey'
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "lesson_completion" CASCADE;--> statement-breakpoint
|
) THEN
|
||||||
DROP POLICY IF EXISTS "User must be an course member or Admin to perform ALL operation" ON "lesson_completion" CASCADE;--> statement-breakpoint
|
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;
|
||||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "lesson_completion" CASCADE;--> statement-breakpoint
|
END IF;
|
||||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "lesson_completion" CASCADE;--> statement-breakpoint
|
END;
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "lesson_language" CASCADE;--> statement-breakpoint
|
$add_course_section_course_fk$;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "lesson_language" CASCADE;--> statement-breakpoint
|
ALTER TABLE "public"."course" DROP COLUMN "version";--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "lesson_language" CASCADE;--> 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 "User must be an org member to UPDATE" ON "lesson_language" 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 "Authenticated users can SELECT" ON "lesson_language_history" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Delete only your own poll" ON "public"."apps_poll" 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 "Update only your own" ON "public"."apps_poll" 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 a course member to INSERT" ON "public"."apps_poll" 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 "User must be course member to SELECT" ON "public"."apps_poll" 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 a course member to INSERT" ON "public"."apps_poll_option" 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 a course member to UPDATE" ON "public"."apps_poll_option" 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 a teacher to DELETE" ON "public"."apps_poll_option" 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 "User must be course member to SELECT" ON "public"."apps_poll_option" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Allow authenticated users to SELECT" ON "option" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Authenticated users can read" ON "public"."apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be an org member to DELETE" ON "option" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Delete your own submission" ON "public"."apps_poll_submission" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be an org member to INSERT" ON "option" 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 "User must be an org member to UPDATE" ON "option" 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 delete" ON "organization" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "public"."community_answer" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "organization" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Delete your own answer" ON "public"."community_answer" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "organization" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."community_answer" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be admin to UPDATE" ON "organization" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Update your own answer" ON "public"."community_answer" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "organization_contacts" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "public"."community_question" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "organization_emaillist" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Delete your own question" ON "public"."community_question" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "organization_plan" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."community_question" 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 "Update your own question" ON "public"."community_question" 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 "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 UPDATE" ON "organization_plan" 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 "Allow authenticated users to read." ON "organizationmember" 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 "Enable insert for authenticated users only" ON "organizationmember" 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 "Only admin can delete" ON "organizationmember" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Delete your own comment" ON "public"."course_newsfeed" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Only admin can update" ON "organizationmember" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Update only your own" ON "public"."course_newsfeed" 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 a course member to INSERT" ON "public"."course_newsfeed" 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 "User must be a course member to SELECT" ON "public"."course_newsfeed" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Only auth users can read profile" ON "profile" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Delete your own" ON "public"."course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Public profiles are viewable by everyone." ON "profile" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Update only your own" ON "public"."course_newsfeed_comment" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User can only delete their profiles" ON "profile" 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 "Users can insert their own profile." ON "profile" 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 "Users can update own profile." ON "profile" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."exercise" 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 "public"."exercise" 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 "public"."exercise" 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 "public"."exercise" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "User must be an org member to UPDATE" ON "question" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."group" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Only authenticated users can select." ON "question_answer" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."group" 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 "Only org admins can delete" ON "public"."group" 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 "Only org admins can update" ON "public"."group" 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 "User must be a course member to INSERT" ON "public"."group_attendance" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Enable read access for all users" ON "question_type" 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 "Authenticated users can SELECT" ON "quiz" 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 "Authenticated users can SELECT" ON "quiz_play" 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 read access for all users" ON "role" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON "public"."groupmember" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Only authenticated users can SELECT" ON "submission" 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 a course member to DELETE" ON "submission" 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 a course member to INSERT" ON "submission" 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 "User must be a course member to UPDATE" ON "submission" CASCADE;--> statement-breakpoint
|
DROP POLICY IF EXISTS "Enable read access for all users" ON "public"."lesson" CASCADE;--> statement-breakpoint
|
||||||
DROP POLICY IF EXISTS "Authenticated users can SELECT" ON "submissionstatus" 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";
|
DROP TYPE "public"."COURSE_VERSION";
|
||||||
--> statement-breakpoint
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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
|
--> 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);
|
||||||
+16
-29
@@ -272,27 +272,18 @@ export const analyticsCountryDaily = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const courseSection = pgTable(
|
export const courseSection = pgTable('course_section', {
|
||||||
'course_section',
|
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||||
{
|
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
||||||
id: uuid().defaultRandom().primaryKey().notNull(),
|
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
title: varchar(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow(),
|
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||||
title: varchar(),
|
order: bigint({ mode: 'number' }).default(sql`'0'`),
|
||||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
courseId: uuid('course_id').references(() => course.id, {
|
||||||
order: bigint({ mode: 'number' }).default(sql`'0'`),
|
onDelete: 'cascade',
|
||||||
courseId: uuid('course_id')
|
onUpdate: 'cascade'
|
||||||
},
|
})
|
||||||
(table) => [
|
});
|
||||||
foreignKey({
|
|
||||||
columns: [table.courseId],
|
|
||||||
foreignColumns: [course.id],
|
|
||||||
name: 'public_course_section_course_id_fkey'
|
|
||||||
})
|
|
||||||
.onUpdate('cascade')
|
|
||||||
.onDelete('cascade')
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export const group = pgTable(
|
export const group = pgTable(
|
||||||
'group',
|
'group',
|
||||||
@@ -1001,7 +992,10 @@ export const lesson = pgTable(
|
|||||||
assetId?: string;
|
assetId?: string;
|
||||||
}[]
|
}[]
|
||||||
>(),
|
>(),
|
||||||
sectionId: uuid('section_id'),
|
sectionId: uuid('section_id').references(() => courseSection.id, {
|
||||||
|
onDelete: 'cascade',
|
||||||
|
onUpdate: 'cascade'
|
||||||
|
}),
|
||||||
slug: varchar()
|
slug: varchar()
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
@@ -1015,13 +1009,6 @@ export const lesson = pgTable(
|
|||||||
foreignColumns: [profile.id],
|
foreignColumns: [profile.id],
|
||||||
name: 'lesson_teacher_id_fkey'
|
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)
|
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 { exercise, lesson } from '../schema';
|
||||||
|
|
||||||
import { db } from '../drizzle';
|
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';
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const backupDir = path.resolve(scriptDir, '../../backups');
|
||||||
|
|
||||||
|
const orderBatchSize = Number.parseInt(process.env.EXERCISE_BACKFILL_BATCH_SIZE ?? '200', 10) || 200;
|
||||||
|
|
||||||
|
/** Optional course scope: `pnpm db:exercise-backfill [courseId]` */
|
||||||
|
const scopedCourseId = process.argv[2]?.trim() || null;
|
||||||
|
|
||||||
|
type LessonRow = {
|
||||||
|
id: string;
|
||||||
|
courseId: string;
|
||||||
|
sectionId: string | null;
|
||||||
|
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() {
|
async function backfillExerciseCourse() {
|
||||||
try {
|
try {
|
||||||
const result = await setExerciseCourseAndSectionId();
|
const courseSectionResult = await setExerciseCourseAndSectionId(scopedCourseId);
|
||||||
|
const orderResult = await assignLinearContentOrder(scopedCourseId);
|
||||||
|
|
||||||
// TypeScript implementation of exercise ordering logic
|
console.log('Exercise course/section backfill completed', courseSectionResult);
|
||||||
// Algorithm:
|
console.log('Linear content order backfill completed', orderResult.summary);
|
||||||
// 1. Fetch each lesson (grouped by course_id, section_id, ordered by lesson order or created_at)
|
console.log('Report written to', orderResult.reportPath);
|
||||||
// 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
|
|
||||||
|
|
||||||
// Step 1: Fetch all lessons with their order (or calculate it from created_at)
|
if (orderResult.failures.length > 0) {
|
||||||
const allLessons = await db
|
console.error(`${orderResult.failures.length} batch(es) failed — see report for ids and errors.`);
|
||||||
.select({
|
process.exit(1);
|
||||||
id: lesson.id,
|
|
||||||
courseId: lesson.courseId,
|
|
||||||
sectionId: lesson.sectionId,
|
|
||||||
order: lesson.order,
|
|
||||||
createdAt: lesson.createdAt
|
|
||||||
})
|
|
||||||
.from(lesson);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
courseId: string;
|
|
||||||
sectionId: string | null;
|
|
||||||
lessonOrder: number;
|
|
||||||
sortOffset: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to backfill exercise course data', 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`
|
return db.execute(sql`
|
||||||
UPDATE exercise AS e
|
UPDATE exercise AS e
|
||||||
SET course_id = l.course_id,
|
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();
|
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';
|
import 'dotenv/config';
|
||||||
@@ -9,6 +17,87 @@ import { admin, anonymous } from 'better-auth/plugins';
|
|||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
import { betterAuth } from 'better-auth';
|
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
|
// We create a custom betterAuth here because this code works well with pg
|
||||||
const auth = betterAuth({
|
const auth = betterAuth({
|
||||||
database: new Pool({
|
database: new Pool({
|
||||||
@@ -25,7 +114,32 @@ const auth = betterAuth({
|
|||||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
|
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: {
|
user: {
|
||||||
modelName: 'user',
|
modelName: 'user',
|
||||||
@@ -99,24 +213,62 @@ type User = SupabaseUser & {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
is_anonymous: boolean;
|
is_anonymous: boolean;
|
||||||
profile_fullname: string;
|
profile_fullname: string;
|
||||||
identities: {
|
identities:
|
||||||
id: string; // auth.identities.id (UUID) - use as account.id so DB accepts it
|
| {
|
||||||
provider: string;
|
id: string; // auth.identities.id (UUID) - use as account.id so DB accepts it
|
||||||
identity_data: {
|
provider: string;
|
||||||
sub: string;
|
identity_data: {
|
||||||
email: string;
|
sub: string;
|
||||||
};
|
email: string;
|
||||||
created_at: string;
|
};
|
||||||
updated_at: string;
|
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 migrateFromSupabase = async () => {
|
||||||
const ctx = await auth.$context;
|
const ctx = await auth.$context;
|
||||||
const db = ctx.options.database;
|
const db = ctx.options.database as Pool;
|
||||||
const users = await db
|
|
||||||
.query(
|
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
|
SELECT
|
||||||
u.*,
|
u.*,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
@@ -129,52 +281,58 @@ const migrateFromSupabase = async () => {
|
|||||||
FROM auth.users u
|
FROM auth.users u
|
||||||
LEFT JOIN auth.identities i ON u.id = i.user_id
|
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
|
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
|
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) {
|
for (const user of users) {
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const userName = user.profile_fullname || user.email;
|
if (!BACKFILL_ACCOUNTS_ONLY) {
|
||||||
console.log('inserting user', user.email);
|
const userName = user.profile_fullname || user.email;
|
||||||
|
console.log('inserting user', user.email);
|
||||||
|
|
||||||
let shouldContinue = false;
|
try {
|
||||||
await ctx.adapter
|
await ctx.adapter.create({
|
||||||
.create({
|
model: 'user',
|
||||||
model: 'user',
|
forceAllowId: true,
|
||||||
forceAllowId: true,
|
data: {
|
||||||
data: {
|
id: user.id,
|
||||||
id: user.id,
|
email: user.email,
|
||||||
email: user.email,
|
name: userName,
|
||||||
name: userName,
|
role: user.is_super_admin ? 'admin' : user.role,
|
||||||
role: user.is_super_admin ? 'admin' : user.role,
|
emailVerified: !!user.email_confirmed_at,
|
||||||
emailVerified: !!user.email_confirmed_at,
|
image: user.raw_user_meta_data?.avatar_url || '',
|
||||||
image: user.raw_user_meta_data?.avatar_url || '',
|
createdAt: new Date(user.created_at),
|
||||||
createdAt: new Date(user.created_at),
|
updatedAt: new Date(user.updated_at)
|
||||||
updatedAt: new Date(user.updated_at)
|
}
|
||||||
}
|
});
|
||||||
})
|
} catch (createUserError) {
|
||||||
.catch((e) => {
|
console.error('create user failed, skipping identities', user.email, createUserError);
|
||||||
console.log('catch');
|
continue;
|
||||||
shouldContinue = true;
|
}
|
||||||
});
|
} else {
|
||||||
|
console.log('backfilling accounts only for', user.email);
|
||||||
if (shouldContinue) {
|
|
||||||
console.log('Continue');
|
|
||||||
// continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const identity of user.identities) {
|
const identities = parseIdentities(user.identities);
|
||||||
|
|
||||||
|
for (const identity of identities) {
|
||||||
const existingAccounts = await ctx.internalAdapter.findAccounts(user.id);
|
const existingAccounts = await ctx.internalAdapter.findAccounts(user.id);
|
||||||
|
|
||||||
if (identity.provider === 'email') {
|
if (identity.provider === 'email') {
|
||||||
console.log('email provider');
|
console.log('email provider');
|
||||||
const hasCredential = existingAccounts.find((account) => account.providerId === 'credential');
|
const hasCredential = existingAccounts.find((accountRow) => accountRow.providerId === 'credential');
|
||||||
console.log('hasCredential', hasCredential);
|
console.log('hasCredential', hasCredential);
|
||||||
if (!hasCredential) {
|
if (!hasCredential) {
|
||||||
await ctx.adapter
|
await ctx.adapter
|
||||||
@@ -198,7 +356,7 @@ const migrateFromSupabase = async () => {
|
|||||||
}
|
}
|
||||||
const supportedProviders = Object.keys(ctx.options.socialProviders || {});
|
const supportedProviders = Object.keys(ctx.options.socialProviders || {});
|
||||||
if (supportedProviders.includes(identity.provider)) {
|
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) {
|
if (!hasAccount) {
|
||||||
await ctx.adapter.create({
|
await ctx.adapter.create({
|
||||||
model: 'account',
|
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