forked from jsnbuchanan/crowd-funder-for-time-pwa
- Fix TypeScript compilation errors in deepLinks service by replacing logConsoleAndDb with logger.error - Add ESLint disable comments for necessary 'any' type usage in worker polyfills and Vue mixins - Add ESLint disable comments for console statements in test files and debugging code - Production build now succeeds with npm run build:web:prod - TypeScript compilation passes with npm run type-check The deepLinks service was using undefined logConsoleAndDb function causing build failures. Worker context polyfills and Vue mixin complexity require 'any' type usage in specific cases. Console statements in test files and debugging code are intentionally used for development.
1853 lines
58 KiB
TypeScript
1853 lines
58 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-06-25 - Added high-level entity operations for code reduction
|
|
*/
|
|
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
import type {
|
|
PlatformService,
|
|
PlatformCapabilities,
|
|
} from "@/services/PlatformService";
|
|
import {
|
|
MASTER_SETTINGS_KEY,
|
|
type Settings,
|
|
type SettingsWithJsonStrings,
|
|
} from "@/db/tables/settings";
|
|
import { logger } from "@/utils/logger";
|
|
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
|
|
import { Account } from "@/db/tables/accounts";
|
|
import { Temp } from "@/db/tables/temp";
|
|
import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
|
|
import {
|
|
generateInsertStatement,
|
|
generateUpdateStatement,
|
|
} from "@/utils/sqlHelpers";
|
|
|
|
// =================================================
|
|
// 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 = {
|
|
// default: 15000, // 15 seconds default TTL
|
|
// } as const;
|
|
|
|
const _memoryLogs: string[] = [];
|
|
|
|
/**
|
|
* 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,
|
|
// Track the current activeDid for change detection
|
|
_currentActiveDid: null as string | 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!;
|
|
},
|
|
|
|
/**
|
|
* Current active DID from settings
|
|
* Used for change detection and component updates
|
|
*/
|
|
currentActiveDid(): string | null {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (this as any)._currentActiveDid;
|
|
},
|
|
|
|
/**
|
|
* Access to in-memory logs array
|
|
* Provides direct access to memoryLogs without requiring databaseUtil import
|
|
*/
|
|
$memoryLogs(): string[] {
|
|
return _memoryLogs;
|
|
},
|
|
|
|
/**
|
|
* Platform detection utilities
|
|
*/
|
|
isCapacitor(): boolean {
|
|
// @ts-expect-error Accessing computed property value from Vue instance
|
|
return this["platformService"].isCapacitor();
|
|
},
|
|
|
|
isWeb(): boolean {
|
|
// @ts-expect-error Accessing computed property value from Vue instance
|
|
return this["platformService"].isWeb();
|
|
},
|
|
|
|
isElectron(): boolean {
|
|
// @ts-expect-error Accessing computed property value from Vue instance
|
|
return this["platformService"].isElectron();
|
|
},
|
|
|
|
/**
|
|
* Platform capabilities
|
|
*/
|
|
capabilities() {
|
|
// @ts-expect-error Accessing computed property value from Vue instance
|
|
return this["platformService"].getCapabilities();
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
/**
|
|
* Watch for changes in the current activeDid
|
|
* Triggers component updates when user switches identities
|
|
*/
|
|
currentActiveDid: {
|
|
handler(newDid: string | null, oldDid: string | null) {
|
|
if (newDid !== oldDid) {
|
|
logger.debug(
|
|
`[PlatformServiceMixin] ActiveDid changed from ${oldDid} to ${newDid}`,
|
|
);
|
|
// // Clear caches that might be affected by the change
|
|
// (this as any).$clearAllCaches();
|
|
}
|
|
},
|
|
immediate: true,
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
// =================================================
|
|
// SELF-CONTAINED UTILITY METHODS (no databaseUtil dependency)
|
|
// =================================================
|
|
|
|
/**
|
|
* Update the current activeDid and trigger change detection
|
|
* This method should be called when the user switches identities
|
|
*/
|
|
async $updateActiveDid(newDid: string | null): Promise<void> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const oldDid = (this as any)._currentActiveDid;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(this as any)._currentActiveDid = newDid;
|
|
|
|
if (newDid !== oldDid) {
|
|
logger.debug(
|
|
`[PlatformServiceMixin] ActiveDid updated from ${oldDid} to ${newDid}`,
|
|
);
|
|
// // Clear caches that might be affected by the change
|
|
// this.$clearAllCaches();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Map database columns to values with proper type conversion
|
|
* Handles boolean conversion from SQLite integers (0/1) to boolean values
|
|
*/
|
|
_mapColumnsToValues(
|
|
columns: string[],
|
|
values: unknown[][],
|
|
): Array<Record<string, unknown>> {
|
|
return values.map((row) => {
|
|
const obj: Record<string, unknown> = {};
|
|
columns.forEach((column, index) => {
|
|
let value = row[index];
|
|
|
|
// Convert SQLite integer booleans to JavaScript booleans
|
|
if (
|
|
column === "isRegistered" ||
|
|
column === "finishedOnboarding" ||
|
|
column === "filterFeedByVisible" ||
|
|
column === "filterFeedByNearby" ||
|
|
column === "hideRegisterPromptOnNewContact" ||
|
|
column === "showContactGivesInline" ||
|
|
column === "showGeneralAdvanced" ||
|
|
column === "showShortcutBvc" ||
|
|
column === "warnIfProdServer" ||
|
|
column === "warnIfTestServer"
|
|
) {
|
|
if (value === 1) {
|
|
value = true;
|
|
} else if (value === 0) {
|
|
value = false;
|
|
}
|
|
// Keep null values as null
|
|
}
|
|
|
|
// Handle JSON fields like contactMethods
|
|
if (column === "contactMethods" && typeof value === "string") {
|
|
try {
|
|
value = JSON.parse(value);
|
|
} catch {
|
|
value = [];
|
|
}
|
|
}
|
|
|
|
obj[column] = value;
|
|
});
|
|
return obj;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Self-contained implementation of parseJsonField
|
|
* Safely parses JSON strings with fallback to default value
|
|
*
|
|
* Consolidate this with src/libs/util.ts parseJsonField
|
|
*/
|
|
_parseJsonField<T>(value: unknown, defaultValue: T): T {
|
|
if (typeof value === "string") {
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
return (value as T) || defaultValue;
|
|
},
|
|
|
|
/**
|
|
* Convert Settings object to SettingsWithJsonStrings for database storage
|
|
* Handles conversion of complex objects like searchBoxes to JSON strings
|
|
* @param settings Settings object to convert
|
|
* @returns SettingsWithJsonStrings object ready for database storage
|
|
*/
|
|
_convertSettingsForStorage(
|
|
settings: Partial<Settings>,
|
|
): Partial<SettingsWithJsonStrings> {
|
|
const converted = { ...settings } as Partial<SettingsWithJsonStrings>;
|
|
|
|
// Convert searchBoxes array to JSON string if present
|
|
if (settings.searchBoxes !== undefined) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(converted as any).searchBoxes = Array.isArray(settings.searchBoxes)
|
|
? JSON.stringify(settings.searchBoxes)
|
|
: String(settings.searchBoxes);
|
|
}
|
|
|
|
return converted;
|
|
},
|
|
|
|
// // =================================================
|
|
// // 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 = this._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 = this._parseJsonField(settings.searchBoxes, []);
|
|
}
|
|
|
|
return settings;
|
|
} catch (error) {
|
|
logger.error(`[Settings Trace] ❌ 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 = this._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 = this._parseJsonField(
|
|
mergedSettings.searchBoxes,
|
|
[],
|
|
);
|
|
}
|
|
|
|
return mergedSettings;
|
|
} catch (error) {
|
|
logger.error(`[Settings Trace] ❌ 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 = this._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)
|
|
// =================================================
|
|
|
|
/**
|
|
* Normalize contact data by parsing JSON strings into proper objects
|
|
* Handles the contactMethods field which can be either a JSON string or an array
|
|
* @param rawContacts Raw contact data from database
|
|
* @returns Normalized Contact[] array
|
|
*/
|
|
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[] {
|
|
return rawContacts.map((contact) => {
|
|
// Create a new contact object with proper typing
|
|
const normalizedContact: Contact = {
|
|
did: contact.did,
|
|
iViewContent: contact.iViewContent,
|
|
name: contact.name,
|
|
nextPubKeyHashB64: contact.nextPubKeyHashB64,
|
|
notes: contact.notes,
|
|
profileImageUrl: contact.profileImageUrl,
|
|
publicKeyBase64: contact.publicKeyBase64,
|
|
seesMe: contact.seesMe,
|
|
registered: contact.registered,
|
|
};
|
|
|
|
// Handle contactMethods field which can be a JSON string or an array
|
|
if (contact.contactMethods !== undefined) {
|
|
if (typeof contact.contactMethods === "string") {
|
|
// Parse JSON string into array
|
|
normalizedContact.contactMethods = this._parseJsonField(
|
|
contact.contactMethods,
|
|
[],
|
|
);
|
|
} else if (Array.isArray(contact.contactMethods)) {
|
|
// Validate that each item in the array is a proper ContactMethod object
|
|
normalizedContact.contactMethods = contact.contactMethods.filter(
|
|
(method) => {
|
|
const isValid =
|
|
method &&
|
|
typeof method === "object" &&
|
|
typeof method.label === "string" &&
|
|
typeof method.type === "string" &&
|
|
typeof method.value === "string";
|
|
|
|
if (!isValid && method !== undefined) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(
|
|
"[ContactNormalization] Invalid contact method:",
|
|
method,
|
|
);
|
|
}
|
|
|
|
return isValid;
|
|
},
|
|
);
|
|
} else {
|
|
// Invalid data, use empty array
|
|
normalizedContact.contactMethods = [];
|
|
}
|
|
} else {
|
|
// No contactMethods, use empty array
|
|
normalizedContact.contactMethods = [];
|
|
}
|
|
|
|
return normalizedContact;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Load all contacts (always fresh) - $contacts()
|
|
* Always fetches fresh data from database for consistency
|
|
* Handles JSON string/object duality for contactMethods field
|
|
* @returns Promise<Contact[]> Array of normalized contact objects
|
|
*/
|
|
async $contacts(): Promise<Contact[]> {
|
|
const rawContacts = (await this.$query(
|
|
"SELECT * FROM contacts ORDER BY name",
|
|
)) as ContactMaybeWithJsonStrings[];
|
|
|
|
return this.$normalizeContacts(rawContacts);
|
|
},
|
|
|
|
/**
|
|
* Ultra-concise shortcut for getting number of contacts
|
|
* @returns Promise<number> Total number of contacts
|
|
*/
|
|
async $contactCount(): Promise<number> {
|
|
const countRow = await this.$one("SELECT COUNT(*) FROM contacts");
|
|
return (countRow?.[0] as number) || 0;
|
|
},
|
|
|
|
/**
|
|
* Ultra-concise shortcut for getting all logs from database
|
|
* @returns Promise<Array<Record<string, unknown>>> Array of log records
|
|
*/
|
|
async $logs(): Promise<Array<Record<string, unknown>>> {
|
|
return await this.$query("SELECT * FROM logs ORDER BY date DESC");
|
|
},
|
|
|
|
/**
|
|
* 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> {
|
|
try {
|
|
// Get default settings first
|
|
const defaultSettings = await this.$getSettings(
|
|
MASTER_SETTINGS_KEY,
|
|
defaults,
|
|
);
|
|
|
|
if (!defaultSettings) {
|
|
return defaults;
|
|
}
|
|
|
|
// Determine which DID to use
|
|
const targetDid = did || defaultSettings.activeDid;
|
|
|
|
// If no target DID, return default settings
|
|
if (!targetDid) {
|
|
return defaultSettings;
|
|
}
|
|
|
|
// Get merged settings using existing method
|
|
const mergedSettings = await this.$getMergedSettings(
|
|
MASTER_SETTINGS_KEY,
|
|
targetDid,
|
|
defaultSettings,
|
|
);
|
|
|
|
// **ELECTRON-SPECIFIC FIX**: Force production API endpoints for Electron
|
|
// This ensures Electron doesn't use localhost development servers that might be saved in user settings
|
|
if (process.env.VITE_PLATFORM === "electron") {
|
|
// Import constants dynamically to get platform-specific values
|
|
const { DEFAULT_ENDORSER_API_SERVER } = await import(
|
|
"../constants/app"
|
|
);
|
|
|
|
mergedSettings.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
|
}
|
|
|
|
// Merge with any provided defaults (these take highest precedence)
|
|
// Filter out undefined and empty string values to prevent overriding real settings
|
|
const filteredDefaults = Object.fromEntries(
|
|
Object.entries(defaults).filter(
|
|
([_, value]) => value !== undefined && value !== "",
|
|
),
|
|
);
|
|
|
|
const finalSettings = { ...mergedSettings, ...filteredDefaults };
|
|
return finalSettings;
|
|
} catch (error) {
|
|
logger.error("[Settings Trace] ❌ Error in $accountSettings:", error);
|
|
|
|
// Fallback to defaults on error
|
|
return defaults;
|
|
}
|
|
},
|
|
|
|
// =================================================
|
|
// SETTINGS UPDATE SHORTCUTS (eliminate 90% boilerplate)
|
|
// =================================================
|
|
|
|
/**
|
|
* Save default settings - $saveSettings()
|
|
* Ultra-concise shortcut for updateDefaultSettings
|
|
*
|
|
* ✅ KEEP: This method will be the primary settings save method after consolidation
|
|
*
|
|
* @param changes Settings changes to save
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $saveSettings(changes: Partial<Settings>): Promise<boolean> {
|
|
try {
|
|
// Remove fields that shouldn't be updated
|
|
const { accountDid, id, ...safeChanges } = changes;
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
void accountDid;
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
void id;
|
|
|
|
if (Object.keys(safeChanges).length === 0) return true;
|
|
|
|
// Convert settings for database storage (handles searchBoxes conversion)
|
|
const convertedChanges = this._convertSettingsForStorage(safeChanges);
|
|
|
|
const setParts: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
Object.entries(convertedChanges).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
setParts.push(`${key} = ?`);
|
|
params.push(value);
|
|
}
|
|
});
|
|
|
|
if (setParts.length === 0) return true;
|
|
|
|
params.push(MASTER_SETTINGS_KEY);
|
|
await this.$dbExec(
|
|
`UPDATE settings SET ${setParts.join(", ")} WHERE id = ?`,
|
|
params,
|
|
);
|
|
|
|
// Update activeDid tracking if it changed
|
|
if (changes.activeDid !== undefined) {
|
|
await this.$updateActiveDid(changes.activeDid);
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error("[PlatformServiceMixin] Error saving settings:", error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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> {
|
|
try {
|
|
// Remove fields that shouldn't be updated
|
|
const { id, ...safeChanges } = changes;
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
void id;
|
|
safeChanges.accountDid = did;
|
|
|
|
if (Object.keys(safeChanges).length === 0) return true;
|
|
|
|
// Convert settings for database storage (handles searchBoxes conversion)
|
|
const convertedChanges = this._convertSettingsForStorage(safeChanges);
|
|
|
|
const setParts: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
Object.entries(convertedChanges).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
setParts.push(`${key} = ?`);
|
|
params.push(value);
|
|
}
|
|
});
|
|
|
|
if (setParts.length === 0) return true;
|
|
|
|
params.push(did);
|
|
const sql = `UPDATE settings SET ${setParts.join(", ")} WHERE accountDid = ?`;
|
|
|
|
await this.$dbExec(sql, params);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(
|
|
"[PlatformServiceMixin] Error saving user settings:",
|
|
error,
|
|
);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
},
|
|
|
|
/**
|
|
* Get fresh contacts from database - $refreshContacts()
|
|
* Always returns fresh data (no caching)
|
|
*/
|
|
async $refreshContacts(): Promise<Contact[]> {
|
|
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);
|
|
},
|
|
|
|
/**
|
|
* Maps a SQLite query result to an array of objects
|
|
* @param record The query result from SQLite
|
|
* @returns Array of objects where each object maps column names to their corresponding values
|
|
*/
|
|
$mapQueryResultToValues(
|
|
record: QueryExecResult | undefined,
|
|
): Array<Record<string, unknown>> {
|
|
if (!record) {
|
|
return [];
|
|
}
|
|
return this.$mapColumnsToValues(record.columns, record.values) as Array<
|
|
Record<string, unknown>
|
|
>;
|
|
},
|
|
|
|
/**
|
|
* Public method for mapping database columns to values
|
|
* Provides the same functionality as _mapColumnsToValues but as a public method
|
|
*/
|
|
$mapColumnsToValues(
|
|
columns: string[],
|
|
values: unknown[][],
|
|
): Array<Record<string, unknown>> {
|
|
return this._mapColumnsToValues(columns, values);
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
contactMethods:
|
|
contact.contactMethods !== undefined
|
|
? Array.isArray(contact.contactMethods)
|
|
? JSON.stringify(contact.contactMethods)
|
|
: contact.contactMethods
|
|
: null,
|
|
};
|
|
|
|
await this.$dbExec(
|
|
`INSERT OR REPLACE INTO contacts
|
|
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, contactMethods)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
safeContact.did,
|
|
safeContact.name,
|
|
safeContact.publicKeyBase64,
|
|
safeContact.seesMe,
|
|
safeContact.registered,
|
|
safeContact.nextPubKeyHashB64,
|
|
safeContact.profileImageUrl,
|
|
safeContact.contactMethods,
|
|
],
|
|
);
|
|
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} = ?`);
|
|
|
|
// Handle contactMethods field - convert array to JSON string
|
|
if (key === "contactMethods" && Array.isArray(value)) {
|
|
params.push(JSON.stringify(value));
|
|
} else {
|
|
params.push(value);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (setParts.length === 0) return true;
|
|
|
|
params.push(did);
|
|
await this.$dbExec(
|
|
`UPDATE contacts SET ${setParts.join(", ")} WHERE did = ?`,
|
|
params,
|
|
);
|
|
|
|
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
|
|
* Handles JSON string/object duality for contactMethods field
|
|
* @returns Promise<Contact[]> Array of normalized contact objects
|
|
*/
|
|
async $getAllContacts(): Promise<Contact[]> {
|
|
const rawContacts = (await this.$query(
|
|
"SELECT * FROM contacts ORDER BY name",
|
|
)) as ContactMaybeWithJsonStrings[];
|
|
|
|
return this.$normalizeContacts(rawContacts);
|
|
},
|
|
|
|
/**
|
|
* Get single contact by DID - $getContact()
|
|
* Eliminates verbose single contact query patterns
|
|
* Handles JSON string/object duality for contactMethods field
|
|
* @param did Contact DID to retrieve
|
|
* @returns Promise<Contact | null> Normalized contact object or null if not found
|
|
*/
|
|
async $getContact(did: string): Promise<Contact | null> {
|
|
const rawContacts = (await this.$query(
|
|
"SELECT * FROM contacts WHERE did = ?",
|
|
[did],
|
|
)) as ContactMaybeWithJsonStrings[];
|
|
|
|
if (rawContacts.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedContacts = this.$normalizeContacts(rawContacts);
|
|
return normalizedContacts[0];
|
|
},
|
|
|
|
/**
|
|
* Delete contact by DID - $deleteContact()
|
|
* Eliminates verbose contact deletion patterns
|
|
* @param did Contact DID to delete
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $deleteContact(did: string): Promise<boolean> {
|
|
try {
|
|
await this.$dbExec("DELETE FROM contacts WHERE did = ?", [did]);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error("[PlatformServiceMixin] Error deleting contact:", error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get all accounts - $getAllAccounts()
|
|
* Retrieves all account metadata from the accounts table
|
|
* @returns Promise<Account[]> Array of account objects
|
|
*/
|
|
async $getAllAccounts(): Promise<Account[]> {
|
|
try {
|
|
return await this.$query<Account>("SELECT * FROM accounts");
|
|
} catch (error) {
|
|
logger.error(
|
|
"[PlatformServiceMixin] Error getting all accounts:",
|
|
error,
|
|
);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get all account DIDs - $getAllAccountDids()
|
|
* Retrieves all account DIDs from the accounts table
|
|
* @returns Promise<string[]> Array of account DIDs
|
|
*/
|
|
async $getAllAccountDids(): Promise<string[]> {
|
|
try {
|
|
const accounts = await this.$query<Account>("SELECT did FROM accounts");
|
|
return accounts.map((account) => account.did);
|
|
} catch (error) {
|
|
logger.error(
|
|
"[PlatformServiceMixin] Error getting all account DIDs:",
|
|
error,
|
|
);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
// =================================================
|
|
// TEMP TABLE METHODS (for temporary storage)
|
|
// =================================================
|
|
|
|
/**
|
|
* Get temporary data by ID - $getTemp()
|
|
* Retrieves temporary data from the temp table
|
|
* @param id Temporary storage ID
|
|
* @returns Promise<Temp | null> Temporary data or null if not found
|
|
*/
|
|
async $getTemp(id: string): Promise<Temp | null> {
|
|
try {
|
|
return await this.$first<Temp>("SELECT * FROM temp WHERE id = ?", [id]);
|
|
} catch (error) {
|
|
logger.error("[PlatformServiceMixin] Error getting temp data:", error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete temporary data by ID - $deleteTemp()
|
|
* Removes temporary data from the temp table
|
|
* @param id Temporary storage ID
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $deleteTemp(id: string): Promise<boolean> {
|
|
try {
|
|
await this.$dbExec("DELETE FROM temp WHERE id = ?", [id]);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error("[PlatformServiceMixin] Error deleting temp data:", error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
/**
|
|
* Update settings - $updateSettings()
|
|
* Ultra-concise shortcut for updating settings (default or user-specific)
|
|
*
|
|
* ⚠️ DEPRECATED: This method will be removed in favor of $saveSettings()
|
|
* Use $saveSettings(changes, did?) instead for better consistency
|
|
*
|
|
* @param changes Settings changes to save
|
|
* @param did Optional DID for user-specific settings
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $updateSettings(
|
|
changes: Partial<Settings>,
|
|
did?: string,
|
|
): Promise<boolean> {
|
|
try {
|
|
// Use self-contained methods which handle the correct schema
|
|
if (did) {
|
|
return await this.$saveUserSettings(did, changes);
|
|
} else {
|
|
return await this.$saveSettings(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,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Update entity with direct SQL - $updateEntity()
|
|
* Eliminates verbose UPDATE patterns for any table
|
|
* @param tableName Name of the table to update
|
|
* @param entity Object containing fields to update
|
|
* @param whereClause WHERE clause for the update (e.g. "id = ?")
|
|
* @param whereParams Parameters for the WHERE clause
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $updateEntity(
|
|
tableName: string,
|
|
entity: Record<string, unknown>,
|
|
whereClause: string,
|
|
whereParams: unknown[],
|
|
): Promise<boolean> {
|
|
try {
|
|
const setParts: string[] = [];
|
|
const params: unknown[] = [];
|
|
|
|
Object.entries(entity).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
setParts.push(`${key} = ?`);
|
|
// Convert values to SQLite-compatible types
|
|
let convertedValue = value ?? null;
|
|
if (convertedValue !== null) {
|
|
if (typeof convertedValue === "object") {
|
|
// Convert objects and arrays to JSON strings
|
|
convertedValue = JSON.stringify(convertedValue);
|
|
} else if (typeof convertedValue === "boolean") {
|
|
// Convert boolean to integer (0 or 1)
|
|
convertedValue = convertedValue ? 1 : 0;
|
|
}
|
|
}
|
|
params.push(convertedValue);
|
|
}
|
|
});
|
|
|
|
if (setParts.length === 0) return true;
|
|
|
|
const sql = `UPDATE ${tableName} SET ${setParts.join(", ")} WHERE ${whereClause}`;
|
|
await this.$dbExec(sql, [...params, ...whereParams]);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(
|
|
`[PlatformServiceMixin] Error updating entity in ${tableName}:`,
|
|
error,
|
|
);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Insert user-specific settings - $insertUserSettings()
|
|
* Creates new settings record for a specific DID
|
|
* @param did DID identifier for the user
|
|
* @param settings Settings to insert (accountDid will be set automatically)
|
|
* @returns Promise<boolean> Success status
|
|
*/
|
|
async $insertUserSettings(
|
|
did: string,
|
|
settings: Partial<Settings>,
|
|
): Promise<boolean> {
|
|
try {
|
|
// Ensure accountDid is set and remove id to avoid conflicts
|
|
const { id, ...safeSettings } = settings;
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
void id;
|
|
const insertSettings = { ...safeSettings, accountDid: did };
|
|
|
|
// Convert to SQL-compatible values
|
|
const fields = Object.keys(insertSettings);
|
|
const values = fields.map((field) => {
|
|
const value = insertSettings[field as keyof typeof insertSettings];
|
|
if (value === undefined) return null;
|
|
if (typeof value === "object" && value !== null) {
|
|
return JSON.stringify(value);
|
|
}
|
|
if (typeof value === "boolean") {
|
|
return value ? 1 : 0;
|
|
}
|
|
return value;
|
|
});
|
|
|
|
const placeholders = fields.map(() => "?").join(", ");
|
|
await this.$dbExec(
|
|
`INSERT OR REPLACE INTO settings (${fields.join(", ")}) VALUES (${placeholders})`,
|
|
values,
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(
|
|
"[PlatformServiceMixin] Error inserting user settings:",
|
|
error,
|
|
);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// =================================================
|
|
// LOGGING METHODS (convenience methods for components)
|
|
// =================================================
|
|
|
|
/**
|
|
* Log message to database - $log()
|
|
* @param message Message to log
|
|
* @param level Log level (info, warn, error)
|
|
* @returns Promise<void>
|
|
*/
|
|
async $log(message: string, level?: string): Promise<void> {
|
|
return logger.toDb(message, level);
|
|
},
|
|
|
|
/**
|
|
* Log error message to database - $logError()
|
|
* @param message Error message to log
|
|
* @returns Promise<void>
|
|
*/
|
|
async $logError(message: string): Promise<void> {
|
|
return logger.toDb(message, "error");
|
|
},
|
|
|
|
/**
|
|
* Log message to console and database - $logAndConsole()
|
|
* @param message Message to log
|
|
* @param isError Whether this is an error message
|
|
* @returns Promise<void>
|
|
*/
|
|
async $logAndConsole(message: string, isError = false): Promise<void> {
|
|
return logger.toConsoleAndDb(message, isError);
|
|
},
|
|
|
|
$appendToMemoryLogs(message: string): void {
|
|
_memoryLogs.push(`${new Date().toISOString()}: ${message}`);
|
|
if (_memoryLogs.length > 1000) {
|
|
_memoryLogs.splice(0, _memoryLogs.length - 1000);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Public wrapper for generateInsertStatement
|
|
*/
|
|
$generateInsertStatement(
|
|
model: Record<string, unknown>,
|
|
tableName: string,
|
|
): { sql: string; params: unknown[] } {
|
|
return generateInsertStatement(model, tableName);
|
|
},
|
|
|
|
/**
|
|
* Public wrapper for generateUpdateStatement
|
|
*/
|
|
$generateUpdateStatement(
|
|
model: Record<string, unknown>,
|
|
tableName: string,
|
|
whereClause: string,
|
|
whereParams: unknown[] = [],
|
|
): { sql: string; params: unknown[] } {
|
|
return generateUpdateStatement(
|
|
model,
|
|
tableName,
|
|
whereClause,
|
|
whereParams,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Debug method to verify settings for a specific DID
|
|
* Useful for troubleshooting settings propagation issues
|
|
* @param did DID to check settings for
|
|
* @returns Promise<Settings | null> Settings object or null if not found
|
|
*/
|
|
async $debugDidSettings(did: string): Promise<Settings | null> {
|
|
try {
|
|
const result = await this.$dbQuery(
|
|
"SELECT * FROM settings WHERE accountDid = ?",
|
|
[did],
|
|
);
|
|
|
|
if (!result?.values?.length) {
|
|
logger.warn(
|
|
`[PlatformServiceMixin] No settings found for DID: ${did}`,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const mappedResults = this._mapColumnsToValues(
|
|
result.columns,
|
|
result.values,
|
|
);
|
|
|
|
if (!mappedResults.length) {
|
|
logger.warn(
|
|
`[PlatformServiceMixin] Failed to map settings for DID: ${did}`,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const settings = mappedResults[0] as Settings;
|
|
|
|
logger.info(`[PlatformServiceMixin] Settings for DID ${did}:`, {
|
|
firstName: settings.firstName,
|
|
isRegistered: settings.isRegistered,
|
|
activeDid: settings.activeDid,
|
|
apiServer: settings.apiServer,
|
|
});
|
|
|
|
return settings;
|
|
} catch (error) {
|
|
logger.error(
|
|
`[PlatformServiceMixin] Error debugging settings for DID ${did}:`,
|
|
error,
|
|
);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Debug method to verify merged settings for a specific DID
|
|
* Shows both default and DID-specific settings
|
|
* @param did DID to check merged settings for
|
|
* @returns Promise<void> Logs debug information
|
|
*/
|
|
async $debugMergedSettings(did: string): Promise<void> {
|
|
try {
|
|
// Get default settings
|
|
const defaultSettings = await this.$getSettings(
|
|
MASTER_SETTINGS_KEY,
|
|
{},
|
|
);
|
|
logger.info(
|
|
`[PlatformServiceMixin] Default settings:`,
|
|
defaultSettings,
|
|
);
|
|
|
|
// Get DID-specific settings
|
|
const didSettings = await this.$debugDidSettings(did);
|
|
|
|
// Get merged settings
|
|
const mergedSettings = await this.$getMergedSettings(
|
|
MASTER_SETTINGS_KEY,
|
|
did,
|
|
defaultSettings || {},
|
|
);
|
|
|
|
logger.info(`[PlatformServiceMixin] Merged settings for ${did}:`, {
|
|
defaultSettings,
|
|
didSettings,
|
|
mergedSettings,
|
|
isRegistered: mergedSettings.isRegistered,
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`,
|
|
error,
|
|
);
|
|
}
|
|
},
|
|
},
|
|
};
|
|
|
|
// =================================================
|
|
// 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[]>;
|
|
$getContact(did: string): Promise<Contact | null>;
|
|
$deleteContact(did: string): Promise<boolean>;
|
|
$contactCount(): Promise<number>;
|
|
$getAllAccounts(): Promise<Account[]>;
|
|
$getAllAccountDids(): Promise<string[]>;
|
|
$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>;
|
|
$updateEntity(
|
|
tableName: string,
|
|
entity: Record<string, unknown>,
|
|
whereClause: string,
|
|
whereParams: unknown[],
|
|
): Promise<boolean>;
|
|
$insertUserSettings(
|
|
did: string,
|
|
settings: Partial<Settings>,
|
|
): Promise<boolean>;
|
|
$getTemp(id: string): Promise<Temp | null>;
|
|
$deleteTemp(id: string): Promise<boolean>;
|
|
|
|
// Logging methods
|
|
$log(message: string, level?: string): Promise<void>;
|
|
$logError(message: string): Promise<void>;
|
|
$logAndConsole(message: string, isError?: boolean): Promise<void>;
|
|
|
|
// Memory logs access
|
|
$memoryLogs: string[];
|
|
|
|
// New additions
|
|
$logs(): Promise<Array<Record<string, unknown>>>;
|
|
|
|
// New additions
|
|
$generateInsertStatement(
|
|
model: Record<string, unknown>,
|
|
tableName: string,
|
|
): { sql: string; params: unknown[] };
|
|
$generateUpdateStatement(
|
|
model: Record<string, unknown>,
|
|
tableName: string,
|
|
whereClause: string,
|
|
whereParams?: unknown[],
|
|
): { sql: string; params: unknown[] };
|
|
$mapQueryResultToValues(
|
|
record: QueryExecResult | undefined,
|
|
): Array<Record<string, unknown>>;
|
|
$mapColumnsToValues(
|
|
columns: string[],
|
|
values: unknown[][],
|
|
): Array<Record<string, unknown>>;
|
|
|
|
// Debug methods
|
|
$debugDidSettings(did: string): Promise<Settings | null>;
|
|
$debugMergedSettings(did: string): Promise<void>;
|
|
}
|
|
|
|
// 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;
|
|
|
|
// ActiveDid tracking
|
|
currentActiveDid: string | null;
|
|
$updateActiveDid(newDid: string | null): Promise<void>;
|
|
|
|
// 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[]>;
|
|
$contactCount(): Promise<number>;
|
|
$settings(defaults?: Settings): Promise<Settings>;
|
|
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
|
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
|
|
|
|
// 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[]>;
|
|
$getContact(did: string): Promise<Contact | null>;
|
|
$deleteContact(did: string): Promise<boolean>;
|
|
$getAllAccounts(): Promise<Account[]>;
|
|
$getAllAccountDids(): Promise<string[]>;
|
|
$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>;
|
|
$updateEntity(
|
|
tableName: string,
|
|
entity: Record<string, unknown>,
|
|
whereClause: string,
|
|
whereParams: unknown[],
|
|
): Promise<boolean>;
|
|
$insertUserSettings(
|
|
did: string,
|
|
settings: Partial<Settings>,
|
|
): Promise<boolean>;
|
|
$getTemp(id: string): Promise<Temp | null>;
|
|
$deleteTemp(id: string): Promise<boolean>;
|
|
|
|
// Logging methods
|
|
$log(message: string, level?: string): Promise<void>;
|
|
$logError(message: string): Promise<void>;
|
|
$logAndConsole(message: string, isError?: boolean): Promise<void>;
|
|
|
|
// Memory logs access
|
|
$memoryLogs: string[];
|
|
|
|
// New additions
|
|
$logs(): Promise<Array<Record<string, unknown>>>;
|
|
|
|
// New additions
|
|
$generateInsertStatement(
|
|
model: Record<string, unknown>,
|
|
tableName: string,
|
|
): { sql: string; params: unknown[] };
|
|
$generateUpdateStatement(
|
|
model: Record<string, unknown>,
|
|
tableName: string,
|
|
whereClause: string,
|
|
whereParams?: unknown[],
|
|
): { sql: string; params: unknown[] };
|
|
$mapQueryResultToValues(
|
|
record: QueryExecResult | undefined,
|
|
): Array<Record<string, unknown>>;
|
|
$mapColumnsToValues(
|
|
columns: string[],
|
|
values: unknown[][],
|
|
): Array<Record<string, unknown>>;
|
|
|
|
// Debug methods
|
|
$debugDidSettings(did: string): Promise<Settings | null>;
|
|
$debugMergedSettings(did: string): Promise<void>;
|
|
}
|
|
}
|