731 lines
19 KiB
JavaScript
731 lines
19 KiB
JavaScript
/**
|
|
* 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',
|
|
ONBOARDING_STATE: 'onboarding_state',
|
|
EMAIL_USAGE: 'email_usage',
|
|
REFERRALS: 'referrals',
|
|
ANALYTICS_EVENTS: 'analytics_events',
|
|
}
|
|
|
|
/**
|
|
* 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: '{}',
|
|
})
|
|
}
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Email usage operations
|
|
*/
|
|
export const emailUsage = {
|
|
async getCurrentMonth(userId) {
|
|
const month = new Date().toISOString().slice(0, 7) // "2026-01"
|
|
return db.findOne(Collections.EMAIL_USAGE, [
|
|
Query.equal('userId', userId),
|
|
Query.equal('month', month),
|
|
])
|
|
},
|
|
|
|
async increment(userId, count) {
|
|
const month = new Date().toISOString().slice(0, 7)
|
|
const existing = await this.getCurrentMonth(userId)
|
|
|
|
if (existing) {
|
|
return db.update(Collections.EMAIL_USAGE, existing.$id, {
|
|
emailsProcessed: (existing.emailsProcessed || 0) + count,
|
|
lastReset: new Date().toISOString(),
|
|
})
|
|
}
|
|
|
|
return db.create(Collections.EMAIL_USAGE, {
|
|
userId,
|
|
month,
|
|
emailsProcessed: count,
|
|
lastReset: new Date().toISOString(),
|
|
})
|
|
},
|
|
|
|
async getUsage(userId) {
|
|
const usage = await this.getCurrentMonth(userId)
|
|
return {
|
|
emailsProcessed: usage?.emailsProcessed || 0,
|
|
month: new Date().toISOString().slice(0, 7),
|
|
}
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Subscriptions operations
|
|
*/
|
|
export const subscriptions = {
|
|
async getByUser(userId) {
|
|
const subscription = await db.findOne(Collections.SUBSCRIPTIONS, [Query.equal('userId', userId)])
|
|
|
|
// If no subscription, user is on free tier
|
|
if (!subscription) {
|
|
const usage = await emailUsage.getUsage(userId)
|
|
return {
|
|
plan: 'free',
|
|
status: 'active',
|
|
isFreeTier: true,
|
|
emailsUsedThisMonth: usage.emailsProcessed,
|
|
emailsLimit: 500, // From config
|
|
}
|
|
}
|
|
|
|
// Check if subscription is active
|
|
const isActive = subscription.status === 'active'
|
|
const isFreeTier = !isActive || subscription.plan === 'free'
|
|
|
|
// Get usage for free tier users
|
|
let emailsUsedThisMonth = 0
|
|
let emailsLimit = -1 // Unlimited for paid
|
|
|
|
if (isFreeTier) {
|
|
const usage = await emailUsage.getUsage(userId)
|
|
emailsUsedThisMonth = usage.emailsProcessed
|
|
emailsLimit = 500 // From config
|
|
}
|
|
|
|
return {
|
|
...subscription,
|
|
plan: subscription.plan || 'free',
|
|
isFreeTier,
|
|
emailsUsedThisMonth,
|
|
emailsLimit,
|
|
}
|
|
},
|
|
|
|
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 = {
|
|
/**
|
|
* Get default preferences structure
|
|
*/
|
|
getDefaults() {
|
|
return {
|
|
vipSenders: [],
|
|
enabledCategories: ['vip', 'customers', 'invoices', 'newsletters', 'promotions', 'social', 'security', 'calendar', 'review'],
|
|
categoryActions: {},
|
|
companyLabels: [],
|
|
autoDetectCompanies: true,
|
|
version: 1,
|
|
categoryAdvanced: {},
|
|
cleanup: {
|
|
enabled: false,
|
|
readItems: {
|
|
enabled: false,
|
|
action: 'archive_read',
|
|
gracePeriodDays: 7,
|
|
},
|
|
promotions: {
|
|
enabled: false,
|
|
matchCategoriesOrLabels: ['promotions', 'newsletters'],
|
|
action: 'archive_read',
|
|
deleteAfterDays: 30,
|
|
},
|
|
safety: {
|
|
requireConfirmForDelete: true,
|
|
dryRun: false,
|
|
maxDeletesPerRun: 100,
|
|
},
|
|
},
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Merge preferences with defaults
|
|
*/
|
|
mergeWithDefaults(preferences) {
|
|
const defaults = this.getDefaults()
|
|
return {
|
|
...defaults,
|
|
...preferences,
|
|
vipSenders: preferences.vipSenders || defaults.vipSenders,
|
|
enabledCategories: preferences.enabledCategories || defaults.enabledCategories,
|
|
categoryActions: preferences.categoryActions || defaults.categoryActions,
|
|
companyLabels: preferences.companyLabels || defaults.companyLabels,
|
|
autoDetectCompanies: preferences.autoDetectCompanies !== undefined ? preferences.autoDetectCompanies : defaults.autoDetectCompanies,
|
|
}
|
|
},
|
|
|
|
async getByUser(userId) {
|
|
const pref = await db.findOne(Collections.USER_PREFERENCES, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
if (pref?.preferencesJson) {
|
|
const parsed = JSON.parse(pref.preferencesJson)
|
|
return { ...pref, preferences: this.mergeWithDefaults(parsed) }
|
|
}
|
|
return { ...pref, preferences: this.getDefaults() }
|
|
},
|
|
|
|
async upsert(userId, preferences) {
|
|
const existing = await db.findOne(Collections.USER_PREFERENCES, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
|
|
// Merge with existing preferences if updating
|
|
let mergedPreferences = preferences
|
|
if (existing?.preferencesJson) {
|
|
const existingPrefs = JSON.parse(existing.preferencesJson)
|
|
mergedPreferences = { ...existingPrefs, ...preferences }
|
|
}
|
|
|
|
const data = { preferencesJson: JSON.stringify(mergedPreferences) }
|
|
|
|
if (existing) {
|
|
return db.update(Collections.USER_PREFERENCES, existing.$id, data)
|
|
}
|
|
return db.create(Collections.USER_PREFERENCES, { userId, ...data })
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Onboarding state operations
|
|
*/
|
|
export const onboardingState = {
|
|
async getByUser(userId) {
|
|
const state = await db.findOne(Collections.ONBOARDING_STATE, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
if (state?.completed_steps_json) {
|
|
return {
|
|
...state,
|
|
completedSteps: JSON.parse(state.completed_steps_json),
|
|
}
|
|
}
|
|
return {
|
|
...state,
|
|
completedSteps: [],
|
|
onboarding_step: state?.onboarding_step || 'not_started',
|
|
}
|
|
},
|
|
|
|
async updateStep(userId, step, completedSteps = []) {
|
|
const existing = await db.findOne(Collections.ONBOARDING_STATE, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
|
|
const data = {
|
|
onboarding_step: step,
|
|
completed_steps_json: JSON.stringify(completedSteps),
|
|
last_updated: new Date().toISOString(),
|
|
}
|
|
|
|
if (existing) {
|
|
return db.update(Collections.ONBOARDING_STATE, existing.$id, data)
|
|
}
|
|
return db.create(Collections.ONBOARDING_STATE, { userId, ...data })
|
|
},
|
|
|
|
async markValueSeen(userId) {
|
|
const existing = await db.findOne(Collections.ONBOARDING_STATE, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
|
|
const data = {
|
|
first_value_seen_at: new Date().toISOString(),
|
|
last_updated: new Date().toISOString(),
|
|
}
|
|
|
|
if (existing) {
|
|
return db.update(Collections.ONBOARDING_STATE, existing.$id, data)
|
|
}
|
|
return db.create(Collections.ONBOARDING_STATE, {
|
|
userId,
|
|
onboarding_step: 'see_results',
|
|
completed_steps_json: JSON.stringify(['connect', 'first_rule', 'see_results']),
|
|
...data,
|
|
})
|
|
},
|
|
|
|
async skip(userId) {
|
|
const existing = await db.findOne(Collections.ONBOARDING_STATE, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
|
|
const data = {
|
|
skipped_at: new Date().toISOString(),
|
|
last_updated: new Date().toISOString(),
|
|
}
|
|
|
|
if (existing) {
|
|
return db.update(Collections.ONBOARDING_STATE, existing.$id, data)
|
|
}
|
|
return db.create(Collections.ONBOARDING_STATE, {
|
|
userId,
|
|
onboarding_step: 'not_started',
|
|
completed_steps_json: JSON.stringify([]),
|
|
...data,
|
|
})
|
|
},
|
|
|
|
async resume(userId) {
|
|
const existing = await db.findOne(Collections.ONBOARDING_STATE, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
|
|
if (existing) {
|
|
return db.update(Collections.ONBOARDING_STATE, existing.$id, {
|
|
skipped_at: null,
|
|
last_updated: new Date().toISOString(),
|
|
})
|
|
}
|
|
// If no state exists, create initial state
|
|
return db.create(Collections.ONBOARDING_STATE, {
|
|
userId,
|
|
onboarding_step: 'connect',
|
|
completed_steps_json: JSON.stringify([]),
|
|
last_updated: new Date().toISOString(),
|
|
})
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Referrals operations
|
|
*/
|
|
export const referrals = {
|
|
async getOrCreateCode(userId) {
|
|
const existing = await db.findOne(Collections.REFERRALS, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
|
|
if (existing) {
|
|
return existing
|
|
}
|
|
|
|
// Generate unique code: USER-ABC123
|
|
const randomPart = Math.random().toString(36).substring(2, 8).toUpperCase()
|
|
const code = `USER-${randomPart}`
|
|
|
|
// Ensure uniqueness
|
|
let uniqueCode = code
|
|
let attempts = 0
|
|
while (attempts < 10) {
|
|
const existingCode = await db.findOne(Collections.REFERRALS, [
|
|
Query.equal('referralCode', uniqueCode),
|
|
])
|
|
if (!existingCode) break
|
|
uniqueCode = `USER-${Math.random().toString(36).substring(2, 8).toUpperCase()}`
|
|
attempts++
|
|
}
|
|
|
|
return db.create(Collections.REFERRALS, {
|
|
userId,
|
|
referralCode: uniqueCode,
|
|
referralCount: 0,
|
|
createdAt: new Date().toISOString(),
|
|
})
|
|
},
|
|
|
|
async getByCode(code) {
|
|
return db.findOne(Collections.REFERRALS, [
|
|
Query.equal('referralCode', code),
|
|
])
|
|
},
|
|
|
|
async incrementCount(userId) {
|
|
const referral = await db.findOne(Collections.REFERRALS, [
|
|
Query.equal('userId', userId),
|
|
])
|
|
|
|
if (referral) {
|
|
return db.update(Collections.REFERRALS, referral.$id, {
|
|
referralCount: (referral.referralCount || 0) + 1,
|
|
})
|
|
}
|
|
return null
|
|
},
|
|
|
|
async getReferrals(userId) {
|
|
return db.list(Collections.REFERRALS, [
|
|
Query.equal('referredBy', userId),
|
|
])
|
|
},
|
|
}
|
|
|
|
/**
|
|
* 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 { Query }
|
|
|
|
export default {
|
|
db,
|
|
products,
|
|
questions,
|
|
submissions,
|
|
emailAccounts,
|
|
emailStats,
|
|
emailDigests,
|
|
subscriptions,
|
|
userPreferences,
|
|
orders,
|
|
Collections,
|
|
}
|