// Native D1 database client for Cloudflare Workers // This bypasses Prisma which has compatibility issues with the Workers runtime export interface D1Result { results: T[]; success: boolean; meta: { duration: number; }; } /** * Get the D1 database binding from Cloudflare context */ export function getD1(): D1Database | null { try { // eslint-disable-next-line @typescript-eslint/no-require-imports const { getCloudflareContext } = require('@opennextjs/cloudflare'); const ctx = getCloudflareContext(); return ctx?.env?.DB || null; } catch (error) { console.error('[getD1] Failed to get Cloudflare context:', error); return null; } } // ============ USER PREFERENCES ============ export interface UserPreferencesRow { id: string; userId: string; substance: string; trackingStartDate: string | null; hasCompletedSetup: number; // SQLite boolean dailyGoal: number | null; userName: string | null; userAge: number | null; religion: string | null; lastNicotineUsageTime: string | null; lastWeedUsageTime: string | null; quitPlanJson: string | null; createdAt: string; updatedAt: string; } export async function getPreferencesD1(userId: string): Promise { const db = getD1(); if (!db) return null; const result = await db.prepare( 'SELECT * FROM UserPreferences WHERE userId = ?' ).bind(userId).first(); return result; } export async function upsertPreferencesD1(userId: string, data: Partial): Promise { const db = getD1(); if (!db) return null; const existing = await getPreferencesD1(userId); const now = new Date().toISOString(); const id = existing?.id || crypto.randomUUID(); if (existing) { // Update const updates: string[] = []; const values: unknown[] = []; if (data.substance !== undefined) { updates.push('substance = ?'); values.push(data.substance); } if (data.trackingStartDate !== undefined) { updates.push('trackingStartDate = ?'); values.push(data.trackingStartDate); } if (data.hasCompletedSetup !== undefined) { updates.push('hasCompletedSetup = ?'); values.push(data.hasCompletedSetup ? 1 : 0); } if (data.dailyGoal !== undefined) { updates.push('dailyGoal = ?'); values.push(data.dailyGoal); } if (data.userName !== undefined) { updates.push('userName = ?'); values.push(data.userName); } if (data.userAge !== undefined) { updates.push('userAge = ?'); values.push(data.userAge); } if (data.religion !== undefined) { updates.push('religion = ?'); values.push(data.religion); } if (data.lastNicotineUsageTime !== undefined) { updates.push('lastNicotineUsageTime = ?'); values.push(data.lastNicotineUsageTime); } if (data.lastWeedUsageTime !== undefined) { updates.push('lastWeedUsageTime = ?'); values.push(data.lastWeedUsageTime); } if (data.quitPlanJson !== undefined) { updates.push('quitPlanJson = ?'); values.push(data.quitPlanJson); } updates.push('updatedAt = ?'); values.push(now); values.push(userId); await db.prepare( `UPDATE UserPreferences SET ${updates.join(', ')} WHERE userId = ?` ).bind(...values).run(); } else { // Insert await db.prepare( `INSERT INTO UserPreferences (id, userId, substance, trackingStartDate, hasCompletedSetup, dailyGoal, userName, userAge, religion, lastNicotineUsageTime, lastWeedUsageTime, quitPlanJson, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).bind( id, userId, data.substance || 'nicotine', data.trackingStartDate || null, data.hasCompletedSetup ? 1 : 0, data.dailyGoal || null, data.userName || null, data.userAge || null, data.religion || null, data.lastNicotineUsageTime || null, data.lastWeedUsageTime || null, data.quitPlanJson || null, now, now ).run(); } return getPreferencesD1(userId); } // ============ USAGE ENTRIES ============ export interface UsageEntryRow { id: string; userId: string; date: string; count: number; substance: string; createdAt: string; updatedAt: string; } export async function getUsageEntriesD1(userId: string): Promise { const db = getD1(); if (!db) return []; const result = await db.prepare( 'SELECT * FROM UsageEntry WHERE userId = ? ORDER BY date DESC' ).bind(userId).all(); return result.results || []; } export async function getUsageEntryD1(userId: string, date: string, substance: string): Promise { const db = getD1(); if (!db) return null; const result = await db.prepare( 'SELECT * FROM UsageEntry WHERE userId = ? AND date = ? AND substance = ?' ).bind(userId, date, substance).first(); return result; } export async function upsertUsageEntryD1(userId: string, date: string, count: number, substance: string, addToExisting = false): Promise { const db = getD1(); if (!db) return null; const existing = await getUsageEntryD1(userId, date, substance); const now = new Date().toISOString(); const id = existing?.id || crypto.randomUUID(); if (existing) { const newCount = addToExisting ? existing.count + count : count; await db.prepare( 'UPDATE UsageEntry SET count = ?, updatedAt = ? WHERE id = ?' ).bind(newCount, now, existing.id).run(); } else { await db.prepare( `INSERT INTO UsageEntry (id, userId, date, count, substance, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?)` ).bind(id, userId, date, count, substance, now, now).run(); } return getUsageEntryD1(userId, date, substance); } export async function deleteUsageEntryD1(userId: string, date: string, substance: string): Promise { const db = getD1(); if (!db) return; await db.prepare( 'DELETE FROM UsageEntry WHERE userId = ? AND date = ? AND substance = ?' ).bind(userId, date, substance).run(); } // ============ ACHIEVEMENTS ============ export interface AchievementRow { id: string; userId: string; badgeId: string; unlockedAt: string; substance: string; } export async function getAchievementsD1(userId: string): Promise { const db = getD1(); if (!db) return []; const result = await db.prepare( 'SELECT * FROM Achievement WHERE userId = ? ORDER BY unlockedAt DESC' ).bind(userId).all(); return result.results || []; } export async function getAchievementD1(userId: string, badgeId: string, substance: string): Promise { const db = getD1(); if (!db) return null; const result = await db.prepare( 'SELECT * FROM Achievement WHERE userId = ? AND badgeId = ? AND substance = ?' ).bind(userId, badgeId, substance).first(); return result; } export async function createAchievementD1(userId: string, badgeId: string, substance: string): Promise { const db = getD1(); if (!db) return null; const existing = await getAchievementD1(userId, badgeId, substance); if (existing) return existing; const id = crypto.randomUUID(); const now = new Date().toISOString(); await db.prepare( `INSERT INTO Achievement (id, userId, badgeId, unlockedAt, substance) VALUES (?, ?, ?, ?, ?)` ).bind(id, userId, badgeId, now, substance).run(); return getAchievementD1(userId, badgeId, substance); } // ============ REMINDER SETTINGS ============ export interface ReminderSettingsRow { id: string; userId: string; enabled: number; // SQLite boolean reminderTime: string; frequency: string; hourlyStart: string | null; hourlyEnd: string | null; timezone: string; lastNotifiedDate: string | null; createdAt: string; updatedAt: string; } export async function getReminderSettingsD1(userId: string): Promise { const db = getD1(); if (!db) return null; const result = await db.prepare( 'SELECT * FROM ReminderSettings WHERE userId = ?' ).bind(userId).first(); return result; } export async function upsertReminderSettingsD1( userId: string, enabled: boolean, reminderTime: string, frequency: string = 'daily', hourlyStart: string = '09:00', hourlyEnd: string = '21:00', timezone: string = 'UTC' ): Promise { const db = getD1(); if (!db) return null; const existing = await getReminderSettingsD1(userId); const now = new Date().toISOString(); const id = existing?.id || crypto.randomUUID(); if (existing) { await db.prepare( 'UPDATE ReminderSettings SET enabled = ?, reminderTime = ?, frequency = ?, hourlyStart = ?, hourlyEnd = ?, timezone = ?, updatedAt = ? WHERE userId = ?' ).bind(enabled ? 1 : 0, reminderTime, frequency, hourlyStart, hourlyEnd, timezone, now, userId).run(); } else { await db.prepare( `INSERT INTO ReminderSettings (id, userId, enabled, reminderTime, frequency, hourlyStart, hourlyEnd, timezone, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).bind(id, userId, enabled ? 1 : 0, reminderTime, frequency, hourlyStart, hourlyEnd, timezone, now, now).run(); } return getReminderSettingsD1(userId); } export interface ReminderUserRow { userId: string; reminderTime: string; frequency: string; hourlyStart: string | null; hourlyEnd: string | null; timezone: string; lastNotifiedDate: string | null; endpoint: string; p256dh: string; auth: string; } export async function getUsersForRemindersD1(): Promise { const db = getD1(); if (!db) return []; const result = await db.prepare( `SELECT r.userId, r.reminderTime, r.frequency, r.hourlyStart, r.hourlyEnd, r.timezone, r.lastNotifiedDate, p.endpoint, p.p256dh, p.auth FROM ReminderSettings r JOIN PushSubscriptions p ON r.userId = p.userId WHERE r.enabled = 1` ).all(); return result.results || []; } export async function updateLastNotifiedD1(userId: string, dateStr: string): Promise { const db = getD1(); if (!db) return; await db.prepare( 'UPDATE ReminderSettings SET lastNotifiedDate = ?, updatedAt = ? WHERE userId = ?' ).bind(dateStr, new Date().toISOString(), userId).run(); } // ============ SAVINGS CONFIG ============ export interface SavingsConfigRow { id: string; userId: string; costPerUnit: number; unitsPerDay: number; savingsGoal: number | null; goalName: string | null; currency: string; substance: string; createdAt: string; updatedAt: string; } export async function getSavingsConfigD1(userId: string): Promise { const db = getD1(); if (!db) return null; const result = await db.prepare( 'SELECT * FROM SavingsConfig WHERE userId = ?' ).bind(userId).first(); return result; } export async function upsertSavingsConfigD1( userId: string, costPerUnit: number, unitsPerDay: number, substance: string, savingsGoal?: number | null, goalName?: string | null, currency?: string ): Promise { const db = getD1(); if (!db) return null; const existing = await getSavingsConfigD1(userId); const now = new Date().toISOString(); const id = existing?.id || crypto.randomUUID(); if (existing) { await db.prepare( 'UPDATE SavingsConfig SET costPerUnit = ?, unitsPerDay = ?, savingsGoal = ?, goalName = ?, currency = ?, substance = ?, updatedAt = ? WHERE userId = ?' ).bind(costPerUnit, unitsPerDay, savingsGoal ?? null, goalName ?? null, currency || 'USD', substance, now, userId).run(); } else { await db.prepare( `INSERT INTO SavingsConfig (id, userId, costPerUnit, unitsPerDay, savingsGoal, goalName, currency, substance, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).bind(id, userId, costPerUnit, unitsPerDay, savingsGoal ?? null, goalName ?? null, currency || 'USD', substance, now, now).run(); } return getSavingsConfigD1(userId); } // ============ PUSH SUBSCRIPTIONS ============ export interface PushSubscriptionRow { id: string; userId: string; endpoint: string; p256dh: string; auth: string; createdAt: string; updatedAt: string; } export async function getPushSubscriptionD1(userId: string): Promise { const db = getD1(); if (!db) return null; const result = await db.prepare( 'SELECT * FROM PushSubscriptions WHERE userId = ?' ).bind(userId).first(); return result; } export async function upsertPushSubscriptionD1( userId: string, endpoint: string, p256dh: string, auth: string ): Promise { const db = getD1(); if (!db) return null; const existing = await getPushSubscriptionD1(userId); const now = new Date().toISOString(); const id = existing?.id || crypto.randomUUID(); if (existing) { await db.prepare( 'UPDATE PushSubscriptions SET endpoint = ?, p256dh = ?, auth = ?, updatedAt = ? WHERE userId = ?' ).bind(endpoint, p256dh, auth, now, userId).run(); } else { await db.prepare( `INSERT INTO PushSubscriptions (id, userId, endpoint, p256dh, auth, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?)` ).bind(id, userId, endpoint, p256dh, auth, now, now).run(); } return getPushSubscriptionD1(userId); }