diff --git a/scripts/drift-check.sh b/scripts/drift-check.sh index 3f851a2a..c174185b 100755 --- a/scripts/drift-check.sh +++ b/scripts/drift-check.sh @@ -3,40 +3,20 @@ # 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. +# schema. The check fails when `migration:generate --check` reports ANY +# difference between the entity metadata and the schema produced by running +# all migrations — model changes without a corresponding migration are +# rejected. 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 + --check drift-check >/dev/null 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 +echo "::error::Schema drift detected — the database schema produced by migrations does not match the entity definitions." +echo "Run 'pnpm run migration:generate' to create a migration for the change, or align the entity definitions." exit 1 diff --git a/scripts/known-drift.txt b/scripts/known-drift.txt deleted file mode 100644 index 33f8f2bc..00000000 --- a/scripts/known-drift.txt +++ /dev/null @@ -1,622 +0,0 @@ -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/assessment/grading/entities/criterion-grade.entity.ts b/src/assessment/grading/entities/criterion-grade.entity.ts index 9ea5887c..87667c9b 100644 --- a/src/assessment/grading/entities/criterion-grade.entity.ts +++ b/src/assessment/grading/entities/criterion-grade.entity.ts @@ -31,11 +31,11 @@ export class CriterionGrade { grade: SubmissionGrade; @Column({ name: 'grade_id', type: 'uuid' }) - @Index() + @Index('IDX_criterion_grades_grade') gradeId: string; @Column({ name: 'criterion_id', type: 'uuid' }) - @Index() + @Index('IDX_criterion_grades_criterion') criterionId: string; /** Selected rubric level (optional when `points` is provided directly). */ diff --git a/src/assessment/grading/entities/feedback-template.entity.ts b/src/assessment/grading/entities/feedback-template.entity.ts index 728b07c6..4d6c4c6b 100644 --- a/src/assessment/grading/entities/feedback-template.entity.ts +++ b/src/assessment/grading/entities/feedback-template.entity.ts @@ -30,27 +30,27 @@ export class FeedbackTemplate { id: string; @Column() - @Index() + @Index('IDX_feedback_templates_name') name: string; @Column({ type: 'text' }) body: string; @Column({ name: 'owner_id', type: 'uuid', nullable: true }) - @Index() + @Index('IDX_feedback_templates_owner') ownerId?: string; /** When true, this template is auto-selected when none is supplied. */ @Column({ default: false }) isDefault: boolean; - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamptz' }) @Index() createdAt: Date; - @UpdateDateColumn() + @UpdateDateColumn({ type: 'timestamptz' }) updatedAt: Date; - @DeleteDateColumn() + @DeleteDateColumn({ type: 'timestamptz' }) deletedAt?: Date; } diff --git a/src/assessment/grading/entities/rubric-criterion.entity.ts b/src/assessment/grading/entities/rubric-criterion.entity.ts index 47b3dd76..33f8bb93 100644 --- a/src/assessment/grading/entities/rubric-criterion.entity.ts +++ b/src/assessment/grading/entities/rubric-criterion.entity.ts @@ -26,7 +26,7 @@ export class RubricCriterion { rubric: Rubric; @Column({ name: 'rubric_id', type: 'uuid' }) - @Index() + @Index('IDX_rubric_criteria_rubric') rubricId: string; @Column() diff --git a/src/assessment/grading/entities/rubric-level.entity.ts b/src/assessment/grading/entities/rubric-level.entity.ts index 080030c8..8319f2c9 100644 --- a/src/assessment/grading/entities/rubric-level.entity.ts +++ b/src/assessment/grading/entities/rubric-level.entity.ts @@ -19,7 +19,7 @@ export class RubricLevel { criterion: RubricCriterion; @Column({ name: 'criterion_id', type: 'uuid' }) - @Index() + @Index('IDX_rubric_levels_criterion') criterionId: string; /** Short label, e.g. "Excellent". */ diff --git a/src/assessment/grading/entities/rubric.entity.ts b/src/assessment/grading/entities/rubric.entity.ts index 48c63747..738c9f4f 100644 --- a/src/assessment/grading/entities/rubric.entity.ts +++ b/src/assessment/grading/entities/rubric.entity.ts @@ -22,7 +22,7 @@ export class Rubric { id: string; @Column() - @Index() + @Index('IDX_rubrics_name') name: string; @Column({ type: 'text', nullable: true }) @@ -30,7 +30,7 @@ export class Rubric { /** Optional owner (instructor/admin) — used for ownership checks. */ @Column({ name: 'owner_id', type: 'uuid', nullable: true }) - @Index() + @Index('IDX_rubrics_owner') ownerId?: string; /** @@ -39,7 +39,7 @@ export class Rubric { * across many assessments by leaving this null. */ @Column({ name: 'assessment_id', type: 'uuid', nullable: true }) - @Index() + @Index('IDX_rubrics_assessment') assessmentId?: string; /** @@ -61,13 +61,13 @@ export class Rubric { }) criteria: RubricCriterion[]; - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamptz' }) @Index() createdAt: Date; - @UpdateDateColumn() + @UpdateDateColumn({ type: 'timestamptz' }) updatedAt: Date; - @DeleteDateColumn() + @DeleteDateColumn({ type: 'timestamptz' }) deletedAt?: Date; } diff --git a/src/assessment/grading/entities/submission-grade.entity.ts b/src/assessment/grading/entities/submission-grade.entity.ts index 6ef0b091..1c7402b1 100644 --- a/src/assessment/grading/entities/submission-grade.entity.ts +++ b/src/assessment/grading/entities/submission-grade.entity.ts @@ -37,7 +37,7 @@ export class SubmissionGrade { /** The graded assessment attempt. */ @Column({ name: 'attempt_id', type: 'uuid' }) - @Index() + @Index('IDX_submission_grades_attempt') attemptId: string; @ManyToOne(() => Rubric, { onDelete: 'RESTRICT' }) @@ -45,7 +45,7 @@ export class SubmissionGrade { rubric: Rubric; @Column({ name: 'rubric_id', type: 'uuid' }) - @Index() + @Index('IDX_submission_grades_rubric') rubricId: string; /** Optional grader (admin/instructor). Null when auto-graded. */ @@ -82,9 +82,9 @@ export class SubmissionGrade { @OneToMany(() => CriterionGrade, (cg) => cg.grade, { cascade: true }) criterionGrades: CriterionGrade[]; - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; - @UpdateDateColumn() + @UpdateDateColumn({ type: 'timestamptz' }) updatedAt: Date; } diff --git a/src/audit-log/audit-log.entity.ts b/src/audit-log/audit-log.entity.ts index 39711c2e..fb3a51a7 100644 --- a/src/audit-log/audit-log.entity.ts +++ b/src/audit-log/audit-log.entity.ts @@ -1,4 +1,11 @@ -import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, Index } from 'typeorm'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, + VersionColumn, +} from 'typeorm'; import { AuditAction, AuditSeverity, AuditCategory } from './enums/audit-action.enum'; export enum HttpMethod { GET = 'GET', @@ -22,18 +29,21 @@ export enum HttpMethod { * policy runs without a full table scan. */ @Entity('audit_logs') -@Index(['userId', 'timestamp']) -@Index(['action', 'timestamp']) -@Index(['category', 'timestamp']) -@Index(['severity', 'timestamp']) -@Index(['entityType', 'entityId', 'timestamp']) -@Index(['ipAddress', 'timestamp']) -@Index(['timestamp']) +@Index('IDX_audit_logs_user_timestamp', ['userId', 'timestamp']) +@Index('IDX_audit_logs_action_timestamp', ['action', 'timestamp']) +@Index('IDX_audit_logs_category_timestamp', ['category', 'timestamp']) +@Index('IDX_audit_logs_severity_timestamp', ['severity', 'timestamp']) +@Index('IDX_audit_logs_entity', ['entityType', 'entityId', 'timestamp']) +@Index('IDX_audit_logs_ip_address', ['ipAddress', 'timestamp']) +@Index('IDX_audit_logs_timestamp', ['timestamp']) @Index(['retentionUntil']) // required for efficient retention policy deletes export class AuditLog { @PrimaryGeneratedColumn('uuid') id: string; + @VersionColumn({ default: 1 }) + version: number; + // ── Actor ────────────────────────────────────────────────────────────────── @Column({ name: 'user_id', nullable: true }) @@ -93,7 +103,7 @@ export class AuditLog { apiEndpoint: string | null; /** Constrained to known HTTP verbs — free strings invite silent typos. */ - @Column({ name: 'http_method', type: 'enum', enum: HttpMethod, nullable: true }) + @Column({ name: 'http_method', nullable: true }) httpMethod: HttpMethod | null; @Column({ name: 'status_code', nullable: true }) @@ -109,7 +119,7 @@ export class AuditLog { // ── Timestamps ───────────────────────────────────────────────────────────── - @CreateDateColumn({ name: 'timestamp' }) + @CreateDateColumn({ name: 'timestamp', type: 'timestamptz' }) timestamp: Date; /** @@ -117,6 +127,6 @@ export class AuditLog { * Null means the record is kept indefinitely (e.g. CRITICAL severity logs). * Indexed — see class-level @Index. */ - @Column({ name: 'retention_until', nullable: true }) + @Column({ name: 'retention_until', type: 'timestamptz', nullable: true }) retentionUntil: Date | null; } diff --git a/src/courses/entities/bulk-operation.entity.ts b/src/courses/entities/bulk-operation.entity.ts index 636a2847..c1fd7f9f 100644 --- a/src/courses/entities/bulk-operation.entity.ts +++ b/src/courses/entities/bulk-operation.entity.ts @@ -60,7 +60,7 @@ export class BulkOperation { @JoinColumn({ name: 'initiated_by_id' }) initiatedBy?: User; - @Index() + @Index('IDX_course_bulk_ops_initiator') @Column({ name: 'initiated_by_id', type: 'uuid', @@ -83,7 +83,7 @@ export class BulkOperation { }) undoneById?: string; - @Index() + @Index('IDX_course_bulk_ops_type') @Column({ type: 'enum', enum: BulkOperationType, @@ -161,7 +161,7 @@ export class BulkOperation { /** * Optimistic locking to prevent concurrent updates. */ - @VersionColumn() + @VersionColumn({ default: 1 }) version: number; @Index() diff --git a/src/courses/entities/course.entity.ts b/src/courses/entities/course.entity.ts index bc2cf76b..ce3bd21b 100644 --- a/src/courses/entities/course.entity.ts +++ b/src/courses/entities/course.entity.ts @@ -56,19 +56,30 @@ export class Course { /** Optional category/tag used for catalog grouping and bulk operations. */ @Column({ nullable: true }) - @Index() + @Index('IDX_course_category') category?: string; /** Difficulty level, e.g. 'beginner' | 'intermediate' | 'advanced'. Used by search filtering. */ @Column({ nullable: true }) - @Index() + @Index('IDX_course_level') level?: string; /** ISO 639-1 language code the course is taught in. Used by search filtering. */ @Column({ nullable: true }) - @Index() + @Index('IDX_course_language') language?: string; + /** ISO 4217 currency code the course is priced in (e.g. "USD"). */ + @Column({ + name: 'currency', + type: 'varchar', + length: 3, + default: 'USD', + nullable: true, + }) + @Index('IDX_course_currency') + currency?: string; + @ManyToOne(() => User, (user) => user.courses) instructor: User; @@ -109,6 +120,7 @@ export class Course { */ @Index('IDX_course_search_vector', { synchronize: false }) @Column({ + name: 'search_vector', type: 'tsvector', select: false, generatedType: 'STORED', diff --git a/src/entities/schema_change.entity.ts b/src/entities/schema_change.entity.ts index 0bf6143c..cd3a2ac1 100644 --- a/src/entities/schema_change.entity.ts +++ b/src/entities/schema_change.entity.ts @@ -42,6 +42,6 @@ export class SchemaChange { @CreateDateColumn({ type: 'timestamp with time zone' }) createdAt: Date; - @ManyToOne(() => SchemaVersion, (sv) => sv.changes, { onDelete: 'CASCADE' }) + @ManyToOne(() => SchemaVersion, (sv) => sv.changes, { onDelete: 'CASCADE', nullable: false }) schemaVersion: SchemaVersion; } diff --git a/src/forum/entities/forum-comment.entity.ts b/src/forum/entities/forum-comment.entity.ts index c4ed7004..8d15b24c 100644 --- a/src/forum/entities/forum-comment.entity.ts +++ b/src/forum/entities/forum-comment.entity.ts @@ -11,8 +11,8 @@ import { import { ForumThread } from './forum-thread.entity'; @Entity('forum_comments') -@Index(['threadId', 'createdAt']) -@Index(['parentId']) +@Index('IDX_forum_comments_threadId_createdAt', ['threadId', 'createdAt']) +@Index('IDX_forum_comments_parentId', ['parentId']) export class ForumComment { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/src/forum/entities/forum-thread.entity.ts b/src/forum/entities/forum-thread.entity.ts index 160dcee0..59766acf 100644 --- a/src/forum/entities/forum-thread.entity.ts +++ b/src/forum/entities/forum-thread.entity.ts @@ -10,7 +10,7 @@ import { import { ForumComment } from './forum-comment.entity'; @Entity('forum_threads') -@Index(['status', 'createdAt']) +@Index('IDX_forum_threads_status_createdAt', ['status', 'createdAt']) export class ForumThread { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/src/forum/entities/forum-vote.entity.ts b/src/forum/entities/forum-vote.entity.ts index 3aab3f53..b68940c4 100644 --- a/src/forum/entities/forum-vote.entity.ts +++ b/src/forum/entities/forum-vote.entity.ts @@ -12,7 +12,7 @@ import { User } from '../../users/entities/user.entity'; @Entity('forum_votes') @Unique(['entityType', 'entityId', 'authorId']) -@Index(['entityType', 'entityId']) +@Index('IDX_forum_votes_entityType_entityId', ['entityType', 'entityId']) export class ForumVote { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/src/gamification/entities/challenge.entity.ts b/src/gamification/entities/challenge.entity.ts index da60049a..7c9b736b 100644 --- a/src/gamification/entities/challenge.entity.ts +++ b/src/gamification/entities/challenge.entity.ts @@ -4,7 +4,7 @@ import { Entity, PrimaryGeneratedColumn, Column, VersionColumn, Index } from 'ty * Represents the challenge entity. */ @Entity('challenges') -@Index(['type']) +@Index('IDX_challenges_type', ['type']) export class Challenge { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/src/gamification/entities/point-transaction.entity.ts b/src/gamification/entities/point-transaction.entity.ts index c6afea44..643fb9be 100644 --- a/src/gamification/entities/point-transaction.entity.ts +++ b/src/gamification/entities/point-transaction.entity.ts @@ -13,8 +13,8 @@ import { User } from '../../users/entities/user.entity'; * Represents the point Transaction entity. */ @Entity('point_transactions') -@Index(['user', 'createdAt']) -@Index(['activityType']) +@Index('IDX_point_transactions_user_createdAt', ['user', 'createdAt']) +@Index('IDX_point_transactions_activityType', ['activityType']) export class PointTransaction { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/src/gamification/entities/tier-reward.entity.ts b/src/gamification/entities/tier-reward.entity.ts index ceac5e12..58f3f409 100644 --- a/src/gamification/entities/tier-reward.entity.ts +++ b/src/gamification/entities/tier-reward.entity.ts @@ -5,15 +5,15 @@ import { Tier } from '../enums/tier.enum'; * Defines the reward granted when a user reaches a specific tier. */ @Entity('tier_rewards') -@Index(['tier']) +@Index('IDX_tier_rewards_tier', ['tier']) export class TierReward { @PrimaryGeneratedColumn('uuid') id: string; - @VersionColumn() + @VersionColumn({ default: 1 }) version: number; - @Column({ type: 'enum', enum: Tier, unique: true }) + @Column({ type: 'enum', enum: Tier, unique: true, enumName: 'tier_enum' }) tier: Tier; @Column() diff --git a/src/gamification/entities/user-challenge.entity.ts b/src/gamification/entities/user-challenge.entity.ts index 15219f20..e76c045c 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(['user', 'challenge']) +@Index('IDX_user_challenges_user_challenge', ['user', 'challenge']) export class UserChallenge { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/src/gamification/entities/user-progress.entity.ts b/src/gamification/entities/user-progress.entity.ts index 6daa713c..73181497 100644 --- a/src/gamification/entities/user-progress.entity.ts +++ b/src/gamification/entities/user-progress.entity.ts @@ -38,7 +38,7 @@ export class UserProgress { @Index() xp: number; - @Column({ type: 'enum', enum: Tier, default: Tier.BRONZE }) - @Index() + @Column({ type: 'enum', enum: Tier, default: Tier.BRONZE, enumName: 'tier_enum' }) + @Index('IDX_user_progress_tier') tier: Tier; } diff --git a/src/messaging/message.entity.ts b/src/messaging/message.entity.ts index 0fd5dff8..55d0b109 100644 --- a/src/messaging/message.entity.ts +++ b/src/messaging/message.entity.ts @@ -29,11 +29,11 @@ export class Message { readAt: Date | null; // Optional relations for convenience (may be lazy loaded) - @ManyToOne(() => User, { eager: false }) + @ManyToOne(() => User, { eager: false, onDelete: 'CASCADE' }) @JoinColumn({ name: 'senderId' }) sender: User; - @ManyToOne(() => User, { eager: false }) + @ManyToOne(() => User, { eager: false, onDelete: 'CASCADE' }) @JoinColumn({ name: 'recipientId' }) recipient: User; } diff --git a/src/migrations/1796000000000-reconcile-schema-drift.ts b/src/migrations/1796000000000-reconcile-schema-drift.ts new file mode 100644 index 00000000..3a5f2db2 --- /dev/null +++ b/src/migrations/1796000000000-reconcile-schema-drift.ts @@ -0,0 +1,773 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ReconcileSchemaDrift1796000000000 implements MigrationInterface { + name = 'ReconcileSchemaDrift1796000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "forum_votes" DROP CONSTRAINT "FK_forum_votes_authorId_users"', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "FK_course_bulk_ops_initiator"', + ); + await queryRunner.query( + 'ALTER TABLE "rubric_levels" DROP CONSTRAINT "FK_rubric_levels_criterion"', + ); + await queryRunner.query( + 'ALTER TABLE "rubric_criteria" DROP CONSTRAINT "FK_rubric_criteria_rubric"', + ); + await queryRunner.query( + 'ALTER TABLE "criterion_grades" DROP CONSTRAINT "FK_criterion_grades_grade"', + ); + await queryRunner.query( + 'ALTER TABLE "submission_grades" DROP CONSTRAINT "FK_submission_grades_rubric"', + ); + await queryRunner.query('DROP INDEX "public"."IDX_audit_logs_entity"'); + await queryRunner.query('DROP INDEX "public"."IDX_audit_logs_ip_address"'); + await queryRunner.query('DROP INDEX "public"."UQ_forum_votes_entityType_entityId_authorId"'); + await queryRunner.query('ALTER TABLE "course_bulk_operations" ADD "undone_by_id" uuid'); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" ADD "reason" character varying(255)', + ); + await queryRunner.query('ALTER TABLE "course_bulk_operations" ADD "notes" text'); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" ADD "version" integer NOT NULL DEFAULT \'1\'', + ); + await queryRunner.query('DROP INDEX "public"."IDX_audit_logs_action_timestamp"'); + await queryRunner.query( + 'ALTER TYPE "public"."audit_logs_action_enum" RENAME TO "audit_logs_action_enum_old"', + ); + await queryRunner.query( + "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')", + ); + await queryRunner.query( + 'ALTER TABLE "audit_logs" ALTER COLUMN "action" TYPE "public"."audit_logs_action_enum" USING "action"::"text"::"public"."audit_logs_action_enum"', + ); + await queryRunner.query('DROP TYPE "public"."audit_logs_action_enum_old"'); + await queryRunner.query('ALTER TABLE "course" DROP COLUMN "searchVector"'); + await queryRunner.query('ALTER TABLE "course" ALTER COLUMN "search_vector" SET NOT NULL'); + await queryRunner.query( + "INSERT INTO \"typeorm_metadata\"(\"database\", \"schema\", \"table\", \"type\", \"name\", \"value\") SELECT current_database(), 'public', 'course', 'GENERATED_COLUMN', 'search_vector', 'to_tsvector(''english'', coalesce(\"title\", '''') || '' '' || coalesce(\"description\", '''') || '' '' || coalesce(\"category\", ''''))'", + ); + await queryRunner.query( + 'CREATE INDEX "IDX_3dad32ba0ff20feee98b1b0c43" ON "lessons" ("title") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_633a748d9720d4c3c25f0fc120" ON "lessons" ("order") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_35fb2307535d90a6ed290af1f4" ON "lessons" ("module_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d80cf66830a0231b8104893b67" ON "lessons" ("deleted_at") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_65395710ac88af450c5123f030" ON "course_module" ("course_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_fc17c7e94154a17e767b7674f1" ON "enrollment" ("user_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_dd1ce01d1164c8bbdda052ced7" ON "enrollment" ("course_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_46ad51ba4f4bb7db2817d75419" ON "enrollment" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_756e18fc1918b63fb4d79b4db6" ON "enrollment" ("enrolledAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d03f18ea29009de1e37a6d156b" ON "enrollment" ("course_id", "enrolledAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_91e3503740d174a4599e81ad8a" ON "enrollment" ("user_id", "enrolledAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d6e7a6c84587eb7809c5b910f8" ON "enrollment" ("course_id", "status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c2c4377a3e7773772bf670eb47" ON "enrollment" ("user_id", "status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_1f69fdcbd7ea5f0e52c3230c00" ON "course_reviews" ("course_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_34cbd320b458b8cf9e0d580304" ON "course_versions" ("course_id") ', + ); + await queryRunner.query('CREATE INDEX "IDX_ac5edecc1aefa58ed0237a7ee4" ON "course" ("title") '); + await queryRunner.query( + 'CREATE INDEX "IDX_baccb82c6179dca139f6b8c768" ON "course" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_deca5c9911b3b2100b36106082" ON "course" ("instructor_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_2bd3a6327596cc908d574a8219" ON "course" ("createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_72058429360b0daed61d94fb4f" ON "course" ("instructor_id", "createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_a27a3c9d6a41c0fa0fd1cb3f85" ON "course" ("status", "createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_da04f89054f39981438894dfe3" ON "permissions" ("createdAt") ', + ); + await queryRunner.query('CREATE INDEX "IDX_648e3f5447f725579d7d4ffdfb" ON "roles" ("name") '); + await queryRunner.query( + 'CREATE INDEX "IDX_4d018866397b1e7e78d03b4566" ON "roles" ("createdAt") ', + ); + await queryRunner.query('CREATE INDEX "IDX_97672ac88f789774dd47f7c8be" ON "users" ("email") '); + await queryRunner.query( + 'CREATE INDEX "IDX_fe0bb3f6520ee0469504521e71" ON "users" ("username") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_fab34e0791096b2a0a1bf8bd7f" ON "users" ("providerId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c58f7e88c286e5e3478960a998" ON "users" ("tenantId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_5fb386b7d3d50fd5657ef76c3d" ON "audit_logs" ("retention_until") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_audit_logs_ip_address" ON "audit_logs" ("ip_address", "timestamp") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_audit_logs_entity" ON "audit_logs" ("entity_type", "entity_id", "timestamp") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_audit_logs_action_timestamp" ON "audit_logs" ("action", "timestamp") ', + ); + await queryRunner.query('CREATE INDEX "IDX_2310ecc5cb8be427097154b18f" ON "tenants" ("slug") '); + await queryRunner.query( + 'CREATE INDEX "IDX_cfd29d0da332d5c46d9b073863" ON "tenant_usage_snapshots" ("tenantId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_616c8b49254550dbee7f917f3a" ON "tenant_usage_snapshots" ("tenantId", "period") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_42bbd07a91fea7e34f175b19fe" ON "tenant_customizations" ("tenantId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_fd467ff7c6435655c8d715f221" ON "tenant_configs" ("tenantId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_e20f751a4d8faf089b1922eaf5" ON "tenant_billing" ("tenantId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_dfc023bb093b1afcbc91798837" ON "user_quota_usage" ("userId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_69752e0d44a35a0dce9d9f06d0" ON "quota_definitions" ("userId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_8d5af5c5dbdc0d91ce1178a004" ON "quota_definitions" ("tier", "isActive") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_6ccf973355b70645eff37774de" ON "subscriptions" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d0a95ef8a28188364c546eb65c" ON "subscriptions" ("user_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_1a15756e257e0eaf01edc85645" ON "subscriptions" ("user_id", "status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_32b41cdb985a296213e9a928b5" ON "payments" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_427785468fb7d2733f59e7d7d3" ON "payments" ("user_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c5fa169d2de9407d99f2c6e4fa" ON "payments" ("course_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_743b9fb1d2a059f2f7860418e4" ON "payments" ("idempotencyKey") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c2a89b63635ae2b4cc76f17a08" ON "payments" ("user_id", "status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_1b692560d438e5fc6edebbb27d" ON "refunds" ("idempotencyKey") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_8dacda0f101c97b73304ddf0fc" ON "instructor_payouts" ("instructor_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_7684def1bb4135f494082b34ee" ON "instructor_payouts" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_e0a5c10dfd4a71af0f150204e9" ON "instructor_payout_profiles" ("instructor_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_580f1dbf7bceb9c2cde8baf7ff" ON "payment_methods" ("userId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_fcd49f4739a516d59675b8134e" ON "payment_methods" ("userId", "isDefault") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_bf8e0f9dd4558ef209ec111782" ON "invoices" ("invoiceNumber") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ac0f09364e3701d9ed35435288" ON "invoices" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_02781c49b25ceb502571f0315f" ON "invoices" ("payment_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_26daf5e433d6fb88ee32ce9363" ON "invoices" ("user_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_238b7a7accc697f9dc38bb2fa6" ON "onboarding_steps" ("slug") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_b76aaa5336dcdff6ca464044b6" ON "onboarding_steps" ("type") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_0cab16d7274814ef7b9b021c64" ON "onboarding_steps" ("order") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_4b459011ef4d3e4f8618b404c0" ON "user_onboarding_progress" ("user_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d9da1249e3125778f926607e9d" ON "user_onboarding_progress" ("step_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c0dda6108b7bbd68c6823d4983" ON "user_onboarding_progress" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_cdef16da64ce64a5bf79062ec1" ON "user_onboarding_progress" ("completedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_a0fa8c64807caecc993a5d399d" ON "user_onboarding_progress" ("user_id", "status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_b3676e630f8875765de2bd587e" ON "onboarding_rewards" ("name") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c93297fa63b08ac2e83972bd5a" ON "onboarding_rewards" ("type") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_b1cc4fe47a41d78d10f26589be" ON "onboarding_rewards" ("isActive") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_692a909ee0fa9383e7859f9b40" ON "notifications" ("userId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ac27b070934c64650ab339f4c6" ON "notifications" ("updatedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_467f6869324eebd9de644f4c28" ON "content_reports" ("contentType") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_79782456f7ad0ce730435a950a" ON "content_reports" ("contentId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_0aababfebc5662624a4a66b639" ON "content_reports" ("reporter_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c001c66a488dafe7aa8aa19402" ON "content_reports" ("assigned_moderator_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ec4970e90eebcc7a16efb37816" ON "content_reports" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_6c7e0cec7358f74ca9222628a1" ON "content_reports" ("moderation_item_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_translations_namespace_locale" ON "translations" ("namespace", "locale") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_6e199fc01241bcb8ec806e0c6a" ON "incidents" ("detectedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_e2102fc64215f0a1fcb9ad4879" ON "incidents" ("status", "severity") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_0c4a4e0584c4d08a779239d250" ON "runbook_executions" ("startedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_041cd4f77805d53e3408cb4ae6" ON "runbook_executions" ("incidentId", "status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_43bd09d8fa296ece993503b6f2" ON "remediation_actions" ("executedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_f9614fbc7820536559cdc65147" ON "remediation_actions" ("incidentId", "status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_b5d0e1b57bc6c761fb49e79bf8" ON "user_progress" ("userId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_014bd57abd7a916e466c5aaf80" ON "user_progress" ("totalPoints") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_7a5ad30aa2b8be04ca4a6d724a" ON "user_progress" ("level") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_22498516df46fa90e200915793" ON "user_progress" ("xp") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c0085fb66787d45eb374ddef38" ON "badges" ("category") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_f1221d9b1aaa64b1f3c98ed46d" ON "user_badges" ("user_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_423597e129f4f9402c976acce2" ON "unsubscribe_tokens" ("token") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_428c3f972502e77d9e2c79051b" ON "unsubscribe_tokens" ("email") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_57833e6da94a22790e6913a509" ON "email_events" ("workflowId", "eventType") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_a07f0454cce5637fd109a105dd" ON "email_events" ("recipientId", "eventType") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_9036fb7240edcc5f620ceff406" ON "email_events" ("campaignId", "eventType") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_3f28c2b1ce5590ea0892267033" ON "campaign_recipients" ("campaignId", "status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d3e1023a52cb7875b6c10d3a15" ON "email_campaigns" ("name") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d9b4760e416c3767d44e5be0fd" ON "email_campaigns" ("subject") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ac1a8d28ac7e2f7aff92b57318" ON "email_campaigns" ("templateId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_fc77d3d8f8101ab793cf17986f" ON "email_campaigns" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_862a5402a4d18274a07a2a7ccc" ON "email_campaigns" ("scheduledAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_5d23a7a8f43556bcd03a18a2b6" ON "email_campaigns" ("sentAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d3f089bf865364504bda9c2ff6" ON "archived_data" ("entityType") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_e0b0b69779afb474ee220573c8" ON "archived_data" ("originalId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d16a377e285494ac71048f2b84" ON "archived_data" ("archivedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_92fd74dcbe9b1adad11ff3091d" ON "archived_data" ("entityType", "originalId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_eda643b345dd73d5fc1a456a4b" ON "course_bulk_operations" ("undone_by_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_4d55d5b6642e4e9677c5af16b3" ON "course_bulk_operations" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_62e0da442361b07616d26593b3" ON "course_bulk_operations" ("createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_e00ea7965d1ed8c53b3c85a56e" ON "cohort_members" ("cohortId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_9360325be3f332793f1734704e" ON "cohort_members" ("userId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_e607ca8d8c3d11ccdc725a3dd0" ON "cohort_members" ("cohortId", "createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ba56e0a5617605dcc62cb33f11" ON "cohort_comments" ("threadId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_3d8ef5f5715cb3d50315007aee" ON "cohort_threads" ("cohortId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_177b41bfa4c6574e30e95da04e" ON "cohort_threads" ("cohortId", "createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_16a0f89062ed4edd4cce26cc5e" ON "cohort_assignments" ("cohortId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_c2e0f2c83c50446d23955dff0a" ON "cohort_assignments" ("cohortId", "createdAt") ', + ); + await queryRunner.query('CREATE INDEX "IDX_d1865366170f468d8eb720f8c8" ON "cohorts" ("name") '); + await queryRunner.query( + 'CREATE INDEX "IDX_a8707945701186685e1b5e9c51" ON "content_metadata" ("expires_at") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_cd5b89a167f9f9821b70ff88bf" ON "content_metadata" ("content_type") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_21202b13906bffe54448b2a553" ON "content_metadata" ("status") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ee13cd8cabbac2258203bb1c95" ON "backup_records" ("completedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d2faa8fa665e7cdbd602d557df" ON "backup_records" ("region") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_1ff95d74c5186613b05e651802" ON "backup_records" ("status", "createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_f1d4afbdb77bbb26a461726b68" ON "recovery_tests" ("testCompletedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_66893187eeb7c3198e3d07c595" ON "recovery_tests" ("backup_record_id") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_06ed7514e57cb8bd0c09ce3be0" ON "recovery_tests" ("status", "createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_4185366a8d6033a3aad864621d" ON "assessment" ("title") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d146560603e5ee3716c519b103" ON "assessment" ("durationMinutes") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_3a50f773964d78f404d73bcb36" ON "assessment" ("createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_91578dceeb42466b9285f29e4b" ON "question" ("type") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_5649f3ee460ee3d00ce2114fc5" ON "question" ("prompt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ec14ddf4e267af8aa908806a97" ON "question" ("createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d6e8c39a1de2c9b659b227c9e6" ON "analytics_events" ("timestamp") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_38c954e266791189dfd7b6ffc4" ON "analytics_events" ("eventType", "createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_d4b972aafe44b57154abba8db3" ON "analytics_events" ("user_id", "eventType", "createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_93f082fa43fdce4811582a4e3c" ON "achievement_progress" ("userId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_20adca3c4217744bfd8a485e98" ON "achievement_progress" ("achievementId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_629092c0eb5931f28a88932b45" ON "achievement_progress" ("isUnlocked") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_3ac6bc9da3e8a56f3f7082012d" ON "user_achievements" ("userId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_6a5a5816f54d0044ba5f3dc2b7" ON "user_achievements" ("achievementId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_1b62007f50cea2887ba68ea710" ON "user_achievements" ("unlockedAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_258ef1682aba959da9301e5dee" ON "achievement_statistics" ("achievementId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_fb0b9985fcfe901ed223a5468a" ON "achievement_statistics" ("date") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_139af264ff823c0f6e17be7c8e" ON "achievement_statistics" ("achievementId", "date") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ef31ae63719e8ca42b0a658438" ON "webhook_retries" ("createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_5ff375f35bf59b074f1a64f1b2" ON "webhook_retries" ("status", "nextRetryTime") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_ba7c709a66f7360180c4d1bcd9" ON "rubrics" ("createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_82294735678621182b86847277" ON "feedback_templates" ("createdAt") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_dc2b9d46195bb3ed28abbf7c9e" ON "roles_permissions_permissions" ("rolesId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_fd4d5d4c7f7ff16c57549b72c6" ON "roles_permissions_permissions" ("permissionsId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_df951a64f09865171d2d7a502b" ON "users_roles_roles" ("usersId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_b2f0366aa9349789527e0c36d9" ON "users_roles_roles" ("rolesId") ', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "CHK_63a6345d11ef40993abe0af7d7" CHECK ("failureCount" >= 0)', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "CHK_454249837abfcca2dce79a2941" CHECK ("successCount" >= 0)', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" ADD CONSTRAINT "CHK_dffedab4e655d35cc59b4ef18f" CHECK ("totalCount" >= 0)', + ); + await queryRunner.query( + 'ALTER TABLE "forum_votes" ADD CONSTRAINT "FK_930875619d15f219f30923b724c" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION', + ); + await queryRunner.query( + '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', + ); + await queryRunner.query( + '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', + ); + await queryRunner.query( + 'ALTER TABLE "rubric_levels" ADD CONSTRAINT "FK_8c0b069d24a8380f5356d22e427" FOREIGN KEY ("criterion_id") REFERENCES "rubric_criteria"("id") ON DELETE CASCADE ON UPDATE NO ACTION', + ); + await queryRunner.query( + 'ALTER TABLE "rubric_criteria" ADD CONSTRAINT "FK_67d38effbc57ecb21bc2901057e" FOREIGN KEY ("rubric_id") REFERENCES "rubrics"("id") ON DELETE CASCADE ON UPDATE NO ACTION', + ); + await queryRunner.query( + 'ALTER TABLE "criterion_grades" ADD CONSTRAINT "FK_e63fa5de1df5c92236101dcfc4b" FOREIGN KEY ("grade_id") REFERENCES "submission_grades"("id") ON DELETE CASCADE ON UPDATE NO ACTION', + ); + await queryRunner.query( + 'ALTER TABLE "submission_grades" ADD CONSTRAINT "FK_37bd4055f0a14a276992573e103" FOREIGN KEY ("rubric_id") REFERENCES "rubrics"("id") ON DELETE RESTRICT ON UPDATE NO ACTION', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "submission_grades" DROP CONSTRAINT "FK_37bd4055f0a14a276992573e103"', + ); + await queryRunner.query( + 'ALTER TABLE "criterion_grades" DROP CONSTRAINT "FK_e63fa5de1df5c92236101dcfc4b"', + ); + await queryRunner.query( + 'ALTER TABLE "rubric_criteria" DROP CONSTRAINT "FK_67d38effbc57ecb21bc2901057e"', + ); + await queryRunner.query( + 'ALTER TABLE "rubric_levels" DROP CONSTRAINT "FK_8c0b069d24a8380f5356d22e427"', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "FK_eda643b345dd73d5fc1a456a4bf"', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "FK_1af4ba5bb120d226b6a4f994209"', + ); + await queryRunner.query( + 'ALTER TABLE "forum_votes" DROP CONSTRAINT "FK_930875619d15f219f30923b724c"', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "CHK_dffedab4e655d35cc59b4ef18f"', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "CHK_454249837abfcca2dce79a2941"', + ); + await queryRunner.query( + 'ALTER TABLE "course_bulk_operations" DROP CONSTRAINT "CHK_63a6345d11ef40993abe0af7d7"', + ); + await queryRunner.query('DROP INDEX "public"."IDX_b2f0366aa9349789527e0c36d9"'); + await queryRunner.query('DROP INDEX "public"."IDX_df951a64f09865171d2d7a502b"'); + await queryRunner.query('DROP INDEX "public"."IDX_fd4d5d4c7f7ff16c57549b72c6"'); + await queryRunner.query('DROP INDEX "public"."IDX_dc2b9d46195bb3ed28abbf7c9e"'); + await queryRunner.query('DROP INDEX "public"."IDX_82294735678621182b86847277"'); + await queryRunner.query('DROP INDEX "public"."IDX_ba7c709a66f7360180c4d1bcd9"'); + await queryRunner.query('DROP INDEX "public"."IDX_5ff375f35bf59b074f1a64f1b2"'); + await queryRunner.query('DROP INDEX "public"."IDX_ef31ae63719e8ca42b0a658438"'); + await queryRunner.query('DROP INDEX "public"."IDX_139af264ff823c0f6e17be7c8e"'); + await queryRunner.query('DROP INDEX "public"."IDX_fb0b9985fcfe901ed223a5468a"'); + await queryRunner.query('DROP INDEX "public"."IDX_258ef1682aba959da9301e5dee"'); + await queryRunner.query('DROP INDEX "public"."IDX_1b62007f50cea2887ba68ea710"'); + await queryRunner.query('DROP INDEX "public"."IDX_6a5a5816f54d0044ba5f3dc2b7"'); + await queryRunner.query('DROP INDEX "public"."IDX_3ac6bc9da3e8a56f3f7082012d"'); + await queryRunner.query('DROP INDEX "public"."IDX_629092c0eb5931f28a88932b45"'); + await queryRunner.query('DROP INDEX "public"."IDX_20adca3c4217744bfd8a485e98"'); + await queryRunner.query('DROP INDEX "public"."IDX_93f082fa43fdce4811582a4e3c"'); + await queryRunner.query('DROP INDEX "public"."IDX_d4b972aafe44b57154abba8db3"'); + await queryRunner.query('DROP INDEX "public"."IDX_38c954e266791189dfd7b6ffc4"'); + await queryRunner.query('DROP INDEX "public"."IDX_d6e8c39a1de2c9b659b227c9e6"'); + await queryRunner.query('DROP INDEX "public"."IDX_ec14ddf4e267af8aa908806a97"'); + await queryRunner.query('DROP INDEX "public"."IDX_5649f3ee460ee3d00ce2114fc5"'); + await queryRunner.query('DROP INDEX "public"."IDX_91578dceeb42466b9285f29e4b"'); + await queryRunner.query('DROP INDEX "public"."IDX_3a50f773964d78f404d73bcb36"'); + await queryRunner.query('DROP INDEX "public"."IDX_d146560603e5ee3716c519b103"'); + await queryRunner.query('DROP INDEX "public"."IDX_4185366a8d6033a3aad864621d"'); + await queryRunner.query('DROP INDEX "public"."IDX_06ed7514e57cb8bd0c09ce3be0"'); + await queryRunner.query('DROP INDEX "public"."IDX_66893187eeb7c3198e3d07c595"'); + await queryRunner.query('DROP INDEX "public"."IDX_f1d4afbdb77bbb26a461726b68"'); + await queryRunner.query('DROP INDEX "public"."IDX_1ff95d74c5186613b05e651802"'); + await queryRunner.query('DROP INDEX "public"."IDX_d2faa8fa665e7cdbd602d557df"'); + await queryRunner.query('DROP INDEX "public"."IDX_ee13cd8cabbac2258203bb1c95"'); + await queryRunner.query('DROP INDEX "public"."IDX_21202b13906bffe54448b2a553"'); + await queryRunner.query('DROP INDEX "public"."IDX_cd5b89a167f9f9821b70ff88bf"'); + await queryRunner.query('DROP INDEX "public"."IDX_a8707945701186685e1b5e9c51"'); + await queryRunner.query('DROP INDEX "public"."IDX_d1865366170f468d8eb720f8c8"'); + await queryRunner.query('DROP INDEX "public"."IDX_c2e0f2c83c50446d23955dff0a"'); + await queryRunner.query('DROP INDEX "public"."IDX_16a0f89062ed4edd4cce26cc5e"'); + await queryRunner.query('DROP INDEX "public"."IDX_177b41bfa4c6574e30e95da04e"'); + await queryRunner.query('DROP INDEX "public"."IDX_3d8ef5f5715cb3d50315007aee"'); + await queryRunner.query('DROP INDEX "public"."IDX_ba56e0a5617605dcc62cb33f11"'); + await queryRunner.query('DROP INDEX "public"."IDX_e607ca8d8c3d11ccdc725a3dd0"'); + await queryRunner.query('DROP INDEX "public"."IDX_9360325be3f332793f1734704e"'); + await queryRunner.query('DROP INDEX "public"."IDX_e00ea7965d1ed8c53b3c85a56e"'); + await queryRunner.query('DROP INDEX "public"."IDX_62e0da442361b07616d26593b3"'); + await queryRunner.query('DROP INDEX "public"."IDX_4d55d5b6642e4e9677c5af16b3"'); + await queryRunner.query('DROP INDEX "public"."IDX_eda643b345dd73d5fc1a456a4b"'); + await queryRunner.query('DROP INDEX "public"."IDX_92fd74dcbe9b1adad11ff3091d"'); + await queryRunner.query('DROP INDEX "public"."IDX_d16a377e285494ac71048f2b84"'); + await queryRunner.query('DROP INDEX "public"."IDX_e0b0b69779afb474ee220573c8"'); + await queryRunner.query('DROP INDEX "public"."IDX_d3f089bf865364504bda9c2ff6"'); + await queryRunner.query('DROP INDEX "public"."IDX_5d23a7a8f43556bcd03a18a2b6"'); + await queryRunner.query('DROP INDEX "public"."IDX_862a5402a4d18274a07a2a7ccc"'); + await queryRunner.query('DROP INDEX "public"."IDX_fc77d3d8f8101ab793cf17986f"'); + await queryRunner.query('DROP INDEX "public"."IDX_ac1a8d28ac7e2f7aff92b57318"'); + await queryRunner.query('DROP INDEX "public"."IDX_d9b4760e416c3767d44e5be0fd"'); + await queryRunner.query('DROP INDEX "public"."IDX_d3e1023a52cb7875b6c10d3a15"'); + await queryRunner.query('DROP INDEX "public"."IDX_3f28c2b1ce5590ea0892267033"'); + await queryRunner.query('DROP INDEX "public"."IDX_9036fb7240edcc5f620ceff406"'); + await queryRunner.query('DROP INDEX "public"."IDX_a07f0454cce5637fd109a105dd"'); + await queryRunner.query('DROP INDEX "public"."IDX_57833e6da94a22790e6913a509"'); + await queryRunner.query('DROP INDEX "public"."IDX_428c3f972502e77d9e2c79051b"'); + await queryRunner.query('DROP INDEX "public"."IDX_423597e129f4f9402c976acce2"'); + await queryRunner.query('DROP INDEX "public"."IDX_f1221d9b1aaa64b1f3c98ed46d"'); + await queryRunner.query('DROP INDEX "public"."IDX_c0085fb66787d45eb374ddef38"'); + await queryRunner.query('DROP INDEX "public"."IDX_22498516df46fa90e200915793"'); + await queryRunner.query('DROP INDEX "public"."IDX_7a5ad30aa2b8be04ca4a6d724a"'); + await queryRunner.query('DROP INDEX "public"."IDX_014bd57abd7a916e466c5aaf80"'); + await queryRunner.query('DROP INDEX "public"."IDX_b5d0e1b57bc6c761fb49e79bf8"'); + await queryRunner.query('DROP INDEX "public"."IDX_f9614fbc7820536559cdc65147"'); + await queryRunner.query('DROP INDEX "public"."IDX_43bd09d8fa296ece993503b6f2"'); + await queryRunner.query('DROP INDEX "public"."IDX_041cd4f77805d53e3408cb4ae6"'); + await queryRunner.query('DROP INDEX "public"."IDX_0c4a4e0584c4d08a779239d250"'); + await queryRunner.query('DROP INDEX "public"."IDX_e2102fc64215f0a1fcb9ad4879"'); + await queryRunner.query('DROP INDEX "public"."IDX_6e199fc01241bcb8ec806e0c6a"'); + await queryRunner.query('DROP INDEX "public"."IDX_translations_namespace_locale"'); + await queryRunner.query('DROP INDEX "public"."IDX_6c7e0cec7358f74ca9222628a1"'); + await queryRunner.query('DROP INDEX "public"."IDX_ec4970e90eebcc7a16efb37816"'); + await queryRunner.query('DROP INDEX "public"."IDX_c001c66a488dafe7aa8aa19402"'); + await queryRunner.query('DROP INDEX "public"."IDX_0aababfebc5662624a4a66b639"'); + await queryRunner.query('DROP INDEX "public"."IDX_79782456f7ad0ce730435a950a"'); + await queryRunner.query('DROP INDEX "public"."IDX_467f6869324eebd9de644f4c28"'); + await queryRunner.query('DROP INDEX "public"."IDX_ac27b070934c64650ab339f4c6"'); + await queryRunner.query('DROP INDEX "public"."IDX_692a909ee0fa9383e7859f9b40"'); + await queryRunner.query('DROP INDEX "public"."IDX_b1cc4fe47a41d78d10f26589be"'); + await queryRunner.query('DROP INDEX "public"."IDX_c93297fa63b08ac2e83972bd5a"'); + await queryRunner.query('DROP INDEX "public"."IDX_b3676e630f8875765de2bd587e"'); + await queryRunner.query('DROP INDEX "public"."IDX_a0fa8c64807caecc993a5d399d"'); + await queryRunner.query('DROP INDEX "public"."IDX_cdef16da64ce64a5bf79062ec1"'); + await queryRunner.query('DROP INDEX "public"."IDX_c0dda6108b7bbd68c6823d4983"'); + await queryRunner.query('DROP INDEX "public"."IDX_d9da1249e3125778f926607e9d"'); + await queryRunner.query('DROP INDEX "public"."IDX_4b459011ef4d3e4f8618b404c0"'); + await queryRunner.query('DROP INDEX "public"."IDX_0cab16d7274814ef7b9b021c64"'); + await queryRunner.query('DROP INDEX "public"."IDX_b76aaa5336dcdff6ca464044b6"'); + await queryRunner.query('DROP INDEX "public"."IDX_238b7a7accc697f9dc38bb2fa6"'); + await queryRunner.query('DROP INDEX "public"."IDX_26daf5e433d6fb88ee32ce9363"'); + await queryRunner.query('DROP INDEX "public"."IDX_02781c49b25ceb502571f0315f"'); + await queryRunner.query('DROP INDEX "public"."IDX_ac0f09364e3701d9ed35435288"'); + await queryRunner.query('DROP INDEX "public"."IDX_bf8e0f9dd4558ef209ec111782"'); + await queryRunner.query('DROP INDEX "public"."IDX_fcd49f4739a516d59675b8134e"'); + await queryRunner.query('DROP INDEX "public"."IDX_580f1dbf7bceb9c2cde8baf7ff"'); + await queryRunner.query('DROP INDEX "public"."IDX_e0a5c10dfd4a71af0f150204e9"'); + await queryRunner.query('DROP INDEX "public"."IDX_7684def1bb4135f494082b34ee"'); + await queryRunner.query('DROP INDEX "public"."IDX_8dacda0f101c97b73304ddf0fc"'); + await queryRunner.query('DROP INDEX "public"."IDX_1b692560d438e5fc6edebbb27d"'); + await queryRunner.query('DROP INDEX "public"."IDX_c2a89b63635ae2b4cc76f17a08"'); + await queryRunner.query('DROP INDEX "public"."IDX_743b9fb1d2a059f2f7860418e4"'); + await queryRunner.query('DROP INDEX "public"."IDX_c5fa169d2de9407d99f2c6e4fa"'); + await queryRunner.query('DROP INDEX "public"."IDX_427785468fb7d2733f59e7d7d3"'); + await queryRunner.query('DROP INDEX "public"."IDX_32b41cdb985a296213e9a928b5"'); + await queryRunner.query('DROP INDEX "public"."IDX_1a15756e257e0eaf01edc85645"'); + await queryRunner.query('DROP INDEX "public"."IDX_d0a95ef8a28188364c546eb65c"'); + await queryRunner.query('DROP INDEX "public"."IDX_6ccf973355b70645eff37774de"'); + await queryRunner.query('DROP INDEX "public"."IDX_8d5af5c5dbdc0d91ce1178a004"'); + await queryRunner.query('DROP INDEX "public"."IDX_69752e0d44a35a0dce9d9f06d0"'); + await queryRunner.query('DROP INDEX "public"."IDX_dfc023bb093b1afcbc91798837"'); + await queryRunner.query('DROP INDEX "public"."IDX_e20f751a4d8faf089b1922eaf5"'); + await queryRunner.query('DROP INDEX "public"."IDX_fd467ff7c6435655c8d715f221"'); + await queryRunner.query('DROP INDEX "public"."IDX_42bbd07a91fea7e34f175b19fe"'); + await queryRunner.query('DROP INDEX "public"."IDX_616c8b49254550dbee7f917f3a"'); + await queryRunner.query('DROP INDEX "public"."IDX_cfd29d0da332d5c46d9b073863"'); + await queryRunner.query('DROP INDEX "public"."IDX_2310ecc5cb8be427097154b18f"'); + await queryRunner.query('DROP INDEX "public"."IDX_audit_logs_action_timestamp"'); + await queryRunner.query('DROP INDEX "public"."IDX_audit_logs_entity"'); + await queryRunner.query('DROP INDEX "public"."IDX_audit_logs_ip_address"'); + await queryRunner.query('DROP INDEX "public"."IDX_5fb386b7d3d50fd5657ef76c3d"'); + await queryRunner.query('DROP INDEX "public"."IDX_c58f7e88c286e5e3478960a998"'); + await queryRunner.query('DROP INDEX "public"."IDX_fab34e0791096b2a0a1bf8bd7f"'); + await queryRunner.query('DROP INDEX "public"."IDX_fe0bb3f6520ee0469504521e71"'); + await queryRunner.query('DROP INDEX "public"."IDX_97672ac88f789774dd47f7c8be"'); + await queryRunner.query('DROP INDEX "public"."IDX_4d018866397b1e7e78d03b4566"'); + await queryRunner.query('DROP INDEX "public"."IDX_648e3f5447f725579d7d4ffdfb"'); + await queryRunner.query('DROP INDEX "public"."IDX_da04f89054f39981438894dfe3"'); + await queryRunner.query('DROP INDEX "public"."IDX_a27a3c9d6a41c0fa0fd1cb3f85"'); + await queryRunner.query('DROP INDEX "public"."IDX_72058429360b0daed61d94fb4f"'); + await queryRunner.query('DROP INDEX "public"."IDX_2bd3a6327596cc908d574a8219"'); + await queryRunner.query('DROP INDEX "public"."IDX_deca5c9911b3b2100b36106082"'); + await queryRunner.query('DROP INDEX "public"."IDX_baccb82c6179dca139f6b8c768"'); + await queryRunner.query('DROP INDEX "public"."IDX_ac5edecc1aefa58ed0237a7ee4"'); + await queryRunner.query('DROP INDEX "public"."IDX_34cbd320b458b8cf9e0d580304"'); + await queryRunner.query('DROP INDEX "public"."IDX_1f69fdcbd7ea5f0e52c3230c00"'); + await queryRunner.query('DROP INDEX "public"."IDX_c2c4377a3e7773772bf670eb47"'); + await queryRunner.query('DROP INDEX "public"."IDX_d6e7a6c84587eb7809c5b910f8"'); + await queryRunner.query('DROP INDEX "public"."IDX_91e3503740d174a4599e81ad8a"'); + await queryRunner.query('DROP INDEX "public"."IDX_d03f18ea29009de1e37a6d156b"'); + await queryRunner.query('DROP INDEX "public"."IDX_756e18fc1918b63fb4d79b4db6"'); + await queryRunner.query('DROP INDEX "public"."IDX_46ad51ba4f4bb7db2817d75419"'); + await queryRunner.query('DROP INDEX "public"."IDX_dd1ce01d1164c8bbdda052ced7"'); + await queryRunner.query('DROP INDEX "public"."IDX_fc17c7e94154a17e767b7674f1"'); + await queryRunner.query('DROP INDEX "public"."IDX_65395710ac88af450c5123f030"'); + await queryRunner.query('DROP INDEX "public"."IDX_d80cf66830a0231b8104893b67"'); + await queryRunner.query('DROP INDEX "public"."IDX_35fb2307535d90a6ed290af1f4"'); + await queryRunner.query('DROP INDEX "public"."IDX_633a748d9720d4c3c25f0fc120"'); + await queryRunner.query('DROP INDEX "public"."IDX_3dad32ba0ff20feee98b1b0c43"'); + await queryRunner.query( + "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')", + ); + await queryRunner.query( + 'ALTER TABLE "audit_logs" ALTER COLUMN "action" TYPE "public"."audit_logs_action_enum_old" USING "action"::"text"::"public"."audit_logs_action_enum_old"', + ); + await queryRunner.query('DROP TYPE "public"."audit_logs_action_enum"'); + await queryRunner.query( + 'ALTER TYPE "public"."audit_logs_action_enum_old" RENAME TO "audit_logs_action_enum"', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_audit_logs_action_timestamp" ON "audit_logs" ("action", "timestamp") ', + ); + await queryRunner.query('ALTER TABLE "course_bulk_operations" DROP COLUMN "version"'); + await queryRunner.query('ALTER TABLE "course_bulk_operations" DROP COLUMN "notes"'); + await queryRunner.query('ALTER TABLE "course_bulk_operations" DROP COLUMN "reason"'); + await queryRunner.query('ALTER TABLE "course_bulk_operations" DROP COLUMN "undone_by_id"'); + await queryRunner.query( + 'CREATE UNIQUE INDEX "UQ_forum_votes_entityType_entityId_authorId" ON "forum_votes" ("entityType", "entityId", "authorId") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_audit_logs_ip_address" ON "audit_logs" ("ip_address") ', + ); + await queryRunner.query( + 'CREATE INDEX "IDX_audit_logs_entity" ON "audit_logs" ("entity_type", "entity_id") ', + ); + await queryRunner.query( + 'ALTER TABLE "submission_grades" ADD CONSTRAINT "FK_submission_grades_rubric" FOREIGN KEY ("rubric_id") REFERENCES "rubrics"("id") ON DELETE RESTRICT ON UPDATE NO ACTION', + ); + await queryRunner.query( + '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', + ); + await queryRunner.query( + 'ALTER TABLE "rubric_criteria" ADD CONSTRAINT "FK_rubric_criteria_rubric" FOREIGN KEY ("rubric_id") REFERENCES "rubrics"("id") ON DELETE CASCADE ON UPDATE NO ACTION', + ); + await queryRunner.query( + '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', + ); + await queryRunner.query( + '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', + ); + await queryRunner.query( + 'ALTER TABLE "forum_votes" ADD CONSTRAINT "FK_forum_votes_authorId_users" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION', + ); + await queryRunner.query( + 'DELETE FROM "typeorm_metadata" WHERE "type" = \'GENERATED_COLUMN\' AND "name" = \'search_vector\' AND "database" = current_database() AND "schema" = \'public\' AND "table" = \'course\'', + ); + await queryRunner.query('ALTER TABLE "course" ALTER COLUMN "search_vector" DROP NOT NULL'); + await queryRunner.query( + "ALTER TABLE \"course\" ADD \"searchVector\" tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce(\"title\", '') || ' ' || coalesce(\"description\", '') || ' ' || coalesce(\"category\", ''))) STORED", + ); + } +} diff --git a/src/user-preferences/entities/user-preference.entity.ts b/src/user-preferences/entities/user-preference.entity.ts index 102e7fff..0013c057 100644 --- a/src/user-preferences/entities/user-preference.entity.ts +++ b/src/user-preferences/entities/user-preference.entity.ts @@ -57,19 +57,21 @@ export class UserPreference { language: AppLanguage; // NEW — REQUIRED FOR LOCALE FORMATTING - @Column({ - type: 'enum', - enum: AppLocale, - default: AppLocale.EN_US, - }) - locale: AppLocale; + @Column({ type: 'varchar', default: 'en-US', nullable: true }) + locale: string; // ========================= // TIMEZONE SUPPORT (REQUIRED) // ========================= - @Column({ default: 'UTC' }) + @Column({ default: 'UTC', nullable: true }) timezone: string; // e.g. Africa/Lagos, Europe/London + // ========================= + // Currency preference + // ========================= + @Column({ type: 'varchar', default: 'USD', nullable: true }) + currency: string; + // ========================= // Notification Settings // ========================= diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index f5e9cc70..228df4e3 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -94,14 +94,14 @@ export class User { isEmailVerified: boolean; @Column({ nullable: true }) - @Index() + @Index('IDX_users_emailVerificationToken') emailVerificationToken?: string; @Column({ type: 'timestamp', nullable: true }) emailVerificationExpires?: Date; @Column({ nullable: true }) - @Index() + @Index('IDX_users_passwordResetToken') passwordResetToken?: string; @Column({ type: 'timestamp', nullable: true }) @@ -127,6 +127,35 @@ export class User { @Column('text', { array: true, default: [] }) mfaRecoveryCodes: string[]; + // ── Localization ────────────────────────────────────────────────────────── + + /** Billing country name (e.g. "Nigeria"). Populated by localization. */ + @Column({ type: 'varchar', nullable: true }) + country?: string; + + /** ISO 3166-1 alpha-2 country code (e.g. "DE"). */ + @Column({ name: 'country_code', type: 'varchar', length: 2, nullable: true }) + @Index('IDX_users_country_code') + countryCode?: string; + + /** IANA timezone name (e.g. "Africa/Lagos"). */ + @Column({ type: 'varchar', nullable: true }) + timezone?: string; + + @Column({ type: 'varchar', nullable: true }) + city?: string; + + /** Preferred currency code (ISO 4217). */ + @Column({ + name: 'preferred_currency', + type: 'varchar', + length: 3, + default: 'USD', + nullable: true, + }) + @Index('IDX_users_preferred_currency') + preferredCurrency?: string; + @ManyToMany(() => Role, (role) => role.users) @JoinTable() roles: Role[]; @@ -158,7 +187,7 @@ export class User { enrollments: Enrollment[]; @CreateDateColumn() - @Index() + @Index('IDX_users_createdAt') createdAt: Date; @UpdateDateColumn()