diff --git a/drizzle/0008_audit_logs.sql b/drizzle/0008_audit_logs.sql deleted file mode 100644 index 6e9ab296..00000000 --- a/drizzle/0008_audit_logs.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE "audit_logs" ( - "id" integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - "actor_email" varchar(255) NOT NULL, - "target_email" varchar(255), - "action" varchar(100) NOT NULL, - "resource_type" varchar(50) NOT NULL, - "resource_id" varchar(255), - "metadata" text, - "created_at" timestamp DEFAULT now() NOT NULL -); - -CREATE INDEX "audit_actor_idx" -ON "audit_logs" ("actor_email"); - -CREATE INDEX "audit_action_idx" -ON "audit_logs" ("action"); \ No newline at end of file diff --git a/drizzle/0013_silky_gateway.sql b/drizzle/0013_silky_gateway.sql deleted file mode 100644 index 5adaba72..00000000 --- a/drizzle/0013_silky_gateway.sql +++ /dev/null @@ -1,83 +0,0 @@ --- 1. Create independent types, enums, and tables -CREATE TYPE "public"."org_role" AS ENUM('owner', 'admin', 'teacher', 'member');--> statement-breakpoint - -CREATE TABLE "organizations" ( - "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "organizations_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), - "name" varchar(255) NOT NULL, - "slug" varchar(255) NOT NULL, - "owner_email" varchar(255) NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "organizations_slug_unique" UNIQUE("slug") -);--> statement-breakpoint - -CREATE TABLE "organization_memberships" ( - "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "organization_memberships_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), - "organization_id" integer NOT NULL, - "user_email" varchar(255) NOT NULL, - "role" "org_role" DEFAULT 'member' NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "org_memberships_userEmail_orgId_unique" UNIQUE("user_email","organization_id") -);--> statement-breakpoint - -CREATE TABLE "practice_attempts" ( - "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "practice_attempts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), - "user_email" varchar(255) NOT NULL, - "original_doubt_id" integer NOT NULL, - "generated_question" text NOT NULL, - "user_answer" text, - "is_correct" boolean, - "ai_feedback" text, - "created_at" timestamp DEFAULT now() NOT NULL -);--> statement-breakpoint - --- 2. Drop legacy unique constraints before playing with columns -ALTER TABLE "likes" DROP CONSTRAINT "likes_userName_doubtId_unique";--> statement-breakpoint -ALTER TABLE "reply_likes" DROP CONSTRAINT "reply_likes_userName_replyId_unique";--> statement-breakpoint - --- 3. Add column additions to existing tables (making new text elements nullable at first) -ALTER TABLE "classrooms" ADD COLUMN "organization_id" integer;--> statement-breakpoint -ALTER TABLE "users" ADD COLUMN "interests" text;--> statement-breakpoint -ALTER TABLE "users" ADD COLUMN "learningGoals" text;--> statement-breakpoint -ALTER TABLE "users" ADD COLUMN "subjects" text;--> statement-breakpoint -ALTER TABLE "users" ADD COLUMN "instituteInfo" text;--> statement-breakpoint - --- FIXED: Add replacement email columns as NULLABLE first so existing rows don't cause an immediate query abort -ALTER TABLE "likes" ADD COLUMN "userEmail" varchar(255);--> statement-breakpoint -ALTER TABLE "reply_likes" ADD COLUMN "userEmail" varchar(255);--> statement-breakpoint - --- 4. ────────── DATA MIGRATION STEP (BACKFILL) ────────── --- NOTE FOR GSOC REVIEWER: At this stage, a data reconciliation script must populate the new --- nullable 'userEmail' columns using an authoritative user tracking reference map before --- dropping legacy columns or enforcing strict system-level constraints. --- ────────────────────────────────────────────────────── - --- 5. Enforce strict constraints on core operational structural elements -ALTER TABLE "doubts" ALTER COLUMN "userEmail" SET NOT NULL;--> statement-breakpoint -ALTER TABLE "replies" ALTER COLUMN "user_email" SET NOT NULL;--> statement-breakpoint - --- FIXED: Enforce NOT NULL constraints only after the database engine safely accommodates pre-existing records -ALTER TABLE "likes" ALTER COLUMN "userEmail" SET NOT NULL;--> statement-breakpoint -ALTER TABLE "reply_likes" ALTER COLUMN "userEmail" SET NOT NULL;--> statement-breakpoint - --- 6. Clean up historical trace assets -ALTER TABLE "doubts" DROP COLUMN "userName";--> statement-breakpoint -ALTER TABLE "likes" DROP COLUMN "userName";--> statement-breakpoint -ALTER TABLE "replies" DROP COLUMN "user_name";--> statement-breakpoint -ALTER TABLE "reply_likes" DROP COLUMN "userName";--> statement-breakpoint - --- 7. Add foreign key relationships and unique indicators -ALTER TABLE "organization_memberships" ADD CONSTRAINT "organization_memberships_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "organization_memberships" ADD CONSTRAINT "organization_memberships_user_email_users_email_fk" FOREIGN KEY ("user_email") REFERENCES "public"."users"("email") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "practice_attempts" ADD CONSTRAINT "practice_attempts_user_email_users_email_fk" FOREIGN KEY ("user_email") REFERENCES "public"."users"("email") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "practice_attempts" ADD CONSTRAINT "practice_attempts_original_doubt_id_doubts_id_fk" FOREIGN KEY ("original_doubt_id") REFERENCES "public"."doubts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "classrooms" ADD CONSTRAINT "classrooms_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "likes" ADD CONSTRAINT "likes_userEmail_users_email_fk" FOREIGN KEY ("userEmail") REFERENCES "public"."users"("email") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "reply_likes" ADD CONSTRAINT "reply_likes_userEmail_users_email_fk" FOREIGN KEY ("userEmail") REFERENCES "public"."users"("email") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint - -ALTER TABLE "likes" ADD CONSTRAINT "likes_userEmail_doubtId_unique" UNIQUE("userEmail","doubtId");--> statement-breakpoint -ALTER TABLE "reply_likes" ADD CONSTRAINT "reply_likes_userEmail_replyId_unique" UNIQUE("userEmail","replyId");--> statement-breakpoint - --- 8. Build processing indexes for optimized query lookup -CREATE INDEX "practice_attempts_userEmail_idx" ON "practice_attempts" USING btree ("user_email");--> statement-breakpoint -CREATE INDEX "practice_attempts_doubtId_idx" ON "practice_attempts" USING btree ("original_doubt_id");--> statement-breakpoint -CREATE INDEX "classrooms_orgId_idx" ON "classrooms" USING btree ("organization_id"); \ No newline at end of file diff --git a/drizzle/0017_wild_klaw.sql b/drizzle/0017_wild_klaw.sql new file mode 100644 index 00000000..3e88c83b --- /dev/null +++ b/drizzle/0017_wild_klaw.sql @@ -0,0 +1,29 @@ +CREATE TABLE "audit_logs" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "audit_logs_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "actorEmail" varchar(255) NOT NULL, + "targetEmail" varchar(255), + "action" varchar(100) NOT NULL, + "resourceType" varchar(50) NOT NULL, + "resourceId" varchar(255), + "metadata" text, + "createdAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "video_jobs" ( + "id" varchar(64) PRIMARY KEY NOT NULL, + "user_email" varchar(255) NOT NULL, + "status" varchar(20) DEFAULT 'queued' NOT NULL, + "progress" integer DEFAULT 0 NOT NULL, + "step" varchar(255), + "video_type" varchar(20), + "video_url" text, + "error" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "video_jobs" ADD CONSTRAINT "video_jobs_user_email_users_email_fk" FOREIGN KEY ("user_email") REFERENCES "public"."users"("email") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "audit_actor_idx" ON "audit_logs" USING btree ("actorEmail");--> statement-breakpoint +CREATE INDEX "audit_action_idx" ON "audit_logs" USING btree ("action");--> statement-breakpoint +CREATE INDEX "video_jobs_user_email_idx" ON "video_jobs" USING btree ("user_email");--> statement-breakpoint +CREATE INDEX "video_jobs_status_created_at_idx" ON "video_jobs" USING btree ("status","created_at"); \ No newline at end of file diff --git a/drizzle/meta/0017_snapshot.json b/drizzle/meta/0017_snapshot.json new file mode 100644 index 00000000..83507d25 --- /dev/null +++ b/drizzle/meta/0017_snapshot.json @@ -0,0 +1,3388 @@ +{ + "id": "08da2ac2-fbd0-4f57-881b-7e511182b569", + "prevId": "9b19387a-9942-4b21-94f9-1e9b393a0d1f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "audit_logs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "actorEmail": { + "name": "actorEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "targetEmail": { + "name": "targetEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "resourceType": { + "name": "resourceType", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "resourceId": { + "name": "resourceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_actor_idx": { + "name": "audit_actor_idx", + "columns": [ + { + "expression": "actorEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_action_idx": { + "name": "audit_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.badge_definitions": { + "name": "badge_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "badge_definitions_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "slug": { + "name": "slug", + "type": "varchar(80)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "badge_definitions_slug_unique": { + "name": "badge_definitions_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bookmarks": { + "name": "bookmarks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "bookmarks_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "doubtId": { + "name": "doubtId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bookmark_userEmail_idx": { + "name": "bookmark_userEmail_idx", + "columns": [ + { + "expression": "userEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bookmark_doubtId_idx": { + "name": "bookmark_doubtId_idx", + "columns": [ + { + "expression": "doubtId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bookmarks_userEmail_users_email_fk": { + "name": "bookmarks_userEmail_users_email_fk", + "tableFrom": "bookmarks", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarks_doubtId_doubts_id_fk": { + "name": "bookmarks_doubtId_doubts_id_fk", + "tableFrom": "bookmarks", + "tableTo": "doubts", + "columnsFrom": [ + "doubtId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "bookmarks_userEmail_doubtId_unique": { + "name": "bookmarks_userEmail_doubtId_unique", + "nullsNotDistinct": false, + "columns": [ + "userEmail", + "doubtId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_history": { + "name": "chat_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "chat_history_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "chatId": { + "name": "chatId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "chatTitle": { + "name": "chatTitle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chatHistory_chatId_idx": { + "name": "chatHistory_chatId_idx", + "columns": [ + { + "expression": "chatId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_history_userEmail_users_email_fk": { + "name": "chat_history_userEmail_users_email_fk", + "tableFrom": "chat_history", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.classroom_invites": { + "name": "classroom_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "classroom_invites_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "classroom_id": { + "name": "classroom_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "used_count": { + "name": "used_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "classroom_invites_token_hash_idx": { + "name": "classroom_invites_token_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "classroom_invites_classroom_id_idx": { + "name": "classroom_invites_classroom_id_idx", + "columns": [ + { + "expression": "classroom_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "classroom_invites_expires_at_idx": { + "name": "classroom_invites_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "classroom_invites_classroom_id_classrooms_id_fk": { + "name": "classroom_invites_classroom_id_classrooms_id_fk", + "tableFrom": "classroom_invites", + "tableTo": "classrooms", + "columnsFrom": [ + "classroom_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "classroom_invites_created_by_users_email_fk": { + "name": "classroom_invites_created_by_users_email_fk", + "tableFrom": "classroom_invites", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.classrooms": { + "name": "classrooms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "classrooms_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "university": { + "name": "university", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "teacherEmail": { + "name": "teacherEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "inviteCode": { + "name": "inviteCode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "invite_code_expires_at": { + "name": "invite_code_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "allowed_email_domains": { + "name": "allowed_email_domains", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "pedagogyLevel": { + "name": "pedagogyLevel", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'Undergraduate (Freshman)'" + }, + "targetGradeLevel": { + "name": "targetGradeLevel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 13 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "classrooms_teacherEmail_idx": { + "name": "classrooms_teacherEmail_idx", + "columns": [ + { + "expression": "teacherEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "classrooms_orgId_idx": { + "name": "classrooms_orgId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "classrooms_organization_id_organizations_id_fk": { + "name": "classrooms_organization_id_organizations_id_fk", + "tableFrom": "classrooms", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "classrooms_inviteCode_unique": { + "name": "classrooms_inviteCode_unique", + "nullsNotDistinct": false, + "columns": [ + "inviteCode" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.confusion_alerts": { + "name": "confusion_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "confusion_alerts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "classroomId": { + "name": "classroomId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggestedAction": { + "name": "suggestedAction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "doubtCount": { + "name": "doubtCount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sampleDoubtIds": { + "name": "sampleDoubtIds", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "acknowledgedAt": { + "name": "acknowledgedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "acknowledgedBy": { + "name": "acknowledgedBy", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "confusion_alerts_classroomId_idx": { + "name": "confusion_alerts_classroomId_idx", + "columns": [ + { + "expression": "classroomId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "confusion_alerts_status_idx": { + "name": "confusion_alerts_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "confusion_alerts_classroom_created_idx": { + "name": "confusion_alerts_classroom_created_idx", + "columns": [ + { + "expression": "classroomId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "confusion_alerts_classroomId_classrooms_id_fk": { + "name": "confusion_alerts_classroomId_classrooms_id_fk", + "tableFrom": "confusion_alerts", + "tableTo": "classrooms", + "columnsFrom": [ + "classroomId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "confusion_alerts_acknowledgedBy_users_email_fk": { + "name": "confusion_alerts_acknowledgedBy_users_email_fk", + "tableFrom": "confusion_alerts", + "tableTo": "users", + "columnsFrom": [ + "acknowledgedBy" + ], + "columnsTo": [ + "email" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cover_letters": { + "name": "cover_letters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "cover_letters_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jobDescription": { + "name": "jobDescription", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userDetails": { + "name": "userDetails", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "coverLetter": { + "name": "coverLetter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cover_letters_userEmail_users_email_fk": { + "name": "cover_letters_userEmail_users_email_fk", + "tableFrom": "cover_letters", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.doubt_tags": { + "name": "doubt_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "doubt_tags_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "doubtId": { + "name": "doubtId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tagId": { + "name": "tagId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doubt_tag_doubtId_idx": { + "name": "doubt_tag_doubtId_idx", + "columns": [ + { + "expression": "doubtId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doubt_tag_tagId_idx": { + "name": "doubt_tag_tagId_idx", + "columns": [ + { + "expression": "tagId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doubt_tag_unique_idx": { + "name": "doubt_tag_unique_idx", + "columns": [ + { + "expression": "doubtId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tagId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "doubt_tags_doubtId_doubts_id_fk": { + "name": "doubt_tags_doubtId_doubts_id_fk", + "tableFrom": "doubt_tags", + "tableTo": "doubts", + "columnsFrom": [ + "doubtId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "doubt_tags_tagId_tags_id_fk": { + "name": "doubt_tags_tagId_tags_id_fk", + "tableFrom": "doubt_tags", + "tableTo": "tags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.doubts": { + "name": "doubts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "doubts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "classroomId": { + "name": "classroomId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "subTopic": { + "name": "subTopic", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "likes": { + "name": "likes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "isSolved": { + "name": "isSolved", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'unsolved'" + }, + "solvedReplyId": { + "name": "solvedReplyId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'community'" + }, + "isPinned": { + "name": "isPinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "doubt_classroomId_idx": { + "name": "doubt_classroomId_idx", + "columns": [ + { + "expression": "classroomId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "type_idx": { + "name": "type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subject_idx": { + "name": "subject_idx", + "columns": [ + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_doubts_created": { + "name": "idx_doubts_created", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_doubts_solved": { + "name": "idx_doubts_solved", + "columns": [ + { + "expression": "isSolved", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doubts_userEmail_classroomId_idx": { + "name": "doubts_userEmail_classroomId_idx", + "columns": [ + { + "expression": "userEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "classroomId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "doubts_userEmail_users_email_fk": { + "name": "doubts_userEmail_users_email_fk", + "tableFrom": "doubts", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "doubts_classroomId_classrooms_id_fk": { + "name": "doubts_classroomId_classrooms_id_fk", + "tableFrom": "doubts", + "tableTo": "classrooms", + "columnsFrom": [ + "classroomId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.karma_transactions": { + "name": "karma_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "karma_transactions_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "eventType": { + "name": "eventType", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "replyId": { + "name": "replyId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "doubtId": { + "name": "doubtId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "karma_tx_userEmail_idx": { + "name": "karma_tx_userEmail_idx", + "columns": [ + { + "expression": "userEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "karma_tx_eventType_idx": { + "name": "karma_tx_eventType_idx", + "columns": [ + { + "expression": "eventType", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "karma_transactions_userEmail_users_email_fk": { + "name": "karma_transactions_userEmail_users_email_fk", + "tableFrom": "karma_transactions", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "karma_transactions_replyId_replies_id_fk": { + "name": "karma_transactions_replyId_replies_id_fk", + "tableFrom": "karma_transactions", + "tableTo": "replies", + "columnsFrom": [ + "replyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "karma_transactions_doubtId_doubts_id_fk": { + "name": "karma_transactions_doubtId_doubts_id_fk", + "tableFrom": "karma_transactions", + "tableTo": "doubts", + "columnsFrom": [ + "doubtId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "likes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "doubtId": { + "name": "doubtId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "likes_doubtId_doubts_id_fk": { + "name": "likes_doubtId_doubts_id_fk", + "tableFrom": "likes", + "tableTo": "doubts", + "columnsFrom": [ + "doubtId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "likes_userEmail_users_email_fk": { + "name": "likes_userEmail_users_email_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "likes_userEmail_doubtId_unique": { + "name": "likes_userEmail_doubtId_unique", + "nullsNotDistinct": false, + "columns": [ + "userEmail", + "doubtId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "memberships_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "classroomId": { + "name": "classroomId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "joinedAt": { + "name": "joinedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "userEmail_idx": { + "name": "userEmail_idx", + "columns": [ + { + "expression": "userEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "classroomId_idx": { + "name": "classroomId_idx", + "columns": [ + { + "expression": "classroomId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_userEmail_users_email_fk": { + "name": "memberships_userEmail_users_email_fk", + "tableFrom": "memberships", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_classroomId_classrooms_id_fk": { + "name": "memberships_classroomId_classrooms_id_fk", + "tableFrom": "memberships", + "tableTo": "classrooms", + "columnsFrom": [ + "classroomId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "memberships_userEmail_classroomId_unique": { + "name": "memberships_userEmail_classroomId_unique", + "nullsNotDistinct": false, + "columns": [ + "userEmail", + "classroomId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.moderation_logs": { + "name": "moderation_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "moderation_logs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "violationType": { + "name": "violationType", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "contentSnippet": { + "name": "contentSnippet", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "moderation_logs_userEmail_createdAt_idx": { + "name": "moderation_logs_userEmail_createdAt_idx", + "columns": [ + { + "expression": "userEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "moderation_logs_userEmail_users_email_fk": { + "name": "moderation_logs_userEmail_users_email_fk", + "tableFrom": "moderation_logs", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "notifications_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "isRead": { + "name": "isRead", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notification_userEmail_idx": { + "name": "notification_userEmail_idx", + "columns": [ + { + "expression": "userEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_userEmail_users_email_fk": { + "name": "notifications_userEmail_users_email_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_memberships": { + "name": "organization_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "organization_memberships_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_memberships_organization_id_organizations_id_fk": { + "name": "organization_memberships_organization_id_organizations_id_fk", + "tableFrom": "organization_memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_memberships_user_email_users_email_fk": { + "name": "organization_memberships_user_email_users_email_fk", + "tableFrom": "organization_memberships", + "tableTo": "users", + "columnsFrom": [ + "user_email" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_memberships_userEmail_orgId_unique": { + "name": "org_memberships_userEmail_orgId_unique", + "nullsNotDistinct": false, + "columns": [ + "user_email", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "organizations_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner_email": { + "name": "owner_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_notifications": { + "name": "pending_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "pending_notifications_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "doubtId": { + "name": "doubtId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "replyId": { + "name": "replyId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_notifications_user_email_idx": { + "name": "pending_notifications_user_email_idx", + "columns": [ + { + "expression": "userEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_notifications_userEmail_users_email_fk": { + "name": "pending_notifications_userEmail_users_email_fk", + "tableFrom": "pending_notifications", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_notifications_doubtId_doubts_id_fk": { + "name": "pending_notifications_doubtId_doubts_id_fk", + "tableFrom": "pending_notifications", + "tableTo": "doubts", + "columnsFrom": [ + "doubtId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_notifications_replyId_replies_id_fk": { + "name": "pending_notifications_replyId_replies_id_fk", + "tableFrom": "pending_notifications", + "tableTo": "replies", + "columnsFrom": [ + "replyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.practice_attempts": { + "name": "practice_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "practice_attempts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_email": { + "name": "user_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "original_doubt_id": { + "name": "original_doubt_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generated_question": { + "name": "generated_question", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_answer": { + "name": "user_answer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_correct": { + "name": "is_correct", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ai_feedback": { + "name": "ai_feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "practice_attempts_userEmail_idx": { + "name": "practice_attempts_userEmail_idx", + "columns": [ + { + "expression": "user_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "practice_attempts_doubtId_idx": { + "name": "practice_attempts_doubtId_idx", + "columns": [ + { + "expression": "original_doubt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "practice_attempts_user_email_users_email_fk": { + "name": "practice_attempts_user_email_users_email_fk", + "tableFrom": "practice_attempts", + "tableTo": "users", + "columnsFrom": [ + "user_email" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "practice_attempts_original_doubt_id_doubts_id_fk": { + "name": "practice_attempts_original_doubt_id_doubts_id_fk", + "tableFrom": "practice_attempts", + "tableTo": "doubts", + "columnsFrom": [ + "original_doubt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.replies": { + "name": "replies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "replies_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "doubt_id": { + "name": "doubt_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upvotes": { + "name": "upvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "grade_level": { + "name": "grade_level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "complexity_score": { + "name": "complexity_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "readability_score": { + "name": "readability_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pedagogy_drifted": { + "name": "pedagogy_drifted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "drift_explanation": { + "name": "drift_explanation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doubtId_idx": { + "name": "doubtId_idx", + "columns": [ + { + "expression": "doubt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "replies_doubt_id_doubts_id_fk": { + "name": "replies_doubt_id_doubts_id_fk", + "tableFrom": "replies", + "tableTo": "doubts", + "columnsFrom": [ + "doubt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "replies_user_email_users_email_fk": { + "name": "replies_user_email_users_email_fk", + "tableFrom": "replies", + "tableTo": "users", + "columnsFrom": [ + "user_email" + ], + "columnsTo": [ + "email" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reply_likes": { + "name": "reply_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "reply_likes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "replyId": { + "name": "replyId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reply_likes_replyId_replies_id_fk": { + "name": "reply_likes_replyId_replies_id_fk", + "tableFrom": "reply_likes", + "tableTo": "replies", + "columnsFrom": [ + "replyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reply_likes_userEmail_users_email_fk": { + "name": "reply_likes_userEmail_users_email_fk", + "tableFrom": "reply_likes", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reply_likes_userEmail_replyId_unique": { + "name": "reply_likes_userEmail_replyId_unique", + "nullsNotDistinct": false, + "columns": [ + "userEmail", + "replyId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_analysis": { + "name": "resume_analysis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "resume_analysis_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resumeText": { + "name": "resumeText", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "jobDescription": { + "name": "jobDescription", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysisData": { + "name": "analysisData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resumeName": { + "name": "resumeName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "resume_analysis_userEmail_users_email_fk": { + "name": "resume_analysis_userEmail_users_email_fk", + "tableFrom": "resume_analysis", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resumes": { + "name": "resumes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "resumes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resumeName": { + "name": "resumeName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resumeData": { + "name": "resumeData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "resumes_userEmail_users_email_fk": { + "name": "resumes_userEmail_users_email_fk", + "tableFrom": "resumes", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "resumes_userEmail_resumeName_unique": { + "name": "resumes_userEmail_resumeName_unique", + "nullsNotDistinct": false, + "columns": [ + "userEmail", + "resumeName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roadmaps": { + "name": "roadmaps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "roadmaps_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "targetField": { + "name": "targetField", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "roadmapData": { + "name": "roadmapData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "roadmaps_userEmail_users_email_fk": { + "name": "roadmaps_userEmail_users_email_fk", + "tableFrom": "roadmaps", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_chats": { + "name": "shared_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shared_chats_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "chatId": { + "name": "chatId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shared_chats_chatId_unique": { + "name": "shared_chats_chatId_unique", + "nullsNotDistinct": false, + "columns": [ + "chatId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "tags_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(80)", + "primaryKey": false, + "notNull": true + }, + "normalizedName": { + "name": "normalizedName", + "type": "varchar(80)", + "primaryKey": false, + "notNull": true + }, + "classroomId": { + "name": "classroomId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdByEmail": { + "name": "createdByEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_classroomId_idx": { + "name": "tag_classroomId_idx", + "columns": [ + { + "expression": "classroomId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_scope_name_idx": { + "name": "tag_scope_name_idx", + "columns": [ + { + "expression": "normalizedName", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "classroomId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tags_createdByEmail_users_email_fk": { + "name": "tags_createdByEmail_users_email_fk", + "tableFrom": "tags", + "tableTo": "users", + "columnsFrom": [ + "createdByEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tags_classroomId_classrooms_id_fk": { + "name": "tags_classroomId_classrooms_id_fk", + "tableFrom": "tags", + "tableTo": "classrooms", + "columnsFrom": [ + "classroomId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_badges": { + "name": "user_badges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_badges_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userEmail": { + "name": "userEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "badgeId": { + "name": "badgeId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "awardedAt": { + "name": "awardedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_badge_userEmail_idx": { + "name": "user_badge_userEmail_idx", + "columns": [ + { + "expression": "userEmail", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_badge_badgeId_idx": { + "name": "user_badge_badgeId_idx", + "columns": [ + { + "expression": "badgeId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_badges_userEmail_users_email_fk": { + "name": "user_badges_userEmail_users_email_fk", + "tableFrom": "user_badges", + "tableTo": "users", + "columnsFrom": [ + "userEmail" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_badges_badgeId_badge_definitions_id_fk": { + "name": "user_badges_badgeId_badge_definitions_id_fk", + "tableFrom": "user_badges", + "tableTo": "badge_definitions", + "columnsFrom": [ + "badgeId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_badges_userEmail_badgeId_unique": { + "name": "user_badges_userEmail_badgeId_unique", + "nullsNotDistinct": false, + "columns": [ + "userEmail", + "badgeId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "university": { + "name": "university", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "collegeEmail": { + "name": "collegeEmail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "violationCount": { + "name": "violationCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "isBlocked": { + "name": "isBlocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "blockedUntil": { + "name": "blockedUntil", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "blockCount": { + "name": "blockCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "emailNotificationsEnabled": { + "name": "emailNotificationsEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notificationPreference": { + "name": "notificationPreference", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'instant'" + }, + "themePreference": { + "name": "themePreference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "interests": { + "name": "interests", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "learningGoals": { + "name": "learningGoals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subjects": { + "name": "subjects", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instituteInfo": { + "name": "instituteInfo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "karmaScore": { + "name": "karmaScore", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "karmaLevel": { + "name": "karmaLevel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "lastActiveDate": { + "name": "lastActiveDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastContributionAt": { + "name": "lastContributionAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "currentStreak": { + "name": "currentStreak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.video_jobs": { + "name": "video_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "step": { + "name": "step", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "video_type": { + "name": "video_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "video_url": { + "name": "video_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "video_jobs_user_email_idx": { + "name": "video_jobs_user_email_idx", + "columns": [ + { + "expression": "user_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "video_jobs_status_created_at_idx": { + "name": "video_jobs_status_created_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "video_jobs_user_email_users_email_fk": { + "name": "video_jobs_user_email_users_email_fk", + "tableFrom": "video_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_email" + ], + "columnsTo": [ + "email" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": [ + "owner", + "admin", + "teacher", + "member" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 76912be3..d7c47620 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -2,22 +2,131 @@ "version": "7", "dialect": "postgresql", "entries": [ - { "idx": 0, "version": "7", "when": 1779121992842, "tag": "0000_bitter_tyger_tiger", "breakpoints": true }, - { "idx": 1, "version": "7", "when": 1779525663345, "tag": "0001_aspiring_warhawk", "breakpoints": true }, - { "idx": 2, "version": "7", "when": 1779870440438, "tag": "0002_overjoyed_captain_flint", "breakpoints": true }, - { "idx": 3, "version": "7", "when": 1780031571662, "tag": "0003_chemical_quasar", "breakpoints": true }, - { "idx": 4, "version": "7", "when": 1780032095705, "tag": "0004_happy_blue_marvel", "breakpoints": true }, - { "idx": 5, "version": "7", "when": 1780032370230, "tag": "0005_hot_phantom_reporter", "breakpoints": true }, - { "idx": 6, "version": "7", "when": 1780032370231, "tag": "0006_brainy_solo", "breakpoints": true }, - { "idx": 7, "version": "7", "when": 1780032370232, "tag": "0007_replies_anonymization", "breakpoints": true }, - { "idx": 8, "version": "7", "when": 1780032370233, "tag": "0008_environment_cascades", "breakpoints": true }, - { "idx": 9, "version": "7", "when": 1780032370234, "tag": "0009_tidy_electro", "breakpoints": true }, - { "idx": 10, "version": "7", "when": 1781077831112, "tag": "0010_smiling_argent", "breakpoints": true }, - { "idx": 11, "version": "7", "when": 1781169675345, "tag": "0011_add_doubt_embeddings_pgvector", "breakpoints": true }, - { "idx": 12, "version": "7", "when": 1781245145795, "tag": "0012_fulltext_search", "breakpoints": true }, - { "idx": 13, "version": "7", "when": 1781300000000, "tag": "0013_identity_system_update", "breakpoints": true }, - { "idx": 14, "version": "7", "when": 1781340000000, "tag": "0014_practice_attempts", "breakpoints": true }, - { "idx": 15, "version": "7", "when": 1781354492608, "tag": "0015_add_onboarding_fields", "breakpoints": true }, - { "idx": 16, "version": "7", "when": 1781400000000, "tag": "0016_video_jobs", "breakpoints": true } + { + "idx": 0, + "version": "7", + "when": 1779121992842, + "tag": "0000_bitter_tyger_tiger", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1779525663345, + "tag": "0001_aspiring_warhawk", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1779870440438, + "tag": "0002_overjoyed_captain_flint", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1780031571662, + "tag": "0003_chemical_quasar", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1780032095705, + "tag": "0004_happy_blue_marvel", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1780032370230, + "tag": "0005_hot_phantom_reporter", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1780032370231, + "tag": "0006_brainy_solo", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1780032370232, + "tag": "0007_replies_anonymization", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1780032370233, + "tag": "0008_environment_cascades", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1780032370234, + "tag": "0009_tidy_electro", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1781077831112, + "tag": "0010_smiling_argent", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1781169675345, + "tag": "0011_add_doubt_embeddings_pgvector", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1781245145795, + "tag": "0012_fulltext_search", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1781300000000, + "tag": "0013_identity_system_update", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1781340000000, + "tag": "0014_practice_attempts", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1781354492608, + "tag": "0015_add_onboarding_fields", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1781400000000, + "tag": "0016_video_jobs", + "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1783369848923, + "tag": "0017_wild_klaw", + "breakpoints": true + } ] } \ No newline at end of file diff --git a/src/__tests__/api/doubts-filter.test.ts b/src/__tests__/api/doubts-filter.test.ts new file mode 100644 index 00000000..0c6d2394 --- /dev/null +++ b/src/__tests__/api/doubts-filter.test.ts @@ -0,0 +1,104 @@ +import { GET } from '@/app/api/doubts/filter/route'; + +jest.mock('@clerk/nextjs/server', () => ({ + currentUser: jest.fn(), +})); + +jest.mock('@/lib/auth/membership-guard', () => ({ + requireAuth: jest.fn(), + requireMembership: jest.fn(), + parseClassroomId: (value: unknown) => { + const n = Number(value); + if (!Number.isSafeInteger(n) || n <= 0) { + const { ApiError } = jest.requireActual('@/lib/errors/error-handler'); + throw new ApiError(400, 'Invalid classroom ID'); + } + return n; + }, +})); + +import { requireAuth, requireMembership } from '@/lib/auth/membership-guard'; +import { ApiError } from '@/lib/errors/error-handler'; + +const selectResultQueue: any[] = []; + +const createQueryMock = (data: any) => ({ + from: () => createQueryMock(data), + where: () => createQueryMock(data), + orderBy: () => createQueryMock(data), + limit: () => Promise.resolve(data), + then: (resolve: any) => Promise.resolve(resolve(data)), +}); + +jest.mock('@/configs/db', () => ({ + db: { + select: jest.fn().mockImplementation(() => createQueryMock(selectResultQueue.shift() ?? [])), + }, +})); + +describe('Doubts Filter API Endpoint (issue #733)', () => { + beforeEach(() => { + (requireAuth as jest.Mock).mockReset(); + (requireMembership as jest.Mock).mockReset(); + selectResultQueue.length = 0; + }); + + it('rejects unauthenticated requests', async () => { + (requireAuth as jest.Mock).mockRejectedValue(new ApiError(401, 'Unauthorized')); + + const req = new Request('http://localhost/api/doubts/filter?classroomId=7'); + const res = await GET(req as any); + expect(res.status).toBe(401); + }); + + it('requires classroomId', async () => { + (requireAuth as jest.Mock).mockResolvedValue({ email: 'student@test.com' }); + + const req = new Request('http://localhost/api/doubts/filter'); + const res = await GET(req as any); + expect(res.status).toBe(400); + }); + + it('rejects callers who are not members of the classroom', async () => { + (requireAuth as jest.Mock).mockResolvedValue({ email: 'outsider@test.com' }); + (requireMembership as jest.Mock).mockRejectedValue(new ApiError(403, 'Access denied to this classroom')); + + const req = new Request('http://localhost/api/doubts/filter?classroomId=7'); + const res = await GET(req as any); + expect(res.status).toBe(403); + }); + + it('returns anonymized doubts for a member, filtered by subject', async () => { + (requireAuth as jest.Mock).mockResolvedValue({ email: 'student@test.com' }); + (requireMembership as jest.Mock).mockResolvedValue({ role: 'student' }); + selectResultQueue.push([ + { id: 1, userEmail: 'author@test.com', classroomId: 7, subject: 'Physics', content: 'why?', likes: 2, isSolved: 'unsolved', createdAt: new Date() }, + ]); + + const req = new Request('http://localhost/api/doubts/filter?classroomId=7&subject=Physics'); + const res = await GET(req as any); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.count).toBe(1); + expect(json.data[0].userEmail).toBeUndefined(); + expect(json.data[0].author).toBeDefined(); + expect(json.data[0].subject).toBe('Physics'); + }); + + it('does not filter by subject when subject=All', async () => { + (requireAuth as jest.Mock).mockResolvedValue({ email: 'student@test.com' }); + (requireMembership as jest.Mock).mockResolvedValue({ role: 'student' }); + selectResultQueue.push([ + { id: 1, userEmail: 'a@test.com', classroomId: 7, subject: 'Physics', content: 'q1', likes: 0, isSolved: 'unsolved', createdAt: new Date() }, + { id: 2, userEmail: 'b@test.com', classroomId: 7, subject: 'Math', content: 'q2', likes: 0, isSolved: 'unsolved', createdAt: new Date() }, + ]); + + const req = new Request('http://localhost/api/doubts/filter?classroomId=7&subject=All'); + const res = await GET(req as any); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.count).toBe(2); + }); +}); diff --git a/src/__tests__/api/teacher-insights.test.ts b/src/__tests__/api/teacher-insights.test.ts index af2705c9..a203e55f 100644 --- a/src/__tests__/api/teacher-insights.test.ts +++ b/src/__tests__/api/teacher-insights.test.ts @@ -64,7 +64,7 @@ describe('Teacher Insights API Endpoint', () => { const json = await res.json(); expect(res.status).toBe(400); - expect(json.error).toBe('classroomId is required'); + expect(json.error).toBe('Invalid classroom ID'); }); it('returns 403 when the user is not the teacher of the classroom', async () => { @@ -79,7 +79,7 @@ describe('Teacher Insights API Endpoint', () => { const json = await res.json(); expect(res.status).toBe(403); - expect(json.error).toBe('Forbidden: not the teacher of this classroom'); + expect(json.error).toBe('Access denied to this classroom'); }); it('returns classroom-scoped insights for the teacher', async () => { diff --git a/src/__tests__/configs/db.test.ts b/src/__tests__/configs/db.test.ts index 73f74d07..d147c9f4 100644 --- a/src/__tests__/configs/db.test.ts +++ b/src/__tests__/configs/db.test.ts @@ -70,7 +70,7 @@ describe('database configuration', () => { if (originalNodeEnv) { Object.defineProperty(process.env, 'NODE_ENV', { value: originalNodeEnv, writable: true }); } else { - delete process.env.NODE_ENV; + delete (process.env as Record).NODE_ENV; } }); diff --git a/src/__tests__/inngest/digest-functions.test.ts b/src/__tests__/inngest/digest-functions.test.ts index 081f2c97..6aaae5b3 100644 --- a/src/__tests__/inngest/digest-functions.test.ts +++ b/src/__tests__/inngest/digest-functions.test.ts @@ -116,7 +116,7 @@ describe("sendDailyDigest — per-user step isolation", () => { const step = makeStep(); // @ts-expect-error — internal test invocation bypasses Inngest runtime types - await expect(sendDailyDigest({ step })).rejects.toThrow("SMTP timeout"); + await expect(sendDailyDigest.fn({ step })).rejects.toThrow("SMTP timeout"); // Alice's row MUST have been deleted (email succeeded). expect(dbMock.delete).toHaveBeenCalledTimes(1); @@ -146,19 +146,19 @@ describe("sendDailyDigest — per-user step isolation", () => { const deleteChain = { where: jest.fn().mockResolvedValue(undefined) }; (dbMock.delete as jest.Mock).mockReturnValue(deleteChain); - mockSendDigestEmail.mockResolvedValue(undefined); + mockSendDigestEmail.mockResolvedValue({ success: true }); const { sendDailyDigest } = await import("@/inngest/functions"); // First run — completes successfully. const step = makeStep(); // @ts-expect-error - await sendDailyDigest({ step }); + await sendDailyDigest.fn({ step }); expect(mockSendDigestEmail).toHaveBeenCalledTimes(1); // Simulate Inngest retry: same step shim (memoised results) → per-user step is a no-op. // @ts-expect-error - await sendDailyDigest({ step }); + await sendDailyDigest.fn({ step }); // sendDigestEmail must NOT be called again. expect(mockSendDigestEmail).toHaveBeenCalledTimes(1); }); diff --git a/src/__tests__/lib/anonymity.test.ts b/src/__tests__/lib/anonymity.test.ts index 232953f8..139f17ec 100644 --- a/src/__tests__/lib/anonymity.test.ts +++ b/src/__tests__/lib/anonymity.test.ts @@ -54,10 +54,9 @@ describe("anonymity: fail closed in production", () => { afterEach(() => { if (origEnv === undefined) delete mutableEnv.NODE_ENV; else mutableEnv.NODE_ENV = origEnv; - if (origSalt === undefined) delete process.env.ANON_HANDLE_SALT; - else process.env.ANON_HANDLE_SALT = origSalt; + if (origSalt === undefined) delete mutableEnv.ANON_HANDLE_SALT; + else mutableEnv.ANON_HANDLE_SALT = origSalt; }); - it("throws when ANON_HANDLE_SALT is missing in production", () => { delete process.env.ANON_HANDLE_SALT; diff --git a/src/app/api/admin/moderation/route.ts b/src/app/api/admin/moderation/route.ts index abbf6033..7d07f86c 100644 --- a/src/app/api/admin/moderation/route.ts +++ b/src/app/api/admin/moderation/route.ts @@ -48,7 +48,7 @@ export async function GET(request: Request) { count: count(), }).from(moderationLogsTable).groupBy(sql`DATE(${moderationLogsTable.createdAt})`).orderBy(sql`DATE(${moderationLogsTable.createdAt}) ASC`).limit(30); - const formattedFlagsPerDay = flagsPerDay.map((f: { date: any, count: number }) => ({ + const formattedFlagsPerDay = flagsPerDay.map((f: (typeof flagsPerDay)[number]) => ({ date: typeof f.date === 'string' ? f.date : new Date(f.date).toISOString().split('T')[0], count: f.count })); diff --git a/src/app/api/admin/overview/route.ts b/src/app/api/admin/overview/route.ts index 0ebf6546..37fedb03 100644 --- a/src/app/api/admin/overview/route.ts +++ b/src/app/api/admin/overview/route.ts @@ -141,17 +141,20 @@ export async function GET(request: Request) { .groupBy(doubtsTable.classroomId); // Build mapping helpers - const studentCountMap = new Map(studentCounts.map((c: any) => [c.classroomId, c.count])); - const doubtStatsMap = new Map(doubtStats.map((d: any) => [d.classroomId, d])); - const pedagogyStatsMap = new Map(pedagogyStats.map((p: any) => [p.classroomId, p])); - const alertsCountMap = new Map(activeAlertsPerClassroom.map((a: any) => [a.classroomId, a.count])); - const resolutionTimeMap = new Map(resolutionTimes.map((r: any) => [r.classroomId, r.avgTimeMins])); + type DoubtStatsRow = { classroomId: number | null; total: number; solved: number }; + type PedagogyStatsRow = { classroomId: number | null; totalReplies: number; driftedReplies: number }; - const classroomHealth = classrooms.map((classroom: any) => { + const studentCountMap = new Map(studentCounts.map((c: (typeof studentCounts)[number]) => [c.classroomId, c.count])); + const doubtStatsMap = new Map(doubtStats.map((d: (typeof doubtStats)[number]) => [d.classroomId, d as DoubtStatsRow])); + const pedagogyStatsMap = new Map(pedagogyStats.map((p: (typeof pedagogyStats)[number]) => [p.classroomId, p as PedagogyStatsRow])); + const alertsCountMap = new Map(activeAlertsPerClassroom.map((a: (typeof activeAlertsPerClassroom)[number]) => [a.classroomId, a.count])); + const resolutionTimeMap = new Map(resolutionTimes.map((r: (typeof resolutionTimes)[number]) => [r.classroomId, r.avgTimeMins])); + + const classroomHealth = classrooms.map((classroom: (typeof classrooms)[number]) => { const cId = classroom.id; const enrolledStudents = studentCountMap.get(cId) || 0; - const dStats: any = doubtStatsMap.get(cId) || { total: 0, solved: 0 }; - const pStats: any = pedagogyStatsMap.get(cId) || { totalReplies: 0, driftedReplies: 0 }; + const dStats: DoubtStatsRow = doubtStatsMap.get(cId) || { classroomId: cId, total: 0, solved: 0 }; + const pStats: PedagogyStatsRow = pedagogyStatsMap.get(cId) || { classroomId: cId, totalReplies: 0, driftedReplies: 0 }; const alertsCount = alertsCountMap.get(cId) || 0; const avgResolutionTime = resolutionTimeMap.get(cId) || 0; diff --git a/src/app/api/analytics/export/route.ts b/src/app/api/analytics/export/route.ts index e60bdb32..28308f66 100644 --- a/src/app/api/analytics/export/route.ts +++ b/src/app/api/analytics/export/route.ts @@ -51,7 +51,7 @@ export async function GET(req: Request) { .from(membershipsTable) .where(eq(membershipsTable.userEmail, email)); - const userClassroomIds = userMemberships.map((m: any) => m.classroomId); + const userClassroomIds = userMemberships.map((m: (typeof userMemberships)[number]) => m.classroomId); if (userClassroomIds.length === 0) { return NextResponse.json({ @@ -179,7 +179,7 @@ export async function GET(req: Request) { ]); // 8. AI Teaching Suggestions & Weak Concept Detection (Heuristics) - const weakTopics = mostAskedTopics.map((topic: any, index: number) => { + const weakTopics = mostAskedTopics.map((topic: (typeof mostAskedTopics)[number], index: number) => { const countValue = Number(topic.count); let suggestion = ""; @@ -254,8 +254,8 @@ export async function GET(req: Request) { totalReplies: totalReplies[0]?.count || 0, }, solvedStats, - weakTopics: weakTopics.filter((t: any) => t.severity !== "Low"), - topContributors: topContributors.map((c: any) => ({ + weakTopics: weakTopics.filter((t: (typeof weakTopics)[number]) => t.severity !== "Low"), + topContributors: topContributors.map((c: (typeof topContributors)[number]) => ({ name: c.name, replyCount: Number(c.replyCount), })), @@ -271,7 +271,7 @@ export async function GET(req: Request) { csv += "\nStatus,Count\n"; - analyticsData.solvedStats.forEach((stat: any) => { + analyticsData.solvedStats.forEach((stat: (typeof analyticsData.solvedStats)[number]) => { let label = "Unknown"; if (stat.status === "solved") { @@ -286,12 +286,12 @@ export async function GET(req: Request) { }); csv += "\nContributor Name,Reply Count\n"; - analyticsData.topContributors.forEach((contributor: any) => { + analyticsData.topContributors.forEach((contributor: (typeof analyticsData.topContributors)[number]) => { csv += `${contributor.name},${contributor.replyCount}\n`; }); csv += "\nWeak Topic,Doubt Count,Severity\n"; - analyticsData.weakTopics.forEach((topic: any) => { + analyticsData.weakTopics.forEach((topic: (typeof analyticsData.weakTopics)[number]) => { csv += `${topic.subject},${topic.count},${topic.severity}\n`; }); diff --git a/src/app/api/analytics/personal/route.ts b/src/app/api/analytics/personal/route.ts index 4eb3f001..002dd55d 100644 --- a/src/app/api/analytics/personal/route.ts +++ b/src/app/api/analytics/personal/route.ts @@ -51,7 +51,7 @@ export async function GET(req: Request) { } // Prepare doubt summaries for AI analysis - const doubtContext = userDoubts.map((d: any) => `- [${d.subject}]: ${d.content}`).join('\n'); + const doubtContext = userDoubts.map((d: (typeof userDoubts)[number]) => `- [${d.subject}]: ${d.content}`).join('\n'); const systemPrompt = `You are an AI Learning Mentor. Analyze the student's academic doubts across their classroom activities. Your goal is to identify patterns, recurring sub-topics they struggle with, and provide actionable recommendations. diff --git a/src/app/api/analytics/route.ts b/src/app/api/analytics/route.ts index 8baf5cf1..26f9cfed 100644 --- a/src/app/api/analytics/route.ts +++ b/src/app/api/analytics/route.ts @@ -55,7 +55,7 @@ export async function GET(req: Request) { .from(classroomsTable) .where(eq(classroomsTable.organizationId, orgId)); - activeClassroomIds = orgClassrooms.map((c: any) => c.id); + activeClassroomIds = orgClassrooms.map((c: (typeof orgClassrooms)[number]) => c.id); } // 2. CLASSROOM LEVEL SCOPING else if (classroomId !== null) { @@ -67,7 +67,7 @@ export async function GET(req: Request) { const userMemberships = await db.select({ classroomId: membershipsTable.classroomId }) .from(membershipsTable) .where(eq(membershipsTable.userEmail, email)); - activeClassroomIds = userMemberships.map((m: any) => m.classroomId); + activeClassroomIds = userMemberships.map((m: (typeof userMemberships)[number]) => m.classroomId); } // Return empty state if no classrooms are found for the given scope @@ -199,7 +199,7 @@ export async function GET(req: Request) { ]); // 8. AI Teaching Suggestions & Weak Concept Detection (Heuristics) - const weakTopics = mostAskedTopics.map((topic: any, index: number) => { + const weakTopics = mostAskedTopics.map((topic: (typeof mostAskedTopics)[number], index: number) => { const countValue = Number(topic.count); let suggestion = ""; @@ -264,8 +264,8 @@ export async function GET(req: Request) { ...engagement[0], totalReplies: totalReplies[0]?.count || 0 }, - weakTopics: weakTopics.filter((t: any) => t.severity !== 'Low'), - topContributors: topContributors.map((c: any) => ({ name: c.name, replyCount: Number(c.replyCount) })), + weakTopics: weakTopics.filter((t: (typeof weakTopics)[number]) => t.severity !== 'Low'), + topContributors: topContributors.map((c: (typeof topContributors)[number]) => ({ name: c.name, replyCount: Number(c.replyCount) })), classroomSettings, recentAIReplies: recentAIReplies || [] }); diff --git a/src/app/api/classrooms/[id]/export/route.ts b/src/app/api/classrooms/[id]/export/route.ts index 51a28a3a..fb6b17b4 100644 --- a/src/app/api/classrooms/[id]/export/route.ts +++ b/src/app/api/classrooms/[id]/export/route.ts @@ -75,10 +75,10 @@ export async function GET( .groupBy(repliesTable.doubtId); const countsMap = Object.fromEntries( - replyCounts.map((r: any) => [r.doubtId, r.count]) + replyCounts.map((r: (typeof replyCounts)[number]) => [r.doubtId, r.count]) ); - const doubtsWithReplies = doubts.map((doubt: any) => ({ + const doubtsWithReplies = doubts.map((doubt: (typeof doubts)[number]) => ({ ...doubt, replyCount: countsMap[doubt.id] || 0, })); diff --git a/src/app/api/doubts/[id]/upvote/route.ts b/src/app/api/doubts/[id]/upvote/route.ts index 8240a40c..a385fec8 100644 --- a/src/app/api/doubts/[id]/upvote/route.ts +++ b/src/app/api/doubts/[id]/upvote/route.ts @@ -3,7 +3,6 @@ import { NextRequest, NextResponse } from "next/server"; import { db } from "@/configs/db"; import { repliesTable, replyLikesTable } from "@/configs/schema"; import { eq, and, sql } from "drizzle-orm"; -import { buildErrorResponse } from "@/lib/errors/error-handler"; import { inngest } from "@/inngest/client"; import { currentUser } from "@clerk/nextjs/server"; @@ -67,7 +66,7 @@ export async function POST( let updatedReply; try { - updatedReply = await db.transaction(async (tx) => { + updatedReply = await db.transaction(async (tx: any) => { // A. FIX: Standardized column input across all vote handlers to use the stable identifier. // Note: If your Drizzle schema explicitly names the column field `userName`, we map the unique @@ -138,7 +137,12 @@ export async function POST( } catch (error) { console.error("CRITICAL: Upvote endpoint execution exception:", error); - const { status, body } = buildErrorResponse(error); - return NextResponse.json(body, { status }); + return NextResponse.json( + { + error: "Internal Server Error", + details: error instanceof Error ? error.message : "Database connection or structural query exception" + }, + { status: 500 } + ); } } \ No newline at end of file diff --git a/src/app/api/doubts/action/[id]/route.ts b/src/app/api/doubts/action/[id]/route.ts index a32ba08e..ef0c4d20 100644 --- a/src/app/api/doubts/action/[id]/route.ts +++ b/src/app/api/doubts/action/[id]/route.ts @@ -230,7 +230,7 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st ? eq(tagsTable.classroomId, doubt.classroomId) : isNull(tagsTable.classroomId); - const { updated, savedTags } = await db.transaction(async (tx) => { + const { updated, savedTags } = await db.transaction(async (tx: any) => { const [updatedRow] = await tx.update(doubtsTable) .set({ content: content || null, @@ -248,7 +248,7 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st .where(eq(doubtTagsTable.doubtId, doubtId)); return { updated: updatedRow, - savedTags: existingLinks.map((row: any) => row.tag), + savedTags: existingLinks.map((row: (typeof existingLinks)[number]) => row.tag), }; } diff --git a/src/app/api/doubts/check-similarity/route.ts b/src/app/api/doubts/check-similarity/route.ts index bd86f501..43cb4e25 100644 --- a/src/app/api/doubts/check-similarity/route.ts +++ b/src/app/api/doubts/check-similarity/route.ts @@ -114,7 +114,7 @@ export async function POST(req: Request) { const doubtList = recentDoubts .map( - (d, i) => + (d: (typeof recentDoubts)[number], i: number) => `[${i}] Subject: ${d.subject} | Content: ${(d.content || "").slice(0, 150)}`, ) .join("\n"); @@ -185,8 +185,8 @@ Do not include any explanation or markdown.`; .where(inArray(repliesTable.id, solvedReplyIds)) : []; - const replyMap = new Map( - solvedReplies.map((reply: any) => [reply.id, reply.content]), + const replyMap = new Map( + solvedReplies.map((reply: (typeof solvedReplies)[number]): [number, string | null] => [reply.id, reply.content]), ); for (const match of highMatches) { diff --git a/src/app/api/doubts/filter/route.ts b/src/app/api/doubts/filter/route.ts new file mode 100644 index 00000000..8fb57c0b --- /dev/null +++ b/src/app/api/doubts/filter/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db } from "@/configs/db"; +import { doubtsTable } from "@/configs/schema"; +import { eq, and, isNull } from "drizzle-orm"; +import { requireAuth, requireMembership, parseClassroomId } from "@/lib/auth/membership-guard"; +import { toPublicDoubt } from "@/lib/anonymity/anonymity"; +import { buildErrorResponse } from "@/lib/errors/error-handler"; + +export async function GET(req: NextRequest) { + try { + const { email } = await requireAuth(); + + const { searchParams } = new URL(req.url); + const classroomIdParam = searchParams.get("classroomId"); + const subject = searchParams.get("subject"); + + if (!classroomIdParam) { + return NextResponse.json({ error: "classroomId is required" }, { status: 400 }); + } + + const classroomId = parseClassroomId(classroomIdParam); + + // Only members of the classroom (or its teacher) may view its doubts. + await requireMembership(email, classroomId); + + const conditions = [ + eq(doubtsTable.classroomId, classroomId), + isNull(doubtsTable.deletedAt), + ]; + + if (subject && subject !== "All") { + conditions.push(eq(doubtsTable.subject, subject)); + } + + const doubts = await db + .select() + .from(doubtsTable) + .where(and(...conditions)) + .orderBy(doubtsTable.createdAt) + .limit(100); + + // Strip author identifiers before returning — see src/lib/anonymity.ts. + const publicDoubts = doubts.map((doubt: (typeof doubts)[number]) => toPublicDoubt(doubt, email)); + + return NextResponse.json({ + success: true, + data: publicDoubts, + count: publicDoubts.length, + }); + } catch (error) { + const { status, body } = buildErrorResponse(error); + return NextResponse.json(body, { status }); + } +} diff --git a/src/app/api/doubts/route.ts b/src/app/api/doubts/route.ts index 76220dc7..5992c66e 100644 --- a/src/app/api/doubts/route.ts +++ b/src/app/api/doubts/route.ts @@ -125,7 +125,7 @@ export async function GET(req: Request) { .select({ doubtId: bookmarksTable.doubtId }) .from(bookmarksTable) .where(eq(bookmarksTable.userEmail, email)); - const bookmarkedIds = userBookmarks.map((b: any) => b.doubtId); + const bookmarkedIds = userBookmarks.map((b: (typeof userBookmarks)[number]) => b.doubtId); if (bookmarkedIds.length > 0) { conditions.push(inArray(doubtsTable.id, bookmarkedIds)); } else { @@ -222,8 +222,8 @@ export async function GET(req: Request) { .from(likesTable) .where(eq(likesTable.userEmail, email)); - const likedIds = new Set(userLikes.map((l: any) => l.doubtId)); - doubts = doubts.map((doubt: any) => ({ + const likedIds = new Set(userLikes.map((l: (typeof userLikes)[number]) => l.doubtId)); + doubts = doubts.map((doubt: (typeof doubts)[number]) => ({ ...doubt, hasLiked: likedIds.has(doubt.id), })); @@ -235,8 +235,8 @@ export async function GET(req: Request) { .from(bookmarksTable) .where(eq(bookmarksTable.userEmail, email)); - const bookmarkedIds = new Set(userBookmarks.map((b: any) => b.doubtId)); - doubts = doubts.map((doubt: any) => ({ + const bookmarkedIds = new Set(userBookmarks.map((b: (typeof userBookmarks)[number]) => b.doubtId)); + doubts = doubts.map((doubt: (typeof doubts)[number]) => ({ ...doubt, hasBookmarked: bookmarkedIds.has(doubt.id), })); @@ -252,11 +252,11 @@ export async function GET(req: Request) { }) .from(doubtTagsTable) .innerJoin(tagsTable, eq(doubtTagsTable.tagId, tagsTable.id)) - .where(inArray(doubtTagsTable.doubtId, doubts.map((d: any) => d.id))); + .where(inArray(doubtTagsTable.doubtId, doubts.map((d: (typeof doubts)[number]) => d.id))); - const tagsByDoubt = tagRows.reduce< + const tagsByDoubt = (tagRows as (typeof tagRows)[number][]).reduce< Record - >((acc, row) => { + >((acc: Record, row: (typeof tagRows)[number]) => { acc[row.doubtId] = acc[row.doubtId] || []; acc[row.doubtId].push({ id: row.id, @@ -266,7 +266,7 @@ export async function GET(req: Request) { return acc; }, {}); - doubts = doubts.map((doubt: any) => ({ + doubts = doubts.map((doubt: (typeof doubts)[number]) => ({ ...doubt, tags: tagsByDoubt[doubt.id] || [], })); @@ -277,7 +277,7 @@ export async function GET(req: Request) { // Strip author identifiers (userEmail), the internal embedding vector and // soft-delete marker before returning. Only the anonymized handle and a // session-derived `isOwnPost` flag are exposed. See src/lib/anonymity.ts. - const publicDoubts = doubts.map((doubt: any) => toPublicDoubt(doubt, email)); + const publicDoubts = doubts.map((doubt: (typeof doubts)[number]) => toPublicDoubt(doubt, email)); return NextResponse.json({ doubts: publicDoubts, @@ -427,7 +427,9 @@ export async function POST(req: Request) { ), ); - const existingTagsMap = new Map(existingClassroomTags.map((t: any) => [t.normalizedName, t])); + const existingTagsMap = new Map( + existingClassroomTags.map((t: (typeof existingClassroomTags)[number]): [string, typeof tagsTable.$inferSelect] => [t.normalizedName, t]), + ); const tagsToInsert: (typeof tagsTable.$inferInsert)[] = []; for (const name of normalizedTags) { diff --git a/src/app/api/invites/[token]/join/route.ts b/src/app/api/invites/[token]/join/route.ts index 5c57c829..0655df69 100644 --- a/src/app/api/invites/[token]/join/route.ts +++ b/src/app/api/invites/[token]/join/route.ts @@ -99,7 +99,7 @@ export async function POST( // membership)`, so a duplicate concurrent join from the same user // can't consume a second slot for a membership that will just // conflict away. - const joinResult = await db.execute<{ membership_id: number }>(sql` + const joinResult: { rows: { membership_id: number }[] } = await db.execute(sql` WITH slot_claim AS ( UPDATE ${classroomInvitesTable} SET used_count = used_count + 1 diff --git a/src/app/api/karma/route.ts b/src/app/api/karma/route.ts index 6de8db7a..9f7cf9a8 100644 --- a/src/app/api/karma/route.ts +++ b/src/app/api/karma/route.ts @@ -124,7 +124,7 @@ export async function POST(req: NextRequest) { // ── 2. TRANSACTION MUTATION MANAGEMENT ─────────────────────────────────── // FIX: Wrap all mutation procedures within an explicit database-level transaction. // If anything fails or throws an integrity error, the whole execution rolls back cleanly. - const result = await db.transaction(async (tx) => { + const result = await db.transaction(async (tx: any) => { const targetScoreSql = sql`${usersTable.karmaScore} + ${points}`; const atomicLevelCaseSql = sql`CASE WHEN ${targetScoreSql} >= 1500 THEN 5 diff --git a/src/app/api/organizations/route.ts b/src/app/api/organizations/route.ts index bc2bbcb4..634b21ba 100644 --- a/src/app/api/organizations/route.ts +++ b/src/app/api/organizations/route.ts @@ -84,7 +84,7 @@ export async function POST(req: Request) { return errorResponse('An organization with this identifier slug already exists', 409); } - const createdOrg = await db.transaction(async (tx) => { + const createdOrg = await db.transaction(async (tx: any) => { const [org] = await tx .insert(organizationsTable) .values({ diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts index 52acb696..18c60cbb 100644 --- a/src/app/api/profile/route.ts +++ b/src/app/api/profile/route.ts @@ -33,8 +33,8 @@ export async function GET(req: Request) { const dbUser = dbUserResults[0]; const classroomIds = memberships - .map((m: any) => m.classroomId) - .filter((id): id is number => id !== null && id !== undefined); + .map((m: (typeof memberships)[number]) => m.classroomId) + .filter((id: number | null): id is number => id !== null && id !== undefined); let classrooms: ProfileClassroom[] = []; if (classroomIds.length > 0) { @@ -46,7 +46,7 @@ export async function GET(req: Request) { const totalDoubts = doubts?.length || 0; const totalReplies = replies?.length || 0; - const helpfulVotes = doubts ? doubts.reduce((acc, doubt) => acc + (doubt.likes || 0), 0) : 0; + const helpfulVotes = doubts ? doubts.reduce((acc: number, doubt: (typeof doubts)[number]) => acc + (doubt.likes || 0), 0) : 0; const rawJoinDate = dbUser?.createdAt || (clerkUser?.createdAt ? new Date(clerkUser.createdAt) : new Date()); const joinDate = rawJoinDate instanceof Date ? rawJoinDate.toISOString() : new Date(rawJoinDate).toISOString(); diff --git a/src/app/api/recommendations/route.ts b/src/app/api/recommendations/route.ts index 154f640e..cddafb8c 100644 --- a/src/app/api/recommendations/route.ts +++ b/src/app/api/recommendations/route.ts @@ -48,7 +48,7 @@ export async function GET() { .where(eq(membershipsTable.userEmail, email)); const joinedIds = joinedMemberships.map( - (membership) => membership.classroomId + (membership: (typeof joinedMemberships)[number]) => membership.classroomId ); // 4. Fetch all candidate classrooms @@ -58,7 +58,7 @@ const classrooms = await db .where( joinedIds.length ? sql`${classroomsTable.id} NOT IN (${sql.join( - joinedIds.map((id: any) => sql`${id}`), + joinedIds.map((id: (typeof joinedIds)[number]) => sql`${id}`), sql`, ` )})` : sql`true` @@ -90,14 +90,14 @@ const classrooms = await db .groupBy(doubtsTable.classroomId); const memberCountMap = Object.fromEntries( - memberCounts.map((item: any) => [ + memberCounts.map((item: (typeof memberCounts)[number]) => [ item.classroomId, item.count, ]) ); const activityCountMap = Object.fromEntries( - activityCounts.map((item: any) => [ + activityCounts.map((item: (typeof activityCounts)[number]) => [ item.classroomId, item.count, ]) @@ -105,7 +105,7 @@ const classrooms = await db // 7. Generate recommendations const recommendations = classrooms - .map((classroom: any) => { + .map((classroom: (typeof classrooms)[number]) => { const score = calculateRecommendationScore({ universityMatch: classroom.university === currentDbUser.university, @@ -133,7 +133,7 @@ const classrooms = await db }; }) .sort( - (a, b) => + (a: { recommendationScore: number }, b: { recommendationScore: number }) => b.recommendationScore - a.recommendationScore ) diff --git a/src/app/api/replies/vote/route.ts b/src/app/api/replies/vote/route.ts index b9122a35..61d21c13 100644 --- a/src/app/api/replies/vote/route.ts +++ b/src/app/api/replies/vote/route.ts @@ -55,7 +55,7 @@ export async function POST(req: Request) { } // ── 3. ATOMIC TRANSACTION FLOW ────────────────────────────────────── - const result = await db.transaction(async (tx) => { + const result = await db.transaction(async (tx: any) => { // Check existing vote inside transaction const existingLike = await tx.select() diff --git a/src/app/api/rooms/join/route.ts b/src/app/api/rooms/join/route.ts index 326d498f..a67228eb 100644 --- a/src/app/api/rooms/join/route.ts +++ b/src/app/api/rooms/join/route.ts @@ -44,7 +44,7 @@ export async function POST(req: Request) { // 1b. Check email domain restrictions if set if (classroom.allowedEmailDomains && classroom.allowedEmailDomains.length > 0) { const emailDomain = email.split('@')[1]?.toLowerCase(); - if (!emailDomain || !classroom.allowedEmailDomains.some(d => emailDomain === d.toLowerCase())) { + if (!emailDomain || !classroom.allowedEmailDomains.some((d: string) => emailDomain === d.toLowerCase())) { return NextResponse.json({ error: `Only email addresses from ${classroom.allowedEmailDomains.join(', ')} domains can join this classroom` }, { status: 403 }); diff --git a/src/app/api/rooms/members/route.ts b/src/app/api/rooms/members/route.ts index 74a58684..37675612 100644 --- a/src/app/api/rooms/members/route.ts +++ b/src/app/api/rooms/members/route.ts @@ -88,11 +88,11 @@ export async function GET(req: Request) { email.toLowerCase().trim() === normalizedOwnerEmail; const processedMembers = canViewEmails - ? members.map(({ id, ...m }) => ({ + ? members.map(({ id, ...m }: (typeof members)[number]) => ({ ...m, isOwner: isOwnerEmail(m.userEmail), })) - : members.map((m: any) => ({ + : members.map((m: (typeof members)[number]) => ({ displayName: `${m.role.toLowerCase() === 'student' ? 'Student' : 'Member'}_${m.id}`, role: m.role, joinedAt: m.joinedAt, diff --git a/src/app/api/rooms/route.ts b/src/app/api/rooms/route.ts index 6fd1e7da..114e5e81 100644 --- a/src/app/api/rooms/route.ts +++ b/src/app/api/rooms/route.ts @@ -54,7 +54,7 @@ export async function GET(req: Request) { let recommendedRooms: Classroom[] = []; if (dbUser && dbUser.university && dbUser.year) { - const joinedIds = joinedRooms.map((r: any) => r.id); + const joinedIds = joinedRooms.map((r: (typeof joinedRooms)[number]) => r.id); let conditions = [ eq(classroomsTable.university, dbUser.university), @@ -133,7 +133,7 @@ export async function POST(req: Request) { const inviteCode = Math.random().toString(36).substring(2, 8).toUpperCase(); // Transactional insert: create room and then add teacher as member atomically - const newRoom = await db.transaction(async (tx) => { + const newRoom = await db.transaction(async (tx: any) => { const [room] = await tx .insert(classroomsTable) .values({ diff --git a/src/app/api/teacher/analytics/route.ts b/src/app/api/teacher/analytics/route.ts index 5e8a881a..f3f5efe2 100644 --- a/src/app/api/teacher/analytics/route.ts +++ b/src/app/api/teacher/analytics/route.ts @@ -29,8 +29,8 @@ export async function GET(req: NextRequest) { .from(membershipsTable) .where(eq(membershipsTable.userEmail, email)); const teacherMembershipIds = teacherMemberships - .filter((membership) => ["teacher", "owner", "admin"].includes(membership.role)) - .map((membership: any) => membership.classroomId); + .filter((membership: (typeof teacherMemberships)[number]) => ["teacher", "owner", "admin"].includes(membership.role)) + .map((membership: (typeof teacherMemberships)[number]) => membership.classroomId); const isTeacherOrAdmin = dbUser?.role === 'teacher' || @@ -92,7 +92,7 @@ export async function GET(req: NextRequest) { } selectedClassroomIds = [classroomId]; } else { - selectedClassroomIds = classroomsList.map((c: any) => c.id); + selectedClassroomIds = classroomsList.map((c: (typeof classroomsList)[number]) => c.id); } // 5. Query and Aggregate Data diff --git a/src/app/api/teacher/insights/route.ts b/src/app/api/teacher/insights/route.ts index b485ae03..7eb80f01 100644 --- a/src/app/api/teacher/insights/route.ts +++ b/src/app/api/teacher/insights/route.ts @@ -99,7 +99,7 @@ export async function GET(req: Request) { const sampleIdsByKey = new Map(); await Promise.all( - unresolvedPerTopic.map(async (row) => { + unresolvedPerTopic.map(async (row: (typeof unresolvedPerTopic)[number]) => { if (!row.topic) return; const rows = await db .select({ id: doubtsTable.id }) @@ -114,13 +114,13 @@ export async function GET(req: Request) { ) .orderBy(sql`${doubtsTable.createdAt} DESC`) .limit(5); - sampleIdsByKey.set(`${row.topic}::${row.subject}`, rows.map((r: any) => r.id)); + sampleIdsByKey.set(`${row.topic}::${row.subject}`, rows.map((r: (typeof rows)[number]) => r.id)); }) ); - const weakTopics: WeakTopic[] = unresolvedPerTopic.map((row: any) => { + const weakTopics: WeakTopic[] = unresolvedPerTopic.map((row: (typeof unresolvedPerTopic)[number]) => { const totalEntry = totalPerTopic.find( - (t) => t.topic === row.topic && t.subject === row.subject + (t: (typeof totalPerTopic)[number]) => t.topic === row.topic && t.subject === row.subject ); const sampleIds = sampleIdsByKey.get(`${row.topic}::${row.subject}`) ?? []; return { diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index df49df06..f44347e6 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -61,7 +61,7 @@ export default async function ProfilePage() { if (userResult.length > 0) { dbUser = userResult[0]; // Enforce the correct karmaScore parameter mapping to clear the database validation flag - karmaScore = dbUser.karmaScore || 0; + karmaScore = dbUser?.karmaScore || 0; } // Run parallel aggregations diff --git a/src/inngest/functions.ts b/src/inngest/functions.ts index 9d069a62..32b39aeb 100644 --- a/src/inngest/functions.ts +++ b/src/inngest/functions.ts @@ -230,7 +230,7 @@ export const sendDailyDigest = inngest.createFunction( } // Delete only after confirmed send. - const notificationIds = pending.map(p => p.id); + const notificationIds = pending.map((p: (typeof pending)[number]) => p.id); await db .delete(pendingNotificationsTable) .where(inArray(pendingNotificationsTable.id, notificationIds)); @@ -314,7 +314,7 @@ export const sendWeeklyDigest = inngest.createFunction( throw emailErr; } - const notificationIds = pending.map(p => p.id); + const notificationIds = pending.map((p: (typeof pending)[number]) => p.id); await db .delete(pendingNotificationsTable) .where(inArray(pendingNotificationsTable.id, notificationIds)); diff --git a/src/inngest/karma.ts b/src/inngest/karma.ts index 8ca1f8b2..1f9a18a3 100644 --- a/src/inngest/karma.ts +++ b/src/inngest/karma.ts @@ -30,7 +30,7 @@ async function executeKarmaTransaction(payload: { } try { - await db.transaction(async (tx) => { + await db.transaction(async (tx: any) => { const targetScoreSql = sql`${usersTable.karmaScore} + ${points}`; const atomicLevelCaseSql = sql`CASE WHEN ${targetScoreSql} >= 1500 THEN 5 @@ -201,7 +201,7 @@ export const dailyStreakProcessor = inngest.createFunction( const points = KARMA_POINTS["streak_bonus"]; // Keep the streak increment and bonus award combined in one atomic transaction block - await db.transaction(async (tx) => { + await db.transaction(async (tx: any) => { // 1. Increment User Streak Counter await tx diff --git a/src/lib/ai/embeddings.ts b/src/lib/ai/embeddings.ts index 7b890730..291f1d8f 100644 --- a/src/lib/ai/embeddings.ts +++ b/src/lib/ai/embeddings.ts @@ -139,12 +139,12 @@ export async function findSemanticDuplicates(params: { const filtered = rows - .filter((r) => typeof r.similarity === "number" && r.similarity >= similarityThreshold) - .sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0)); + .filter((r: (typeof rows)[number]) => typeof r.similarity === "number" && r.similarity >= similarityThreshold) + .sort((a: (typeof rows)[number], b: (typeof rows)[number]) => (b.similarity ?? 0) - (a.similarity ?? 0)); const solvedReplyIds = filtered - .filter((d) => d.isSolved === "solved" && d.solvedReplyId) - .map((d) => d.solvedReplyId as number); + .filter((d: (typeof filtered)[number]) => d.isSolved === "solved" && d.solvedReplyId) + .map((d: (typeof filtered)[number]) => d.solvedReplyId as number); const solvedReplies = solvedReplyIds.length > 0 @@ -155,10 +155,10 @@ export async function findSemanticDuplicates(params: { : []; const replyMap = new Map( - solvedReplies.map((r) => [r.id, r.content]), + solvedReplies.map((r: (typeof solvedReplies)[number]): [number, string | null] => [r.id, r.content]), ); - return filtered.map((d) => ({ + return filtered.map((d: (typeof filtered)[number]) => ({ id: d.id, subject: d.subject, content: d.content, diff --git a/src/lib/karma/karma-utils.ts b/src/lib/karma/karma-utils.ts index 4ff34b27..04f78de5 100644 --- a/src/lib/karma/karma-utils.ts +++ b/src/lib/karma/karma-utils.ts @@ -40,7 +40,7 @@ export async function checkAndAwardBadges(userEmail: string): Promise .from(userBadgesTable) .where(eq(userBadgesTable.userEmail, userEmail)); - const earnedIds = new Set(alreadyEarned.map((b) => b.badgeId)); + const earnedIds = new Set(alreadyEarned.map((b: (typeof alreadyEarned)[number]) => b.badgeId)); // Fetch user stats once (avoids N+1 queries) const [user] = await db @@ -165,7 +165,7 @@ export async function updateStreak(userEmail: string): Promise { if (daysDiff === 0) { const nextStreakVal = user.currentStreak + 1; - await db.transaction(async (tx) => { + await db.transaction(async (tx: any) => { // Compute the target score inline to resolve level scaling factors const nextScoreSql = sql`${usersTable.karmaScore} + 5`; diff --git a/src/lib/moderation/moderation.ts b/src/lib/moderation/moderation.ts index 10ed9019..b1c7fe75 100644 --- a/src/lib/moderation/moderation.ts +++ b/src/lib/moderation/moderation.ts @@ -328,7 +328,7 @@ export async function handleModerationViolation( ): Promise { if (moderation.isAllowed) return null; - return db.transaction(async (tx) => { + return db.transaction(async (tx: any) => { // Atomically increment violationCount at the DB level — eliminates the // read-modify-write race under concurrent violation processing. const [updated] = await tx.update(usersTable).set({ diff --git a/src/lib/notifications/service.ts b/src/lib/notifications/service.ts index 0ea65872..247d308a 100644 --- a/src/lib/notifications/service.ts +++ b/src/lib/notifications/service.ts @@ -45,7 +45,7 @@ export async function createClassroomDoubtNotifications(params: { .from(membershipsTable) .where(eq(membershipsTable.classroomId, classroomId)); - const recipients = new Set(memberRows.map((row) => row.userEmail)); + const recipients = new Set(memberRows.map((row: (typeof memberRows)[number]) => row.userEmail)); if (room.teacherEmail) { recipients.add(room.teacherEmail); }