1035 lines
27 KiB
TypeScript
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),
|
|
);
|
|
}
|