// db/sqlUserRepository.ts // PostgreSQL database operations for users, roles, state keys, tokens, and financial balances // deno-lint-ignore deno/not-installed-npm import { eq } from "drizzle-orm"; import { brain_question_progress, financial_balances, quiz_configs, quiz_questions, quiz_results, roles, session_configs, stateKeys, user_pins, userRoles, users, userTokens, webTokens, } from "./schema.ts"; import { and, gte, lte } from "drizzle-orm"; import { db } from "./db.ts"; import { User } from "./interfaces.ts"; // Quiz Question type for database operations export interface QuizQuestion { id: string; quiz_id: string; owner_id: string; question_index?: number; language_1: string; language_2: string; text_1: string; audio_file_1: string; text_2: string; audio_file_2: string; order_num: number; background_file: string; background_image_data: string; video_file: string; video_file_data: string; } // Quiz Result type for database operations export interface QuizResult { success: boolean; total_time: number; help_count: number; } // Re-export types from userService for backward compatibility export interface FinancialBalance { id: string; user_id: string; balance: number; balance_date: number; encrypted_data: string | null; created_on: number; updated_on: number; } export interface Role { id: string; description: string; } export interface UserWithRole { id: string; name: string; email: string; role: string; roleDescription: string; } // ============================================================================ // SQL USER OPERATIONS // ============================================================================ export async function sqlGetAllUsers() { return await db.select().from(users); } export async function sqlDeleteUser(id: string) { return await db.delete(users).where(eq(users.id, id)).returning(); } export async function sqlCreateUserRole(user_id: string, role_id: string) { const now = Date.now(); return await db.insert(userRoles) .values({ id: crypto.randomUUID(), userId: user_id, roleId: role_id, created_on: now, updated_on: now, }) .returning(); } // Upsert user by OAuth2 ID export async function sqlUpsertUser(id: string, name: string, email: string) { const existingUser = await db.select().from(users).where(eq(users.id, id)); if (existingUser.length > 0) { // Update existing user profile return await db.update(users) .set({ name, email }) .where(eq(users.id, id)) .returning(); } else { // Insert new user profile return await db.insert(users) .values({ id, name, email, phone: "", address_1: "", address_2: "", city: "", state: "", zip: "", country: "", started_on: Date.now(), updated_on: Date.now(), }) .returning(); } } // Upsert full user record (all profile fields) by user_id export async function sqlUpsertUserFull( user: { id: string } & Partial, ) { const now = Date.now(); const insertValues = { id: user.id, name: user.name ?? "", email: user.email ?? "", phone: user.phone ?? "", address_1: user.address_1 ?? "", address_2: user.address_2 ?? "", city: user.city ?? "", state: user.state ?? "", zip: user.zip ?? "", country: user.country ?? "", user_type: user.user_type ?? null, onboarded_landlord: user.onboarded_landlord ?? false, onboarded_tenant: user.onboarded_tenant ?? false, email_verified: user.email_verified ?? null, banking_setup_complete: user.banking_setup_complete ?? null, onboarding_step: user.onboarding_step ?? null, encrypted_data: user.encrypted_data ?? null, started_on: user.started_on ?? now, updated_on: user.updated_on ?? now, }; const updateValues = { name: insertValues.name, email: insertValues.email, phone: insertValues.phone, address_1: insertValues.address_1, address_2: insertValues.address_2, city: insertValues.city, state: insertValues.state, zip: insertValues.zip, country: insertValues.country, user_type: insertValues.user_type, onboarded_landlord: insertValues.onboarded_landlord, onboarded_tenant: insertValues.onboarded_tenant, email_verified: insertValues.email_verified, banking_setup_complete: insertValues.banking_setup_complete, onboarding_step: insertValues.onboarding_step, encrypted_data: insertValues.encrypted_data, // Keep original started_on, only bump updated_on updated_on: now, }; return await db.insert(users) .values(insertValues) .onConflictDoUpdate({ target: users.id, set: updateValues, }) .returning(); } // Update all user fields by user_id export async function sqlUpdateUser(id: string, fields: Partial) { return await db.update(users) .set(fields) .where(eq(users.id, id)) .returning(); } // Get user roles by user_id export async function sqlGetUserRoles(id: string) { return await db.select({ roleId: userRoles.roleId }).from(userRoles).where( eq(userRoles.userId, id), ); } export async function sqlGetAllUserRoles() { return await db.select().from(userRoles); } // Delete user role by user_id export async function sqlDeleteUserRole(id: string) { return await db.delete(userRoles) .where(eq(userRoles.userId, id)) .returning(); } // Update user role by user_id export async function sqlUpdateUserRole(id: string, role_id: string) { return await db.update(userRoles) .set({ roleId: role_id }) .where(eq(userRoles.userId, id)) .returning(); } // Get all users with their roles for admin center export async function sqlGetAllUsersWithRoles() { const results = await db .select({ id: users.id, name: users.name || "", email: users.email || "", role: roles.id || "", roleDescription: roles.description || "", }) .from(users) .leftJoin(userRoles, eq(users.id, userRoles.userId)) .leftJoin(roles, eq(userRoles.roleId, roles.id)); // Group by user const userMap: Record = {}; for (const row of results) { if (!userMap[row.id]) { userMap[row.id] = { id: row.id, name: row.name || "", email: row.email || "", role: row.role || "", roleDescription: row.roleDescription || "", }; } } return Object.values(userMap); } // Get all available roles export async function sqlGetAllRoles() { return await db.select().from(roles); } export async function sqlGetUserWithRoles(userId: string) { const user = await sqlGetUser(userId); if (!user) { return null; } const userRoles = await sqlGetUserRoles(userId); return { id: user?.id, name: user?.name, email: user?.email, phone: user?.phone, address_1: user?.address_1, address_2: user?.address_2, city: user?.city, state: user?.state, zip: user?.zip, country: user?.country, roles: userRoles, }; } // ============================================================================ // SQL STATE KEY OPERATIONS // ============================================================================ export async function sqlGetAllStateKeys() { return await db.select().from(stateKeys); } export async function sqlGetStateKeys(stateId: string) { return await db.select().from(stateKeys).where( eq(stateKeys.state_id, stateId), ); } export async function sqlSetStateKey( sealKey: string, stateId: string, cookieHash: string, ) { const now = Date.now(); return await db.insert(stateKeys) .values({ seal_key: sealKey, state_id: stateId, cookie_hash: cookieHash, created_on: now, }) .onConflictDoUpdate({ target: stateKeys.seal_key, set: { state_id: stateId, cookie_hash: cookieHash, created_on: now, }, }) .returning(); } export async function sqlDeleteStateKey(sealKey: string) { return await db.delete(stateKeys).where(eq(stateKeys.seal_key, sealKey)); } // ============================================================================ // SQL USER TOKENS OPERATIONS // ============================================================================ export async function sqlGetUserTokens(userId: string) { const result = await db.select().from(userTokens).where( eq(userTokens.user_id, userId), ); return result[0] || null; } export async function sqlSetUserTokens( userId: string, stateId: string, accessToken: string, refreshToken: string, ) { const now = Date.now(); return await db.insert(userTokens) .values({ user_id: userId, state_id: stateId, access_token: accessToken, refresh_token: refreshToken, created_on: now, updated_on: now, }) .onConflictDoUpdate({ target: userTokens.user_id, set: { state_id: stateId, access_token: accessToken, refresh_token: refreshToken, updated_on: now, }, }) .returning(); } export async function sqlGetAllUserTokens() { return await db.select().from(userTokens); } export async function sqlDeleteUserTokens(userId: string) { return await db.delete(userTokens).where(eq(userTokens.user_id, userId)); } // ============================================================================ // SQL WEB TOKENS OPERATIONS // ============================================================================ export async function sqlCreateWebToken( tokenId: string, stateId: string, userId: string, tokenType: string, tokenValue: string, expiresOn: number, metadata?: unknown | null, ) { const now = Date.now(); return await db.insert(webTokens) .values({ token_id: tokenId, state_id: stateId, user_id: userId, token_type: tokenType, token_value: tokenValue, expires_on: expiresOn, created_on: now, updated_on: now, last_used_on: null, is_active: true, metadata: metadata ?? null, }) .returning(); } export async function sqlGetWebToken(tokenId: string) { const result = await db.select().from(webTokens).where( eq(webTokens.token_id, tokenId), ); return result[0] || null; } export async function sqlGetWebTokenByValue(tokenValue: string) { const result = await db.select().from(webTokens).where( eq(webTokens.token_value, tokenValue), ); return result[0] || null; } export async function sqlGetWebTokensByStateId(stateId: string) { return await db.select().from(webTokens).where( eq(webTokens.state_id, stateId), ); } export async function sqlGetWebTokensByUserId(userId: string) { return await db.select().from(webTokens).where(eq(webTokens.user_id, userId)); } export async function sqlUpdateWebToken( tokenId: string, fields: Partial, ) { return await db.update(webTokens) .set({ ...fields, updated_on: Date.now(), }) .where(eq(webTokens.token_id, tokenId)) .returning(); } export async function sqlDeleteWebToken(tokenId: string) { return await db.delete(webTokens).where(eq(webTokens.token_id, tokenId)); } export async function sqlDeleteExpiredWebTokens(now = Date.now()) { return await db.delete(webTokens).where( lte(webTokens.expires_on, now), ); } export async function sqlGetActiveWebTokens(now = Date.now()) { return await db.select().from(webTokens).where( and( eq(webTokens.is_active, true), gte(webTokens.expires_on, now), ), ); } export async function sqlGetUser(userId: string) { const result = await db.select().from(users).where(eq(users.id, userId)); return result[0] || null; } // Get user by ID (alias for compatibility) export async function sqlGetUserById(userId: string) { const result = await sqlGetUser(userId); return result || null; } export async function sqlCreateUser( id: string, name: string, email: string, phone: string, address_1: string, address_2: string, city: string, state: string, zip: string, country: string, ) { const now = Date.now(); return await db.insert(users) .values({ id, name, email, phone, address_1, address_2, city, state, zip, country, started_on: now, updated_on: now, }) .returning(); } // ============================================================================ // SQL FINANCIAL BALANCE OPERATIONS // ============================================================================ // Upsert financial balance for a user export async function sqlUpsertFinancialBalance( user_id: string, balance: number, balance_date: number, ) { const now = Date.now(); // Check if record exists const existing = await db.select().from(financial_balances).where( eq(financial_balances.user_id, user_id), ); if (existing.length > 0) { // Update existing record const newBalance: Partial = { balance, balance_date, updated_on: now, }; return await db.update(financial_balances) .set(newBalance) .where(eq(financial_balances.id, existing[0].id)) .returning(); } else { // Insert new record return await db.insert(financial_balances) .values({ id: crypto.randomUUID(), user_id, balance, balance_date, created_on: now, updated_on: now, }) .returning(); } } export async function sqlDeleteFinancialBalance(id: string) { return await db.delete(financial_balances).where( eq(financial_balances.id, id), ); } export async function sqlGetAllFinancialBalances() { return await db.select().from(financial_balances); } export async function sqlGetFinancialBalancesByUser(user_id: string) { return await db.select().from(financial_balances).where( eq(financial_balances.user_id, user_id), ); } // ============================================================================ // SQL USER PINS OPERATIONS // ============================================================================ export async function sqlGetUserPin(userId: string) { const result = await db.select().from(user_pins).where( eq(user_pins.user_id, userId), ); return result[0] || null; } export async function sqlSetUserPin( userId: string, pin: string, ) { const now = Date.now(); return await db.insert(user_pins) .values({ user_id: userId, pin, created_on: now, modified_on: now, }) .onConflictDoUpdate({ target: user_pins.user_id, set: { pin, modified_on: now, }, }) .returning(); } export async function sqlUpdateUserPin( userId: string, pin: string, ) { const now = Date.now(); return await db.update(user_pins) .set({ pin, modified_on: now, }) .where(eq(user_pins.user_id, userId)) .returning(); } export async function sqlGetAllUserPins() { return await db.select().from(user_pins); } export async function sqlDeleteUserPin(userId: string) { return await db.delete(user_pins).where(eq(user_pins.user_id, userId)); } // ============================================================================ // SQL SESSION CONFIG OPERATIONS // ============================================================================ export async function sqlGetSessionConfig(deviceId: string) { const result = await db.select().from(session_configs).where( eq(session_configs.device_id, deviceId), ); return result[0] || null; } export async function sqlSetSessionConfig( deviceId: string, userId: string, otherData: string, activeRoute: string, pageData: string, activeQuizIndex: number, activeQuestionIndex: number, quizConfigs?: Array>, ) { const now = Date.now(); return await db.insert(session_configs) .values({ device_id: deviceId, user_id: userId, other_data: otherData, active_date: now, quiz_configs: quizConfigs || [], active_route: activeRoute, page_data: pageData, active_quiz_index: activeQuizIndex, active_question_index: activeQuestionIndex, }) .onConflictDoUpdate({ target: session_configs.device_id, set: { user_id: userId, other_data: otherData, active_date: now, quiz_configs: quizConfigs || [], active_route: activeRoute, page_data: pageData, active_quiz_index: activeQuizIndex, active_question_index: activeQuestionIndex, }, }) .returning(); } export async function sqlUpdateSessionConfig( deviceId: string, fields: Partial, ) { return await db.update(session_configs) .set(fields) .where(eq(session_configs.device_id, deviceId)) .returning(); } export async function sqlDeleteSessionConfig(deviceId: string) { return await db.delete(session_configs).where( eq(session_configs.device_id, deviceId), ); } export async function sqlGetAllSessionConfigs() { return await db.select().from(session_configs); } export async function sqlGetSessionConfigsByUser(userId: string) { return await db.select().from(session_configs).where( eq(session_configs.user_id, userId), ); } // ============================================================================ // SQL QUIZ CONFIG OPERATIONS // ============================================================================ export async function sqlGetQuizConfig(quizConfigId: string) { const result = await db.select().from(quiz_configs).where( eq(quiz_configs.quiz_config_id, quizConfigId), ); return result[0] || null; } export async function sqlCreateQuizConfig( quizConfigId: string, quizName: string, ownerId: string, quizMode: string, questions?: QuizQuestion[], results?: QuizResult[], tabName?: string, tabIndex?: number, listIndex?: number, ) { const now = Date.now(); return await db.insert(quiz_configs) .values({ quiz_config_id: quizConfigId, quiz_name: quizName, owner_id: ownerId, quiz_mode: quizMode, active_date: now, tab_name: tabName, tab_index: tabIndex ?? 0, list_index: listIndex ?? 0, questions: questions || [], results: results || [], }) .returning(); } export async function sqlUpdateQuizConfig( quizConfigId: string, fields: Partial, ) { return await db.update(quiz_configs) .set(fields) .where(eq(quiz_configs.quiz_config_id, quizConfigId)) .returning(); } export async function sqlDeleteQuizConfig(quizConfigId: string) { return await db.delete(quiz_configs).where( eq(quiz_configs.quiz_config_id, quizConfigId), ); } export async function sqlGetAllQuizConfigs() { return await db.select().from(quiz_configs); } export async function sqlGetQuizConfigsByOwner(ownerId: string) { return await db.select().from(quiz_configs).where( eq(quiz_configs.owner_id, ownerId), ); } // ============================================================================ // SQL QUIZ QUESTIONS OPERATIONS // ============================================================================ export async function sqlGetQuizQuestion(questionId: string) { const result = await db.select().from(quiz_questions).where( eq(quiz_questions.id, questionId), ); return result[0] || null; } export async function sqlGetQuizQuestionsByQuiz(quizId: string) { return await db.select().from(quiz_questions).where( eq(quiz_questions.quiz_id, quizId), ).orderBy(quiz_questions.order_num); } export async function sqlCreateQuizQuestion( id: string, quizId: string, ownerId: string, language1: string, language2: string, text1: string, audioFile1: string, text2: string, audioFile2: string, orderNum: number, backgroundFile?: string, backgroundImageData?: string, videoFile?: string, videoFileData?: string, ) { return await db.insert(quiz_questions) .values({ id, quiz_id: quizId, owner_id: ownerId, language_1: language1, language_2: language2, text_1: text1, audio_file_1: audioFile1, text_2: text2, audio_file_2: audioFile2, order_num: orderNum, background_file: backgroundFile || "", background_image_data: backgroundImageData || "", video_file: videoFile || "", video_file_data: videoFileData || "", }) .returning(); } export async function sqlUpdateQuizQuestion( questionId: string, fields: Partial, ) { return await db.update(quiz_questions) .set(fields) .where(eq(quiz_questions.id, questionId)) .returning(); } export async function sqlDeleteQuizQuestion(questionId: string) { return await db.delete(quiz_questions).where( eq(quiz_questions.id, questionId), ); } export async function sqlGetAllQuizQuestions() { return await db.select().from(quiz_questions); } export async function sqlDeleteQuizQuestionsByQuiz(quizId: string) { return await db.delete(quiz_questions).where( eq(quiz_questions.quiz_id, quizId), ); } // ============================================================================ // SQL QUIZ RESULTS OPERATIONS // ============================================================================ export async function sqlGetQuizResult(resultId: string) { const result = await db.select().from(quiz_results).where( eq(quiz_results.id, resultId), ); return result[0] || null; } export async function sqlGetQuizResultsByQuiz(quizConfigId: string) { return await db.select().from(quiz_results).where( eq(quiz_results.quiz_config_id, quizConfigId), ); } export async function sqlGetQuizResultsByUser(userId: string) { return await db.select().from(quiz_results).where( eq(quiz_results.user_id, userId), ); } export async function sqlCreateQuizResult( id: string, quizConfigId: string, userId: string, success: boolean, totalTime: number, helpCount: number, ) { const now = Date.now(); return await db.insert(quiz_results) .values({ id, quiz_config_id: quizConfigId, user_id: userId, success, total_time: totalTime, help_count: helpCount, completed_on: now, }) .returning(); } export async function sqlUpdateQuizResult( resultId: string, fields: Partial, ) { return await db.update(quiz_results) .set(fields) .where(eq(quiz_results.id, resultId)) .returning(); } export async function sqlDeleteQuizResult(resultId: string) { return await db.delete(quiz_results).where( eq(quiz_results.id, resultId), ); } export async function sqlGetAllQuizResults() { return await db.select().from(quiz_results); } export async function sqlDeleteQuizResultsByQuiz(quizConfigId: string) { return await db.delete(quiz_results).where( eq(quiz_results.quiz_config_id, quizConfigId), ); } // ============================================ // Brain Question Progress (Spaced Repetition with Mode Progression) // ============================================ export interface BrainProgress { id: string; user_id: string; quiz_id: string | null; question_id: string; current_mode: string; mode_index: number | null; attempts: number | null; first_try_successes: number | null; consecutive_first_try: number | null; last_result: boolean | null; next_due_at: number | null; interval_days: number | null; ease_factor: number | null; is_review_only: boolean | null; created_on: number | null; updated_on: number | null; } export async function sqlGetBrainProgress( userId: string, questionId: string, ): Promise { // ID is now mode-agnostic: `${userId}_${questionId}` const id = `${userId}_${questionId}`; const result = await db.select().from(brain_question_progress).where( eq(brain_question_progress.id, id), ); return result[0]; } export async function sqlGetBrainProgressById( id: string, ): Promise { const result = await db.select().from(brain_question_progress).where( eq(brain_question_progress.id, id), ); return result[0]; } export async function sqlGetBrainProgressByUser( userId: string, ): Promise { return await db.select().from(brain_question_progress).where( eq(brain_question_progress.user_id, userId), ); } export async function sqlGetBrainProgressDue( userId: string, beforeTimestamp: number, ): Promise { return await db.select().from(brain_question_progress).where( and( eq(brain_question_progress.user_id, userId), lte(brain_question_progress.next_due_at, beforeTimestamp), ), ); } export async function sqlGetAllBrainProgress(): Promise { return await db.select().from(brain_question_progress); } export async function sqlUpsertBrainProgress( progress: BrainProgress, ): Promise { const now = Date.now(); const existing = await sqlGetBrainProgressById(progress.id); if (existing) { return await db.update(brain_question_progress) .set({ quiz_id: progress.quiz_id, current_mode: progress.current_mode, mode_index: progress.mode_index ?? 0, attempts: progress.attempts, first_try_successes: progress.first_try_successes, consecutive_first_try: progress.consecutive_first_try ?? 0, last_result: progress.last_result, next_due_at: progress.next_due_at, interval_days: progress.interval_days, ease_factor: progress.ease_factor, is_review_only: progress.is_review_only ?? false, updated_on: now, }) .where(eq(brain_question_progress.id, progress.id)) .returning(); } else { return await db.insert(brain_question_progress) .values({ id: progress.id, user_id: progress.user_id, quiz_id: progress.quiz_id, question_id: progress.question_id, current_mode: progress.current_mode, mode_index: progress.mode_index ?? 0, attempts: progress.attempts ?? 0, first_try_successes: progress.first_try_successes ?? 0, consecutive_first_try: progress.consecutive_first_try ?? 0, last_result: progress.last_result, next_due_at: progress.next_due_at, interval_days: progress.interval_days ?? 1, ease_factor: progress.ease_factor ?? 2.5, is_review_only: progress.is_review_only ?? false, created_on: now, updated_on: now, }) .returning(); } } export async function sqlDeleteBrainProgress(id: string) { return await db.delete(brain_question_progress).where( eq(brain_question_progress.id, id), ); } export async function sqlDeleteBrainProgressByUser(userId: string) { return await db.delete(brain_question_progress).where( eq(brain_question_progress.user_id, userId), ); } export async function sqlDeleteBrainProgressByQuiz(quizId: string) { return await db.delete(brain_question_progress).where( eq(brain_question_progress.quiz_id, quizId), ); }