mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
feat: enhance database service with centralized error logging and improve media handling across services
This commit is contained in:
1 parent
04046702d3
commit
ad0cc1a7f5
5 files changed
+384
-165
No files matched your search
+175
-120
@@ -1,11 +1,19 @@
|
||||
// noinspection SqlNoDataSourceInspection,SqlResolve
|
||||
|
||||
/**
|
||||
* Database service — all tables are created at runtime by expo-sqlite.
|
||||
* The IDE cannot resolve them statically; the SQL warnings are safe to ignore.
|
||||
*/
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import CredentialEncryption from './CredentialEncryption';
|
||||
import { logError } from './ErrorHandler';
|
||||
|
||||
let db = null;
|
||||
let initPromise = null;
|
||||
|
||||
const DatabaseService = {
|
||||
// Open the database and create all tables
|
||||
// ─── Initialisation ────────────────────────────────────────
|
||||
|
||||
initDatabase: async () => {
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
@@ -14,12 +22,17 @@ const DatabaseService = {
|
||||
if (db) return true;
|
||||
|
||||
if (typeof SQLite.openDatabaseAsync !== 'function') {
|
||||
throw new Error('SQLite openDatabaseAsync API not available');
|
||||
logError('DB Init', 'SQLite openDatabaseAsync API not available');
|
||||
return false;
|
||||
}
|
||||
|
||||
db = await SQLite.openDatabaseAsync('PhotosApp.db');
|
||||
if (!db) throw new Error('SQLite openDatabaseAsync returned null');
|
||||
if (!db) {
|
||||
logError('DB Init', 'SQLite openDatabaseAsync returned null');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create all tables in one batch
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -27,10 +40,7 @@ const DatabaseService = {
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await db.execAsync(`
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS linked_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -40,10 +50,7 @@ const DatabaseService = {
|
||||
is_default INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
await db.execAsync(`
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS media (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -58,20 +65,20 @@ const DatabaseService = {
|
||||
synced INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
)
|
||||
);
|
||||
`);
|
||||
|
||||
// Add optional columns (safe migration — errors ignored if columns already exist)
|
||||
try { await db.execAsync(`ALTER TABLE users ADD COLUMN auto_backup_services TEXT DEFAULT '[]'`); } catch (_) {}
|
||||
try { await db.execAsync(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT 'light'`); } catch (_) {}
|
||||
// Safe migrations — errors ignored if columns already exist
|
||||
try { await db.execAsync("ALTER TABLE users ADD COLUMN auto_backup_services TEXT DEFAULT '[]'"); } catch (_) {}
|
||||
try { await db.execAsync("ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT 'light'"); } catch (_) {}
|
||||
|
||||
console.log('Database initialized successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Database initialization error:', error);
|
||||
logError('DB Init', error);
|
||||
db = null;
|
||||
initPromise = null;
|
||||
throw error;
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -80,12 +87,18 @@ const DatabaseService = {
|
||||
|
||||
ensureDb: async () => {
|
||||
await DatabaseService.initDatabase();
|
||||
if (!db) throw new Error('Database not initialized');
|
||||
if (!db) {
|
||||
logError('DB', 'Database not initialized');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// ─── Users ─────────────────────────────────────────────────
|
||||
|
||||
createUser: async (username, email, password) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!(await DatabaseService.ensureDb())) return null;
|
||||
const hashedPassword = await CredentialEncryption.hashPassword(password);
|
||||
const result = await db.runAsync(
|
||||
'INSERT INTO users (username, email, password) VALUES (?, ?, ?)',
|
||||
@@ -93,98 +106,111 @@ const DatabaseService = {
|
||||
);
|
||||
return result.lastInsertRowId;
|
||||
} catch (error) {
|
||||
console.error('Error creating user:', error);
|
||||
throw error;
|
||||
logError('Create User', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Authenticate by username or email
|
||||
loginUser: async (identifier, password) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!(await DatabaseService.ensureDb())) return null;
|
||||
const row = await db.getFirstAsync(
|
||||
'SELECT * FROM users WHERE username = ? OR email = ?',
|
||||
[identifier, identifier]
|
||||
);
|
||||
if (!row) return null;
|
||||
|
||||
const valid = await CredentialEncryption.verifyPassword(password, row.password);
|
||||
if (!valid) return null;
|
||||
|
||||
return row;
|
||||
return valid ? row : null;
|
||||
} catch (error) {
|
||||
console.error('Error logging in user:', error);
|
||||
throw error;
|
||||
logError('Login User', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
userExists: async (username, email) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!(await DatabaseService.ensureDb())) return false;
|
||||
const row = await db.getFirstAsync(
|
||||
'SELECT id FROM users WHERE username = ? OR email = ?',
|
||||
[username, email]
|
||||
);
|
||||
return !!row;
|
||||
} catch (error) {
|
||||
console.error('Error checking if user exists:', error);
|
||||
throw error;
|
||||
logError('User Exists', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getUserById: async (userId) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!userId && userId !== 0) throw new Error('User ID is required');
|
||||
if (!(await DatabaseService.ensureDb())) return null;
|
||||
if (!userId && userId !== 0) {
|
||||
logError('Get User', 'User ID is required');
|
||||
return null;
|
||||
}
|
||||
return (await db.getFirstAsync('SELECT * FROM users WHERE id = ?', [userId])) || null;
|
||||
} catch (error) {
|
||||
console.error('Error getting user:', error);
|
||||
throw error;
|
||||
logError('Get User', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Link an external service account (Google, iCloud, NAS) to a local user
|
||||
// ─── Linked Accounts ──────────────────────────────────────
|
||||
|
||||
linkAccount: async (userId, serviceType, accountData, isDefault = false) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!userId) throw new Error('User ID is required');
|
||||
if (!serviceType) throw new Error('Service type is required');
|
||||
if (!accountData || typeof accountData !== 'object') throw new Error('Account data must be a valid object');
|
||||
if (!(await DatabaseService.ensureDb())) return null;
|
||||
if (!userId || !serviceType || !accountData || typeof accountData !== 'object') {
|
||||
logError('Link Account', 'userId, serviceType, and accountData object are required');
|
||||
return null;
|
||||
}
|
||||
|
||||
const encryptedData = CredentialEncryption.encrypt(accountData);
|
||||
if (!encryptedData) throw new Error('Encryption returned empty data');
|
||||
if (!encryptedData) {
|
||||
logError('Link Account', 'Encryption returned empty data');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Replace any existing link for the same service type
|
||||
await db.runAsync('DELETE FROM linked_accounts WHERE user_id = ? AND service_type = ?', [userId, serviceType]);
|
||||
|
||||
await db.runAsync(
|
||||
'DELETE FROM linked_accounts WHERE user_id = ? AND service_type = ?',
|
||||
[userId, serviceType]
|
||||
);
|
||||
const result = await db.runAsync(
|
||||
'INSERT INTO linked_accounts (user_id, service_type, account_data, is_default) VALUES (?, ?, ?, ?)',
|
||||
[userId, serviceType, encryptedData, isDefault ? 1 : 0]
|
||||
);
|
||||
return result.lastInsertRowId;
|
||||
} catch (error) {
|
||||
console.error('Error linking account:', error);
|
||||
throw new Error('Failed to link account: ' + error.message);
|
||||
logError('Link Account', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getLinkedAccounts: async (userId) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!userId && userId !== 0) throw new Error('User ID is required');
|
||||
if (!(await DatabaseService.ensureDb())) return [];
|
||||
if (!userId && userId !== 0) {
|
||||
logError('Get Linked Accounts', 'User ID is required');
|
||||
return [];
|
||||
}
|
||||
const rows = await db.getAllAsync(
|
||||
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_enabled = 1', [userId]
|
||||
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_enabled = 1',
|
||||
[userId]
|
||||
);
|
||||
return rows.map((row) => ({ ...row, account_data: CredentialEncryption.decrypt(row.account_data) }));
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
account_data: CredentialEncryption.decrypt(row.account_data),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error getting linked accounts:', error);
|
||||
throw error;
|
||||
logError('Get Linked Accounts', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
getLinkedAccountByService: async (userId, serviceType) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!(await DatabaseService.ensureDb())) return null;
|
||||
const row = await db.getFirstAsync(
|
||||
'SELECT * FROM linked_accounts WHERE user_id = ? AND service_type = ? AND is_enabled = 1',
|
||||
[userId, serviceType]
|
||||
@@ -192,40 +218,83 @@ const DatabaseService = {
|
||||
if (!row) return null;
|
||||
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
|
||||
} catch (error) {
|
||||
console.error('Error getting linked account:', error);
|
||||
throw error;
|
||||
logError('Get Linked Account By Service', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
setDefaultService: async (userId, accountId) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!(await DatabaseService.ensureDb())) return;
|
||||
await db.runAsync('UPDATE linked_accounts SET is_default = 0 WHERE user_id = ?', [userId]);
|
||||
await db.runAsync('UPDATE linked_accounts SET is_default = 1 WHERE id = ? AND user_id = ?', [accountId, userId]);
|
||||
} catch (error) {
|
||||
console.error('Error setting default service:', error);
|
||||
throw error;
|
||||
logError('Set Default Service', error);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultService: async (userId) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!(await DatabaseService.ensureDb())) return null;
|
||||
const row = await db.getFirstAsync(
|
||||
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_default = 1', [userId]
|
||||
'SELECT * FROM linked_accounts WHERE user_id = ? AND is_default = 1',
|
||||
[userId]
|
||||
);
|
||||
if (!row) return null;
|
||||
return { ...row, account_data: CredentialEncryption.decrypt(row.account_data) };
|
||||
} catch (error) {
|
||||
console.error('Error getting default service:', error);
|
||||
throw error;
|
||||
logError('Get Default Service', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
toggleAccountEnabled: async (accountId, enabled) => {
|
||||
try {
|
||||
if (!(await DatabaseService.ensureDb())) return;
|
||||
await db.runAsync(
|
||||
'UPDATE linked_accounts SET is_enabled = ? WHERE id = ?',
|
||||
[enabled ? 1 : 0, accountId]
|
||||
);
|
||||
} catch (error) {
|
||||
logError('Toggle Account', error);
|
||||
}
|
||||
},
|
||||
|
||||
removeLinkedAccount: async (accountId) => {
|
||||
try {
|
||||
if (!(await DatabaseService.ensureDb())) return;
|
||||
await db.runAsync('DELETE FROM linked_accounts WHERE id = ?', [accountId]);
|
||||
} catch (error) {
|
||||
logError('Remove Account', error);
|
||||
}
|
||||
},
|
||||
|
||||
getAllLinkedAccounts: async (userId) => {
|
||||
try {
|
||||
if (!(await DatabaseService.ensureDb())) return [];
|
||||
const rows = await db.getAllAsync(
|
||||
'SELECT * FROM linked_accounts WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
account_data: CredentialEncryption.decrypt(row.account_data),
|
||||
}));
|
||||
} catch (error) {
|
||||
logError('Get All Linked Accounts', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// ─── Media ─────────────────────────────────────────────────
|
||||
|
||||
addMedia: async (userId, sourceService, sourceId, title, description, localPath, remotePath, mediaType, createdDate) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!userId || !sourceService) throw new Error('userId and sourceService are required');
|
||||
if (!(await DatabaseService.ensureDb())) return null;
|
||||
if (!userId || !sourceService) {
|
||||
logError('Add Media', 'userId and sourceService are required');
|
||||
return null;
|
||||
}
|
||||
const result = await db.runAsync(
|
||||
`INSERT INTO media (user_id, source_service, source_id, title, description, local_path, remote_path, media_type, created_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
@@ -234,8 +303,8 @@ const DatabaseService = {
|
||||
);
|
||||
return result.lastInsertRowId;
|
||||
} catch (error) {
|
||||
console.error('Error adding media:', error);
|
||||
throw error;
|
||||
logError('Add Media', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -245,10 +314,10 @@ const DatabaseService = {
|
||||
*/
|
||||
saveMediaItem: async (userId, item) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!userId || !item.source_service) return;
|
||||
if (!(await DatabaseService.ensureDb())) return null;
|
||||
if (!userId || !item?.source_service) return null;
|
||||
|
||||
// Check for existing entry to avoid duplicates
|
||||
// Skip if already exists
|
||||
if (item.source_id) {
|
||||
const existing = await db.getFirstAsync(
|
||||
'SELECT id FROM media WHERE user_id = ? AND source_id = ?',
|
||||
@@ -266,108 +335,94 @@ const DatabaseService = {
|
||||
);
|
||||
return result.lastInsertRowId;
|
||||
} catch (error) {
|
||||
console.error('Error saving media item:', error);
|
||||
logError('Save Media Item', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getMediaByUser: async (userId) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!userId && userId !== 0) throw new Error('User ID is required');
|
||||
return await db.getAllAsync('SELECT * FROM media WHERE user_id = ? ORDER BY created_date DESC', [userId]);
|
||||
if (!(await DatabaseService.ensureDb())) return [];
|
||||
if (!userId && userId !== 0) {
|
||||
logError('Get Media', 'User ID is required');
|
||||
return [];
|
||||
}
|
||||
return await db.getAllAsync(
|
||||
'SELECT * FROM media WHERE user_id = ? ORDER BY created_date DESC',
|
||||
[userId]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error getting media:', error);
|
||||
throw error;
|
||||
logError('Get Media', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
getMediaByService: async (userId, serviceType) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!(await DatabaseService.ensureDb())) return [];
|
||||
return await db.getAllAsync(
|
||||
'SELECT * FROM media WHERE user_id = ? AND source_service = ? ORDER BY created_date DESC',
|
||||
[userId, serviceType]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error getting media by service:', error);
|
||||
throw error;
|
||||
logError('Get Media By Service', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
searchMedia: async (userId, query) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
if (!(await DatabaseService.ensureDb())) return [];
|
||||
const searchTerm = `%${query}%`;
|
||||
return await db.getAllAsync(
|
||||
'SELECT * FROM media WHERE user_id = ? AND (title LIKE ? OR description LIKE ?) ORDER BY created_date DESC',
|
||||
[userId, searchTerm, searchTerm]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error searching media:', error);
|
||||
throw error;
|
||||
logError('Search Media', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// ─── Settings ──────────────────────────────────────────────
|
||||
|
||||
getAutoBackupServices: async (userId) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
const row = await db.getFirstAsync('SELECT auto_backup_services FROM users WHERE id = ?', [userId]);
|
||||
if (!(await DatabaseService.ensureDb())) return [];
|
||||
const row = await db.getFirstAsync(
|
||||
'SELECT auto_backup_services FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
if (!row?.auto_backup_services) return [];
|
||||
return JSON.parse(row.auto_backup_services);
|
||||
} catch (error) {
|
||||
console.error('Error getting auto backup services:', error);
|
||||
logError('Get Auto Backup', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
setAutoBackupServices: async (userId, services) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
await db.runAsync('UPDATE users SET auto_backup_services = ? WHERE id = ?', [JSON.stringify(services), userId]);
|
||||
if (!(await DatabaseService.ensureDb())) return;
|
||||
await db.runAsync(
|
||||
'UPDATE users SET auto_backup_services = ? WHERE id = ?',
|
||||
[JSON.stringify(services), userId]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error setting auto backup services:', error);
|
||||
throw error;
|
||||
logError('Set Auto Backup', error);
|
||||
}
|
||||
},
|
||||
|
||||
toggleAccountEnabled: async (accountId, enabled) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
await db.runAsync('UPDATE linked_accounts SET is_enabled = ? WHERE id = ?', [enabled ? 1 : 0, accountId]);
|
||||
} catch (error) {
|
||||
console.error('Error toggling account:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
removeLinkedAccount: async (accountId) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
await db.runAsync('DELETE FROM linked_accounts WHERE id = ?', [accountId]);
|
||||
} catch (error) {
|
||||
console.error('Error removing account:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getAllLinkedAccounts: async (userId) => {
|
||||
try {
|
||||
await DatabaseService.ensureDb();
|
||||
const rows = await db.getAllAsync('SELECT * FROM linked_accounts WHERE user_id = ?', [userId]);
|
||||
return rows.map((row) => ({ ...row, account_data: CredentialEncryption.decrypt(row.account_data) }));
|
||||
} catch (error) {
|
||||
console.error('Error getting all linked accounts:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
// ─── Lifecycle ─────────────────────────────────────────────
|
||||
|
||||
closeDatabase: async () => {
|
||||
try {
|
||||
if (db?.closeAsync) await db.closeAsync();
|
||||
} catch (error) {
|
||||
logError('Close DB', error);
|
||||
} finally {
|
||||
db = null;
|
||||
initPromise = null;
|
||||
} catch (error) {
|
||||
console.error('Error closing database:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user