forked from jsnbuchanan/crowd-funder-for-time-pwa
Convert undefined values to null in database operations to prevent "tried to bind a value of an unknown type" errors in SQL.js worker. - Fix $insertContact() method undefined-to-null conversion - Fix $insertEntity() method undefined-to-null conversion - Preserve boolean false/0 values vs null distinction - Maintain parameterized queries for SQL injection protection - Fix contact creation errors in ContactsView component Resolves database binding failures when inserting contacts with undefined properties. All linting errors resolved.
1000 lines
31 KiB
TypeScript
1000 lines
31 KiB
TypeScript
/**
|
|
* Enhanced PlatformService Mixin with ultra-concise database operations and caching
|
|
*
|
|
* Provides cached platform service access and utility methods for Vue components.
|
|
* Eliminates repetitive PlatformServiceFactory.getInstance() calls across components.
|
|
*
|
|
* Features:
|
|
* - Cached platform service instance (created once per component)
|
|
* - Enhanced database utility methods with comprehensive error handling
|
|
* - Ultra-concise database interaction methods ($db, $exec, $one, etc.)
|
|
* - Automatic query result mapping and JSON field parsing
|
|
* - Specialized shortcuts for common queries (contacts, settings)
|
|
* - Transaction support with automatic rollback on errors
|
|
* - Mixin pattern for easy integration with existing class components
|
|
* - Enhanced utility methods for common patterns
|
|
* - Robust error handling and logging
|
|
* - Ultra-concise database interaction methods
|
|
* - Smart caching layer with TTL for performance optimization
|
|
* - Settings shortcuts for ultra-frequent update patterns
|
|
* - High-level entity operations (insertContact, updateContact, etc.)
|
|
* - Result mapping helpers to eliminate verbose row processing
|
|
*
|
|
* Benefits:
|
|
* - Eliminates repeated PlatformServiceFactory.getInstance() calls
|
|
* - Provides consistent error handling across components
|
|
* - Reduces boilerplate database code by up to 80%
|
|
* - Maintains type safety with TypeScript
|
|
* - Includes common database utility patterns
|
|
* - Enhanced error handling and logging
|
|
* - Ultra-concise method names for frequent operations
|
|
* - Automatic caching for settings and contacts (massive performance gain)
|
|
* - Settings update shortcuts reduce 90% of update boilerplate
|
|
* - Entity operations eliminate verbose SQL INSERT/UPDATE patterns
|
|
* - Result mapping helpers reduce row processing boilerplate by 75%
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 4.1.0
|
|
* @since 2025-07-02
|
|
* @updated 2025-01-25 - Added high-level entity operations for code reduction
|
|
*/
|
|
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
import type {
|
|
PlatformService,
|
|
PlatformCapabilities,
|
|
} from "@/services/PlatformService";
|
|
import { mapColumnsToValues, parseJsonField } from "@/db/databaseUtil";
|
|
import { MASTER_SETTINGS_KEY, type Settings } from "@/db/tables/settings";
|
|
import * as databaseUtil from "@/db/databaseUtil";
|
|
import { logger } from "@/utils/logger";
|
|
import { Contact } from "@/db/tables/contacts";
|
|
import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
|
|
|
|
// =================================================
|
|
// TYPESCRIPT INTERFACES
|
|
// =================================================
|
|
|
|
/**
|
|
* Cache entry interface for storing data with TTL
|
|
*/
|
|
interface CacheEntry<T> {
|
|
data: T;
|
|
timestamp: number;
|
|
ttl: number; // milliseconds
|
|
}
|
|
|
|
/**
|
|
* Vue component interface that uses the PlatformServiceMixin
|
|
* This provides proper typing for the cache system
|
|
*/
|
|
interface VueComponentWithMixin {
|
|
_platformService: PlatformService | null;
|
|
$options: { name?: string };
|
|
activeDid?: string;
|
|
platformService(): PlatformService;
|
|
}
|
|
|
|
/**
|
|
* Global cache store for mixin instances
|
|
* Uses WeakMap to avoid memory leaks when components are destroyed
|
|
*/
|
|
const componentCaches = new WeakMap<
|
|
VueComponentWithMixin,
|
|
Map<string, CacheEntry<unknown>>
|
|
>();
|
|
|
|
/**
|
|
* Cache configuration constants
|
|
*/
|
|
const CACHE_DEFAULTS = {
|
|
settings: 30000, // 30 seconds TTL for settings
|
|
contacts: 60000, // 60 seconds TTL for contacts
|
|
accounts: 30000, // 30 seconds TTL for accounts
|
|
default: 15000, // 15 seconds default TTL
|
|
} as const;
|
|
|
|
/**
|
|
* Enhanced mixin that provides cached platform service access and utility methods
|
|
* with smart caching layer for ultimate performance optimization
|
|
*/
|
|
export const PlatformServiceMixin = {
|
|
data() {
|
|
return {
|
|
// Cache the platform service instance at component level
|
|
_platformService: null as PlatformService | null,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
/**
|
|
* Cached platform service instance
|
|
* Created once per component lifecycle
|
|
*/
|
|
platformService(): PlatformService {
|
|
if (!(this as unknown as VueComponentWithMixin)._platformService) {
|
|
(this as unknown as VueComponentWithMixin)._platformService =
|
|
PlatformServiceFactory.getInstance();
|
|
}
|
|
return (this as unknown as VueComponentWithMixin)._platformService!;
|
|
},
|
|
|
|
/**
|
|
* Platform detection utilities
|
|
*/
|
|
isCapacitor(): boolean {
|
|
return (this as unknown as VueComponentWithMixin)
|
|
.platformService()
|
|
.isCapacitor();
|
|
},
|
|
|
|
isWeb(): boolean {
|
|
return (this as unknown as VueComponentWithMixin)
|
|
.platformService()
|
|
.isWeb();
|
|
},
|
|
|
|
isElectron(): boolean {
|
|
return (this as unknown as VueComponentWithMixin)
|
|
.platformService()
|
|
.isElectron();
|
|
},
|
|
|
|
/**
|
|
* Platform capabilities
|
|
*/
|
|
capabilities() {
|
|
return (this as unknown as VueComponentWithMixin)
|
|
.platformService()
|
|
.getCapabilities();
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
// =================================================
|
|
// CACHING UTILITY METHODS
|
|
// =================================================
|
|
|
|
/**
|
|
* Get or initialize cache for this component instance
|
|
*/
|
|
_getCache(): Map<string, CacheEntry<unknown>> {
|
|
let cache = componentCaches.get(this as unknown as VueComponentWithMixin);
|
|
if (!cache) {
|
|
cache = new Map();
|
|
componentCaches.set(this as unknown as VueComponentWithMixin, cache);
|
|
}
|
|
return cache;
|
|
},
|
|
|
|
/**
|
|
* Check if cache entry is valid (not expired)
|
|
*/
|
|
_isCacheValid(entry: CacheEntry<unknown>): boolean {
|
|
return Date.now() - entry.timestamp < entry.ttl;
|
|
},
|
|
|
|
/**
|
|
* Get data from cache if valid, otherwise return null
|
|
*/
|
|
_getCached<T>(key: string): T | null {
|
|
const cache = this._getCache();
|
|
const entry = cache.get(key);
|
|
if (entry && this._isCacheValid(entry)) {
|
|
return entry.data as T;
|
|
}
|
|
cache.delete(key); // Clean up expired entries
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Store data in cache with TTL
|
|
*/
|
|
_setCached<T>(key: string, data: T, ttl?: number): T {
|
|
const cache = this._getCache();
|
|
const actualTtl = ttl || CACHE_DEFAULTS.default;
|
|
cache.set(key, {
|
|
data,
|
|
timestamp: Date.now(),
|
|
ttl: actualTtl,
|
|
});
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* Invalidate specific cache entry
|
|
*/
|
|
_invalidateCache(key: string): void {
|
|
const cache = this._getCache();
|
|
cache.delete(key);
|
|
},
|
|
|
|
/**
|
|
* Clear all cache entries for this component
|
|
*/
|
|
_clearCache(): void {
|
|
const cache = this._getCache();
|
|
cache.clear();
|
|
},
|
|
|
|
// =================================================
|
|
// ENHANCED DATABASE METHODS (with error handling)
|
|
// =================================================
|
|
|
|
/**
|
|
* Enhanced database query method with error handling
|
|
*/
|
|
async $dbQuery(sql: string, params?: unknown[]) {
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return await (this as any).platformService.dbQuery(sql, params);
|
|
} catch (error) {
|
|
logger.error(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
`[${(this as any).$options.name}] Database query failed:`,
|
|
{
|
|
sql,
|
|
params,
|
|
error,
|
|
},
|
|
);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Enhanced database execution method with error handling
|
|
*/
|
|
async $dbExec(sql: string, params?: unknown[]) {
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return await (this as any).platformService.dbExec(sql, params);
|
|
} catch (error) {
|
|
logger.error(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
`[${(this as any).$options.name}] Database exec failed:`,
|
|
{
|
|
sql,
|
|
params,
|
|
error,
|
|
},
|
|
);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Enhanced database single row query method with error handling
|
|
*/
|
|
async $dbGetOneRow(sql: string, params?: unknown[]) {
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return await (this as any).platformService.dbGetOneRow(sql, params);
|
|
} catch (error) {
|
|
logger.error(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
`[${(this as any).$options.name}] Database single row query failed:`,
|
|
{
|
|
sql,
|
|
params,
|
|
error,
|
|
},
|
|
);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Utility method for retrieving and parsing settings
|
|
* Common pattern used across many components
|
|
*/
|
|
async $getSettings(
|
|
key: string,
|
|
fallback: Settings | null = null,
|
|
): Promise<Settings | null> {
|
|
try {
|
|
const result = await this.$dbQuery(
|
|
"SELECT * FROM settings WHERE id = ? OR accountDid = ?",
|
|
[key, key],
|
|
);
|
|
|
|
if (!result?.values?.length) {
|
|
return fallback;
|
|
}
|
|
|
|
const mappedResults = mapColumnsToValues(result.columns, result.values);
|
|
|
|
if (!mappedResults.length) {
|
|
return fallback;
|
|
}
|
|
|
|
const settings = mappedResults[0] as Settings;
|
|
|
|
// Handle JSON field parsing
|
|
if (settings.searchBoxes) {
|
|
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
|
}
|
|
|
|
return settings;
|
|
} catch (error) {
|
|
logger.error(
|
|
`[${(this as unknown as VueComponentWithMixin).$options.name}] Failed to get settings:`,
|
|
{
|
|
key,
|
|
error,
|
|
},
|
|
);
|
|
return fallback;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Utility method for merging default and account-specific settings
|
|
* Handles the common pattern of layered settings
|
|
*/
|
|
async $getMergedSettings(
|
|
defaultKey: string,
|
|
accountDid?: string,
|
|
defaultFallback: Settings = {},
|
|
): Promise<Settings> {
|
|
try {
|
|
// Get default settings
|
|
const defaultSettings = await this.$getSettings(
|
|
defaultKey,
|
|
defaultFallback,
|
|
);
|
|
|
|
// If no account DID, return defaults
|
|
if (!accountDid) {
|
|
return defaultSettings || defaultFallback;
|
|
}
|
|
|
|
// Get account-specific overrides
|
|
const accountResult = await this.$dbQuery(
|
|
"SELECT * FROM settings WHERE accountDid = ?",
|
|
[accountDid],
|
|
);
|
|
|
|
if (!accountResult?.values?.length) {
|
|
return defaultSettings || defaultFallback;
|
|
}
|
|
|
|
// Map and filter non-null overrides
|
|
const mappedResults = mapColumnsToValues(
|
|
accountResult.columns,
|
|
accountResult.values,
|
|
);
|
|
|
|
if (!mappedResults.length) {
|
|
return defaultSettings || defaultFallback;
|
|
}
|
|
|
|
const overrideSettings = mappedResults[0] as Settings;
|
|
|
|
const filteredOverrides = Object.fromEntries(
|
|
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
|
);
|
|
|
|
// Merge settings with overrides taking precedence
|
|
const mergedSettings = {
|
|
...defaultSettings,
|
|
...filteredOverrides,
|
|
} as Settings;
|
|
|
|
// Handle JSON field parsing
|
|
if (mergedSettings.searchBoxes) {
|
|
mergedSettings.searchBoxes = parseJsonField(
|
|
mergedSettings.searchBoxes,
|
|
[],
|
|
);
|
|
}
|
|
|
|
return mergedSettings;
|
|
} catch (error) {
|
|
logger.error(
|
|
`[${(this as unknown as VueComponentWithMixin).$options.name}] Failed to get merged settings:`,
|
|
{
|
|
defaultKey,
|
|
accountDid,
|
|
error,
|
|
},
|
|
);
|
|
return defaultFallback;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Transaction wrapper with automatic rollback on error
|
|
*/
|
|
async $withTransaction<T>(callback: () => Promise<T>): Promise<T> {
|
|
try {
|
|
await this.$dbExec("BEGIN TRANSACTION");
|
|
const result = await callback();
|
|
await this.$dbExec("COMMIT");
|
|
return result;
|
|
} catch (error) {
|
|
await this.$dbExec("ROLLBACK");
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// =================================================
|
|
// ULTRA-CONCISE DATABASE METHODS (shortest names)
|
|
// =================================================
|
|
|
|
/**
|
|
* Ultra-short database query - just $db()
|
|
* @param sql SQL query string
|
|
* @param params Query parameters
|
|
*/
|
|
async $db(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<QueryExecResult | undefined> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return await (this as any).platformService.dbQuery(sql, params);
|
|
},
|
|
|
|
/**
|
|
* Ultra-short database exec - just $exec()
|
|
* @param sql SQL statement string
|
|
* @param params Statement parameters
|
|
*/
|
|
async $exec(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<DatabaseExecResult> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return await (this as any).platformService.dbExec(sql, params);
|
|
},
|
|
|
|
/**
|
|
* Ultra-short single row query - just $one()
|
|
* @param sql SQL query string
|
|
* @param params Query parameters
|
|
*/
|
|
async $one(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<unknown[] | undefined> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return await (this as any).platformService.dbGetOneRow(sql, params);
|
|
},
|
|
|
|
// =================================================
|
|
// QUERY + MAPPING COMBO METHODS (ultimate conciseness)
|
|
// =================================================
|
|
|
|
/**
|
|
* Query with automatic result mapping - $query()
|
|
* Combines database query + mapping in one call
|
|
* @param sql SQL query string
|
|
* @param params Query parameters
|
|
* @returns Mapped array of results
|
|
*/
|
|
async $query<T = Record<string, unknown>>(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<T[]> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const result = await (this as any).platformService.dbQuery(sql, params);
|
|
if (!result?.columns || !result?.values) {
|
|
return [];
|
|
}
|
|
const mappedResults = mapColumnsToValues(result.columns, result.values);
|
|
return mappedResults as T[];
|
|
},
|
|
|
|
/**
|
|
* Get first result with automatic mapping - $first()
|
|
* @param sql SQL query string
|
|
* @param params Query parameters
|
|
* @returns First mapped result or null
|
|
*/
|
|
async $first<T = Record<string, unknown>>(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<T | null> {
|
|
const results = await this.$query(sql, params);
|
|
return results.length > 0 ? (results[0] as T) : null;
|
|
},
|
|
|
|
// =================================================
|
|
// CACHED SPECIALIZED SHORTCUTS (massive performance boost)
|
|
// =================================================
|
|
|
|
/**
|
|
* Load all contacts with caching - $contacts()
|
|
* Ultra-concise shortcut with 60s TTL for performance
|
|
* @returns Cached mapped array of all contacts
|
|
*/
|
|
async $contacts(): Promise<Contact[]> {
|
|
const cacheKey = "contacts_all";
|
|
const cached = this._getCached<Contact[]>(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
|
|
const contacts = await this.$query(
|
|
"SELECT * FROM contacts ORDER BY name",
|
|
);
|
|
return this._setCached(
|
|
cacheKey,
|
|
contacts as Contact[],
|
|
CACHE_DEFAULTS.contacts,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Load settings with optional defaults WITHOUT caching - $settings()
|
|
* Settings are loaded fresh every time for immediate consistency
|
|
* @param defaults Optional default values
|
|
* @returns Fresh settings object from database
|
|
*/
|
|
async $settings(defaults: Settings = {}): Promise<Settings> {
|
|
const settings = await this.$getSettings(MASTER_SETTINGS_KEY, defaults);
|
|
|
|
if (!settings) {
|
|
return defaults;
|
|
}
|
|
|
|
// **ELECTRON-SPECIFIC FIX**: Apply platform-specific API server override
|
|
// This ensures Electron always uses production endpoints regardless of cached settings
|
|
if (process.env.VITE_PLATFORM === "electron") {
|
|
// Import constants dynamically to get platform-specific values
|
|
const { DEFAULT_ENDORSER_API_SERVER } = await import(
|
|
"../constants/app"
|
|
);
|
|
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
|
}
|
|
|
|
return settings; // Return fresh data without caching
|
|
},
|
|
|
|
/**
|
|
* Load account-specific settings WITHOUT caching - $accountSettings()
|
|
* Settings are loaded fresh every time for immediate consistency
|
|
* @param did DID identifier (optional, uses current active DID)
|
|
* @param defaults Optional default values
|
|
* @returns Fresh merged settings object from database
|
|
*/
|
|
async $accountSettings(
|
|
did?: string,
|
|
defaults: Settings = {},
|
|
): Promise<Settings> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const currentDid = did || (this as any).activeDid;
|
|
|
|
let settings;
|
|
if (!currentDid) {
|
|
settings = await this.$settings(defaults);
|
|
} else {
|
|
settings = await this.$getMergedSettings(
|
|
MASTER_SETTINGS_KEY,
|
|
currentDid,
|
|
defaults,
|
|
);
|
|
}
|
|
|
|
return settings; // Return fresh data without caching
|
|
},
|
|
|
|
// =================================================
|
|
// SETTINGS UPDATE SHORTCUTS (eliminate 90% boilerplate)
|
|
// =================================================
|
|
|
|
/**
|
|
* Save default settings - $saveSettings()
|
|
* Ultra-concise shortcut for updateDefaultSettings
|
|
* @param changes Settings changes to save
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $saveSettings(changes: Partial<Settings>): Promise<boolean> {
|
|
return await databaseUtil.updateDefaultSettings(changes);
|
|
},
|
|
|
|
/**
|
|
* Save user-specific settings - $saveUserSettings()
|
|
* Ultra-concise shortcut for updateDidSpecificSettings
|
|
* @param did DID identifier
|
|
* @param changes Settings changes to save
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $saveUserSettings(
|
|
did: string,
|
|
changes: Partial<Settings>,
|
|
): Promise<boolean> {
|
|
return await databaseUtil.updateDidSpecificSettings(did, changes);
|
|
},
|
|
|
|
/**
|
|
* Save settings for current active user - $saveMySettings()
|
|
* Ultra-concise shortcut using activeDid from component
|
|
* @param changes Settings changes to save
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $saveMySettings(changes: Partial<Settings>): Promise<boolean> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const currentDid = (this as any).activeDid;
|
|
if (!currentDid) {
|
|
return await this.$saveSettings(changes);
|
|
}
|
|
return await this.$saveUserSettings(currentDid, changes);
|
|
},
|
|
|
|
// =================================================
|
|
// CACHE MANAGEMENT METHODS
|
|
// =================================================
|
|
|
|
/**
|
|
* Refresh settings from database - $refreshSettings()
|
|
* Since settings are no longer cached, this simply returns fresh settings
|
|
*/
|
|
async $refreshSettings(): Promise<Settings> {
|
|
return await this.$settings();
|
|
},
|
|
|
|
/**
|
|
* Manually refresh contacts cache - $refreshContacts()
|
|
* Forces reload of contacts from database
|
|
*/
|
|
async $refreshContacts(): Promise<Contact[]> {
|
|
this._invalidateCache("contacts_all");
|
|
return await this.$contacts();
|
|
},
|
|
|
|
/**
|
|
* Clear all caches for this component - $clearAllCaches()
|
|
* Useful for manual cache management
|
|
*/
|
|
$clearAllCaches(): void {
|
|
this._clearCache();
|
|
},
|
|
|
|
// =================================================
|
|
// HIGH-LEVEL ENTITY OPERATIONS (eliminate verbose SQL patterns)
|
|
// =================================================
|
|
|
|
/**
|
|
* Map SQL query results to typed objects - $mapResults()
|
|
* Eliminates verbose row mapping patterns
|
|
* @param results SQL query results
|
|
* @param mapper Function to map each row to an object
|
|
* @returns Array of mapped objects
|
|
*/
|
|
$mapResults<T>(
|
|
results: QueryExecResult | undefined,
|
|
mapper: (row: unknown[]) => T,
|
|
): T[] {
|
|
if (!results?.values) return [];
|
|
return results.values.map(mapper);
|
|
},
|
|
|
|
/**
|
|
* Insert or replace contact - $insertContact()
|
|
* Eliminates verbose INSERT OR REPLACE patterns
|
|
* @param contact Contact object to insert
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $insertContact(contact: Partial<Contact>): Promise<boolean> {
|
|
try {
|
|
// Convert undefined values to null for SQL.js compatibility
|
|
const safeContact = {
|
|
did: contact.did !== undefined ? contact.did : null,
|
|
name: contact.name !== undefined ? contact.name : null,
|
|
publicKeyBase64:
|
|
contact.publicKeyBase64 !== undefined
|
|
? contact.publicKeyBase64
|
|
: null,
|
|
seesMe: contact.seesMe !== undefined ? contact.seesMe : null,
|
|
registered:
|
|
contact.registered !== undefined ? contact.registered : null,
|
|
nextPubKeyHashB64:
|
|
contact.nextPubKeyHashB64 !== undefined
|
|
? contact.nextPubKeyHashB64
|
|
: null,
|
|
profileImageUrl:
|
|
contact.profileImageUrl !== undefined
|
|
? contact.profileImageUrl
|
|
: null,
|
|
};
|
|
|
|
await this.$dbExec(
|
|
`INSERT OR REPLACE INTO contacts
|
|
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
safeContact.did,
|
|
safeContact.name,
|
|
safeContact.publicKeyBase64,
|
|
safeContact.seesMe,
|
|
safeContact.registered,
|
|
safeContact.nextPubKeyHashB64,
|
|
safeContact.profileImageUrl,
|
|
],
|
|
);
|
|
// Invalidate contacts cache
|
|
this._invalidateCache("contacts_all");
|
|
return true;
|
|
} catch (error) {
|
|
logger.error("[PlatformServiceMixin] Error inserting contact:", error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update contact - $updateContact()
|
|
* Eliminates verbose UPDATE patterns
|
|
* @param did Contact DID to update
|
|
* @param changes Partial contact changes
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $updateContact(
|
|
did: string,
|
|
changes: Partial<Contact>,
|
|
): Promise<boolean> {
|
|
try {
|
|
const setParts: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
Object.entries(changes).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
setParts.push(`${key} = ?`);
|
|
params.push(value);
|
|
}
|
|
});
|
|
|
|
if (setParts.length === 0) return true;
|
|
|
|
params.push(did);
|
|
await this.$dbExec(
|
|
`UPDATE contacts SET ${setParts.join(", ")} WHERE did = ?`,
|
|
params,
|
|
);
|
|
|
|
// Invalidate contacts cache
|
|
this._invalidateCache("contacts_all");
|
|
return true;
|
|
} catch (error) {
|
|
logger.error("[PlatformServiceMixin] Error updating contact:", error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get all contacts as typed objects - $getAllContacts()
|
|
* Eliminates verbose query + mapping patterns
|
|
* @returns Promise<Contact[]> Array of contact objects
|
|
*/
|
|
async $getAllContacts(): Promise<Contact[]> {
|
|
const results = await this.$dbQuery(
|
|
"SELECT did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl FROM contacts ORDER BY name",
|
|
);
|
|
|
|
return this.$mapResults(results, (row: unknown[]) => ({
|
|
did: row[0] as string,
|
|
name: row[1] as string,
|
|
publicKeyBase64: row[2] as string,
|
|
seesMe: Boolean(row[3]),
|
|
registered: Boolean(row[4]),
|
|
nextPubKeyHashB64: row[5] as string,
|
|
profileImageUrl: row[6] as string,
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Generic entity insertion - $insertEntity()
|
|
* Eliminates verbose INSERT patterns for any entity
|
|
* @param tableName Database table name
|
|
* @param entity Entity object to insert
|
|
* @param fields Array of field names to insert
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $insertEntity(
|
|
tableName: string,
|
|
entity: Record<string, unknown>,
|
|
fields: string[],
|
|
): Promise<boolean> {
|
|
try {
|
|
const placeholders = fields.map(() => "?").join(", ");
|
|
// Convert undefined values to null for SQL.js compatibility
|
|
const values = fields.map((field) =>
|
|
entity[field] !== undefined ? entity[field] : null,
|
|
);
|
|
|
|
await this.$dbExec(
|
|
`INSERT OR REPLACE INTO ${tableName} (${fields.join(", ")}) VALUES (${placeholders})`,
|
|
values,
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(
|
|
`[PlatformServiceMixin] Error inserting entity into ${tableName}:`,
|
|
error,
|
|
);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update settings with direct SQL - $updateSettings()
|
|
* Eliminates verbose settings update patterns
|
|
* @param changes Settings changes to apply
|
|
* @param did Optional DID for user-specific settings
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $updateSettings(
|
|
changes: Partial<Settings>,
|
|
did?: string,
|
|
): Promise<boolean> {
|
|
try {
|
|
// Use databaseUtil methods which handle the correct schema
|
|
if (did) {
|
|
return await databaseUtil.updateDidSpecificSettings(did, changes);
|
|
} else {
|
|
return await databaseUtil.updateDefaultSettings(changes);
|
|
}
|
|
} catch (error) {
|
|
logger.error("[PlatformServiceMixin] Error updating settings:", error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get settings row as array - $getSettingsRow()
|
|
* Eliminates verbose settings retrieval patterns
|
|
* @param fields Array of field names to retrieve
|
|
* @param did Optional DID for user-specific settings
|
|
* @returns Promise<unknown[]> Settings row as array
|
|
*/
|
|
async $getSettingsRow(
|
|
fields: string[],
|
|
did?: string,
|
|
): Promise<unknown[] | undefined> {
|
|
// Use correct settings table schema
|
|
const whereClause = did ? "WHERE accountDid = ?" : "WHERE id = ?";
|
|
const params = did ? [did] : [MASTER_SETTINGS_KEY];
|
|
|
|
return await this.$one(
|
|
`SELECT ${fields.join(", ")} FROM settings ${whereClause}`,
|
|
params,
|
|
);
|
|
},
|
|
},
|
|
};
|
|
|
|
// =================================================
|
|
// TYPESCRIPT INTERFACES
|
|
// =================================================
|
|
|
|
/**
|
|
* Enhanced interface with caching utility methods
|
|
*/
|
|
export interface IPlatformServiceMixin {
|
|
platformService: PlatformService;
|
|
$dbQuery(
|
|
sql: string,
|
|
params?: unknown[],
|
|
): Promise<QueryExecResult | undefined>;
|
|
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
|
|
$dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
|
|
$getSettings(
|
|
key: string,
|
|
fallback?: Settings | null,
|
|
): Promise<Settings | null>;
|
|
$getMergedSettings(
|
|
defaultKey: string,
|
|
accountDid?: string,
|
|
defaultFallback?: Settings,
|
|
): Promise<Settings>;
|
|
$withTransaction<T>(callback: () => Promise<T>): Promise<T>;
|
|
isCapacitor: boolean;
|
|
isWeb: boolean;
|
|
isElectron: boolean;
|
|
capabilities: PlatformCapabilities;
|
|
|
|
// High-level entity operations
|
|
$mapResults<T>(
|
|
results: QueryExecResult | undefined,
|
|
mapper: (row: unknown[]) => T,
|
|
): T[];
|
|
$insertContact(contact: Partial<Contact>): Promise<boolean>;
|
|
$updateContact(did: string, changes: Partial<Contact>): Promise<boolean>;
|
|
$getAllContacts(): Promise<Contact[]>;
|
|
$insertEntity(
|
|
tableName: string,
|
|
entity: Record<string, unknown>,
|
|
fields: string[],
|
|
): Promise<boolean>;
|
|
$updateSettings(changes: Partial<Settings>, did?: string): Promise<boolean>;
|
|
$getSettingsRow(
|
|
fields: string[],
|
|
did?: string,
|
|
): Promise<unknown[] | undefined>;
|
|
}
|
|
|
|
// TypeScript declaration merging to eliminate (this as any) type assertions
|
|
declare module "@vue/runtime-core" {
|
|
interface ComponentCustomProperties {
|
|
// Core platform service access
|
|
platformService: PlatformService;
|
|
isCapacitor: boolean;
|
|
isWeb: boolean;
|
|
isElectron: boolean;
|
|
capabilities: PlatformCapabilities;
|
|
|
|
// Ultra-concise database methods (shortest possible names)
|
|
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
|
|
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
|
|
$one(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
|
|
|
|
// Query + mapping combo methods
|
|
$query<T = Record<string, unknown>>(
|
|
sql: string,
|
|
params?: unknown[],
|
|
): Promise<T[]>;
|
|
$first<T = Record<string, unknown>>(
|
|
sql: string,
|
|
params?: unknown[],
|
|
): Promise<T | null>;
|
|
|
|
// Enhanced utility methods
|
|
$dbQuery(
|
|
sql: string,
|
|
params?: unknown[],
|
|
): Promise<QueryExecResult | undefined>;
|
|
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
|
|
$dbGetOneRow(
|
|
sql: string,
|
|
params?: unknown[],
|
|
): Promise<unknown[] | undefined>;
|
|
$getSettings(
|
|
key: string,
|
|
defaults?: Settings | null,
|
|
): Promise<Settings | null>;
|
|
$getMergedSettings(
|
|
key: string,
|
|
did?: string,
|
|
defaults?: Settings,
|
|
): Promise<Settings>;
|
|
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
|
|
// Specialized shortcuts - contacts cached, settings fresh
|
|
$contacts(): Promise<Contact[]>;
|
|
$settings(defaults?: Settings): Promise<Settings>;
|
|
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
|
|
|
// Settings update shortcuts (eliminate 90% boilerplate)
|
|
$saveSettings(changes: Partial<Settings>): Promise<boolean>;
|
|
$saveUserSettings(
|
|
did: string,
|
|
changes: Partial<Settings>,
|
|
): Promise<boolean>;
|
|
$saveMySettings(changes: Partial<Settings>): Promise<boolean>;
|
|
|
|
// Cache management methods
|
|
$refreshSettings(): Promise<Settings>;
|
|
$refreshContacts(): Promise<Contact[]>;
|
|
$clearAllCaches(): void;
|
|
|
|
// High-level entity operations (eliminate verbose SQL patterns)
|
|
$mapResults<T>(
|
|
results: QueryExecResult | undefined,
|
|
mapper: (row: unknown[]) => T,
|
|
): T[];
|
|
$insertContact(contact: Partial<Contact>): Promise<boolean>;
|
|
$updateContact(did: string, changes: Partial<Contact>): Promise<boolean>;
|
|
$getAllContacts(): Promise<Contact[]>;
|
|
$insertEntity(
|
|
tableName: string,
|
|
entity: Record<string, unknown>,
|
|
fields: string[],
|
|
): Promise<boolean>;
|
|
$updateSettings(changes: Partial<Settings>, did?: string): Promise<boolean>;
|
|
$getSettingsRow(
|
|
fields: string[],
|
|
did?: string,
|
|
): Promise<unknown[] | undefined>;
|
|
}
|
|
}
|