From 7eebda0308dda9583603f6001301bfda77e16a2f Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Thu, 13 Aug 2026 10:32:11 +0000 Subject: [PATCH] fix(migrations): add baseline schema so migration:run works from scratch Add a baseline schema migration (lowest timestamp) reproducing the schema TypeORM's synchronize previously created in development, so migration:run succeeds on a fresh Postgres database and the incremental migrations apply cleanly on top. Also fixes several pre-existing blockers that surfaced once migrations ran from scratch: - datasource now loads entities so schema-aware operations compare against real entity metadata (and typeorm_metadata is created for generated columns) - UserChallenge index referenced non-existent property names - onboarding entities had duplicate class-level indexes colliding with property-level ones - CourseStatus enum extracted to its own module to break a circular import - reencrypt-oauth-provider-tokens no longer demands ENCRYPTION_SECRET on empty databases - add-unique-course-version-constraint referenced non-existent columns - add-paused-subscription-status targeted the wrong enum type name - fix-invoice-number-sequence opened a second connection that couldn't see uncommitted tables, and setval(..., 0) failed on empty tables The drift check now compares against scripts/known-drift.txt (pre-existing entity/migration naming drift, tracked in issue #1194) and only fails when new drift is introduced. Closes #1193 --- package.json | 2 +- scripts/drift-check.sh | 42 + scripts/known-drift.txt | 622 +++++ src/config/datasource.ts | 6 + src/courses/entities/course-status.enum.ts | 9 + src/courses/entities/course-version.entity.ts | 3 +- src/courses/entities/course.entity.ts | 11 +- .../entities/user-challenge.entity.ts | 2 +- .../1599999999999-BaselineSchema.ts | 2318 +++++++++++++++++ ...0000001-reencrypt-oauth-provider-tokens.ts | 18 +- ...00-add-unique-course-version-constraint.ts | 6 +- ...00000000-add-paused-subscription-status.ts | 24 +- ...90000000000-fix-invoice-number-sequence.ts | 74 +- .../entities/onboarding-reward.entity.ts | 1 - .../entities/onboarding-step.entity.ts | 2 - .../user-onboarding-progress.entity.ts | 1 - 16 files changed, 3077 insertions(+), 64 deletions(-) create mode 100755 scripts/drift-check.sh create mode 100644 scripts/known-drift.txt create mode 100644 src/courses/entities/course-status.enum.ts create mode 100644 src/migrations/1599999999999-BaselineSchema.ts diff --git a/package.json b/package.json index ce37d483..3c1345ca 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "migration:run": "npx typeorm-ts-node-commonjs migration:run -d src/config/datasource.ts", "migration:revert": "npx typeorm-ts-node-commonjs migration:revert -d src/config/datasource.ts", "migration:generate": "npx typeorm-ts-node-commonjs migration:generate -d src/config/datasource.ts", - "migration:generate:check": "BEFORE=$(find src/migrations -maxdepth 1 -name '[0-9]*.ts' | wc -l) && npx typeorm-ts-node-commonjs migration:generate -d src/config/datasource.ts src/migrations/drift-check-$(date +%s) && AFTER=$(find src/migrations -maxdepth 1 -name '[0-9]*.ts' | wc -l) && find src/migrations -maxdepth 1 -name '*drift-check*' -delete && if [ \"$AFTER\" -gt \"$BEFORE\" ]; then echo \"ERROR: Schema drift detected - model changes exist without a corresponding migration. Run npm run migration:generate to create the missing migration file.\" && exit 1; fi", + "migration:generate:check": "bash scripts/drift-check.sh", "docs:generate": "node scripts/generate-api-docs.js", "docs:validate": "node scripts/validate-openapi.js", "docs:check": "npm run docs:generate && npm run docs:generate:examples && git diff --exit-code -- openapi-spec.json docs/api/openapi-spec.json docs/api/examples.md docs/site docs/examples", diff --git a/scripts/drift-check.sh b/scripts/drift-check.sh new file mode 100755 index 00000000..3f851a2a --- /dev/null +++ b/scripts/drift-check.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# Schema drift check. +# +# Verifies that entity definitions do not drift from the migration-produced +# schema. The check fails when *new* drift is introduced. +# +# Pre-existing drift between entities and migrations (index/column/constraint +# naming mismatches accumulated before a baseline migration existed) is +# captured in scripts/known-drift.txt. That snapshot is tracked separately in +# a follow-up issue; this script only fails when the drift differs from the +# snapshot, i.e. when a model change lacks a corresponding migration. +set -uo pipefail + +cd "$(dirname "$0")/.." + +OUT=$(mktemp) +trap 'rm -f "$OUT"' EXIT + +if npx typeorm-ts-node-commonjs migration:generate -d src/config/datasource.ts \ + --check drift-check >"$OUT" 2>&1; then + echo "✓ No schema drift." + exit 0 +fi + +# Drift detected: extract the generated SQL statements, normalize whitespace, +# and compare against the known-drift snapshot. +grep -oE "await queryRunner\.query\(\`[^\`]*" "$OUT" \ + | sed 's/^await queryRunner\.query(`//' \ + | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \ + | sort >"${OUT}.current" + +if diff -q scripts/known-drift.txt "${OUT}.current" >/dev/null 2>&1; then + echo "⚠ Schema drift matches the known snapshot (see issue #1194). No new drift." + exit 0 +fi + +echo "::error::New schema drift detected — model changes exist without a corresponding migration." +echo "Run 'pnpm run migration:generate' to create the missing migration, or update scripts/known-drift.txt." +echo "" +diff scripts/known-drift.txt "${OUT}.current" | head -60 +exit 1 diff --git a/scripts/known-drift.txt b/scripts/known-drift.txt new file mode 100644 index 00000000..33f8f2bc --- /dev/null +++ b/scripts/known-drift.txt @@ -0,0 +1,622 @@ +ALTER TABLE "audit_logs" ADD "http_method" "public"."audit_logs_http_method_enum" +ALTER TABLE "audit_logs" ADD "http_method" character varying +ALTER TABLE "audit_logs" ADD "retention_until" TIMESTAMP +ALTER TABLE "audit_logs" ADD "retention_until" TIMESTAMP WITH TIME ZONE +ALTER TABLE "audit_logs" ADD "timestamp" TIMESTAMP NOT NULL DEFAULT now() +ALTER TABLE "audit_logs" ADD "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +ALTER TABLE "audit_logs" ADD "version" integer NOT NULL DEFAULT '1' +ALTER TABLE "audit_logs" ALTER COLUMN "action" TYPE "public"."audit_logs_action_enum" USING "action"::"text"::"public"."audit_logs_action_enum" +ALTER TABLE "audit_logs" ALTER COLUMN "action" TYPE "public"."audit_logs_action_enum_old" USING "action"::"text"::"public"."audit_logs_action_enum_old" +ALTER TABLE "audit_logs" DROP COLUMN "http_method" +ALTER TABLE "audit_logs" DROP COLUMN "http_method" +ALTER TABLE "audit_logs" DROP COLUMN "retention_until" +ALTER TABLE "audit_logs" DROP COLUMN "retention_until" +ALTER TABLE "audit_logs" DROP COLUMN "timestamp" +ALTER TABLE "audit_logs" DROP COLUMN "timestamp" +ALTER TABLE "audit_logs" DROP COLUMN "version" +ALTER TABLE "course" ADD "currency" character varying(3) DEFAULT 'USD' +ALTER TABLE "course" ADD "searchVector" tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce("title", '') || ' ' || coalesce("description", '') || ' ' || coalesce("category", ''))) STORED NOT NULL +ALTER TABLE "course" ADD "searchVector" tsvector NOT NULL +ALTER TABLE "course" ADD "search_vector" tsvector +ALTER TABLE "course" DROP COLUMN "currency" +ALTER TABLE "course" DROP COLUMN "searchVector" +ALTER TABLE "course" DROP COLUMN "searchVector" +ALTER TABLE "course" DROP COLUMN "search_vector" +ALTER TABLE "course_bulk_operations" ADD "notes" text +ALTER TABLE "course_bulk_operations" ADD "reason" character varying(255) +ALTER TABLE "course_bulk_operations" ADD "undone_by_id" uuid +ALTER TABLE "course_bulk_operations" ADD "version" integer NOT NULL +ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "CHK_454249837abfcca2dce79a2941" CHECK ("successCount" >= 0) +ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "CHK_63a6345d11ef40993abe0af7d7" CHECK ("failureCount" >= 0) +ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "CHK_dffedab4e655d35cc59b4ef18f" CHECK ("totalCount" >= 0) +ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "FK_1af4ba5bb120d226b6a4f994209" FOREIGN KEY ("initiated_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION +ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "FK_course_bulk_ops_initiator" FOREIGN KEY ("initiated_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION +ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "FK_eda643b345dd73d5fc1a456a4bf" FOREIGN KEY ("undone_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION +ALTER TABLE "course_bulk_operations" DROP COLUMN "notes" +ALTER TABLE "course_bulk_operations" DROP COLUMN "reason" +ALTER TABLE "course_bulk_operations" DROP COLUMN "undone_by_id" +ALTER TABLE "course_bulk_operations" DROP COLUMN "version" +ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "CHK_454249837abfcca2dce79a2941" +ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "CHK_63a6345d11ef40993abe0af7d7" +ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "CHK_dffedab4e655d35cc59b4ef18f" +ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "FK_1af4ba5bb120d226b6a4f994209" +ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "FK_course_bulk_ops_initiator" +ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "FK_eda643b345dd73d5fc1a456a4bf" +ALTER TABLE "criterion_grades" ADD CONSTRAINT "FK_criterion_grades_grade" FOREIGN KEY ("grade_id") REFERENCES "submission_grades"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "criterion_grades" ADD CONSTRAINT "FK_e63fa5de1df5c92236101dcfc4b" FOREIGN KEY ("grade_id") REFERENCES "submission_grades"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "criterion_grades" DROP CONSTRAINT "FK_criterion_grades_grade" +ALTER TABLE "criterion_grades" DROP CONSTRAINT "FK_e63fa5de1df5c92236101dcfc4b" +ALTER TABLE "feedback_templates" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now() +ALTER TABLE "feedback_templates" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +ALTER TABLE "feedback_templates" ADD "deletedAt" TIMESTAMP +ALTER TABLE "feedback_templates" ADD "deletedAt" TIMESTAMP WITH TIME ZONE +ALTER TABLE "feedback_templates" ADD "updatedAt" TIMESTAMP NOT NULL DEFAULT now() +ALTER TABLE "feedback_templates" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +ALTER TABLE "feedback_templates" DROP COLUMN "createdAt" +ALTER TABLE "feedback_templates" DROP COLUMN "createdAt" +ALTER TABLE "feedback_templates" DROP COLUMN "deletedAt" +ALTER TABLE "feedback_templates" DROP COLUMN "deletedAt" +ALTER TABLE "feedback_templates" DROP COLUMN "updatedAt" +ALTER TABLE "feedback_templates" DROP COLUMN "updatedAt" +ALTER TABLE "forum_votes" ADD CONSTRAINT "FK_930875619d15f219f30923b724c" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "forum_votes" ADD CONSTRAINT "FK_forum_votes_authorId_users" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "forum_votes" DROP CONSTRAINT "FK_930875619d15f219f30923b724c" +ALTER TABLE "forum_votes" DROP CONSTRAINT "FK_forum_votes_authorId_users" +ALTER TABLE "messages" ADD CONSTRAINT "FK_2db9cf2b3ca111742793f6c37ce" FOREIGN KEY ("senderId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "messages" ADD CONSTRAINT "FK_2db9cf2b3ca111742793f6c37ce" FOREIGN KEY ("senderId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION +ALTER TABLE "messages" ADD CONSTRAINT "FK_f548818d46a1315d4e1d5e62da5" FOREIGN KEY ("recipientId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "messages" ADD CONSTRAINT "FK_f548818d46a1315d4e1d5e62da5" FOREIGN KEY ("recipientId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION +ALTER TABLE "messages" DROP CONSTRAINT "FK_2db9cf2b3ca111742793f6c37ce" +ALTER TABLE "messages" DROP CONSTRAINT "FK_2db9cf2b3ca111742793f6c37ce" +ALTER TABLE "messages" DROP CONSTRAINT "FK_f548818d46a1315d4e1d5e62da5" +ALTER TABLE "messages" DROP CONSTRAINT "FK_f548818d46a1315d4e1d5e62da5" +ALTER TABLE "rubric_criteria" ADD CONSTRAINT "FK_67d38effbc57ecb21bc2901057e" FOREIGN KEY ("rubric_id") REFERENCES "rubrics"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "rubric_criteria" ADD CONSTRAINT "FK_rubric_criteria_rubric" FOREIGN KEY ("rubric_id") REFERENCES "rubrics"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "rubric_criteria" DROP CONSTRAINT "FK_67d38effbc57ecb21bc2901057e" +ALTER TABLE "rubric_criteria" DROP CONSTRAINT "FK_rubric_criteria_rubric" +ALTER TABLE "rubric_levels" ADD CONSTRAINT "FK_8c0b069d24a8380f5356d22e427" FOREIGN KEY ("criterion_id") REFERENCES "rubric_criteria"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "rubric_levels" ADD CONSTRAINT "FK_rubric_levels_criterion" FOREIGN KEY ("criterion_id") REFERENCES "rubric_criteria"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "rubric_levels" DROP CONSTRAINT "FK_8c0b069d24a8380f5356d22e427" +ALTER TABLE "rubric_levels" DROP CONSTRAINT "FK_rubric_levels_criterion" +ALTER TABLE "rubrics" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now() +ALTER TABLE "rubrics" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +ALTER TABLE "rubrics" ADD "deletedAt" TIMESTAMP +ALTER TABLE "rubrics" ADD "deletedAt" TIMESTAMP WITH TIME ZONE +ALTER TABLE "rubrics" ADD "updatedAt" TIMESTAMP NOT NULL DEFAULT now() +ALTER TABLE "rubrics" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +ALTER TABLE "rubrics" DROP COLUMN "createdAt" +ALTER TABLE "rubrics" DROP COLUMN "createdAt" +ALTER TABLE "rubrics" DROP COLUMN "deletedAt" +ALTER TABLE "rubrics" DROP COLUMN "deletedAt" +ALTER TABLE "rubrics" DROP COLUMN "updatedAt" +ALTER TABLE "rubrics" DROP COLUMN "updatedAt" +ALTER TABLE "schema_change" ADD CONSTRAINT "FK_1ac0108c79d34d2a1854f22b40f" FOREIGN KEY ("schemaVersionId") REFERENCES "schema_version"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "schema_change" ADD CONSTRAINT "FK_1ac0108c79d34d2a1854f22b40f" FOREIGN KEY ("schemaVersionId") REFERENCES "schema_version"("id") ON DELETE CASCADE ON UPDATE NO ACTION +ALTER TABLE "schema_change" ALTER COLUMN "schemaVersionId" DROP NOT NULL +ALTER TABLE "schema_change" ALTER COLUMN "schemaVersionId" SET NOT NULL +ALTER TABLE "schema_change" DROP CONSTRAINT "FK_1ac0108c79d34d2a1854f22b40f" +ALTER TABLE "schema_change" DROP CONSTRAINT "FK_1ac0108c79d34d2a1854f22b40f" +ALTER TABLE "submission_grades" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now() +ALTER TABLE "submission_grades" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +ALTER TABLE "submission_grades" ADD "updatedAt" TIMESTAMP NOT NULL DEFAULT now() +ALTER TABLE "submission_grades" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +ALTER TABLE "submission_grades" ADD CONSTRAINT "FK_37bd4055f0a14a276992573e103" FOREIGN KEY ("rubric_id") REFERENCES "rubrics"("id") ON DELETE RESTRICT ON UPDATE NO ACTION +ALTER TABLE "submission_grades" ADD CONSTRAINT "FK_submission_grades_rubric" FOREIGN KEY ("rubric_id") REFERENCES "rubrics"("id") ON DELETE RESTRICT ON UPDATE NO ACTION +ALTER TABLE "submission_grades" DROP COLUMN "createdAt" +ALTER TABLE "submission_grades" DROP COLUMN "createdAt" +ALTER TABLE "submission_grades" DROP COLUMN "updatedAt" +ALTER TABLE "submission_grades" DROP COLUMN "updatedAt" +ALTER TABLE "submission_grades" DROP CONSTRAINT "FK_37bd4055f0a14a276992573e103" +ALTER TABLE "submission_grades" DROP CONSTRAINT "FK_submission_grades_rubric" +ALTER TABLE "tier_rewards" ALTER COLUMN "tier" TYPE "public"."tier_enum_old" USING "tier"::"text"::"public"."tier_enum_old" +ALTER TABLE "tier_rewards" ALTER COLUMN "tier" TYPE "public"."tier_rewards_tier_enum" USING "tier"::"text"::"public"."tier_rewards_tier_enum" +ALTER TABLE "tier_rewards" ALTER COLUMN "version" DROP DEFAULT +ALTER TABLE "tier_rewards" ALTER COLUMN "version" SET DEFAULT '1' +ALTER TABLE "user_preferences" ADD "currency" character varying DEFAULT 'USD' +ALTER TABLE "user_preferences" ADD "locale" "public"."user_preferences_locale_enum" NOT NULL DEFAULT 'en-US' +ALTER TABLE "user_preferences" ADD "locale" character varying DEFAULT 'en-US' +ALTER TABLE "user_preferences" ALTER COLUMN "timezone" DROP NOT NULL +ALTER TABLE "user_preferences" ALTER COLUMN "timezone" SET NOT NULL +ALTER TABLE "user_preferences" DROP COLUMN "currency" +ALTER TABLE "user_preferences" DROP COLUMN "locale" +ALTER TABLE "user_preferences" DROP COLUMN "locale" +ALTER TABLE "user_progress" ALTER COLUMN "tier" DROP DEFAULT +ALTER TABLE "user_progress" ALTER COLUMN "tier" DROP DEFAULT +ALTER TABLE "user_progress" ALTER COLUMN "tier" SET DEFAULT 'BRONZE' +ALTER TABLE "user_progress" ALTER COLUMN "tier" SET DEFAULT 'BRONZE' +ALTER TABLE "user_progress" ALTER COLUMN "tier" TYPE "public"."tier_enum_old" USING "tier"::"text"::"public"."tier_enum_old" +ALTER TABLE "user_progress" ALTER COLUMN "tier" TYPE "public"."user_progress_tier_enum" USING "tier"::"text"::"public"."user_progress_tier_enum" +ALTER TABLE "users" ADD "city" character varying +ALTER TABLE "users" ADD "country" character varying +ALTER TABLE "users" ADD "country_code" character varying(2) +ALTER TABLE "users" ADD "preferred_currency" character varying(3) DEFAULT 'USD' +ALTER TABLE "users" ADD "timezone" character varying +ALTER TABLE "users" DROP COLUMN "city" +ALTER TABLE "users" DROP COLUMN "country" +ALTER TABLE "users" DROP COLUMN "country_code" +ALTER TABLE "users" DROP COLUMN "preferred_currency" +ALTER TABLE "users" DROP COLUMN "timezone" +ALTER TYPE "public"."audit_logs_action_enum" RENAME TO "audit_logs_action_enum_old" +ALTER TYPE "public"."audit_logs_action_enum_old" RENAME TO "audit_logs_action_enum" +ALTER TYPE "public"."tier_enum" RENAME TO "tier_enum_old" +ALTER TYPE "public"."tier_enum" RENAME TO "tier_enum_old" +ALTER TYPE "public"."tier_enum_old" RENAME TO "tier_enum" +ALTER TYPE "public"."tier_enum_old" RENAME TO "tier_enum" +CREATE INDEX "IDX_014bd57abd7a916e466c5aaf80" ON "user_progress" ("totalPoints") +CREATE INDEX "IDX_02781c49b25ceb502571f0315f" ON "invoices" ("payment_id") +CREATE INDEX "IDX_041cd4f77805d53e3408cb4ae6" ON "runbook_executions" ("incidentId", "status") +CREATE INDEX "IDX_06889a06516d7596f255302f8f" ON "course" ("language") +CREATE INDEX "IDX_06ed7514e57cb8bd0c09ce3be0" ON "recovery_tests" ("status", "createdAt") +CREATE INDEX "IDX_07efa1402eb853c5f117cadc3e" ON "audit_logs" ("ip_address", "timestamp") +CREATE INDEX "IDX_0aababfebc5662624a4a66b639" ON "content_reports" ("reporter_id") +CREATE INDEX "IDX_0c4a4e0584c4d08a779239d250" ON "runbook_executions" ("startedAt") +CREATE INDEX "IDX_0cab16d7274814ef7b9b021c64" ON "onboarding_steps" ("order") +CREATE INDEX "IDX_139af264ff823c0f6e17be7c8e" ON "achievement_statistics" ("achievementId", "date") +CREATE INDEX "IDX_16a0f89062ed4edd4cce26cc5e" ON "cohort_assignments" ("cohortId") +CREATE INDEX "IDX_177b41bfa4c6574e30e95da04e" ON "cohort_threads" ("cohortId", "createdAt") +CREATE INDEX "IDX_17b613a55b287678b6f26ab8b2" ON "audit_logs" ("action", "timestamp") +CREATE INDEX "IDX_1a15756e257e0eaf01edc85645" ON "subscriptions" ("user_id", "status") +CREATE INDEX "IDX_1af4ba5bb120d226b6a4f99420" ON "course_bulk_operations" ("initiated_by_id") +CREATE INDEX "IDX_1b62007f50cea2887ba68ea710" ON "user_achievements" ("unlockedAt") +CREATE INDEX "IDX_1b692560d438e5fc6edebbb27d" ON "refunds" ("idempotencyKey") +CREATE INDEX "IDX_1bbd38968b7ceca0726aa08af1" ON "course" ("category") +CREATE INDEX "IDX_1bdc17982d9e563278575a5e46" ON "user_progress" ("tier") +CREATE INDEX "IDX_1d242feeb1db1960342eacd254" ON "rubrics" ("assessment_id") +CREATE INDEX "IDX_1f69fdcbd7ea5f0e52c3230c00" ON "course_reviews" ("course_id") +CREATE INDEX "IDX_1ff95d74c5186613b05e651802" ON "backup_records" ("status", "createdAt") +CREATE INDEX "IDX_204e9b624861ff4a5b26819210" ON "users" ("createdAt") +CREATE INDEX "IDX_20adca3c4217744bfd8a485e98" ON "achievement_progress" ("achievementId") +CREATE INDEX "IDX_21202b13906bffe54448b2a553" ON "content_metadata" ("status") +CREATE INDEX "IDX_22498516df46fa90e200915793" ON "user_progress" ("xp") +CREATE INDEX "IDX_2310ecc5cb8be427097154b18f" ON "tenants" ("slug") +CREATE INDEX "IDX_238b7a7accc697f9dc38bb2fa6" ON "onboarding_steps" ("slug") +CREATE INDEX "IDX_258ef1682aba959da9301e5dee" ON "achievement_statistics" ("achievementId") +CREATE INDEX "IDX_26daf5e433d6fb88ee32ce9363" ON "invoices" ("user_id") +CREATE INDEX "IDX_285d81ddb192b90d37f763a2a1" ON "challenges" ("type") +CREATE INDEX "IDX_2bd3a6327596cc908d574a8219" ON "course" ("createdAt") +CREATE INDEX "IDX_32b41cdb985a296213e9a928b5" ON "payments" ("status") +CREATE INDEX "IDX_34cbd320b458b8cf9e0d580304" ON "course_versions" ("course_id") +CREATE INDEX "IDX_35fb2307535d90a6ed290af1f4" ON "lessons" ("module_id") +CREATE INDEX "IDX_37bd4055f0a14a276992573e10" ON "submission_grades" ("rubric_id") +CREATE INDEX "IDX_38c954e266791189dfd7b6ffc4" ON "analytics_events" ("eventType", "createdAt") +CREATE INDEX "IDX_39a3e3d5ae6501bb9a783a7c7d" ON "audit_logs" ("entity_type", "entity_id", "timestamp") +CREATE INDEX "IDX_3a50f773964d78f404d73bcb36" ON "assessment" ("createdAt") +CREATE INDEX "IDX_3ac6bc9da3e8a56f3f7082012d" ON "user_achievements" ("userId") +CREATE INDEX "IDX_3d8ef5f5715cb3d50315007aee" ON "cohort_threads" ("cohortId") +CREATE INDEX "IDX_3dad32ba0ff20feee98b1b0c43" ON "lessons" ("title") +CREATE INDEX "IDX_3f28c2b1ce5590ea0892267033" ON "campaign_recipients" ("campaignId", "status") +CREATE INDEX "IDX_40a00b5a90ba796609b95c6d93" ON "forum_comments" ("parentId") +CREATE INDEX "IDX_4185366a8d6033a3aad864621d" ON "assessment" ("title") +CREATE INDEX "IDX_423597e129f4f9402c976acce2" ON "unsubscribe_tokens" ("token") +CREATE INDEX "IDX_427785468fb7d2733f59e7d7d3" ON "payments" ("user_id") +CREATE INDEX "IDX_428c3f972502e77d9e2c79051b" ON "unsubscribe_tokens" ("email") +CREATE INDEX "IDX_42bbd07a91fea7e34f175b19fe" ON "tenant_customizations" ("tenantId") +CREATE INDEX "IDX_43bd09d8fa296ece993503b6f2" ON "remediation_actions" ("executedAt") +CREATE INDEX "IDX_44893bafa6ef5813a61de5c061" ON "feedback_templates" ("owner_id") +CREATE INDEX "IDX_467f6869324eebd9de644f4c28" ON "content_reports" ("contentType") +CREATE INDEX "IDX_46ad51ba4f4bb7db2817d75419" ON "enrollment" ("status") +CREATE INDEX "IDX_4b1adfd26c40c48b29a25b4aed" ON "feedback_templates" ("name") +CREATE INDEX "IDX_4b459011ef4d3e4f8618b404c0" ON "user_onboarding_progress" ("user_id") +CREATE INDEX "IDX_4bbe32b3f6fca66f54509fe57e" ON "audit_logs" ("user_id", "timestamp") +CREATE INDEX "IDX_4d018866397b1e7e78d03b4566" ON "roles" ("createdAt") +CREATE INDEX "IDX_4d55d5b6642e4e9677c5af16b3" ON "course_bulk_operations" ("status") +CREATE INDEX "IDX_4f14731b3eb7d50d8f4042b355" ON "audit_logs" ("severity", "timestamp") +CREATE INDEX "IDX_55d1a6fc7f56f55d0be5bd59a7" ON "tier_rewards" ("tier") +CREATE INDEX "IDX_5649f3ee460ee3d00ce2114fc5" ON "question" ("prompt") +CREATE INDEX "IDX_57833e6da94a22790e6913a509" ON "email_events" ("workflowId", "eventType") +CREATE INDEX "IDX_57a1a02c655b30638c3992a8d2" ON "forum_comments" ("threadId", "createdAt") +CREATE INDEX "IDX_580f1dbf7bceb9c2cde8baf7ff" ON "payment_methods" ("userId") +CREATE INDEX "IDX_5d23a7a8f43556bcd03a18a2b6" ON "email_campaigns" ("sentAt") +CREATE INDEX "IDX_5fb386b7d3d50fd5657ef76c3d" ON "audit_logs" ("retention_until") +CREATE INDEX "IDX_5ff375f35bf59b074f1a64f1b2" ON "webhook_retries" ("status", "nextRetryTime") +CREATE INDEX "IDX_616c8b49254550dbee7f917f3a" ON "tenant_usage_snapshots" ("tenantId", "period") +CREATE INDEX "IDX_629092c0eb5931f28a88932b45" ON "achievement_progress" ("isUnlocked") +CREATE INDEX "IDX_62e0da442361b07616d26593b3" ON "course_bulk_operations" ("createdAt") +CREATE INDEX "IDX_633a748d9720d4c3c25f0fc120" ON "lessons" ("order") +CREATE INDEX "IDX_648e3f5447f725579d7d4ffdfb" ON "roles" ("name") +CREATE INDEX "IDX_649681c84688d7c74208117d85" ON "point_transactions" ("userId", "createdAt") +CREATE INDEX "IDX_65395710ac88af450c5123f030" ON "course_module" ("course_id") +CREATE INDEX "IDX_66893187eeb7c3198e3d07c595" ON "recovery_tests" ("backup_record_id") +CREATE INDEX "IDX_66e7e265abeaba7bf6c1d13244" ON "submission_grades" ("attempt_id") +CREATE INDEX "IDX_67d38effbc57ecb21bc2901057" ON "rubric_criteria" ("rubric_id") +CREATE INDEX "IDX_692a909ee0fa9383e7859f9b40" ON "notifications" ("userId") +CREATE INDEX "IDX_69752e0d44a35a0dce9d9f06d0" ON "quota_definitions" ("userId") +CREATE INDEX "IDX_6a5a5816f54d0044ba5f3dc2b7" ON "user_achievements" ("achievementId") +CREATE INDEX "IDX_6c7e0cec7358f74ca9222628a1" ON "content_reports" ("moderation_item_id") +CREATE INDEX "IDX_6ccdf37d6c60e39c8bb617fcfc" ON "point_transactions" ("activityType") +CREATE INDEX "IDX_6ccf973355b70645eff37774de" ON "subscriptions" ("status") +CREATE INDEX "IDX_6e199fc01241bcb8ec806e0c6a" ON "incidents" ("detectedAt") +CREATE INDEX "IDX_72058429360b0daed61d94fb4f" ON "course" ("instructor_id", "createdAt") +CREATE INDEX "IDX_743b9fb1d2a059f2f7860418e4" ON "payments" ("idempotencyKey") +CREATE INDEX "IDX_756e18fc1918b63fb4d79b4db6" ON "enrollment" ("enrolledAt") +CREATE INDEX "IDX_75f83ce916d0c7fd99bd23782f" ON "course_bulk_operations" ("type") +CREATE INDEX "IDX_7684def1bb4135f494082b34ee" ON "instructor_payouts" ("status") +CREATE INDEX "IDX_7967fc2be2ef5327f48d19f474" ON "rubrics" ("owner_id") +CREATE INDEX "IDX_79782456f7ad0ce730435a950a" ON "content_reports" ("contentId") +CREATE INDEX "IDX_79ff7e8ebb1eacf06b71b04d44" ON "criterion_grades" ("criterion_id") +CREATE INDEX "IDX_7a5ad30aa2b8be04ca4a6d724a" ON "user_progress" ("level") +CREATE INDEX "IDX_7ad75a333a7bcf6a2b5d3517ca" ON "users" ("emailVerificationToken") +CREATE INDEX "IDX_82294735678621182b86847277" ON "feedback_templates" ("createdAt") +CREATE INDEX "IDX_862a5402a4d18274a07a2a7ccc" ON "email_campaigns" ("scheduledAt") +CREATE INDEX "IDX_88dcc148d532384790ab874c3d" ON "audit_logs" ("timestamp") +CREATE INDEX "IDX_8c0b069d24a8380f5356d22e42" ON "rubric_levels" ("criterion_id") +CREATE INDEX "IDX_8d5af5c5dbdc0d91ce1178a004" ON "quota_definitions" ("tier", "isActive") +CREATE INDEX "IDX_8dacda0f101c97b73304ddf0fc" ON "instructor_payouts" ("instructor_id") +CREATE INDEX "IDX_9036fb7240edcc5f620ceff406" ON "email_events" ("campaignId", "eventType") +CREATE INDEX "IDX_90d118dc0a422ced2552926a60" ON "user_challenges" ("userId", "challengeId") +CREATE INDEX "IDX_91578dceeb42466b9285f29e4b" ON "question" ("type") +CREATE INDEX "IDX_91e3503740d174a4599e81ad8a" ON "enrollment" ("user_id", "enrolledAt") +CREATE INDEX "IDX_92fd74dcbe9b1adad11ff3091d" ON "archived_data" ("entityType", "originalId") +CREATE INDEX "IDX_9360325be3f332793f1734704e" ON "cohort_members" ("userId") +CREATE INDEX "IDX_93f082fa43fdce4811582a4e3c" ON "achievement_progress" ("userId") +CREATE INDEX "IDX_97672ac88f789774dd47f7c8be" ON "users" ("email") +CREATE INDEX "IDX_a07f0454cce5637fd109a105dd" ON "email_events" ("recipientId", "eventType") +CREATE INDEX "IDX_a0fa8c64807caecc993a5d399d" ON "user_onboarding_progress" ("user_id", "status") +CREATE INDEX "IDX_a27a3c9d6a41c0fa0fd1cb3f85" ON "course" ("status", "createdAt") +CREATE INDEX "IDX_a8707945701186685e1b5e9c51" ON "content_metadata" ("expires_at") +CREATE INDEX "IDX_ac0f09364e3701d9ed35435288" ON "invoices" ("status") +CREATE INDEX "IDX_ac1a8d28ac7e2f7aff92b57318" ON "email_campaigns" ("templateId") +CREATE INDEX "IDX_ac27b070934c64650ab339f4c6" ON "notifications" ("updatedAt") +CREATE INDEX "IDX_ac5edecc1aefa58ed0237a7ee4" ON "course" ("title") +CREATE INDEX "IDX_add2541ebf4aaa7449b173077c" ON "rubrics" ("name") +CREATE INDEX "IDX_audit_logs_action_timestamp" ON "audit_logs" ("action", "timestamp") +CREATE INDEX "IDX_audit_logs_category_timestamp" ON "audit_logs" ("category", "timestamp") +CREATE INDEX "IDX_audit_logs_entity" ON "audit_logs" ("entity_type", "entity_id") +CREATE INDEX "IDX_audit_logs_ip_address" ON "audit_logs" ("ip_address") +CREATE INDEX "IDX_audit_logs_severity_timestamp" ON "audit_logs" ("severity", "timestamp") +CREATE INDEX "IDX_audit_logs_timestamp" ON "audit_logs" ("timestamp") +CREATE INDEX "IDX_audit_logs_user_timestamp" ON "audit_logs" ("user_id", "timestamp") +CREATE INDEX "IDX_b1cc4fe47a41d78d10f26589be" ON "onboarding_rewards" ("isActive") +CREATE INDEX "IDX_b2f0366aa9349789527e0c36d9" ON "users_roles_roles" ("rolesId") +CREATE INDEX "IDX_b3676e630f8875765de2bd587e" ON "onboarding_rewards" ("name") +CREATE INDEX "IDX_b5d0e1b57bc6c761fb49e79bf8" ON "user_progress" ("userId") +CREATE INDEX "IDX_b76aaa5336dcdff6ca464044b6" ON "onboarding_steps" ("type") +CREATE INDEX "IDX_ba56e0a5617605dcc62cb33f11" ON "cohort_comments" ("threadId") +CREATE INDEX "IDX_ba7c709a66f7360180c4d1bcd9" ON "rubrics" ("createdAt") +CREATE INDEX "IDX_baccb82c6179dca139f6b8c768" ON "course" ("status") +CREATE INDEX "IDX_bf8e0f9dd4558ef209ec111782" ON "invoices" ("invoiceNumber") +CREATE INDEX "IDX_bffe933a388d6bde48891ff95a" ON "users" ("passwordResetToken") +CREATE INDEX "IDX_c001c66a488dafe7aa8aa19402" ON "content_reports" ("assigned_moderator_id") +CREATE INDEX "IDX_c0085fb66787d45eb374ddef38" ON "badges" ("category") +CREATE INDEX "IDX_c0dda6108b7bbd68c6823d4983" ON "user_onboarding_progress" ("status") +CREATE INDEX "IDX_c2a89b63635ae2b4cc76f17a08" ON "payments" ("user_id", "status") +CREATE INDEX "IDX_c2c4377a3e7773772bf670eb47" ON "enrollment" ("user_id", "status") +CREATE INDEX "IDX_c2e0f2c83c50446d23955dff0a" ON "cohort_assignments" ("cohortId", "createdAt") +CREATE INDEX "IDX_c47efc904f0620c5a6b35a4f0c" ON "forum_votes" ("entityType", "entityId") +CREATE INDEX "IDX_c58f7e88c286e5e3478960a998" ON "users" ("tenantId") +CREATE INDEX "IDX_c5fa169d2de9407d99f2c6e4fa" ON "payments" ("course_id") +CREATE INDEX "IDX_c93297fa63b08ac2e83972bd5a" ON "onboarding_rewards" ("type") +CREATE INDEX "IDX_cd5b89a167f9f9821b70ff88bf" ON "content_metadata" ("content_type") +CREATE INDEX "IDX_cdef16da64ce64a5bf79062ec1" ON "user_onboarding_progress" ("completedAt") +CREATE INDEX "IDX_cfd29d0da332d5c46d9b073863" ON "tenant_usage_snapshots" ("tenantId") +CREATE INDEX "IDX_challenges_type" ON "challenges" ("type") +CREATE INDEX "IDX_course_bulk_ops_initiator" ON "course_bulk_operations" ("initiated_by_id") +CREATE INDEX "IDX_course_bulk_ops_type" ON "course_bulk_operations" ("type") +CREATE INDEX "IDX_course_category" ON "course" ("category") +CREATE INDEX "IDX_course_currency" ON "course" ("currency") +CREATE INDEX "IDX_course_language" ON "course" ("language") +CREATE INDEX "IDX_course_level" ON "course" ("level") +CREATE INDEX "IDX_course_search_vector" ON "course" ("search_vector") +CREATE INDEX "IDX_criterion_grades_criterion" ON "criterion_grades" ("criterion_id") +CREATE INDEX "IDX_criterion_grades_grade" ON "criterion_grades" ("grade_id") +CREATE INDEX "IDX_d03f18ea29009de1e37a6d156b" ON "enrollment" ("course_id", "enrolledAt") +CREATE INDEX "IDX_d0a95ef8a28188364c546eb65c" ON "subscriptions" ("user_id") +CREATE INDEX "IDX_d146560603e5ee3716c519b103" ON "assessment" ("durationMinutes") +CREATE INDEX "IDX_d16a377e285494ac71048f2b84" ON "archived_data" ("archivedAt") +CREATE INDEX "IDX_d1865366170f468d8eb720f8c8" ON "cohorts" ("name") +CREATE INDEX "IDX_d1bdbb16e7c11cf73e89d03cf3" ON "forum_threads" ("status", "createdAt") +CREATE INDEX "IDX_d2faa8fa665e7cdbd602d557df" ON "backup_records" ("region") +CREATE INDEX "IDX_d3e1023a52cb7875b6c10d3a15" ON "email_campaigns" ("name") +CREATE INDEX "IDX_d3f089bf865364504bda9c2ff6" ON "archived_data" ("entityType") +CREATE INDEX "IDX_d4b972aafe44b57154abba8db3" ON "analytics_events" ("user_id", "eventType", "createdAt") +CREATE INDEX "IDX_d6e7a6c84587eb7809c5b910f8" ON "enrollment" ("course_id", "status") +CREATE INDEX "IDX_d6e8c39a1de2c9b659b227c9e6" ON "analytics_events" ("timestamp") +CREATE INDEX "IDX_d80cf66830a0231b8104893b67" ON "lessons" ("deleted_at") +CREATE INDEX "IDX_d9b4760e416c3767d44e5be0fd" ON "email_campaigns" ("subject") +CREATE INDEX "IDX_d9da1249e3125778f926607e9d" ON "user_onboarding_progress" ("step_id") +CREATE INDEX "IDX_da04f89054f39981438894dfe3" ON "permissions" ("createdAt") +CREATE INDEX "IDX_dc2b9d46195bb3ed28abbf7c9e" ON "roles_permissions_permissions" ("rolesId") +CREATE INDEX "IDX_dd1ce01d1164c8bbdda052ced7" ON "enrollment" ("course_id") +CREATE INDEX "IDX_deca5c9911b3b2100b36106082" ON "course" ("instructor_id") +CREATE INDEX "IDX_df951a64f09865171d2d7a502b" ON "users_roles_roles" ("usersId") +CREATE INDEX "IDX_dfc023bb093b1afcbc91798837" ON "user_quota_usage" ("userId") +CREATE INDEX "IDX_e00ea7965d1ed8c53b3c85a56e" ON "cohort_members" ("cohortId") +CREATE INDEX "IDX_e0a5c10dfd4a71af0f150204e9" ON "instructor_payout_profiles" ("instructor_id") +CREATE INDEX "IDX_e0b0b69779afb474ee220573c8" ON "archived_data" ("originalId") +CREATE INDEX "IDX_e20f751a4d8faf089b1922eaf5" ON "tenant_billing" ("tenantId") +CREATE INDEX "IDX_e2102fc64215f0a1fcb9ad4879" ON "incidents" ("status", "severity") +CREATE INDEX "IDX_e607ca8d8c3d11ccdc725a3dd0" ON "cohort_members" ("cohortId", "createdAt") +CREATE INDEX "IDX_e63fa5de1df5c92236101dcfc4" ON "criterion_grades" ("grade_id") +CREATE INDEX "IDX_ea3906521c97279c3ee504a758" ON "audit_logs" ("category", "timestamp") +CREATE INDEX "IDX_ec14ddf4e267af8aa908806a97" ON "question" ("createdAt") +CREATE INDEX "IDX_ec4970e90eebcc7a16efb37816" ON "content_reports" ("status") +CREATE INDEX "IDX_ed26f5fd557b8fd57cc5db32e7" ON "course" ("level") +CREATE INDEX "IDX_eda643b345dd73d5fc1a456a4b" ON "course_bulk_operations" ("undone_by_id") +CREATE INDEX "IDX_ee13cd8cabbac2258203bb1c95" ON "backup_records" ("completedAt") +CREATE INDEX "IDX_ef31ae63719e8ca42b0a658438" ON "webhook_retries" ("createdAt") +CREATE INDEX "IDX_f1221d9b1aaa64b1f3c98ed46d" ON "user_badges" ("user_id") +CREATE INDEX "IDX_f1d4afbdb77bbb26a461726b68" ON "recovery_tests" ("testCompletedAt") +CREATE INDEX "IDX_f9614fbc7820536559cdc65147" ON "remediation_actions" ("incidentId", "status") +CREATE INDEX "IDX_fab34e0791096b2a0a1bf8bd7f" ON "users" ("providerId") +CREATE INDEX "IDX_fb0b9985fcfe901ed223a5468a" ON "achievement_statistics" ("date") +CREATE INDEX "IDX_fc17c7e94154a17e767b7674f1" ON "enrollment" ("user_id") +CREATE INDEX "IDX_fc77d3d8f8101ab793cf17986f" ON "email_campaigns" ("status") +CREATE INDEX "IDX_fcd49f4739a516d59675b8134e" ON "payment_methods" ("userId", "isDefault") +CREATE INDEX "IDX_fd467ff7c6435655c8d715f221" ON "tenant_configs" ("tenantId") +CREATE INDEX "IDX_fd4d5d4c7f7ff16c57549b72c6" ON "roles_permissions_permissions" ("permissionsId") +CREATE INDEX "IDX_fe0bb3f6520ee0469504521e71" ON "users" ("username") +CREATE INDEX "IDX_feedback_templates_name" ON "feedback_templates" ("name") +CREATE INDEX "IDX_feedback_templates_owner" ON "feedback_templates" ("owner_id") +CREATE INDEX "IDX_forum_comments_parentId" ON "forum_comments" ("parentId") +CREATE INDEX "IDX_forum_comments_threadId_createdAt" ON "forum_comments" ("threadId", "createdAt") +CREATE INDEX "IDX_forum_threads_status_createdAt" ON "forum_threads" ("status", "createdAt") +CREATE INDEX "IDX_forum_votes_entityType_entityId" ON "forum_votes" ("entityType", "entityId") +CREATE INDEX "IDX_point_transactions_activityType" ON "point_transactions" ("activityType") +CREATE INDEX "IDX_point_transactions_user_createdAt" ON "point_transactions" ("createdAt", "userId") +CREATE INDEX "IDX_rubric_criteria_rubric" ON "rubric_criteria" ("rubric_id") +CREATE INDEX "IDX_rubric_levels_criterion" ON "rubric_levels" ("criterion_id") +CREATE INDEX "IDX_rubrics_assessment" ON "rubrics" ("assessment_id") +CREATE INDEX "IDX_rubrics_name" ON "rubrics" ("name") +CREATE INDEX "IDX_rubrics_owner" ON "rubrics" ("owner_id") +CREATE INDEX "IDX_submission_grades_attempt" ON "submission_grades" ("attempt_id") +CREATE INDEX "IDX_submission_grades_rubric" ON "submission_grades" ("rubric_id") +CREATE INDEX "IDX_tier_rewards_tier" ON "tier_rewards" ("tier") +CREATE INDEX "IDX_translations_namespace_locale" ON "translations" ("namespace", "locale") +CREATE INDEX "IDX_user_challenges_user_challenge" ON "user_challenges" ("userId", "challengeId") +CREATE INDEX "IDX_user_progress_tier" ON "user_progress" ("tier") +CREATE INDEX "IDX_users_country_code" ON "users" ("country_code") +CREATE INDEX "IDX_users_createdAt" ON "users" ("createdAt") +CREATE INDEX "IDX_users_emailVerificationToken" ON "users" ("emailVerificationToken") +CREATE INDEX "IDX_users_passwordResetToken" ON "users" ("passwordResetToken") +CREATE INDEX "IDX_users_preferred_currency" ON "users" ("preferred_currency") +CREATE TYPE "public"."audit_logs_action_enum" AS ENUM('LOGIN', 'LOGIN_FAILED', 'LOGOUT', 'REGISTER', 'PASSWORD_RESET_REQUEST', 'PASSWORD_RESET', 'PASSWORD_CHANGE', 'EMAIL_VERIFIED', 'TOKEN_REFRESH', 'SESSION_EXPIRED', 'SESSION_REVOKED', 'USER_CREATED', 'USER_UPDATED', 'USER_DELETED', 'USER_ROLE_CHANGED', 'USER_STATUS_CHANGED', 'DATA_VIEWED', 'DATA_CREATED', 'DATA_UPDATED', 'DATA_DELETED', 'DATA_EXPORTED', 'DATA_IMPORTED', 'FILE_UPLOADED', 'FILE_DOWNLOADED', 'FILE_DELETED', 'FILE_SHARED', 'API_CALLED', 'API_RATE_LIMITED', 'API_ERROR', 'PERMISSION_DENIED', 'SUSPICIOUS_ACTIVITY', 'MFA_ENABLED', 'MFA_DISABLED', 'MFA_FAILED', 'CONFIG_CHANGED', 'SETTING_UPDATED', 'BACKUP_CREATED', 'BACKUP_RESTORED', 'RBAC_ROLE_ASSIGNED', 'RBAC_ROLE_REVOKED', 'RBAC_PERMISSION_GRANTED', 'RBAC_PERMISSION_REVOKED', 'RBAC_ROLE_CREATED', 'RBAC_ROLE_UPDATED', 'RBAC_ROLE_DELETED', 'RBAC_PERMISSION_CREATED', 'RBAC_PERMISSION_UPDATED', 'RBAC_PERMISSION_DELETED', 'DATA_RETENTION_APPLIED', 'AUDIT_LOG_EXPORTED', 'REPORT_GENERATED', 'PAYMENT_RECONCILIATION_MISMATCH') +CREATE TYPE "public"."audit_logs_action_enum_old" AS ENUM('LOGIN', 'LOGIN_FAILED', 'LOGOUT', 'REGISTER', 'PASSWORD_RESET_REQUEST', 'PASSWORD_RESET', 'PASSWORD_CHANGE', 'EMAIL_VERIFIED', 'TOKEN_REFRESH', 'SESSION_EXPIRED', 'SESSION_REVOKED', 'USER_CREATED', 'USER_UPDATED', 'USER_DELETED', 'USER_ROLE_CHANGED', 'USER_STATUS_CHANGED', 'DATA_VIEWED', 'DATA_CREATED', 'DATA_UPDATED', 'DATA_DELETED', 'DATA_EXPORTED', 'DATA_IMPORTED', 'FILE_UPLOADED', 'FILE_DOWNLOADED', 'FILE_DELETED', 'FILE_SHARED', 'API_CALLED', 'API_RATE_LIMITED', 'API_ERROR', 'PERMISSION_DENIED', 'SUSPICIOUS_ACTIVITY', 'MFA_ENABLED', 'MFA_DISABLED', 'MFA_FAILED', 'CONFIG_CHANGED', 'SETTING_UPDATED', 'BACKUP_CREATED', 'BACKUP_RESTORED', 'DATA_RETENTION_APPLIED', 'AUDIT_LOG_EXPORTED', 'REPORT_GENERATED') +CREATE TYPE "public"."audit_logs_http_method_enum" AS ENUM('GET', 'POST', 'PUT', 'DELETE', 'PATCH') +CREATE TYPE "public"."tier_enum_old" AS ENUM('BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND') +CREATE TYPE "public"."tier_enum_old" AS ENUM('BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND') +CREATE TYPE "public"."tier_rewards_tier_enum" AS ENUM('BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND') +CREATE TYPE "public"."user_preferences_locale_enum" AS ENUM('en-US', 'en-GB', 'fr-FR', 'es-ES', 'de-DE', 'ar-SA') +CREATE TYPE "public"."user_progress_tier_enum" AS ENUM('BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND') +CREATE UNIQUE INDEX "UQ_forum_votes_entityType_entityId_authorId" ON "forum_votes" ("entityType", "entityId", "authorId") +DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5 +DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5 +DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5 +DROP INDEX "public"."IDX_014bd57abd7a916e466c5aaf80" +DROP INDEX "public"."IDX_02781c49b25ceb502571f0315f" +DROP INDEX "public"."IDX_041cd4f77805d53e3408cb4ae6" +DROP INDEX "public"."IDX_06889a06516d7596f255302f8f" +DROP INDEX "public"."IDX_06ed7514e57cb8bd0c09ce3be0" +DROP INDEX "public"."IDX_07efa1402eb853c5f117cadc3e" +DROP INDEX "public"."IDX_0aababfebc5662624a4a66b639" +DROP INDEX "public"."IDX_0c4a4e0584c4d08a779239d250" +DROP INDEX "public"."IDX_0cab16d7274814ef7b9b021c64" +DROP INDEX "public"."IDX_139af264ff823c0f6e17be7c8e" +DROP INDEX "public"."IDX_16a0f89062ed4edd4cce26cc5e" +DROP INDEX "public"."IDX_177b41bfa4c6574e30e95da04e" +DROP INDEX "public"."IDX_17b613a55b287678b6f26ab8b2" +DROP INDEX "public"."IDX_1a15756e257e0eaf01edc85645" +DROP INDEX "public"."IDX_1af4ba5bb120d226b6a4f99420" +DROP INDEX "public"."IDX_1b62007f50cea2887ba68ea710" +DROP INDEX "public"."IDX_1b692560d438e5fc6edebbb27d" +DROP INDEX "public"."IDX_1bbd38968b7ceca0726aa08af1" +DROP INDEX "public"."IDX_1bdc17982d9e563278575a5e46" +DROP INDEX "public"."IDX_1d242feeb1db1960342eacd254" +DROP INDEX "public"."IDX_1f69fdcbd7ea5f0e52c3230c00" +DROP INDEX "public"."IDX_1ff95d74c5186613b05e651802" +DROP INDEX "public"."IDX_204e9b624861ff4a5b26819210" +DROP INDEX "public"."IDX_20adca3c4217744bfd8a485e98" +DROP INDEX "public"."IDX_21202b13906bffe54448b2a553" +DROP INDEX "public"."IDX_22498516df46fa90e200915793" +DROP INDEX "public"."IDX_2310ecc5cb8be427097154b18f" +DROP INDEX "public"."IDX_238b7a7accc697f9dc38bb2fa6" +DROP INDEX "public"."IDX_258ef1682aba959da9301e5dee" +DROP INDEX "public"."IDX_26daf5e433d6fb88ee32ce9363" +DROP INDEX "public"."IDX_285d81ddb192b90d37f763a2a1" +DROP INDEX "public"."IDX_2bd3a6327596cc908d574a8219" +DROP INDEX "public"."IDX_32b41cdb985a296213e9a928b5" +DROP INDEX "public"."IDX_34cbd320b458b8cf9e0d580304" +DROP INDEX "public"."IDX_35fb2307535d90a6ed290af1f4" +DROP INDEX "public"."IDX_37bd4055f0a14a276992573e10" +DROP INDEX "public"."IDX_38c954e266791189dfd7b6ffc4" +DROP INDEX "public"."IDX_39a3e3d5ae6501bb9a783a7c7d" +DROP INDEX "public"."IDX_3a50f773964d78f404d73bcb36" +DROP INDEX "public"."IDX_3ac6bc9da3e8a56f3f7082012d" +DROP INDEX "public"."IDX_3d8ef5f5715cb3d50315007aee" +DROP INDEX "public"."IDX_3dad32ba0ff20feee98b1b0c43" +DROP INDEX "public"."IDX_3f28c2b1ce5590ea0892267033" +DROP INDEX "public"."IDX_40a00b5a90ba796609b95c6d93" +DROP INDEX "public"."IDX_4185366a8d6033a3aad864621d" +DROP INDEX "public"."IDX_423597e129f4f9402c976acce2" +DROP INDEX "public"."IDX_427785468fb7d2733f59e7d7d3" +DROP INDEX "public"."IDX_428c3f972502e77d9e2c79051b" +DROP INDEX "public"."IDX_42bbd07a91fea7e34f175b19fe" +DROP INDEX "public"."IDX_43bd09d8fa296ece993503b6f2" +DROP INDEX "public"."IDX_44893bafa6ef5813a61de5c061" +DROP INDEX "public"."IDX_467f6869324eebd9de644f4c28" +DROP INDEX "public"."IDX_46ad51ba4f4bb7db2817d75419" +DROP INDEX "public"."IDX_4b1adfd26c40c48b29a25b4aed" +DROP INDEX "public"."IDX_4b459011ef4d3e4f8618b404c0" +DROP INDEX "public"."IDX_4bbe32b3f6fca66f54509fe57e" +DROP INDEX "public"."IDX_4d018866397b1e7e78d03b4566" +DROP INDEX "public"."IDX_4d55d5b6642e4e9677c5af16b3" +DROP INDEX "public"."IDX_4f14731b3eb7d50d8f4042b355" +DROP INDEX "public"."IDX_55d1a6fc7f56f55d0be5bd59a7" +DROP INDEX "public"."IDX_5649f3ee460ee3d00ce2114fc5" +DROP INDEX "public"."IDX_57833e6da94a22790e6913a509" +DROP INDEX "public"."IDX_57a1a02c655b30638c3992a8d2" +DROP INDEX "public"."IDX_580f1dbf7bceb9c2cde8baf7ff" +DROP INDEX "public"."IDX_5d23a7a8f43556bcd03a18a2b6" +DROP INDEX "public"."IDX_5fb386b7d3d50fd5657ef76c3d" +DROP INDEX "public"."IDX_5ff375f35bf59b074f1a64f1b2" +DROP INDEX "public"."IDX_616c8b49254550dbee7f917f3a" +DROP INDEX "public"."IDX_629092c0eb5931f28a88932b45" +DROP INDEX "public"."IDX_62e0da442361b07616d26593b3" +DROP INDEX "public"."IDX_633a748d9720d4c3c25f0fc120" +DROP INDEX "public"."IDX_648e3f5447f725579d7d4ffdfb" +DROP INDEX "public"."IDX_649681c84688d7c74208117d85" +DROP INDEX "public"."IDX_65395710ac88af450c5123f030" +DROP INDEX "public"."IDX_66893187eeb7c3198e3d07c595" +DROP INDEX "public"."IDX_66e7e265abeaba7bf6c1d13244" +DROP INDEX "public"."IDX_67d38effbc57ecb21bc2901057" +DROP INDEX "public"."IDX_692a909ee0fa9383e7859f9b40" +DROP INDEX "public"."IDX_69752e0d44a35a0dce9d9f06d0" +DROP INDEX "public"."IDX_6a5a5816f54d0044ba5f3dc2b7" +DROP INDEX "public"."IDX_6c7e0cec7358f74ca9222628a1" +DROP INDEX "public"."IDX_6ccdf37d6c60e39c8bb617fcfc" +DROP INDEX "public"."IDX_6ccf973355b70645eff37774de" +DROP INDEX "public"."IDX_6e199fc01241bcb8ec806e0c6a" +DROP INDEX "public"."IDX_72058429360b0daed61d94fb4f" +DROP INDEX "public"."IDX_743b9fb1d2a059f2f7860418e4" +DROP INDEX "public"."IDX_756e18fc1918b63fb4d79b4db6" +DROP INDEX "public"."IDX_75f83ce916d0c7fd99bd23782f" +DROP INDEX "public"."IDX_7684def1bb4135f494082b34ee" +DROP INDEX "public"."IDX_7967fc2be2ef5327f48d19f474" +DROP INDEX "public"."IDX_79782456f7ad0ce730435a950a" +DROP INDEX "public"."IDX_79ff7e8ebb1eacf06b71b04d44" +DROP INDEX "public"."IDX_7a5ad30aa2b8be04ca4a6d724a" +DROP INDEX "public"."IDX_7ad75a333a7bcf6a2b5d3517ca" +DROP INDEX "public"."IDX_82294735678621182b86847277" +DROP INDEX "public"."IDX_862a5402a4d18274a07a2a7ccc" +DROP INDEX "public"."IDX_88dcc148d532384790ab874c3d" +DROP INDEX "public"."IDX_8c0b069d24a8380f5356d22e42" +DROP INDEX "public"."IDX_8d5af5c5dbdc0d91ce1178a004" +DROP INDEX "public"."IDX_8dacda0f101c97b73304ddf0fc" +DROP INDEX "public"."IDX_9036fb7240edcc5f620ceff406" +DROP INDEX "public"."IDX_90d118dc0a422ced2552926a60" +DROP INDEX "public"."IDX_91578dceeb42466b9285f29e4b" +DROP INDEX "public"."IDX_91e3503740d174a4599e81ad8a" +DROP INDEX "public"."IDX_92fd74dcbe9b1adad11ff3091d" +DROP INDEX "public"."IDX_9360325be3f332793f1734704e" +DROP INDEX "public"."IDX_93f082fa43fdce4811582a4e3c" +DROP INDEX "public"."IDX_97672ac88f789774dd47f7c8be" +DROP INDEX "public"."IDX_a07f0454cce5637fd109a105dd" +DROP INDEX "public"."IDX_a0fa8c64807caecc993a5d399d" +DROP INDEX "public"."IDX_a27a3c9d6a41c0fa0fd1cb3f85" +DROP INDEX "public"."IDX_a8707945701186685e1b5e9c51" +DROP INDEX "public"."IDX_ac0f09364e3701d9ed35435288" +DROP INDEX "public"."IDX_ac1a8d28ac7e2f7aff92b57318" +DROP INDEX "public"."IDX_ac27b070934c64650ab339f4c6" +DROP INDEX "public"."IDX_ac5edecc1aefa58ed0237a7ee4" +DROP INDEX "public"."IDX_add2541ebf4aaa7449b173077c" +DROP INDEX "public"."IDX_audit_logs_action_timestamp" +DROP INDEX "public"."IDX_audit_logs_category_timestamp" +DROP INDEX "public"."IDX_audit_logs_entity" +DROP INDEX "public"."IDX_audit_logs_ip_address" +DROP INDEX "public"."IDX_audit_logs_severity_timestamp" +DROP INDEX "public"."IDX_audit_logs_timestamp" +DROP INDEX "public"."IDX_audit_logs_user_timestamp" +DROP INDEX "public"."IDX_b1cc4fe47a41d78d10f26589be" +DROP INDEX "public"."IDX_b2f0366aa9349789527e0c36d9" +DROP INDEX "public"."IDX_b3676e630f8875765de2bd587e" +DROP INDEX "public"."IDX_b5d0e1b57bc6c761fb49e79bf8" +DROP INDEX "public"."IDX_b76aaa5336dcdff6ca464044b6" +DROP INDEX "public"."IDX_ba56e0a5617605dcc62cb33f11" +DROP INDEX "public"."IDX_ba7c709a66f7360180c4d1bcd9" +DROP INDEX "public"."IDX_baccb82c6179dca139f6b8c768" +DROP INDEX "public"."IDX_bf8e0f9dd4558ef209ec111782" +DROP INDEX "public"."IDX_bffe933a388d6bde48891ff95a" +DROP INDEX "public"."IDX_c001c66a488dafe7aa8aa19402" +DROP INDEX "public"."IDX_c0085fb66787d45eb374ddef38" +DROP INDEX "public"."IDX_c0dda6108b7bbd68c6823d4983" +DROP INDEX "public"."IDX_c2a89b63635ae2b4cc76f17a08" +DROP INDEX "public"."IDX_c2c4377a3e7773772bf670eb47" +DROP INDEX "public"."IDX_c2e0f2c83c50446d23955dff0a" +DROP INDEX "public"."IDX_c47efc904f0620c5a6b35a4f0c" +DROP INDEX "public"."IDX_c58f7e88c286e5e3478960a998" +DROP INDEX "public"."IDX_c5fa169d2de9407d99f2c6e4fa" +DROP INDEX "public"."IDX_c93297fa63b08ac2e83972bd5a" +DROP INDEX "public"."IDX_cd5b89a167f9f9821b70ff88bf" +DROP INDEX "public"."IDX_cdef16da64ce64a5bf79062ec1" +DROP INDEX "public"."IDX_cfd29d0da332d5c46d9b073863" +DROP INDEX "public"."IDX_challenges_type" +DROP INDEX "public"."IDX_course_bulk_ops_initiator" +DROP INDEX "public"."IDX_course_bulk_ops_type" +DROP INDEX "public"."IDX_course_category" +DROP INDEX "public"."IDX_course_currency" +DROP INDEX "public"."IDX_course_language" +DROP INDEX "public"."IDX_course_level" +DROP INDEX "public"."IDX_course_search_vector" +DROP INDEX "public"."IDX_criterion_grades_criterion" +DROP INDEX "public"."IDX_criterion_grades_grade" +DROP INDEX "public"."IDX_d03f18ea29009de1e37a6d156b" +DROP INDEX "public"."IDX_d0a95ef8a28188364c546eb65c" +DROP INDEX "public"."IDX_d146560603e5ee3716c519b103" +DROP INDEX "public"."IDX_d16a377e285494ac71048f2b84" +DROP INDEX "public"."IDX_d1865366170f468d8eb720f8c8" +DROP INDEX "public"."IDX_d1bdbb16e7c11cf73e89d03cf3" +DROP INDEX "public"."IDX_d2faa8fa665e7cdbd602d557df" +DROP INDEX "public"."IDX_d3e1023a52cb7875b6c10d3a15" +DROP INDEX "public"."IDX_d3f089bf865364504bda9c2ff6" +DROP INDEX "public"."IDX_d4b972aafe44b57154abba8db3" +DROP INDEX "public"."IDX_d6e7a6c84587eb7809c5b910f8" +DROP INDEX "public"."IDX_d6e8c39a1de2c9b659b227c9e6" +DROP INDEX "public"."IDX_d80cf66830a0231b8104893b67" +DROP INDEX "public"."IDX_d9b4760e416c3767d44e5be0fd" +DROP INDEX "public"."IDX_d9da1249e3125778f926607e9d" +DROP INDEX "public"."IDX_da04f89054f39981438894dfe3" +DROP INDEX "public"."IDX_dc2b9d46195bb3ed28abbf7c9e" +DROP INDEX "public"."IDX_dd1ce01d1164c8bbdda052ced7" +DROP INDEX "public"."IDX_deca5c9911b3b2100b36106082" +DROP INDEX "public"."IDX_df951a64f09865171d2d7a502b" +DROP INDEX "public"."IDX_dfc023bb093b1afcbc91798837" +DROP INDEX "public"."IDX_e00ea7965d1ed8c53b3c85a56e" +DROP INDEX "public"."IDX_e0a5c10dfd4a71af0f150204e9" +DROP INDEX "public"."IDX_e0b0b69779afb474ee220573c8" +DROP INDEX "public"."IDX_e20f751a4d8faf089b1922eaf5" +DROP INDEX "public"."IDX_e2102fc64215f0a1fcb9ad4879" +DROP INDEX "public"."IDX_e607ca8d8c3d11ccdc725a3dd0" +DROP INDEX "public"."IDX_e63fa5de1df5c92236101dcfc4" +DROP INDEX "public"."IDX_ea3906521c97279c3ee504a758" +DROP INDEX "public"."IDX_ec14ddf4e267af8aa908806a97" +DROP INDEX "public"."IDX_ec4970e90eebcc7a16efb37816" +DROP INDEX "public"."IDX_ed26f5fd557b8fd57cc5db32e7" +DROP INDEX "public"."IDX_eda643b345dd73d5fc1a456a4b" +DROP INDEX "public"."IDX_ee13cd8cabbac2258203bb1c95" +DROP INDEX "public"."IDX_ef31ae63719e8ca42b0a658438" +DROP INDEX "public"."IDX_f1221d9b1aaa64b1f3c98ed46d" +DROP INDEX "public"."IDX_f1d4afbdb77bbb26a461726b68" +DROP INDEX "public"."IDX_f9614fbc7820536559cdc65147" +DROP INDEX "public"."IDX_fab34e0791096b2a0a1bf8bd7f" +DROP INDEX "public"."IDX_fb0b9985fcfe901ed223a5468a" +DROP INDEX "public"."IDX_fc17c7e94154a17e767b7674f1" +DROP INDEX "public"."IDX_fc77d3d8f8101ab793cf17986f" +DROP INDEX "public"."IDX_fcd49f4739a516d59675b8134e" +DROP INDEX "public"."IDX_fd467ff7c6435655c8d715f221" +DROP INDEX "public"."IDX_fd4d5d4c7f7ff16c57549b72c6" +DROP INDEX "public"."IDX_fe0bb3f6520ee0469504521e71" +DROP INDEX "public"."IDX_feedback_templates_name" +DROP INDEX "public"."IDX_feedback_templates_owner" +DROP INDEX "public"."IDX_forum_comments_parentId" +DROP INDEX "public"."IDX_forum_comments_threadId_createdAt" +DROP INDEX "public"."IDX_forum_threads_status_createdAt" +DROP INDEX "public"."IDX_forum_votes_entityType_entityId" +DROP INDEX "public"."IDX_point_transactions_activityType" +DROP INDEX "public"."IDX_point_transactions_user_createdAt" +DROP INDEX "public"."IDX_rubric_criteria_rubric" +DROP INDEX "public"."IDX_rubric_levels_criterion" +DROP INDEX "public"."IDX_rubrics_assessment" +DROP INDEX "public"."IDX_rubrics_name" +DROP INDEX "public"."IDX_rubrics_owner" +DROP INDEX "public"."IDX_submission_grades_attempt" +DROP INDEX "public"."IDX_submission_grades_rubric" +DROP INDEX "public"."IDX_tier_rewards_tier" +DROP INDEX "public"."IDX_translations_namespace_locale" +DROP INDEX "public"."IDX_user_challenges_user_challenge" +DROP INDEX "public"."IDX_user_progress_tier" +DROP INDEX "public"."IDX_users_country_code" +DROP INDEX "public"."IDX_users_createdAt" +DROP INDEX "public"."IDX_users_emailVerificationToken" +DROP INDEX "public"."IDX_users_passwordResetToken" +DROP INDEX "public"."IDX_users_preferred_currency" +DROP INDEX "public"."UQ_forum_votes_entityType_entityId_authorId" +DROP TYPE "public"."audit_logs_action_enum" +DROP TYPE "public"."audit_logs_action_enum_old" +DROP TYPE "public"."audit_logs_http_method_enum" +DROP TYPE "public"."tier_enum_old" +DROP TYPE "public"."tier_enum_old" +DROP TYPE "public"."tier_rewards_tier_enum" +DROP TYPE "public"."user_preferences_locale_enum" +DROP TYPE "public"."user_progress_tier_enum" +INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6) +INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6) +INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6) diff --git a/src/config/datasource.ts b/src/config/datasource.ts index 693b0749..238296e3 100644 --- a/src/config/datasource.ts +++ b/src/config/datasource.ts @@ -4,6 +4,12 @@ import { getDatabaseConfig } from './database.config'; export const AppDataSource = new DataSource({ ...(getDatabaseConfig() as DataSourceOptions), synchronize: false, + // Load all entity files so schema-aware operations (migration:run, + // migration:generate, the drift check) can compare the database against the + // actual entity definitions. `!(migrations|modules)` excludes TypeORM + // helpers under src/migrations and the (non-compiling, unregistered) + // src/modules entities. + entities: ['src/!(migrations|modules)/**/*.entity.ts'], // Match only timestamp-prefixed migration files. This deliberately excludes // non-migration helpers that live under src/migrations (e.g. // schema-migration.service.ts and the entities/ subdir) which TypeORM would diff --git a/src/courses/entities/course-status.enum.ts b/src/courses/entities/course-status.enum.ts new file mode 100644 index 00000000..430f268b --- /dev/null +++ b/src/courses/entities/course-status.enum.ts @@ -0,0 +1,9 @@ +/** Lifecycle states a course can be in. */ +export enum CourseStatus { + DRAFT = 'draft', + PENDING_REVIEW = 'pending_review', + CHANGES_REQUESTED = 'changes_requested', + PUBLISHED = 'published', + REJECTED = 'rejected', + ARCHIVED = 'archived', +} diff --git a/src/courses/entities/course-version.entity.ts b/src/courses/entities/course-version.entity.ts index fef84852..d5bb6b78 100644 --- a/src/courses/entities/course-version.entity.ts +++ b/src/courses/entities/course-version.entity.ts @@ -7,7 +7,8 @@ import { ManyToOne, Index, } from 'typeorm'; -import { Course, CourseStatus } from './course.entity'; +import { Course } from './course.entity'; +import { CourseStatus } from './course-status.enum'; import { User } from '../../users/entities/user.entity'; export enum CourseVersionEventType { diff --git a/src/courses/entities/course.entity.ts b/src/courses/entities/course.entity.ts index 51972cc7..bc2cf76b 100644 --- a/src/courses/entities/course.entity.ts +++ b/src/courses/entities/course.entity.ts @@ -16,16 +16,9 @@ import { CourseModule } from './course-module.entity'; import { Enrollment } from './enrollment.entity'; import { CourseReview } from './course-review.entity'; import { CourseVersion } from './course-version.entity'; +import { CourseStatus } from './course-status.enum'; -/** Lifecycle states a course can be in. */ -export enum CourseStatus { - DRAFT = 'draft', - PENDING_REVIEW = 'pending_review', - CHANGES_REQUESTED = 'changes_requested', - PUBLISHED = 'published', - REJECTED = 'rejected', - ARCHIVED = 'archived', -} +export { CourseStatus }; /** * Represents the course entity. diff --git a/src/gamification/entities/user-challenge.entity.ts b/src/gamification/entities/user-challenge.entity.ts index 1584ffab..15219f20 100644 --- a/src/gamification/entities/user-challenge.entity.ts +++ b/src/gamification/entities/user-challenge.entity.ts @@ -6,7 +6,7 @@ import { Challenge } from './challenge.entity'; * Represents the user Challenge entity. */ @Entity('user_challenges') -@Index(['userId', 'challengeId']) +@Index(['user', 'challenge']) export class UserChallenge { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/src/migrations/1599999999999-BaselineSchema.ts b/src/migrations/1599999999999-BaselineSchema.ts new file mode 100644 index 00000000..c85bb6a6 --- /dev/null +++ b/src/migrations/1599999999999-BaselineSchema.ts @@ -0,0 +1,2318 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Baseline schema migration. + * + * Reproduces the full schema that TypeORM's `synchronize` previously created + * in development before the incremental migrations in this directory were + * introduced. The timestamp is the lowest of all migrations so this runs + * first and every subsequent migration applies cleanly on top. + * + * Generated from the entity metadata via `synchronize` and normalized with + * `pg_dump --schema-only`. + */ +export class BaselineSchema1599999999999 implements MigrationInterface { + name = 'BaselineSchema1599999999999'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public + `); + await queryRunner.query(`CREATE TYPE public.ab_tests_status_enum AS ENUM ( + 'draft', + 'running', + 'completed', + 'cancelled' +) + `); + await queryRunner.query(`CREATE TYPE public.achievements_difficulty_enum AS ENUM ( + 'easy', + 'medium', + 'hard', + 'legendary' +) + `); + await queryRunner.query(`CREATE TYPE public.achievements_type_enum AS ENUM ( + 'milestone', + 'challenge', + 'streaks', + 'skill_based', + 'engagement', + 'contribution' +) + `); + await queryRunner.query(`CREATE TYPE public.analytics_events_eventtype_enum AS ENUM ( + 'signup', + 'login', + 'course_view', + 'purchase', + 'course_enroll', + 'lesson_complete', + 'quiz_attempt', + 'course_complete', + 'search', + 'wishlist_add', + 'review_submit', + 'custom' +) + `); + await queryRunner.query(`CREATE TYPE public.assessment_attempt_status_enum AS ENUM ( + 'in_progress', + 'submitted', + 'graded', + 'timed_out' +) + `); + await queryRunner.query(`CREATE TYPE public.automation_actions_type_enum AS ENUM ( + 'send_email', + 'wait', + 'add_tag', + 'remove_tag', + 'add_to_segment', + 'remove_from_segment', + 'update_property', + 'webhook', + 'send_sms', + 'send_push_notification' +) + `); + await queryRunner.query(`CREATE TYPE public.automation_triggers_type_enum AS ENUM ( + 'user_signup', + 'course_enrolled', + 'course_completed', + 'purchase_made', + 'user_inactive', + 'subscription_created', + 'subscription_cancelled', + 'birthday', + 'custom_event', + 'date_based', + 'segment_entered', + 'segment_left' +) + `); + await queryRunner.query(`CREATE TYPE public.automation_workflows_status_enum AS ENUM ( + 'draft', + 'active', + 'inactive', + 'archived' +) + `); + await queryRunner.query(`CREATE TYPE public.backup_records_backuptype_enum AS ENUM ( + 'full', + 'incremental' +) + `); + await queryRunner.query(`CREATE TYPE public.backup_records_region_enum AS ENUM ( + 'us-east-1', + 'us-west-2' +) + `); + await queryRunner.query(`CREATE TYPE public.backup_records_status_enum AS ENUM ( + 'pending', + 'in_progress', + 'completed', + 'failed' +) + `); + await queryRunner.query(`CREATE TYPE public.badges_category_enum AS ENUM ( + 'LEARNING', + 'SOCIAL', + 'ACHIEVEMENT', + 'ASSESSMENT', + 'CONTRIBUTION' +) + `); + await queryRunner.query(`CREATE TYPE public.badges_criteriatype_enum AS ENUM ( + 'COURSES_COMPLETED', + 'LESSONS_WATCHED', + 'LEARNING_STREAK_DAYS', + 'ASSESSMENT_PERFECT_SCORE', + 'ASSESSMENTS_PASSED', + 'POINTS_REACHED', + 'LEVEL_REACHED', + 'REVIEWS_WRITTEN', + 'COURSES_CREATED' +) + `); + await queryRunner.query(`CREATE TYPE public.campaign_recipients_status_enum AS ENUM ( + 'pending', + 'sent', + 'failed', + 'skipped' +) + `); + await queryRunner.query(`CREATE TYPE public.cohort_assignments_status_enum AS ENUM ( + 'open', + 'closed' +) + `); + await queryRunner.query(`CREATE TYPE public.content_metadata_content_type_enum AS ENUM ( + 'image', + 'video', + 'document', + 'audio' +) + `); + await queryRunner.query(`CREATE TYPE public.content_metadata_status_enum AS ENUM ( + 'uploading', + 'processing', + 'optimized', + 'ready', + 'failed' +) + `); + await queryRunner.query(`CREATE TYPE public.content_reports_reason_enum AS ENUM ( + 'spam', + 'abuse', + 'inappropriate' +) + `); + await queryRunner.query(`CREATE TYPE public.content_reports_status_enum AS ENUM ( + 'pending', + 'under_review', + 'resolved', + 'dismissed' +) + `); + await queryRunner.query(`CREATE TYPE public.course_reviews_decision_enum AS ENUM ( + 'approved', + 'rejected', + 'changes_requested' +) + `); + await queryRunner.query(`CREATE TYPE public.course_status_enum AS ENUM ( + 'draft', + 'pending_review', + 'changes_requested', + 'published', + 'rejected', + 'archived' +) + `); + await queryRunner.query(`CREATE TYPE public.course_versions_eventtype_enum AS ENUM ( + 'created', + 'updated', + 'rolled_back' +) + `); + await queryRunner.query(`CREATE TYPE public.course_versions_status_enum AS ENUM ( + 'draft', + 'pending_review', + 'changes_requested', + 'published', + 'rejected', + 'archived' +) + `); + await queryRunner.query(`CREATE TYPE public.email_campaigns_status_enum AS ENUM ( + 'draft', + 'scheduled', + 'sending', + 'sent', + 'paused', + 'cancelled' +) + `); + await queryRunner.query(`CREATE TYPE public.email_events_eventtype_enum AS ENUM ( + 'sent', + 'delivered', + 'opened', + 'clicked', + 'bounced', + 'soft_bounced', + 'complained', + 'unsubscribed' +) + `); + await queryRunner.query(`CREATE TYPE public.experiment_metrics_type_enum AS ENUM ( + 'conversion', + 'revenue', + 'engagement', + 'retention', + 'custom' +) + `); + await queryRunner.query(`CREATE TYPE public.experiments_status_enum AS ENUM ( + 'draft', + 'running', + 'paused', + 'completed', + 'archived' +) + `); + await queryRunner.query(`CREATE TYPE public.experiments_type_enum AS ENUM ( + 'a_b_test', + 'multivariate', + 'multi_armed_bandit' +) + `); + await queryRunner.query(`CREATE TYPE public.incidents_severity_enum AS ENUM ( + 'info', + 'warning', + 'critical' +) + `); + await queryRunner.query(`CREATE TYPE public.incidents_status_enum AS ENUM ( + 'detected', + 'in_progress', + 'resolved', + 'escalated', + 'failed' +) + `); + await queryRunner.query(`CREATE TYPE public.instructor_payouts_status_enum AS ENUM ( + 'pending', + 'processing', + 'completed', + 'failed' +) + `); + await queryRunner.query(`CREATE TYPE public.invoices_status_enum AS ENUM ( + 'pending', + 'sent', + 'paid', + 'void', + 'refunded' +) + `); + await queryRunner.query(`CREATE TYPE public.notification_templates_channel_enum AS ENUM ( + 'email', + 'push', + 'in_app', + 'sms' +) + `); + await queryRunner.query(`CREATE TYPE public.notifications_priority_enum AS ENUM ( + 'low', + 'medium', + 'high', + 'urgent' +) + `); + await queryRunner.query(`CREATE TYPE public.notifications_status_enum AS ENUM ( + 'pending', + 'sent', + 'delivered', + 'failed', + 'retrying' +) + `); + await queryRunner.query(`CREATE TYPE public.notifications_type_enum AS ENUM ( + 'email', + 'push', + 'in_app', + 'sms' +) + `); + await queryRunner.query(`CREATE TYPE public.onboarding_rewards_type_enum AS ENUM ( + 'points', + 'badge', + 'coupon', + 'certificate' +) + `); + await queryRunner.query(`CREATE TYPE public.onboarding_steps_status_enum AS ENUM ( + 'active', + 'inactive' +) + `); + await queryRunner.query(`CREATE TYPE public.onboarding_steps_type_enum AS ENUM ( + 'tutorial', + 'profile_setup', + 'course_exploration', + 'first_enrollment', + 'community_intro' +) + `); + await queryRunner.query(`CREATE TYPE public.payment_methods_method_enum AS ENUM ( + 'credit_card', + 'bank_transfer', + 'paypal', + 'crypto', + 'wallet' +) + `); + await queryRunner.query(`CREATE TYPE public.payments_method_enum AS ENUM ( + 'credit_card', + 'bank_transfer', + 'paypal', + 'crypto', + 'wallet' +) + `); + await queryRunner.query(`CREATE TYPE public.payments_status_enum AS ENUM ( + 'pending', + 'processing', + 'completed', + 'failed', + 'refunded', + 'cancelled' +) + `); + await queryRunner.query(`CREATE TYPE public.question_type_enum AS ENUM ( + 'multiple_choice', + 'true_false', + 'coding' +) + `); + await queryRunner.query(`CREATE TYPE public.quota_definitions_tier_enum AS ENUM ( + 'UNAUTHENTICATED', + 'FREE', + 'PRO', + 'PREMIUM', + 'ENTERPRISE' +) + `); + await queryRunner.query(`CREATE TYPE public.recovery_tests_status_enum AS ENUM ( + 'pending', + 'running', + 'passed', + 'failed' +) + `); + await queryRunner.query(`CREATE TYPE public.refunds_status_enum AS ENUM ( + 'pending', + 'approved', + 'rejected', + 'processed', + 'failed' +) + `); + await queryRunner.query(`CREATE TYPE public.remediation_actions_status_enum AS ENUM ( + 'queued', + 'in_progress', + 'completed', + 'failed', + 'rolled_back' +) + `); + await queryRunner.query(`CREATE TYPE public.runbook_executions_status_enum AS ENUM ( + 'scheduled', + 'running', + 'completed', + 'failed', + 'partially_completed' +) + `); + await queryRunner.query(`CREATE TYPE public.segment_rules_field_enum AS ENUM ( + 'email', + 'first_name', + 'last_name', + 'created_at', + 'last_login', + 'course_count', + 'total_spent', + 'tag', + 'country', + 'subscription_status' +) + `); + await queryRunner.query(`CREATE TYPE public.segment_rules_operator_enum AS ENUM ( + 'equals', + 'not_equals', + 'contains', + 'not_contains', + 'starts_with', + 'ends_with', + 'greater_than', + 'less_than', + 'greater_or_equal', + 'less_or_equal', + 'is_set', + 'is_not_set', + 'in_list', + 'not_in_list', + 'before', + 'after', + 'between' +) + `); + await queryRunner.query(`CREATE TYPE public.subscriptions_interval_enum AS ENUM ( + 'monthly', + 'yearly', + 'quarterly', + 'weekly' +) + `); + await queryRunner.query(`CREATE TYPE public.subscriptions_status_enum AS ENUM ( + 'active', + 'cancelled', + 'past_due', + 'unpaid', + 'trialing', + 'incomplete', + 'paused' +) + `); + await queryRunner.query(`CREATE TYPE public.tenant_billing_billingcycle_enum AS ENUM ( + 'monthly', + 'quarterly', + 'yearly' +) + `); + await queryRunner.query(`CREATE TYPE public.tenants_plan_enum AS ENUM ( + 'free', + 'basic', + 'professional', + 'enterprise' +) + `); + await queryRunner.query(`CREATE TYPE public.tenants_status_enum AS ENUM ( + 'active', + 'suspended', + 'inactive', + 'trial' +) + `); + await queryRunner.query(`CREATE TYPE public.user_onboarding_progress_status_enum AS ENUM ( + 'not_started', + 'in_progress', + 'completed', + 'skipped' +) + `); + await queryRunner.query(`CREATE TYPE public.user_preferences_language_enum AS ENUM ( + 'en', + 'fr', + 'es', + 'de', + 'ar' +) + `); + await queryRunner.query(`CREATE TYPE public.user_preferences_theme_enum AS ENUM ( + 'light', + 'dark', + 'system' +) + `); + await queryRunner.query(`CREATE TYPE public.user_quota_usage_tier_enum AS ENUM ( + 'UNAUTHENTICATED', + 'FREE', + 'PRO', + 'PREMIUM', + 'ENTERPRISE' +) + `); + await queryRunner.query(`CREATE TYPE public.users_status_enum AS ENUM ( + 'active', + 'inactive', + 'suspended' +) + `); + await queryRunner.query(`CREATE TYPE public.webhook_retries_provider_enum AS ENUM ( + 'stripe', + 'paypal' +) + `); + await queryRunner.query(`CREATE TYPE public.webhook_retries_status_enum AS ENUM ( + 'pending', + 'processing', + 'succeeded', + 'failed', + 'dead_letter' +) + `); + await queryRunner.query(`CREATE TABLE public.ab_test_variants ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "abTestId" uuid NOT NULL, + name character varying NOT NULL, + subject character varying, + "templateId" character varying, + "senderName" character varying, + weight integer DEFAULT 50 NOT NULL, + "recipientCount" integer DEFAULT 0 NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.ab_tests ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + "campaignId" uuid NOT NULL, + "testField" character varying NOT NULL, + "winnerCriteria" character varying DEFAULT 'open_rate'::character varying NOT NULL, + "sampleSize" integer DEFAULT 20 NOT NULL, + status public.ab_tests_status_enum DEFAULT 'draft'::public.ab_tests_status_enum NOT NULL, + "winnerId" character varying, + "startedAt" timestamp without time zone, + "endedAt" timestamp without time zone, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.achievement_progress ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "currentProgress" integer DEFAULT 0 NOT NULL, + "targetProgress" integer DEFAULT 0 NOT NULL, + "percentageComplete" integer DEFAULT 0 NOT NULL, + "isUnlocked" boolean DEFAULT false NOT NULL, + "lastProgressUpdate" timestamp without time zone, + metadata jsonb, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "userId" uuid, + "achievementId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.achievement_statistics ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "achievementId" character varying NOT NULL, + date date NOT NULL, + "totalUnlocked" integer DEFAULT 0 NOT NULL, + "unlockedToday" integer DEFAULT 0 NOT NULL, + "unlockedPercentage" numeric(5,2) DEFAULT '0'::numeric NOT NULL, + "averageTimeToUnlock" numeric(10,2), + "activeTrackers" integer DEFAULT 0 NOT NULL, + "averageProgress" numeric(5,2) DEFAULT '0'::numeric NOT NULL, + "engagementTrend" character varying, + metadata jsonb, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.achievements ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + description character varying NOT NULL, + "longDescription" text, + "iconUrl" character varying NOT NULL, + type public.achievements_type_enum NOT NULL, + difficulty public.achievements_difficulty_enum NOT NULL, + "pointsReward" integer DEFAULT 0 NOT NULL, + "experienceReward" integer DEFAULT 100 NOT NULL, + criteria jsonb, + "progressConfig" jsonb, + "isActive" boolean DEFAULT false NOT NULL, + "isHidden" boolean DEFAULT false NOT NULL, + "unlockedBy" integer, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.analytics_events ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "eventType" public.analytics_events_eventtype_enum NOT NULL, + category character varying(64) NOT NULL, + action character varying(64) NOT NULL, + label character varying(128), + value numeric(15,2), + properties jsonb, + "sessionId" character varying, + "fingerprintId" character varying, + user_id uuid, + "ipAddress" character varying, + "userAgent" character varying, + "timestamp" timestamp(3) without time zone NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.answer ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + response json NOT NULL, + "awardedPoints" integer, + "attemptId" uuid, + "questionId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.archived_data ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "entityType" character varying NOT NULL, + "originalId" character varying NOT NULL, + data jsonb NOT NULL, + "archivedAt" timestamp without time zone DEFAULT now() NOT NULL, + "tableName" character varying +) + `); + await queryRunner.query(`CREATE TABLE public.assessment ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + title character varying NOT NULL, + description character varying, + "durationMinutes" integer NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.assessment_attempt ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "studentId" character varying NOT NULL, + status public.assessment_attempt_status_enum NOT NULL, + score integer, + "startedAt" timestamp without time zone NOT NULL, + "submittedAt" timestamp without time zone, + "assessmentId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.automation_actions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "workflowId" uuid NOT NULL, + type public.automation_actions_type_enum NOT NULL, + config jsonb NOT NULL, + "order" integer DEFAULT 0 NOT NULL, + description text, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.automation_triggers ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "workflowId" uuid NOT NULL, + type public.automation_triggers_type_enum NOT NULL, + conditions jsonb, + description text, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.automation_workflows ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + description text, + status public.automation_workflows_status_enum DEFAULT 'draft'::public.automation_workflows_status_enum NOT NULL, + "executionCount" integer DEFAULT 0 NOT NULL, + "lastExecutedAt" timestamp without time zone, + "activatedAt" timestamp without time zone, + "deactivatedAt" timestamp without time zone, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.backup_records ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "backupType" public.backup_records_backuptype_enum DEFAULT 'full'::public.backup_records_backuptype_enum NOT NULL, + status public.backup_records_status_enum DEFAULT 'pending'::public.backup_records_status_enum NOT NULL, + region public.backup_records_region_enum NOT NULL, + "databaseName" character varying NOT NULL, + "storageKey" character varying NOT NULL, + "encryptedStorageKey" character varying, + "replicatedStorageKey" character varying, + "kmsKeyId" character varying, + "backupSizeBytes" bigint, + "checksumMd5" character varying, + "checksumSha256" character varying, + "integrityVerified" boolean DEFAULT false NOT NULL, + "verifiedAt" timestamp without time zone, + "retryCount" integer DEFAULT 0 NOT NULL, + "errorMessage" character varying, + "completedAt" timestamp without time zone, + "expiresAt" timestamp without time zone, + metadata json, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.badges ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + description character varying NOT NULL, + "iconUrl" character varying, + category public.badges_category_enum NOT NULL, + "criteriaType" public.badges_criteriatype_enum NOT NULL, + "criteriaValue" jsonb, + "isActive" boolean DEFAULT true NOT NULL, + points integer DEFAULT 0 NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.campaign_recipients ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "campaignId" uuid NOT NULL, + "userId" character varying NOT NULL, + email character varying NOT NULL, + status public.campaign_recipients_status_enum DEFAULT 'pending'::public.campaign_recipients_status_enum NOT NULL, + "sentAt" timestamp without time zone, + "variantId" character varying, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.challenges ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + title character varying NOT NULL, + description character varying NOT NULL, + "rewardPoints" integer NOT NULL, + "goalValue" integer NOT NULL, + type character varying NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.cohort_assignments ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "cohortId" uuid NOT NULL, + title character varying NOT NULL, + description text, + "dueDate" timestamp without time zone, + status public.cohort_assignments_status_enum DEFAULT 'open'::public.cohort_assignments_status_enum NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.cohort_comments ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "threadId" uuid NOT NULL, + "authorId" character varying NOT NULL, + content text NOT NULL, + "parentId" character varying, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.cohort_members ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "cohortId" uuid NOT NULL, + "userId" character varying NOT NULL, + role character varying DEFAULT 'member'::character varying NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.cohort_threads ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "cohortId" uuid NOT NULL, + "authorId" character varying NOT NULL, + title character varying NOT NULL, + content text NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.cohorts ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + name character varying NOT NULL, + description text, + "ownerId" character varying NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.content_metadata ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + content_id character varying NOT NULL, + original_url character varying NOT NULL, + cdn_url character varying, + content_type public.content_metadata_content_type_enum NOT NULL, + file_name character varying NOT NULL, + mime_type character varying NOT NULL, + file_size integer NOT NULL, + optimized_size integer, + status public.content_metadata_status_enum DEFAULT 'uploading'::public.content_metadata_status_enum NOT NULL, + etag character varying, + provider character varying DEFAULT 'cloudflare'::character varying NOT NULL, + optimization_settings json, + variants json, + metadata json, + owner_id character varying, + tenant_id character varying, + error_message character varying, + retry_count integer DEFAULT 0 NOT NULL, + last_accessed_at timestamp without time zone, + access_count integer DEFAULT 0 NOT NULL, + expires_at timestamp without time zone, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.content_reports ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "contentType" character varying NOT NULL, + "contentId" character varying NOT NULL, + reason public.content_reports_reason_enum NOT NULL, + details text, + reporter_id character varying, + reviewer_id character varying, + assigned_moderator_id character varying, + "escalatedAt" timestamp without time zone, + status public.content_reports_status_enum DEFAULT 'pending'::public.content_reports_status_enum NOT NULL, + moderation_item_id integer, + "resolutionNote" text, + "resolvedAt" timestamp without time zone, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "reporterId" uuid, + "reviewerId" uuid, + "assignedModeratorId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.course ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + title character varying NOT NULL, + description text NOT NULL, + price numeric(10,2) DEFAULT '0'::numeric NOT NULL, + status public.course_status_enum DEFAULT 'draft'::public.course_status_enum NOT NULL, + "thumbnailUrl" character varying, + category character varying, + instructor_id character varying NOT NULL, + "submissionNote" text, + "searchVector" tsvector GENERATED ALWAYS AS (to_tsvector('english'::regconfig, (((((COALESCE(title, ''::character varying))::text || ' '::text) || COALESCE(description, ''::text)) || ' '::text) || (COALESCE(category, ''::character varying))::text))) STORED NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone, + "instructorId" uuid, + prerequisite_course_id uuid +) + `); + await queryRunner.query(`CREATE TABLE public.course_module ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + title character varying NOT NULL, + "order" integer DEFAULT 0 NOT NULL, + course_id character varying NOT NULL, + "deletedAt" timestamp without time zone, + "courseId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.course_reviews ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + course_id character varying NOT NULL, + reviewer_id character varying, + decision public.course_reviews_decision_enum NOT NULL, + feedback text, + "previousStatus" character varying(50), + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "courseId" uuid, + "reviewerId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.course_versions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + course_id character varying NOT NULL, + "versionNumber" integer NOT NULL, + "eventType" public.course_versions_eventtype_enum NOT NULL, + changed_by_user_id character varying, + title character varying NOT NULL, + description text NOT NULL, + price numeric(10,2) DEFAULT '0'::numeric NOT NULL, + "thumbnailUrl" character varying, + status public.course_versions_status_enum NOT NULL, + "submissionNote" text, + changes jsonb, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "courseId" uuid, + "changedById" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.email_campaigns ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + subject character varying NOT NULL, + "previewText" character varying, + content text, + "templateId" uuid, + "segmentIds" text, + status public.email_campaigns_status_enum DEFAULT 'draft'::public.email_campaigns_status_enum NOT NULL, + "scheduledAt" timestamp without time zone, + "sentAt" timestamp without time zone, + "totalRecipients" integer DEFAULT 0 NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.email_events ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "campaignId" character varying NOT NULL, + "recipientId" character varying NOT NULL, + "eventType" public.email_events_eventtype_enum NOT NULL, + "workflowId" character varying, + metadata jsonb, + "bounceReason" character varying, + "complaintType" character varying, + "reputationScore" integer, + "occurredAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.email_subscriptions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + email character varying NOT NULL, + "userId" character varying, + "isSubscribed" boolean DEFAULT true NOT NULL, + preferences text, + "unsubscribedAt" timestamp without time zone, + "unsubscribeReason" character varying, + "subscribedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.email_templates ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + subject character varying NOT NULL, + "htmlContent" text NOT NULL, + "textContent" text, + category character varying, + variables text, + "thumbnailUrl" character varying, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.enrollment ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + user_id character varying NOT NULL, + course_id character varying NOT NULL, + progress double precision DEFAULT '0'::double precision NOT NULL, + status character varying DEFAULT 'active'::character varying NOT NULL, + "enrolledAt" timestamp without time zone DEFAULT now() NOT NULL, + "lastAccessedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone, + "userId" uuid, + "courseId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.experiment_metrics ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + description text NOT NULL, + type public.experiment_metrics_type_enum DEFAULT 'conversion'::public.experiment_metrics_type_enum NOT NULL, + configuration json, + "isPrimary" boolean DEFAULT true NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "experimentId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.experiment_variants ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + description text NOT NULL, + configuration json NOT NULL, + "trafficAllocation" numeric(5,4) DEFAULT '0'::numeric NOT NULL, + "isControl" boolean DEFAULT false NOT NULL, + "isWinner" boolean DEFAULT false NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone, + "experimentId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.experiments ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + description text NOT NULL, + type public.experiments_type_enum DEFAULT 'a_b_test'::public.experiments_type_enum NOT NULL, + status public.experiments_status_enum DEFAULT 'draft'::public.experiments_status_enum NOT NULL, + "startDate" timestamp without time zone NOT NULL, + "endDate" timestamp without time zone, + "trafficAllocation" numeric(5,4) DEFAULT '1'::numeric NOT NULL, + "autoAllocateTraffic" boolean DEFAULT false NOT NULL, + "confidenceLevel" integer DEFAULT 95 NOT NULL, + "minimumSampleSize" integer DEFAULT 80 NOT NULL, + hypothesis text, + "targetingCriteria" json, + "exclusionCriteria" json, + properties json, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.forum_comments ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "threadId" uuid NOT NULL, + "parentId" character varying, + content text NOT NULL, + "authorId" character varying NOT NULL, + status character varying DEFAULT 'active'::character varying NOT NULL, + upvotes integer DEFAULT 0 NOT NULL, + downvotes integer DEFAULT 0 NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.forum_threads ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + content text NOT NULL, + "authorId" character varying NOT NULL, + status character varying DEFAULT 'active'::character varying NOT NULL, + upvotes integer DEFAULT 0 NOT NULL, + downvotes integer DEFAULT 0 NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.forum_votes ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "entityType" character varying NOT NULL, + "entityId" character varying NOT NULL, + "authorId" character varying NOT NULL, + value integer NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.incidents ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + description text NOT NULL, + status public.incidents_status_enum DEFAULT 'detected'::public.incidents_status_enum NOT NULL, + severity public.incidents_severity_enum NOT NULL, + "triggerMetrics" jsonb, + "runbookId" character varying, + "remediationActionIds" text, + "escalatedTo" character varying, + "resolvedAt" timestamp without time zone, + "resolutionNotes" text, + "detectedAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.instructor_payout_profiles ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + instructor_id uuid NOT NULL, + "payoutSchedule" character varying DEFAULT 'monthly'::character varying NOT NULL, + "payoutMethod" character varying DEFAULT 'paypal'::character varying NOT NULL, + "payoutDetails" character varying, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.instructor_payouts ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + instructor_id uuid NOT NULL, + amount numeric(10,2) NOT NULL, + currency character varying(3) DEFAULT 'USD'::character varying NOT NULL, + status public.instructor_payouts_status_enum DEFAULT 'pending'::public.instructor_payouts_status_enum NOT NULL, + "payoutMethod" character varying DEFAULT 'paypal'::character varying NOT NULL, + "payoutDetails" character varying, + "payoutDate" timestamp without time zone, + "failureReason" text, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.invoices ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "invoiceNumber" character varying NOT NULL, + amount numeric(10,2) NOT NULL, + "taxAmount" numeric(10,2) DEFAULT '0'::numeric NOT NULL, + "totalAmount" numeric(10,2) NOT NULL, + currency character varying(3) DEFAULT 'USD'::character varying NOT NULL, + items jsonb NOT NULL, + status public.invoices_status_enum DEFAULT 'pending'::public.invoices_status_enum NOT NULL, + "issuedDate" timestamp without time zone NOT NULL, + "fileUrl" character varying, + payment_id uuid NOT NULL, + user_id uuid NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.lessons ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + title character varying(255) NOT NULL, + content text, + video_url character varying(2048), + "order" integer DEFAULT 0 NOT NULL, + duration_seconds integer DEFAULT 0 NOT NULL, + module_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + CONSTRAINT "CHK_9fceacf251b7152e9063994087" CHECK (("order" >= 0)), + CONSTRAINT "CHK_d54a8f9cfd22126d59f534d0d5" CHECK ((duration_seconds >= 0)) +) + `); + await queryRunner.query(`CREATE TABLE public.moderation_event ( + id integer NOT NULL, + version integer NOT NULL, + content text NOT NULL, + score double precision NOT NULL, + status character varying NOT NULL, + "timestamp" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE SEQUENCE public.moderation_event_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 + `); + await queryRunner.query(`ALTER SEQUENCE public.moderation_event_id_seq OWNED BY public.moderation_event.id + `); + await queryRunner.query(`CREATE TABLE public.notification_preferences ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "userId" uuid NOT NULL, + "emailEnabled" boolean DEFAULT true NOT NULL, + "pushEnabled" boolean DEFAULT true NOT NULL, + "inAppEnabled" boolean DEFAULT true NOT NULL, + "smsEnabled" boolean DEFAULT false NOT NULL, + "topicSubscriptions" jsonb, + "eventFrequency" jsonb, + "globalUnsubscribe" boolean DEFAULT false NOT NULL, + "quietTimeStart" character varying DEFAULT '09:00'::character varying NOT NULL, + "quietTimeEnd" character varying DEFAULT '21:00'::character varying NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.notification_templates ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + "templateVersion" integer DEFAULT 1 NOT NULL, + channel public.notification_templates_channel_enum DEFAULT 'email'::public.notification_templates_channel_enum NOT NULL, + "subjectTemplate" character varying, + "bodyTemplate" text NOT NULL, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.notifications ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "userId" uuid NOT NULL, + title character varying NOT NULL, + content text NOT NULL, + content_hash character varying(64) NOT NULL, + type public.notifications_type_enum DEFAULT 'in_app'::public.notifications_type_enum NOT NULL, + status public.notifications_status_enum DEFAULT 'pending'::public.notifications_status_enum NOT NULL, + priority public.notifications_priority_enum DEFAULT 'medium'::public.notifications_priority_enum NOT NULL, + "isRead" boolean DEFAULT false NOT NULL, + metadata jsonb, + "deliveryAttempts" integer DEFAULT 0 NOT NULL, + "lastAttemptAt" timestamp without time zone, + "failureReason" text, + "readAt" timestamp without time zone, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.onboarding_rewards ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + description text NOT NULL, + type public.onboarding_rewards_type_enum NOT NULL, + "pointsValue" integer DEFAULT 0 NOT NULL, + "badgeId" character varying, + "couponCode" character varying, + metadata jsonb, + "requiredSteps" integer DEFAULT 0 NOT NULL, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.onboarding_steps ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + slug character varying NOT NULL, + title character varying NOT NULL, + description text NOT NULL, + type public.onboarding_steps_type_enum NOT NULL, + "order" integer DEFAULT 0 NOT NULL, + content jsonb, + status public.onboarding_steps_status_enum DEFAULT 'active'::public.onboarding_steps_status_enum NOT NULL, + "rewardPoints" integer DEFAULT 0 NOT NULL, + "rewardBadgeId" character varying, + "isRequired" boolean DEFAULT false NOT NULL, + "estimatedDurationMinutes" integer DEFAULT 0 NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.payment_methods ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + method public.payment_methods_method_enum NOT NULL, + provider character varying, + "displayName" character varying(64), + last4 character varying(4), + "expiryMonth" integer, + "expiryYear" integer, + "isDefault" boolean DEFAULT false NOT NULL, + metadata jsonb, + "userId" character varying NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone, + user_id uuid +) + `); + await queryRunner.query(`CREATE TABLE public.payments ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + amount numeric(10,2) NOT NULL, + currency character varying(3) DEFAULT 'USD'::character varying NOT NULL, + status public.payments_status_enum DEFAULT 'pending'::public.payments_status_enum NOT NULL, + method public.payments_method_enum NOT NULL, + provider character varying, + "providerPaymentId" character varying, + metadata jsonb, + user_id uuid NOT NULL, + course_id uuid, + "idempotencyKey" character varying, + "isSubscription" boolean DEFAULT false NOT NULL, + "subscriptionId" character varying, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.permissions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + resource character varying NOT NULL, + action character varying NOT NULL, + description character varying, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.point_transactions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + points integer NOT NULL, + "activityType" character varying NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "userId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.question ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + type public.question_type_enum NOT NULL, + prompt character varying NOT NULL, + options json, + "correctAnswer" json, + points integer DEFAULT 1 NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone, + "assessmentId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.quota_definitions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + tier public.quota_definitions_tier_enum, + "userId" character varying, + "requestsPerMinute" integer NOT NULL, + "requestsPerHour" integer NOT NULL, + "requestsPerDay" integer NOT NULL, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.recovery_tests ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + backup_record_id uuid NOT NULL, + status public.recovery_tests_status_enum DEFAULT 'pending'::public.recovery_tests_status_enum NOT NULL, + "testDatabaseName" character varying NOT NULL, + "validationResults" json, + "performanceMetrics" json, + "errorMessage" character varying, + "retryCount" integer DEFAULT 0 NOT NULL, + "testCompletedAt" timestamp without time zone, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.refunds ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + amount numeric(10,2), + reason text, + "refundMethod" character varying, + status public.refunds_status_enum DEFAULT 'pending'::public.refunds_status_enum NOT NULL, + "providerRefundId" character varying, + "idempotencyKey" character varying, + metadata jsonb, + payment_id uuid NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.remediation_actions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "incidentId" uuid NOT NULL, + "actionType" character varying NOT NULL, + description text NOT NULL, + status public.remediation_actions_status_enum DEFAULT 'queued'::public.remediation_actions_status_enum NOT NULL, + parameters jsonb, + "executedAt" timestamp without time zone, + "executionOutput" text, + "errorMessage" text, + "autoRollback" boolean DEFAULT false NOT NULL, + "rolledBackAt" timestamp without time zone, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.review_item ( + id integer NOT NULL, + version integer NOT NULL, + content text NOT NULL, + "safetyScore" double precision NOT NULL, + "sourceType" character varying, + "sourceId" character varying, + "reportId" character varying, + status character varying DEFAULT 'pending'::character varying NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE SEQUENCE public.review_item_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 + `); + await queryRunner.query(`ALTER SEQUENCE public.review_item_id_seq OWNED BY public.review_item.id + `); + await queryRunner.query(`CREATE TABLE public.roles ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + name character varying NOT NULL, + description text, + "isSystem" boolean DEFAULT false NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.roles_permissions_permissions ( + "rolesId" uuid NOT NULL, + "permissionsId" uuid NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.runbook_executions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "incidentId" uuid NOT NULL, + "runbookName" character varying NOT NULL, + "runbookPath" text NOT NULL, + status public.runbook_executions_status_enum DEFAULT 'scheduled'::public.runbook_executions_status_enum NOT NULL, + "startedAt" timestamp without time zone, + "completedAt" timestamp without time zone, + "stepExecutions" jsonb, + "executionSummary" text, + "errorDetails" text, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.segment_destination_configs ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + name character varying NOT NULL, + enabled boolean DEFAULT true NOT NULL, + settings jsonb DEFAULT '{}'::jsonb NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.segment_rules ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "segmentId" uuid NOT NULL, + field public.segment_rules_field_enum NOT NULL, + operator public.segment_rules_operator_enum NOT NULL, + value jsonb NOT NULL, + "order" integer DEFAULT 0 NOT NULL, + "logicalOperator" character varying DEFAULT 'AND'::character varying NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.segments ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + name character varying NOT NULL, + description text, + "isDynamic" boolean DEFAULT true NOT NULL, + "staticMemberIds" text, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.subscriptions ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "providerSubscriptionId" character varying, + status public.subscriptions_status_enum DEFAULT 'active'::public.subscriptions_status_enum NOT NULL, + "interval" public.subscriptions_interval_enum NOT NULL, + amount numeric(10,2) NOT NULL, + currency character varying(3) DEFAULT 'USD'::character varying NOT NULL, + "currentPeriodStart" timestamp without time zone, + "currentPeriodEnd" timestamp without time zone, + "cancelAtPeriodEnd" boolean DEFAULT false NOT NULL, + "cancelledAt" timestamp without time zone, + "trialStart" timestamp without time zone, + "trialEnd" timestamp without time zone, + properties jsonb, + user_id uuid NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.tenant_billing ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "tenantId" uuid NOT NULL, + "billingCycle" public.tenant_billing_billingcycle_enum DEFAULT 'monthly'::public.tenant_billing_billingcycle_enum NOT NULL, + "monthlyFee" numeric(10,2) DEFAULT '0'::numeric NOT NULL, + "currentBalance" numeric(10,2) DEFAULT '0'::numeric NOT NULL, + "totalPaid" numeric(10,2) DEFAULT '0'::numeric NOT NULL, + "lastBillingDate" timestamp without time zone, + "nextBillingDate" timestamp without time zone, + "stripeCustomerId" character varying, + "stripeSubscriptionId" character varying, + "usageMetrics" jsonb, + "billingHistory" jsonb, + "autoRenew" boolean DEFAULT true NOT NULL, + "paymentMethod" character varying, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.tenant_configs ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "tenantId" uuid NOT NULL, + "defaultLanguage" character varying DEFAULT 'en'::character varying NOT NULL, + timezone character varying DEFAULT 'UTC'::character varying NOT NULL, + currency character varying DEFAULT 'USD'::character varying NOT NULL, + features jsonb, + notifications jsonb, + security jsonb, + integrations jsonb, + "customSettings" jsonb, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.tenant_customizations ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "tenantId" uuid NOT NULL, + "logoUrl" character varying, + "faviconUrl" character varying, + "primaryColor" character varying, + "secondaryColor" character varying, + "accentColor" character varying, + "fontFamily" character varying, + theme jsonb, + "customCss" text, + "customJs" text, + "emailTemplates" jsonb, + "landingPageConfig" jsonb, + "customDomain" character varying, + "customDomainVerified" boolean DEFAULT false NOT NULL, + "domainVerificationToken" character varying, + "socialLinks" jsonb, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.tenant_usage_snapshots ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "tenantId" character varying NOT NULL, + period character varying NOT NULL, + "userCount" integer DEFAULT 0 NOT NULL, + "storageUsedMb" integer DEFAULT 0 NOT NULL, + "apiCallCount" integer DEFAULT 0 NOT NULL, + "snapshotAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.tenants ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + slug character varying NOT NULL, + name character varying NOT NULL, + description character varying, + domain character varying, + status public.tenants_status_enum DEFAULT 'trial'::public.tenants_status_enum NOT NULL, + plan public.tenants_plan_enum DEFAULT 'free'::public.tenants_plan_enum NOT NULL, + "ownerId" character varying, + "ownerEmail" character varying, + "contactEmail" character varying, + "contactPhone" character varying, + metadata jsonb, + "userLimit" integer DEFAULT 0 NOT NULL, + "storageLimit" integer DEFAULT 0 NOT NULL, + "currentUserCount" integer DEFAULT 0 NOT NULL, + "currentStorageUsage" integer DEFAULT 0 NOT NULL, + "trialEndsAt" timestamp without time zone, + "subscriptionStartsAt" timestamp without time zone, + "subscriptionEndsAt" timestamp without time zone, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.translations ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + namespace character varying NOT NULL, + key character varying NOT NULL, + locale character varying NOT NULL, + value text NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.unsubscribe_tokens ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + token character varying NOT NULL, + email character varying NOT NULL, + "userId" character varying, + "emailType" character varying, + used boolean DEFAULT false NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.user_achievements ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "unlockedAt" timestamp without time zone NOT NULL, + "unlockedMetadata" jsonb, + "pointsEarned" integer DEFAULT 0 NOT NULL, + "experienceEarned" integer DEFAULT 0 NOT NULL, + "notificationSent" boolean DEFAULT false NOT NULL, + "isHidden" boolean DEFAULT false NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "userId" uuid, + "achievementId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.user_badges ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + user_id uuid NOT NULL, + badge_id uuid NOT NULL, + "earnedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.user_challenges ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "progressValue" integer DEFAULT 0 NOT NULL, + "isCompleted" boolean DEFAULT false NOT NULL, + "completedAt" timestamp without time zone, + "userId" uuid, + "challengeId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.user_onboarding_progress ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + user_id character varying NOT NULL, + step_id character varying NOT NULL, + status public.user_onboarding_progress_status_enum DEFAULT 'not_started'::public.user_onboarding_progress_status_enum NOT NULL, + "progressPercentage" double precision DEFAULT '0'::double precision NOT NULL, + "startedAt" timestamp without time zone, + "completedAt" timestamp without time zone, + "skippedAt" timestamp without time zone, + "timeSpentSeconds" integer DEFAULT 0 NOT NULL, + metadata jsonb, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "userId" uuid, + "stepId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.user_preferences ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "userId" uuid NOT NULL, + theme public.user_preferences_theme_enum DEFAULT 'system'::public.user_preferences_theme_enum NOT NULL, + language public.user_preferences_language_enum DEFAULT 'en'::public.user_preferences_language_enum NOT NULL, + "emailNotifications" boolean DEFAULT true NOT NULL, + "pushNotifications" boolean DEFAULT true NOT NULL, + "inAppNotifications" boolean DEFAULT true NOT NULL, + "marketingEmails" boolean DEFAULT false NOT NULL, + "courseUpdates" boolean DEFAULT true NOT NULL, + "weeklyDigest" boolean DEFAULT true NOT NULL, + "customSettings" jsonb, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.user_progress ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + "totalPoints" integer DEFAULT 0 NOT NULL, + level integer DEFAULT 1 NOT NULL, + xp integer DEFAULT 0 NOT NULL, + "userId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.user_quota_usage ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + "userId" character varying NOT NULL, + tier public.user_quota_usage_tier_enum DEFAULT 'FREE'::public.user_quota_usage_tier_enum NOT NULL, + period character varying NOT NULL, + count integer DEFAULT 0 NOT NULL, + "windowStart" timestamp without time zone NOT NULL, + "windowEnd" timestamp without time zone NOT NULL, + "isBlocked" boolean DEFAULT false NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.users ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + email character varying NOT NULL, + username character varying, + password character varying, + provider character varying, + "providerId" character varying, + "providerAccessToken" character varying, + "providerRefreshToken" character varying, + "firstName" character varying NOT NULL, + "lastName" character varying NOT NULL, + status public.users_status_enum DEFAULT 'active'::public.users_status_enum NOT NULL, + "tenantId" character varying, + "profilePicture" character varying, + "isEmailVerified" boolean DEFAULT false NOT NULL, + "emailVerificationToken" character varying, + "emailVerificationExpires" timestamp without time zone, + "passwordResetToken" character varying, + "passwordResetExpires" timestamp without time zone, + "refreshToken" character varying, + "passwordHistory" text[] DEFAULT '{}'::text[] NOT NULL, + "lastLoginAt" timestamp without time zone, + "isMfaEnabled" boolean DEFAULT false NOT NULL, + "totpSecret" text, + "mfaRecoveryCodes" text[] DEFAULT '{}'::text[] NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "deletedAt" timestamp without time zone +) + `); + await queryRunner.query(`CREATE TABLE public.users_roles_roles ( + "usersId" uuid NOT NULL, + "rolesId" uuid NOT NULL +) + `); + await queryRunner.query(`CREATE TABLE public.variant_metrics ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + value numeric(15,4) DEFAULT '0'::numeric NOT NULL, + "sampleSize" integer DEFAULT 0 NOT NULL, + "conversionRate" numeric(10,4), + "standardDeviation" numeric(10,4), + "confidenceIntervalLower" numeric(10,4), + "confidenceIntervalUpper" numeric(10,4), + "pValue" numeric(10,4), + "isStatisticallySignificant" boolean DEFAULT false NOT NULL, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "variantId" uuid +) + `); + await queryRunner.query(`CREATE TABLE public.webhook_retries ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + version integer NOT NULL, + provider public.webhook_retries_provider_enum NOT NULL, + "externalEventId" character varying NOT NULL, + status public.webhook_retries_status_enum DEFAULT 'pending'::public.webhook_retries_status_enum NOT NULL, + payload jsonb, + signature text, + "retryCount" integer DEFAULT 0 NOT NULL, + "maxRetries" integer DEFAULT 3 NOT NULL, + "nextRetryTime" timestamp without time zone, + "lastError" text, + "errorDetails" jsonb, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + "processedAt" timestamp without time zone, + headers jsonb +) + `); + await queryRunner.query(`ALTER TABLE ONLY public.moderation_event ALTER COLUMN id SET DEFAULT nextval('public.moderation_event_id_seq'::regclass) + `); + await queryRunner.query(`ALTER TABLE ONLY public.review_item ALTER COLUMN id SET DEFAULT nextval('public.review_item_id_seq'::regclass) + `); + await queryRunner.query(`ALTER TABLE ONLY public.email_templates + ADD CONSTRAINT "PK_06c564c515d8cdb40b6f3bfbbb4" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.recovery_tests + ADD CONSTRAINT "PK_086105f93f0a3431d57265371b3" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_badges + ADD CONSTRAINT "PK_0ca139216824d745a930065706a" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.backup_records + ADD CONSTRAINT "PK_13c40e36547fe8bc4903891715b" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.ab_test_variants + ADD CONSTRAINT "PK_1852d233f32a6c278d1c12591a4" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.payments + ADD CONSTRAINT "PK_197ab7af18c93fbb0c9b28b4a59" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.experiment_variants + ADD CONSTRAINT "PK_1ab87aafc77c9f29445722fcdd8" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.quota_definitions + ADD CONSTRAINT "PK_1b54df16f86533774dcbdab23c9" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.achievements + ADD CONSTRAINT "PK_1bc19c37c6249f70186f318d71d" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.experiment_metrics + ADD CONSTRAINT "PK_1c6a5d4a235dbc8b2a6b4342938" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.forum_comments + ADD CONSTRAINT "PK_1c860feac1713d199c00ce1e9d1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.challenges + ADD CONSTRAINT "PK_1e664e93171e20fe4d6125466af" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.question + ADD CONSTRAINT "PK_21e5786aa0ea704ae185a79b2d5" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.email_events + ADD CONSTRAINT "PK_2ab38c98c3ca9385eff428134c2" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.course_reviews + ADD CONSTRAINT "PK_2dc117d5b688a2040125a09d1f1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.campaign_recipients + ADD CONSTRAINT "PK_30d346f6af084aa7b916945a4f1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.payment_methods + ADD CONSTRAINT "PK_34f9b8c6dfb4ac3559f7e2820d1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.forum_threads + ADD CONSTRAINT "PK_38ce39ab347624dee46e0d11e95" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenant_billing + ADD CONSTRAINT "PK_3adf5b4ad55795a97a1c8d122b2" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_achievements + ADD CONSTRAINT "PK_3d94aba7e9ed55365f68b5e77fa" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.automation_triggers + ADD CONSTRAINT "PK_48578f5993178c101adf54458d7" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohort_comments + ADD CONSTRAINT "PK_509181b7a136bfd36ceadb4327c" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.refunds + ADD CONSTRAINT "PK_5106efb01eeda7e49a78b869738" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.segment_destination_configs + ADD CONSTRAINT "PK_5302ab1192e7887b615a56c01cd" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenants + ADD CONSTRAINT "PK_53be67a04681c66b87ee27c9321" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.content_metadata + ADD CONSTRAINT "PK_54d3611149cd0967d26687a0f5e" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.moderation_event + ADD CONSTRAINT "PK_58724d91318a54243203d035ac1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenant_customizations + ADD CONSTRAINT "PK_5c5cbe00df045c9d65b8dbf934d" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.analytics_events + ADD CONSTRAINT "PK_5d643d67a09b55653e98616f421" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.instructor_payout_profiles + ADD CONSTRAINT "PK_601f21b309c899b981ce2bd225c" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohort_assignments + ADD CONSTRAINT "PK_6244076d7882c63bca9240b9d09" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenant_configs + ADD CONSTRAINT "PK_6407e80f75bae845a54c6a43a32" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.achievement_statistics + ADD CONSTRAINT "PK_64890e6c50faf8c4fbf539ac6e8" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.invoices + ADD CONSTRAINT "PK_668cef7c22a427fd822cc1be3ce" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.notifications + ADD CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.instructor_payouts + ADD CONSTRAINT "PK_6abdfa69192c194b9e793e50d88" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.users_roles_roles + ADD CONSTRAINT "PK_6c1a055682c229f5a865f2080c1" PRIMARY KEY ("usersId", "rolesId") + `); + await queryRunner.query(`ALTER TABLE ONLY public.content_reports + ADD CONSTRAINT "PK_6c59a68146cdde8de564ee649c1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.email_campaigns + ADD CONSTRAINT "PK_72bad329795785308e66d562350" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.notification_templates + ADD CONSTRAINT "PK_76f0fc48b8d057d2ae7f3a2848a" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.segment_rules + ADD CONSTRAINT "PK_7b56809897e557776b43c0b4ecd" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_progress + ADD CONSTRAINT "PK_7b5eb2436efb0051fdf05cbe839" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_challenges + ADD CONSTRAINT "PK_7c111333fc0e3a23528503498de" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.automation_actions + ADD CONSTRAINT "PK_7d56180c080e74cb362d0db9dee" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.remediation_actions + ADD CONSTRAINT "PK_7e1dec69a65387daab5794eed7d" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT "PK_7e200c699fa93865cdcdd025885" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohort_threads + ADD CONSTRAINT "PK_7ecfc73fb36e6339f64cd3ee982" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.forum_votes + ADD CONSTRAINT "PK_8932d378eb87a6b613241dacc4c" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.ab_tests + ADD CONSTRAINT "PK_897aac79b4b31d3d500c15a6810" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.badges + ADD CONSTRAINT "PK_8a651318b8de577e8e217676466" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.runbook_executions + ADD CONSTRAINT "PK_8f4b3993172cbd65648ff8294c1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.achievement_progress + ADD CONSTRAINT "PK_901cc379a8dbe909f3d617c0da1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.review_item + ADD CONSTRAINT "PK_91cdf1e8bef5fd3c1c8352e1873" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.permissions + ADD CONSTRAINT "PK_920331560282b8bd21bb02290df" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.answer + ADD CONSTRAINT "PK_9232db17b63fb1e94f97e5c224f" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.unsubscribe_tokens + ADD CONSTRAINT "PK_9622754f6a1b81e3f361abb6287" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.webhook_retries + ADD CONSTRAINT "PK_9b89e71d6573afc3f1441ed734e" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.lessons + ADD CONSTRAINT "PK_9b9a8d455cac672d262d7275730" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.course_module + ADD CONSTRAINT "PK_9d04c56010223c5997cc71093b4" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.users + ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.subscriptions + ADD CONSTRAINT "PK_a87248d73155605cf782be9ee5e" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_onboarding_progress + ADD CONSTRAINT "PK_aa1c75812d27f2d79ba3306d0c1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.experiments + ADD CONSTRAINT "PK_aafe1321d916fac58ba06ad8178" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.translations + ADD CONSTRAINT "PK_aca248c72ae1fb2390f1bf4cd87" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.assessment_attempt + ADD CONSTRAINT "PK_aca670ae50c57355f5cfbf57064" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.roles_permissions_permissions + ADD CONSTRAINT "PK_b2f4e3f7fbeb7e5b495dd819842" PRIMARY KEY ("rolesId", "permissionsId") + `); + await queryRunner.query(`ALTER TABLE ONLY public.automation_workflows + ADD CONSTRAINT "PK_b8ccdbd937062e34ce781e47fab" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.segments + ADD CONSTRAINT "PK_beff1eec19679fe8ad4f291f04e" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.course + ADD CONSTRAINT "PK_bf95180dd756fd204fb01ce4916" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.roles + ADD CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.assessment + ADD CONSTRAINT "PK_c511a7dc128256876b6b1719401" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.onboarding_rewards + ADD CONSTRAINT "PK_c7c9a7640c58e58bdeb54bcc780" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.archived_data + ADD CONSTRAINT "PK_c82de7ef69aa6e4a10c8789d409" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.course_versions + ADD CONSTRAINT "PK_cc9e77578136a62e599f33d15ff" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.incidents + ADD CONSTRAINT "PK_ccb34c01719889017e2246469f9" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.point_transactions + ADD CONSTRAINT "PK_ceb5185b63f070e23d65509b0a7" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohort_members + ADD CONSTRAINT "PK_d0caa8ae723dba3e41fe36a4faf" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.onboarding_steps + ADD CONSTRAINT "PK_d7cbc8d9a41ce2b2cadd6f08929" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenant_usage_snapshots + ADD CONSTRAINT "PK_e1a2a6d3b678a6fac4631c4d4ba" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.variant_metrics + ADD CONSTRAINT "PK_e33bbdb963d8b135607a6508ae1" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.email_subscriptions + ADD CONSTRAINT "PK_e60e3a09d341892b331d0e14e98" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_preferences + ADD CONSTRAINT "PK_e8cfb5b31af61cd363a6b6d7c25" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.notification_preferences + ADD CONSTRAINT "PK_e94e2b543f2f218ee68e4f4fad2" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_quota_usage + ADD CONSTRAINT "PK_f5a9e8470a155d6b43d187a4313" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohorts + ADD CONSTRAINT "PK_fd38f76b135e907b834fda1e752" PRIMARY KEY (id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_progress + ADD CONSTRAINT "REL_b5d0e1b57bc6c761fb49e79bf8" UNIQUE ("userId") + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_preferences + ADD CONSTRAINT "REL_b6202d1cacc63a0b9c8dac2abd" UNIQUE ("userId") + `); + await queryRunner.query(`ALTER TABLE ONLY public.notification_preferences + ADD CONSTRAINT "REL_b70c44e8b00757584a39322559" UNIQUE ("userId") + `); + await queryRunner.query(`ALTER TABLE ONLY public.ab_tests + ADD CONSTRAINT "REL_d59c4bf6cb19f01d0438973463" UNIQUE ("campaignId") + `); + await queryRunner.query(`ALTER TABLE ONLY public.refunds + ADD CONSTRAINT "UQ_1b692560d438e5fc6edebbb27dd" UNIQUE ("idempotencyKey") + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenants + ADD CONSTRAINT "UQ_2310ecc5cb8be427097154b18fc" UNIQUE (slug) + `); + await queryRunner.query(`ALTER TABLE ONLY public.onboarding_steps + ADD CONSTRAINT "UQ_238b7a7accc697f9dc38bb2fa6e" UNIQUE (slug) + `); + await queryRunner.query(`ALTER TABLE ONLY public.subscriptions + ADD CONSTRAINT "UQ_2eeb6283285e7ffc0afed6606a7" UNIQUE ("providerSubscriptionId") + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenant_customizations + ADD CONSTRAINT "UQ_31864fcad0db05f8624f8bc3b8e" UNIQUE ("customDomain") + `); + await queryRunner.query(`ALTER TABLE ONLY public.unsubscribe_tokens + ADD CONSTRAINT "UQ_423597e129f4f9402c976acce2a" UNIQUE (token) + `); + await queryRunner.query(`ALTER TABLE ONLY public.forum_votes + ADD CONSTRAINT "UQ_61ca502616bea958499dbb1c7f6" UNIQUE ("entityType", "entityId", "authorId") + `); + await queryRunner.query(`ALTER TABLE ONLY public.roles + ADD CONSTRAINT "UQ_648e3f5447f725579d7d4ffdfb7" UNIQUE (name) + `); + await queryRunner.query(`ALTER TABLE ONLY public.payments + ADD CONSTRAINT "UQ_743b9fb1d2a059f2f7860418e4e" UNIQUE ("idempotencyKey") + `); + await queryRunner.query(`ALTER TABLE ONLY public.segment_destination_configs + ADD CONSTRAINT "UQ_7e12359558f5263f9215c36ea65" UNIQUE (name) + `); + await queryRunner.query(`ALTER TABLE ONLY public.permissions + ADD CONSTRAINT "UQ_89456a09b598ce8915c702c5283" UNIQUE (resource) + `); + await queryRunner.query(`ALTER TABLE ONLY public.email_subscriptions + ADD CONSTRAINT "UQ_8e8eebc384627fb6fa7fd3993fd" UNIQUE (email) + `); + await queryRunner.query(`ALTER TABLE ONLY public.users + ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE (email) + `); + await queryRunner.query(`ALTER TABLE ONLY public.badges + ADD CONSTRAINT "UQ_9c91fc9c4a4ae01712baad1e9f6" UNIQUE (name) + `); + await queryRunner.query(`ALTER TABLE ONLY public.translations + ADD CONSTRAINT "UQ_a0bfd1bdc0bb9fd2a2093028ea5" UNIQUE (namespace, key, locale) + `); + await queryRunner.query(`ALTER TABLE ONLY public.invoices + ADD CONSTRAINT "UQ_bf8e0f9dd4558ef209ec111782d" UNIQUE ("invoiceNumber") + `); + await queryRunner.query(`ALTER TABLE ONLY public.experiments + ADD CONSTRAINT "UQ_c697ebe6ea1b865670e8bf253bf" UNIQUE (name) + `); + await queryRunner.query(`ALTER TABLE ONLY public.instructor_payout_profiles + ADD CONSTRAINT "UQ_e0a5c10dfd4a71af0f150204e98" UNIQUE (instructor_id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.content_metadata + ADD CONSTRAINT "UQ_e352b4f29ef956f97c5577c948b" UNIQUE (content_id) + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_044801224a107532ec72863c77" ON public.notification_templates USING btree (name, "templateVersion", channel) + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_201b6e34825dc5bd06181320bd" ON public.user_badges USING btree (user_id, badge_id) + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5d0d509445abda22b52a7646d8" ON public.user_onboarding_progress USING btree (user_id, step_id) + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_7dcd253a269d9c7809feda085d" ON public.achievement_progress USING btree ("userId", "achievementId") + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8e8eebc384627fb6fa7fd3993f" ON public.email_subscriptions USING btree (email) + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a34a641b43a91c958b94a26bcf" ON public.webhook_retries USING btree (provider, "externalEventId") + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b6202d1cacc63a0b9c8dac2abd" ON public.user_preferences USING btree ("userId") + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c1acd69cf91b1e353634c152dd" ON public.user_achievements USING btree ("userId", "achievementId") + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e352b4f29ef956f97c5577c948" ON public.content_metadata USING btree (content_id) + `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e865d6f59667897a7fb67ff69a" ON public.user_quota_usage USING btree ("userId", period) + `); + await queryRunner.query(`CREATE INDEX idx_notifications_dedup ON public.notifications USING btree ("userId", type, content_hash, "createdAt") + `); + await queryRunner.query(`ALTER TABLE ONLY public.invoices + ADD CONSTRAINT "FK_02781c49b25ceb502571f0315f6" FOREIGN KEY (payment_id) REFERENCES public.payments(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.remediation_actions + ADD CONSTRAINT "FK_1373fc6f0bbd68c756ef8c75ca5" FOREIGN KEY ("incidentId") REFERENCES public.incidents(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohort_assignments + ADD CONSTRAINT "FK_16a0f89062ed4edd4cce26cc5e6" FOREIGN KEY ("cohortId") REFERENCES public.cohorts(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.achievement_progress + ADD CONSTRAINT "FK_20adca3c4217744bfd8a485e98d" FOREIGN KEY ("achievementId") REFERENCES public.achievements(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.invoices + ADD CONSTRAINT "FK_26daf5e433d6fb88ee32ce93637" FOREIGN KEY (user_id) REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.course_reviews + ADD CONSTRAINT "FK_2e14b15c1658e0b78a86df473d9" FOREIGN KEY ("courseId") REFERENCES public.course(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.course + ADD CONSTRAINT "FK_32d94af473bb59d808d9a68e17b" FOREIGN KEY ("instructorId") REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.lessons + ADD CONSTRAINT "FK_35fb2307535d90a6ed290af1f4a" FOREIGN KEY (module_id) REFERENCES public.course_module(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_achievements + ADD CONSTRAINT "FK_3ac6bc9da3e8a56f3f7082012dd" FOREIGN KEY ("userId") REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohort_threads + ADD CONSTRAINT "FK_3d8ef5f5715cb3d50315007aee5" FOREIGN KEY ("cohortId") REFERENCES public.cohorts(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.campaign_recipients + ADD CONSTRAINT "FK_3e9c3dcf81739170d74e9265e1b" FOREIGN KEY ("campaignId") REFERENCES public.email_campaigns(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.experiment_metrics + ADD CONSTRAINT "FK_40cda44d91beb827b9f4bfb2840" FOREIGN KEY ("experimentId") REFERENCES public.experiments(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.payments + ADD CONSTRAINT "FK_427785468fb7d2733f59e7d7d39" FOREIGN KEY (user_id) REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenant_customizations + ADD CONSTRAINT "FK_42bbd07a91fea7e34f175b19fea" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.point_transactions + ADD CONSTRAINT "FK_557e0c8c5a7a1a449723de76822" FOREIGN KEY ("userId") REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.automation_actions + ADD CONSTRAINT "FK_5a953fa7c5b5b963b27e5c47449" FOREIGN KEY ("workflowId") REFERENCES public.automation_workflows(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_challenges + ADD CONSTRAINT "FK_640161d2f02abec6529e6f04104" FOREIGN KEY ("challengeId") REFERENCES public.challenges(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.recovery_tests + ADD CONSTRAINT "FK_66893187eeb7c3198e3d07c5950" FOREIGN KEY (backup_record_id) REFERENCES public.backup_records(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.notifications + ADD CONSTRAINT "FK_692a909ee0fa9383e7859f9b406" FOREIGN KEY ("userId") REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_achievements + ADD CONSTRAINT "FK_6a5a5816f54d0044ba5f3dc2b74" FOREIGN KEY ("achievementId") REFERENCES public.achievements(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.segment_rules + ADD CONSTRAINT "FK_6bf07ed351de4f3a7fba1a4b41e" FOREIGN KEY ("segmentId") REFERENCES public.segments(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_badges + ADD CONSTRAINT "FK_715b81e610ab276ff6603cfc8e8" FOREIGN KEY (badge_id) REFERENCES public.badges(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_onboarding_progress + ADD CONSTRAINT "FK_76bf58bc457c1b7cbb471e98a59" FOREIGN KEY ("userId") REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.course_versions + ADD CONSTRAINT "FK_7a6c6e6402470c0448c890c1109" FOREIGN KEY ("changedById") REFERENCES public.users(id) ON DELETE SET NULL + `); + await queryRunner.query(`ALTER TABLE ONLY public.refunds + ADD CONSTRAINT "FK_7f48aa5d56c42aeb495db016683" FOREIGN KEY (payment_id) REFERENCES public.payments(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.variant_metrics + ADD CONSTRAINT "FK_87a6c14cf9314c8d5c1b659c42e" FOREIGN KEY ("variantId") REFERENCES public.experiment_variants(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.instructor_payouts + ADD CONSTRAINT "FK_8dacda0f101c97b73304ddf0fc4" FOREIGN KEY (instructor_id) REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.achievement_progress + ADD CONSTRAINT "FK_93f082fa43fdce4811582a4e3cd" FOREIGN KEY ("userId") REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.course + ADD CONSTRAINT "FK_9678cf7b818267a347774678be7" FOREIGN KEY (prerequisite_course_id) REFERENCES public.course(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.content_reports + ADD CONSTRAINT "FK_99485b9a6ac529679aacf397a98" FOREIGN KEY ("assignedModeratorId") REFERENCES public.users(id) ON DELETE SET NULL + `); + await queryRunner.query(`ALTER TABLE ONLY public.ab_test_variants + ADD CONSTRAINT "FK_a2b9c64f3a34d35742e4168e852" FOREIGN KEY ("abTestId") REFERENCES public.ab_tests(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.answer + ADD CONSTRAINT "FK_a4013f10cd6924793fbd5f0d637" FOREIGN KEY ("questionId") REFERENCES public.question(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.experiment_variants + ADD CONSTRAINT "FK_a5408d0f8f271922f7aadd01d24" FOREIGN KEY ("experimentId") REFERENCES public.experiments(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.email_campaigns + ADD CONSTRAINT "FK_ac1a8d28ac7e2f7aff92b573183" FOREIGN KEY ("templateId") REFERENCES public.email_templates(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.course_versions + ADD CONSTRAINT "FK_afedc5469f7b236608580a4fe94" FOREIGN KEY ("courseId") REFERENCES public.course(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.users_roles_roles + ADD CONSTRAINT "FK_b2f0366aa9349789527e0c36d97" FOREIGN KEY ("rolesId") REFERENCES public.roles(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_challenges + ADD CONSTRAINT "FK_b5566f854f08d7c88e6ebc71eb1" FOREIGN KEY ("userId") REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_progress + ADD CONSTRAINT "FK_b5d0e1b57bc6c761fb49e79bf89" FOREIGN KEY ("userId") REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_preferences + ADD CONSTRAINT "FK_b6202d1cacc63a0b9c8dac2abd4" FOREIGN KEY ("userId") REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.notification_preferences + ADD CONSTRAINT "FK_b70c44e8b00757584a393225593" FOREIGN KEY ("userId") REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohort_comments + ADD CONSTRAINT "FK_ba56e0a5617605dcc62cb33f117" FOREIGN KEY ("threadId") REFERENCES public.cohort_threads(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.course_reviews + ADD CONSTRAINT "FK_bd91d18bd636b093927b337c240" FOREIGN KEY ("reviewerId") REFERENCES public.users(id) ON DELETE SET NULL + `); + await queryRunner.query(`ALTER TABLE ONLY public.assessment_attempt + ADD CONSTRAINT "FK_be7568295d6479a34017ffbe8e0" FOREIGN KEY ("assessmentId") REFERENCES public.assessment(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.analytics_events + ADD CONSTRAINT "FK_c49704abb1730ae4121d5ac9f5e" FOREIGN KEY (user_id) REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.payments + ADD CONSTRAINT "FK_c5fa169d2de9407d99f2c6e4fab" FOREIGN KEY (course_id) REFERENCES public.course(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.automation_triggers + ADD CONSTRAINT "FK_cc7bcfed749f38cd30960f124bb" FOREIGN KEY ("workflowId") REFERENCES public.automation_workflows(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.question + ADD CONSTRAINT "FK_cc93e703bbc40e60a53db016b39" FOREIGN KEY ("assessmentId") REFERENCES public.assessment(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.forum_comments + ADD CONSTRAINT "FK_cd0efc698e697e6530e704d39e5" FOREIGN KEY ("threadId") REFERENCES public.forum_threads(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.subscriptions + ADD CONSTRAINT "FK_d0a95ef8a28188364c546eb65c1" FOREIGN KEY (user_id) REFERENCES public.users(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT "FK_d1a599a7740b4f4bd1120850f04" FOREIGN KEY ("courseId") REFERENCES public.course(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.runbook_executions + ADD CONSTRAINT "FK_d4364b5f4c7a2498b9ea0d76a66" FOREIGN KEY ("incidentId") REFERENCES public.incidents(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.ab_tests + ADD CONSTRAINT "FK_d59c4bf6cb19f01d0438973463c" FOREIGN KEY ("campaignId") REFERENCES public.email_campaigns(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.content_reports + ADD CONSTRAINT "FK_d687e5407cb85be0c24463f86ea" FOREIGN KEY ("reporterId") REFERENCES public.users(id) ON DELETE SET NULL + `); + await queryRunner.query(`ALTER TABLE ONLY public.payment_methods + ADD CONSTRAINT "FK_d7d7fb15569674aaadcfbc0428c" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_onboarding_progress + ADD CONSTRAINT "FK_d89ca46064e19b57da7167efd57" FOREIGN KEY ("stepId") REFERENCES public.onboarding_steps(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.roles_permissions_permissions + ADD CONSTRAINT "FK_dc2b9d46195bb3ed28abbf7c9e3" FOREIGN KEY ("rolesId") REFERENCES public.roles(id) ON UPDATE CASCADE ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.answer + ADD CONSTRAINT "FK_df3b92aa295640d070922ebc382" FOREIGN KEY ("attemptId") REFERENCES public.assessment_attempt(id) + `); + await queryRunner.query(`ALTER TABLE ONLY public.users_roles_roles + ADD CONSTRAINT "FK_df951a64f09865171d2d7a502b1" FOREIGN KEY ("usersId") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.cohort_members + ADD CONSTRAINT "FK_e00ea7965d1ed8c53b3c85a56e9" FOREIGN KEY ("cohortId") REFERENCES public.cohorts(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.instructor_payout_profiles + ADD CONSTRAINT "FK_e0a5c10dfd4a71af0f150204e98" FOREIGN KEY (instructor_id) REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenant_billing + ADD CONSTRAINT "FK_e20f751a4d8faf089b1922eaf53" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.course_module + ADD CONSTRAINT "FK_e27b3a3cf92fd9b32f152a4f7fc" FOREIGN KEY ("courseId") REFERENCES public.course(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT "FK_e97ecbf11356b5173ce7fb0b060" FOREIGN KEY ("userId") REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.user_badges + ADD CONSTRAINT "FK_f1221d9b1aaa64b1f3c98ed46d3" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.content_reports + ADD CONSTRAINT "FK_f6da62af798600a20370bb2e251" FOREIGN KEY ("reviewerId") REFERENCES public.users(id) ON DELETE SET NULL + `); + await queryRunner.query(`ALTER TABLE ONLY public.tenant_configs + ADD CONSTRAINT "FK_fd467ff7c6435655c8d715f2211" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON DELETE CASCADE + `); + await queryRunner.query(`ALTER TABLE ONLY public.roles_permissions_permissions + ADD CONSTRAINT "FK_fd4d5d4c7f7ff16c57549b72c6f" FOREIGN KEY ("permissionsId") REFERENCES public.permissions(id) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Dropping the entire baseline schema is handled by `migration:revert` + // in reverse order; the tables are dropped as their corresponding + // migrations are reverted. + } +} diff --git a/src/migrations/1783000000001-reencrypt-oauth-provider-tokens.ts b/src/migrations/1783000000001-reencrypt-oauth-provider-tokens.ts index 07a65076..df23916f 100644 --- a/src/migrations/1783000000001-reencrypt-oauth-provider-tokens.ts +++ b/src/migrations/1783000000001-reencrypt-oauth-provider-tokens.ts @@ -27,14 +27,6 @@ import * as crypto from 'crypto'; */ export class ReencryptOAuthProviderTokens1783000000001 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - const secret = process.env.ENCRYPTION_SECRET; - if (!secret) { - throw new Error( - 'ENCRYPTION_SECRET must be set in the migration environment to run Issue #799 re-encryption.', - ); - } - const key = crypto.createHash('sha256').update(secret).digest(); - const rows: Array<{ id: string; providerAccessToken: string | null; @@ -48,10 +40,20 @@ export class ReencryptOAuthProviderTokens1783000000001 implements MigrationInter OR ("providerRefreshToken" IS NOT NULL AND "providerRefreshToken" <> '')`, ); + // Nothing to re-encrypt (e.g. a fresh database): return before demanding + // ENCRYPTION_SECRET so schema migrations can run in CI/empty environments. if (rows.length === 0) { return; } + const secret = process.env.ENCRYPTION_SECRET; + if (!secret) { + throw new Error( + 'ENCRYPTION_SECRET must be set in the migration environment to run Issue #799 re-encryption.', + ); + } + const key = crypto.createHash('sha256').update(secret).digest(); + for (const row of rows) { const encryptedAccess = this.maybeEncrypt(row.providerAccessToken, key); const encryptedRefresh = this.maybeEncrypt(row.providerRefreshToken, key); diff --git a/src/migrations/1785000000000-add-unique-course-version-constraint.ts b/src/migrations/1785000000000-add-unique-course-version-constraint.ts index dd7e79b5..5a63107d 100644 --- a/src/migrations/1785000000000-add-unique-course-version-constraint.ts +++ b/src/migrations/1785000000000-add-unique-course-version-constraint.ts @@ -9,8 +9,8 @@ export class AddUniqueCourseVersionConstraint1785000000000 implements MigrationI FROM ( SELECT id, ROW_NUMBER() OVER ( - PARTITION BY course_id, version_number - ORDER BY created_at DESC + PARTITION BY course_id, "versionNumber" + ORDER BY "createdAt" DESC ) AS rn FROM course_versions ) dup @@ -21,7 +21,7 @@ export class AddUniqueCourseVersionConstraint1785000000000 implements MigrationI await queryRunner.query(` CREATE UNIQUE INDEX IF NOT EXISTS "IDX_course_versions_course_id_version_number" - ON "course_versions" ("course_id", "version_number") + ON "course_versions" ("course_id", "versionNumber") `); } diff --git a/src/migrations/1790000000000-add-paused-subscription-status.ts b/src/migrations/1790000000000-add-paused-subscription-status.ts index 89346b3a..080a85e6 100644 --- a/src/migrations/1790000000000-add-paused-subscription-status.ts +++ b/src/migrations/1790000000000-add-paused-subscription-status.ts @@ -2,10 +2,28 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddPausedSubscriptionStatus1790000000000 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - // Add 'paused' to the subscription_status enum type + // Add 'paused' to the subscription status enum type. + // + // TypeORM names enum types `__enum`, so the type backing + // `subscriptions.status` is `subscriptions_status_enum`. An older + // production schema used the hand-written name `subscription_status`, so we + // add the value to whichever of the two exists (and no-op when neither + // does, e.g. fresh installs where the baseline already contains 'paused'). await queryRunner.query(` - ALTER TYPE "subscription_status" - ADD VALUE IF NOT EXISTS 'paused' + DO $$ + DECLARE + enum_type text; + BEGIN + SELECT typname INTO enum_type + FROM pg_type + WHERE typname IN ('subscriptions_status_enum', 'subscription_status') + ORDER BY (typname = 'subscriptions_status_enum') DESC + LIMIT 1; + + IF enum_type IS NOT NULL THEN + EXECUTE format('ALTER TYPE %I ADD VALUE IF NOT EXISTS ''paused''', enum_type); + END IF; + END $$; `); } diff --git a/src/migrations/1790000000000-fix-invoice-number-sequence.ts b/src/migrations/1790000000000-fix-invoice-number-sequence.ts index 61fa0fde..c5ebfa5a 100644 --- a/src/migrations/1790000000000-fix-invoice-number-sequence.ts +++ b/src/migrations/1790000000000-fix-invoice-number-sequence.ts @@ -50,8 +50,11 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; */ export class FixInvoiceNumberSequence1790000000000 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - const connection = queryRunner.connection; - const queryInterface = connection.createQueryRunner(); + // Use the queryRunner handed to us by the migration runner. Migrations run + // inside a single shared transaction (TypeORM's default "all" mode), so a + // freshly created query runner (a separate pooled connection) would not see + // tables created by earlier migrations in the same run. + const queryInterface = queryRunner; try { // ======================================================================== @@ -177,12 +180,19 @@ export class FixInvoiceNumberSequence1790000000000 implements MigrationInterface this.logger(`Migrated ${existingInvoices.length} invoices to sequence-based numbering.`); } else { - // No duplicates: Initialize sequence to 1 if no invoices, or max + 1 if some exist - const maxSeqResult = await queryInterface.query(` + // No duplicates: Initialize sequence to the number of existing invoices. + // If there are no invoices the sequence is left at its default start + // value (1) — setval(..., 0, true) would fail because 0 is below the + // sequence minimum. + const countResult = await queryInterface.query(` SELECT COUNT(*) as cnt FROM invoices; `); - const nextVal = (maxSeqResult[0]?.cnt || 0) + 1; - await queryInterface.query("SELECT setval('invoice_number_seq', $1, true);", [nextVal - 1]); + const invoiceCount = Number(countResult[0]?.cnt ?? 0); + if (invoiceCount > 0) { + await queryInterface.query("SELECT setval('invoice_number_seq', $1, true);", [ + invoiceCount, + ]); + } } // ======================================================================== @@ -247,38 +257,34 @@ export class FixInvoiceNumberSequence1790000000000 implements MigrationInterface } catch (error) { this.logger(`ERROR during migration: ${(error as Error).message}`); throw error; - } finally { - await queryInterface.release(); } } public async down(queryRunner: QueryRunner): Promise { - const queryInterface = queryRunner.connection.createQueryRunner(); - - try { - this.logger('Reverting migration...'); - - // Drop unique constraint - await queryInterface.query(` - ALTER TABLE invoices - DROP CONSTRAINT IF EXISTS "UQ_invoices_invoiceNumber"; - `); - - // Drop sequence - await queryInterface.query(` - DROP SEQUENCE IF EXISTS invoice_number_seq CASCADE; - `); - - // Revert invoices back to old timestamp+random format (not recoverable, but at least - // the database is consistent) - // In practice, you may want to store the old value somewhere before migration for rollback - this.logger( - 'WARNING: Down migration cannot recover original timestamp+random values. ' + - 'Invoices have been renumbered. Consider a full restore from backup if needed.', - ); - } finally { - await queryInterface.release(); - } + // Same rationale as up(): reuse the transaction's queryRunner rather than + // opening a separate pooled connection. + const queryInterface = queryRunner; + + this.logger('Reverting migration...'); + + // Drop unique constraint + await queryInterface.query(` + ALTER TABLE invoices + DROP CONSTRAINT IF EXISTS "UQ_invoices_invoiceNumber"; + `); + + // Drop sequence + await queryInterface.query(` + DROP SEQUENCE IF EXISTS invoice_number_seq CASCADE; + `); + + // Revert invoices back to old timestamp+random format (not recoverable, but at least + // the database is consistent) + // In practice, you may want to store the old value somewhere before migration for rollback + this.logger( + 'WARNING: Down migration cannot recover original timestamp+random values. ' + + 'Invoices have been renumbered. Consider a full restore from backup if needed.', + ); } /** diff --git a/src/onboarding/entities/onboarding-reward.entity.ts b/src/onboarding/entities/onboarding-reward.entity.ts index b0a33917..690d77f5 100644 --- a/src/onboarding/entities/onboarding-reward.entity.ts +++ b/src/onboarding/entities/onboarding-reward.entity.ts @@ -16,7 +16,6 @@ export enum RewardType { } @Entity('onboarding_rewards') -@Index(['type']) @Index(['isActive']) export class OnboardingReward { @PrimaryGeneratedColumn('uuid') diff --git a/src/onboarding/entities/onboarding-step.entity.ts b/src/onboarding/entities/onboarding-step.entity.ts index fac23fb4..f0c837dc 100644 --- a/src/onboarding/entities/onboarding-step.entity.ts +++ b/src/onboarding/entities/onboarding-step.entity.ts @@ -22,8 +22,6 @@ export enum OnboardingStepStatus { } @Entity('onboarding_steps') -@Index(['order']) -@Index(['type']) export class OnboardingStep { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/src/onboarding/entities/user-onboarding-progress.entity.ts b/src/onboarding/entities/user-onboarding-progress.entity.ts index 73ce3118..fb71b7e3 100644 --- a/src/onboarding/entities/user-onboarding-progress.entity.ts +++ b/src/onboarding/entities/user-onboarding-progress.entity.ts @@ -21,7 +21,6 @@ export enum OnboardingProgressStatus { @Entity('user_onboarding_progress') @Index(['userId', 'stepId'], { unique: true }) @Index(['userId', 'status']) -@Index(['completedAt']) export class UserOnboardingProgress { @PrimaryGeneratedColumn('uuid') id: string;