Files
rxminder/services/database/ProductionDatabaseStrategy.ts
William Valentin 8c591563c9 feat: consolidate architecture and eliminate code duplication
🏗️ Major architectural improvements:

Database Layer:
- Consolidated duplicate CouchDB services (~800 lines of duplicated code eliminated)
- Implemented strategy pattern with MockDatabaseStrategy and ProductionDatabaseStrategy
- Created unified DatabaseService with automatic environment detection
- Maintained backward compatibility via updated factory pattern

Configuration System:
- Centralized all environment variables in single config/app.config.ts
- Added comprehensive configuration validation with clear error messages
- Eliminated hardcoded base URLs and scattered env var access across 8+ files
- Supports both legacy and new environment variable names

Logging Infrastructure:
- Replaced 25+ scattered console.log statements with structured Logger service
- Added log levels (ERROR, WARN, INFO, DEBUG, TRACE) and contexts (AUTH, DATABASE, API, UI)
- Production-safe logging with automatic level adjustment
- Development helpers for debugging and performance monitoring

Docker & Deployment:
- Removed duplicate docker/Dockerfile configuration
- Enhanced root Dockerfile with comprehensive environment variable support
- Added proper health checks and security improvements

Code Quality:
- Fixed package name consistency (rxminder → RxMinder)
- Updated services to use centralized configuration and logging
- Resolved all ESLint errors and warnings
- Added comprehensive documentation and migration guides

📊 Impact:
- Eliminated ~500 lines of duplicate code
- Single source of truth for database, configuration, and logging
- Better type safety and error handling
- Improved development experience and maintainability

📚 Documentation:
- Added ARCHITECTURE_MIGRATION.md with detailed migration guide
- Created IMPLEMENTATION_SUMMARY.md with metrics and benefits
- Inline documentation for all new services and interfaces

🔄 Backward Compatibility:
- All existing code continues to work unchanged
- Legacy services show deprecation warnings but remain functional
- Gradual migration path available for development teams

Breaking Changes: None (full backward compatibility maintained)
2025-09-08 01:09:48 -07:00

413 lines
10 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import {
User,
Medication,
UserSettings,
TakenDoses,
CustomReminder,
CouchDBDocument,
UserRole,
} from '../../types';
import { AccountStatus } from '../auth/auth.constants';
import { DatabaseStrategy, DatabaseError } from './types';
import { getDatabaseConfig } from '../../config/app.config';
import { logger } from '../logging';
export class ProductionDatabaseStrategy implements DatabaseStrategy {
private baseUrl: string;
private auth: string;
constructor() {
// Get CouchDB configuration from centralized config
const dbConfig = getDatabaseConfig();
this.baseUrl = dbConfig.url;
this.auth = btoa(`${dbConfig.username}:${dbConfig.password}`);
logger.db.query('Initializing production database strategy', {
url: dbConfig.url,
username: dbConfig.username,
});
// Initialize databases
this.initializeDatabases();
}
private async initializeDatabases(): Promise<void> {
const databases = [
'users',
'medications',
'settings',
'taken_doses',
'reminders',
];
for (const dbName of databases) {
try {
await this.createDatabaseIfNotExists(dbName);
} catch (error) {
logger.db.error(
`Failed to initialize database ${dbName}`,
error as Error
);
}
}
}
private async createDatabaseIfNotExists(dbName: string): Promise<void> {
try {
// Check if database exists
const response = await fetch(`${this.baseUrl}/${dbName}`, {
method: 'HEAD',
headers: {
Authorization: `Basic ${this.auth}`,
},
});
if (response.status === 404) {
// Database doesn't exist, create it
const createResponse = await fetch(`${this.baseUrl}/${dbName}`, {
method: 'PUT',
headers: {
Authorization: `Basic ${this.auth}`,
'Content-Type': 'application/json',
},
});
if (!createResponse.ok) {
throw new DatabaseError(
`Failed to create database ${dbName}`,
createResponse.status
);
}
}
} catch (error) {
if (error instanceof DatabaseError) {
throw error;
}
throw new DatabaseError(
`Database initialization failed for ${dbName}`,
500
);
}
}
private async makeRequest<T>(
method: string,
path: string,
body?: unknown
): Promise<T> {
const url = `${this.baseUrl}${path}`;
const headers: Record<string, string> = {
Authorization: `Basic ${this.auth}`,
'Content-Type': 'application/json',
};
const config: RequestInit = {
method,
headers,
};
if (body) {
config.body = JSON.stringify(body);
}
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorText = await response.text();
throw new DatabaseError(
`HTTP ${response.status}: ${errorText}`,
response.status
);
}
return await response.json();
} catch (error) {
if (error instanceof DatabaseError) {
throw error;
}
throw new DatabaseError(`Network error: ${error}`, 500);
}
}
private async getDoc<T extends CouchDBDocument>(
dbName: string,
id: string
): Promise<T | null> {
try {
return await this.makeRequest<T>('GET', `/${dbName}/${id}`);
} catch (error) {
if (error instanceof DatabaseError && error.status === 404) {
return null;
}
throw error;
}
}
private async putDoc<T extends CouchDBDocument>(
dbName: string,
doc: T
): Promise<T> {
const response = await this.makeRequest<{ id: string; rev: string }>(
'PUT',
`/${dbName}/${doc._id}`,
doc
);
return {
...doc,
_rev: response.rev,
};
}
private async deleteDoc(
dbName: string,
id: string,
rev: string
): Promise<boolean> {
try {
await this.makeRequest('DELETE', `/${dbName}/${id}?rev=${rev}`);
return true;
} catch (error) {
if (error instanceof DatabaseError && error.status === 404) {
return false;
}
throw error;
}
}
private async queryByKey<T>(
dbName: string,
startKey: string,
endKey?: string
): Promise<T[]> {
const params = new URLSearchParams({
startkey: JSON.stringify(startKey),
include_docs: 'true',
});
if (endKey) {
params.append('endkey', JSON.stringify(endKey));
}
const response = await this.makeRequest<{
rows: Array<{ doc: T }>;
}>('GET', `/${dbName}/_all_docs?${params}`);
return response.rows.map(row => row.doc);
}
// User operations
async createUser(user: Omit<User, '_id' | '_rev'>): Promise<User> {
const newUser: User = {
...user,
_id: uuidv4(),
_rev: '', // Will be set by CouchDB
status: user.status || AccountStatus.ACTIVE,
role: user.role || UserRole.USER,
createdAt: user.createdAt || new Date(),
};
return this.putDoc('users', newUser);
}
async updateUser(user: User): Promise<User> {
return this.putDoc('users', user);
}
async getUserById(id: string): Promise<User | null> {
return this.getDoc<User>('users', id);
}
async findUserByEmail(email: string): Promise<User | null> {
const response = await this.makeRequest<{
rows: Array<{ doc: User }>;
}>('POST', '/users/_find', {
selector: { email },
limit: 1,
});
return response.rows[0]?.doc || null;
}
async deleteUser(id: string): Promise<boolean> {
const user = await this.getDoc<User>('users', id);
if (!user) {
return false;
}
return this.deleteDoc('users', id, user._rev);
}
async getAllUsers(): Promise<User[]> {
const response = await this.makeRequest<{
rows: Array<{ doc: User }>;
}>('GET', '/users/_all_docs?include_docs=true');
return response.rows.map(row => row.doc);
}
// Medication operations
async createMedication(
userId: string,
medication: Omit<Medication, '_id' | '_rev'>
): Promise<Medication> {
const newMedication: Medication = {
...medication,
_id: `${userId}-med-${uuidv4()}`,
_rev: '',
};
return this.putDoc('medications', newMedication);
}
async updateMedication(medication: Medication): Promise<Medication> {
return this.putDoc('medications', medication);
}
async getMedications(userId: string): Promise<Medication[]> {
return this.queryByKey<Medication>(
'medications',
`${userId}-med-`,
`${userId}-med-\ufff0`
);
}
async deleteMedication(id: string): Promise<boolean> {
const medication = await this.getDoc<Medication>('medications', id);
if (!medication) {
return false;
}
return this.deleteDoc('medications', id, medication._rev);
}
// User settings operations
async getUserSettings(userId: string): Promise<UserSettings> {
const existing = await this.getDoc<UserSettings>('settings', userId);
if (existing) {
return existing;
}
// Create default settings if none exist
const defaultSettings: UserSettings = {
_id: userId,
_rev: '',
notificationsEnabled: true,
hasCompletedOnboarding: false,
};
return this.putDoc('settings', defaultSettings);
}
async updateUserSettings(settings: UserSettings): Promise<UserSettings> {
return this.putDoc('settings', settings);
}
// Taken doses operations
async getTakenDoses(userId: string): Promise<TakenDoses> {
const existing = await this.getDoc<TakenDoses>('taken_doses', userId);
if (existing) {
return existing;
}
// Create default taken doses record if none exists
const defaultTakenDoses: TakenDoses = {
_id: userId,
_rev: '',
doses: {},
};
return this.putDoc('taken_doses', defaultTakenDoses);
}
async updateTakenDoses(takenDoses: TakenDoses): Promise<TakenDoses> {
return this.putDoc('taken_doses', takenDoses);
}
// Custom reminders operations
async createCustomReminder(
userId: string,
reminder: Omit<CustomReminder, '_id' | '_rev'>
): Promise<CustomReminder> {
const newReminder: CustomReminder = {
...reminder,
_id: `${userId}-reminder-${uuidv4()}`,
_rev: '',
};
return this.putDoc('reminders', newReminder);
}
async updateCustomReminder(
reminder: CustomReminder
): Promise<CustomReminder> {
return this.putDoc('reminders', reminder);
}
async getCustomReminders(userId: string): Promise<CustomReminder[]> {
return this.queryByKey<CustomReminder>(
'reminders',
`${userId}-reminder-`,
`${userId}-reminder-\ufff0`
);
}
async deleteCustomReminder(id: string): Promise<boolean> {
const reminder = await this.getDoc<CustomReminder>('reminders', id);
if (!reminder) {
return false;
}
return this.deleteDoc('reminders', id, reminder._rev);
}
// User operations with password
async createUserWithPassword(
email: string,
hashedPassword: string,
username?: string
): Promise<User> {
// Check if user already exists
const existingUser = await this.findUserByEmail(email);
if (existingUser) {
throw new DatabaseError('User already exists with this email', 409);
}
return this.createUser({
username: username || email.split('@')[0],
email,
password: hashedPassword,
emailVerified: false,
status: AccountStatus.PENDING,
role: UserRole.USER,
createdAt: new Date(),
});
}
async createUserFromOAuth(
email: string,
username: string,
_provider: string
): Promise<User> {
// Check if user already exists
const existingUser = await this.findUserByEmail(email);
if (existingUser) {
// Update last login and return existing user
return this.updateUser({
...existingUser,
lastLoginAt: new Date(),
});
}
return this.createUser({
username,
email,
emailVerified: true, // OAuth emails are considered verified
status: AccountStatus.ACTIVE,
role: UserRole.USER,
createdAt: new Date(),
lastLoginAt: new Date(),
});
}
}