/** * Database Service * Centralized Appwrite database operations */ import { Client, Databases, Query, ID } from 'node-appwrite' import { config } from '../config/index.mjs' import { NotFoundError } from '../middleware/errorHandler.mjs' // Initialize Appwrite client const client = new Client() .setEndpoint(config.appwrite.endpoint) .setProject(config.appwrite.projectId) .setKey(config.appwrite.apiKey) const databases = new Databases(client) const DB_ID = config.appwrite.databaseId /** * Collection names */ export const Collections = { PRODUCTS: 'products', QUESTIONS: 'questions', SUBMISSIONS: 'submissions', ANSWERS: 'answers', ORDERS: 'orders', EMAIL_ACCOUNTS: 'email_accounts', EMAIL_STATS: 'email_stats', EMAIL_DIGESTS: 'email_digests', SUBSCRIPTIONS: 'subscriptions', USER_PREFERENCES: 'user_preferences', } /** * Generic database operations */ export const db = { /** * Create a document */ async create(collection, data, id = ID.unique()) { return await databases.createDocument(DB_ID, collection, id, data) }, /** * Get a document by ID */ async get(collection, id) { try { return await databases.getDocument(DB_ID, collection, id) } catch (error) { if (error.code === 404) { throw new NotFoundError(collection) } throw error } }, /** * Update a document */ async update(collection, id, data) { return await databases.updateDocument(DB_ID, collection, id, data) }, /** * Delete a document */ async delete(collection, id) { return await databases.deleteDocument(DB_ID, collection, id) }, /** * List documents with optional queries */ async list(collection, queries = []) { const response = await databases.listDocuments(DB_ID, collection, queries) return response.documents }, /** * Find one document matching queries */ async findOne(collection, queries) { const docs = await this.list(collection, [...queries, Query.limit(1)]) return docs[0] || null }, /** * Check if document exists */ async exists(collection, id) { try { await databases.getDocument(DB_ID, collection, id) return true } catch { return false } }, /** * Count documents */ async count(collection, queries = []) { const response = await databases.listDocuments(DB_ID, collection, [ ...queries, Query.limit(1), ]) return response.total }, } /** * Product operations */ export const products = { async getBySlug(slug) { return db.findOne(Collections.PRODUCTS, [ Query.equal('slug', slug), Query.equal('isActive', true), ]) }, async getActive() { return db.list(Collections.PRODUCTS, [Query.equal('isActive', true)]) }, } /** * Questions operations */ export const questions = { async getByProduct(productId) { return db.list(Collections.QUESTIONS, [ Query.equal('productId', productId), Query.equal('isActive', true), Query.orderAsc('step'), Query.orderAsc('order'), ]) }, } /** * Submissions operations */ export const submissions = { async create(data) { return db.create(Collections.SUBMISSIONS, data) }, async updateStatus(id, status) { return db.update(Collections.SUBMISSIONS, id, { status }) }, async getByUser(userId) { return db.list(Collections.SUBMISSIONS, [Query.equal('userId', userId)]) }, } /** * Email accounts operations */ export const emailAccounts = { async create(data) { return db.create(Collections.EMAIL_ACCOUNTS, data) }, async getByUser(userId) { return db.list(Collections.EMAIL_ACCOUNTS, [ Query.equal('userId', userId), Query.equal('isActive', true), ]) }, async get(id) { return db.get(Collections.EMAIL_ACCOUNTS, id) }, async updateLastSync(id) { return db.update(Collections.EMAIL_ACCOUNTS, id, { lastSync: new Date().toISOString(), }) }, async deactivate(id) { return db.update(Collections.EMAIL_ACCOUNTS, id, { isActive: false }) }, } /** * Email stats operations */ export const emailStats = { async getByUser(userId) { return db.findOne(Collections.EMAIL_STATS, [Query.equal('userId', userId)]) }, async create(userId, data) { return db.create(Collections.EMAIL_STATS, { userId, ...data }) }, async increment(userId, counts) { const stats = await this.getByUser(userId) if (stats) { return db.update(Collections.EMAIL_STATS, stats.$id, { totalSorted: (stats.totalSorted || 0) + (counts.total || 0), todaySorted: (stats.todaySorted || 0) + (counts.today || 0), weekSorted: (stats.weekSorted || 0) + (counts.week || 0), timeSavedMinutes: (stats.timeSavedMinutes || 0) + (counts.timeSaved || 0), }) } else { return this.create(userId, { totalSorted: counts.total || 0, todaySorted: counts.today || 0, weekSorted: counts.week || 0, timeSavedMinutes: counts.timeSaved || 0, categoriesJson: '{}', }) } }, async updateCategories(userId, categories) { const stats = await this.getByUser(userId) if (stats) { return db.update(Collections.EMAIL_STATS, stats.$id, { categoriesJson: JSON.stringify(categories), }) } return null }, async resetDaily() { // Reset daily counters - would be called by a cron job const allStats = await db.list(Collections.EMAIL_STATS, []) for (const stat of allStats) { await db.update(Collections.EMAIL_STATS, stat.$id, { todaySorted: 0 }) } }, async resetWeekly() { // Reset weekly counters - would be called by a cron job const allStats = await db.list(Collections.EMAIL_STATS, []) for (const stat of allStats) { await db.update(Collections.EMAIL_STATS, stat.$id, { weekSorted: 0, categoriesJson: '{}', }) } }, } /** * Subscriptions operations */ export const subscriptions = { async getByUser(userId) { return db.findOne(Collections.SUBSCRIPTIONS, [Query.equal('userId', userId)]) }, async getByStripeId(stripeSubscriptionId) { return db.findOne(Collections.SUBSCRIPTIONS, [ Query.equal('stripeSubscriptionId', stripeSubscriptionId), ]) }, async create(data) { return db.create(Collections.SUBSCRIPTIONS, data) }, async update(id, data) { return db.update(Collections.SUBSCRIPTIONS, id, data) }, async upsertByUser(userId, data) { const existing = await this.getByUser(userId) if (existing) { return this.update(existing.$id, data) } return this.create({ userId, ...data }) }, } /** * User preferences operations */ export const userPreferences = { async getByUser(userId) { const pref = await db.findOne(Collections.USER_PREFERENCES, [ Query.equal('userId', userId), ]) if (pref?.preferencesJson) { return { ...pref, preferences: JSON.parse(pref.preferencesJson) } } return pref }, async upsert(userId, preferences) { const existing = await db.findOne(Collections.USER_PREFERENCES, [ Query.equal('userId', userId), ]) const data = { preferencesJson: JSON.stringify(preferences) } if (existing) { return db.update(Collections.USER_PREFERENCES, existing.$id, data) } return db.create(Collections.USER_PREFERENCES, { userId, ...data }) }, } /** * Orders operations */ export const orders = { async create(submissionId, orderData) { return db.create(Collections.ORDERS, { submissionId, orderDataJson: JSON.stringify(orderData), }) }, } /** * Email digests operations */ export const emailDigests = { async create(userId, digestData) { const date = new Date().toISOString().split('T')[0] // YYYY-MM-DD return db.create(Collections.EMAIL_DIGESTS, { userId, date, statsJson: JSON.stringify(digestData.stats || {}), highlightsJson: JSON.stringify(digestData.highlights || []), suggestionsJson: JSON.stringify(digestData.suggestions || []), totalSorted: digestData.totalSorted || 0, inboxCleared: digestData.inboxCleared || 0, timeSavedMinutes: digestData.timeSavedMinutes || 0, createdAt: new Date().toISOString(), }) }, async getByUserToday(userId) { const today = new Date().toISOString().split('T')[0] const digest = await db.findOne(Collections.EMAIL_DIGESTS, [ Query.equal('userId', userId), Query.equal('date', today), ]) if (digest) { return { ...digest, stats: JSON.parse(digest.statsJson || '{}'), highlights: JSON.parse(digest.highlightsJson || '[]'), suggestions: JSON.parse(digest.suggestionsJson || '[]'), } } return null }, async getByUserRecent(userId, days = 7) { const startDate = new Date() startDate.setDate(startDate.getDate() - days) const digests = await db.list(Collections.EMAIL_DIGESTS, [ Query.equal('userId', userId), Query.greaterThanEqual('date', startDate.toISOString().split('T')[0]), Query.orderDesc('date'), ]) return digests.map(d => ({ ...d, stats: JSON.parse(d.statsJson || '{}'), highlights: JSON.parse(d.highlightsJson || '[]'), suggestions: JSON.parse(d.suggestionsJson || '[]'), })) }, async updateToday(userId, updates) { const today = new Date().toISOString().split('T')[0] const existing = await db.findOne(Collections.EMAIL_DIGESTS, [ Query.equal('userId', userId), Query.equal('date', today), ]) if (existing) { const updateData = {} if (updates.stats) updateData.statsJson = JSON.stringify(updates.stats) if (updates.highlights) updateData.highlightsJson = JSON.stringify(updates.highlights) if (updates.suggestions) updateData.suggestionsJson = JSON.stringify(updates.suggestions) if (updates.totalSorted !== undefined) { updateData.totalSorted = (existing.totalSorted || 0) + updates.totalSorted } if (updates.inboxCleared !== undefined) { updateData.inboxCleared = (existing.inboxCleared || 0) + updates.inboxCleared } if (updates.timeSavedMinutes !== undefined) { updateData.timeSavedMinutes = (existing.timeSavedMinutes || 0) + updates.timeSavedMinutes } return db.update(Collections.EMAIL_DIGESTS, existing.$id, updateData) } // Create new digest for today return this.create(userId, { stats: updates.stats || {}, highlights: updates.highlights || [], suggestions: updates.suggestions || [], totalSorted: updates.totalSorted || 0, inboxCleared: updates.inboxCleared || 0, timeSavedMinutes: updates.timeSavedMinutes || 0, }) }, } export default { db, products, questions, submissions, emailAccounts, emailStats, emailDigests, subscriptions, userPreferences, orders, Collections, }