integratedhub/db/sqlUserRepository.ts
2026-01-20 09:02:26 -06:00

1035 lines
27 KiB
TypeScript

// 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<User>,
) {
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<User>) {
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<string, UserWithRole> = {};
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<typeof webTokens.$inferInsert>,
) {
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<FinancialBalance> = {
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<Array<string>>,
) {
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<typeof session_configs.$inferInsert>,
) {
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<typeof quiz_configs.$inferInsert>,
) {
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<typeof quiz_questions.$inferInsert>,
) {
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<typeof quiz_results.$inferInsert>,
) {
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<BrainProgress | undefined> {
// 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<BrainProgress | undefined> {
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<BrainProgress[]> {
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<BrainProgress[]> {
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<BrainProgress[]> {
return await db.select().from(brain_question_progress);
}
export async function sqlUpsertBrainProgress(
progress: BrainProgress,
): Promise<BrainProgress[]> {
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),
);
}