From bc618bb13b786859778ce37893a3f0b920c0693c Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sat, 16 Aug 2025 13:51:01 +0000 Subject: [PATCH] feat(typescript): implement type-safe database error handling and eliminate any types - Add comprehensive database error interfaces (DatabaseConstraintError, DatabaseStorageError, DexieError) - Implement type guards for database error handling (isDatabaseError, isDatabaseConstraintError, etc.) - Replace any types with proper TypeScript types in ContactsView, ProjectsView, and IdentitySwitcherView - Implement type-safe error handling patterns using new type guards - Fix dynamic property access with keyof operator for type safety Resolves Priority 1 type safety issues in database operations, project management, and identity switching. --- src/interfaces/common.ts | 78 ++++++++++++++++++++++++++++++ src/views/ContactsView.vue | 35 +++++++++----- src/views/IdentitySwitcherView.vue | 4 +- src/views/ProjectsView.vue | 12 +++-- 4 files changed, 111 insertions(+), 18 deletions(-) diff --git a/src/interfaces/common.ts b/src/interfaces/common.ts index 7aca4120..9267cf70 100644 --- a/src/interfaces/common.ts +++ b/src/interfaces/common.ts @@ -98,3 +98,81 @@ export interface VerifiableCredentialClaim { credentialSubject: ClaimObject; [key: string]: unknown; } + +/** + * Database constraint error types for consistent error handling + */ +export interface DatabaseConstraintError extends Error { + name: "ConstraintError"; + message: string; + constraint?: string; +} + +/** + * Database storage error types for IndexedDB/SQLite operations + */ +export interface DatabaseStorageError extends Error { + name: "StorageError"; + message: string; + code?: string; + constraint?: string; +} + +/** + * Legacy Dexie error types for migration compatibility + */ +export interface DexieError extends Error { + name: string; + message: string; + inner?: unknown; + stack?: string; +} + +/** + * Type guard for database constraint errors + */ +export function isDatabaseConstraintError( + error: unknown, +): error is DatabaseConstraintError { + return error instanceof Error && error.name === "ConstraintError"; +} + +/** + * Type guard for database storage errors + */ +export function isDatabaseStorageError( + error: unknown, +): error is DatabaseStorageError { + return error instanceof Error && error.name === "StorageError"; +} + +/** + * Type guard for legacy Dexie errors + */ +export function isDexieError(error: unknown): error is DexieError { + return ( + error instanceof Error && + (error.name === "DexieError" || + error.message.includes("Key already exists in the object store") || + error.message.includes("ConstraintError")) + ); +} + +/** + * Unified error type for database operations + */ +export type DatabaseError = + | DatabaseConstraintError + | DatabaseStorageError + | DexieError; + +/** + * Type guard for any database error + */ +export function isDatabaseError(error: unknown): error is DatabaseError { + return ( + isDatabaseConstraintError(error) || + isDatabaseStorageError(error) || + isDexieError(error) + ); +} diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 23208aae..965cf711 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -170,6 +170,7 @@ import { logger } from "../utils/logger"; // No longer needed - using PlatformServiceMixin methods // import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; +import { isDatabaseError } from "@/interfaces/common"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { APP_SERVER } from "@/constants/app"; import { @@ -376,7 +377,11 @@ export default class ContactsView extends Vue { "", async (name) => { await this.addContact({ - did: (registration.vc.credentialSubject.agent as any).identifier, + did: ( + registration.vc.credentialSubject.agent as { + identifier: string; + } + ).identifier, name: name, registered: true, }); @@ -387,7 +392,11 @@ export default class ContactsView extends Vue { async () => { // on cancel, will still add the contact await this.addContact({ - did: (registration.vc.credentialSubject.agent as any).identifier, + did: ( + registration.vc.credentialSubject.agent as { + identifier: string; + } + ).identifier, name: "(person who invited you)", registered: true, }); @@ -396,8 +405,7 @@ export default class ContactsView extends Vue { this.showOnboardingInfo(); }, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error: unknown) { const fullError = "Error redeeming invite: " + errorStringForLog(error); this.$logAndConsole(fullError, true); let message = "Got an error sending the invite."; @@ -881,20 +889,21 @@ export default class ContactsView extends Vue { /** * Handle errors during contact addition */ - private handleContactAddError(err: any): void { + private handleContactAddError(err: unknown): void { const fullError = "Error when adding contact to storage: " + errorStringForLog(err); this.$logAndConsole(fullError, true); let message = NOTIFY_CONTACT_IMPORT_ERROR.message; - if ( - (err as any).message?.indexOf("Key already exists in the object store.") > - -1 - ) { - message = NOTIFY_CONTACT_IMPORT_CONFLICT.message; - } - if ((err as any).name === "ConstraintError") { - message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message; + + // Use type-safe error checking with our new type guards + if (isDatabaseError(err)) { + if (err.message.includes("Key already exists in the object store")) { + message = NOTIFY_CONTACT_IMPORT_CONFLICT.message; + } + if (err.name === "ConstraintError") { + message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message; + } } this.notify.error(message, TIMEOUTS.LONG); diff --git a/src/views/IdentitySwitcherView.vue b/src/views/IdentitySwitcherView.vue index 59eedbdf..3dcc6972 100644 --- a/src/views/IdentitySwitcherView.vue +++ b/src/views/IdentitySwitcherView.vue @@ -234,7 +234,9 @@ export default class IdentitySwitcherView extends Vue { { did, settingsKeys: Object.keys(newSettings).filter( - (k) => (newSettings as any)[k] !== undefined, + (k) => + k in newSettings && + newSettings[k as keyof typeof newSettings] !== undefined, ), }, ); diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue index 7c4f0560..a87ce52e 100644 --- a/src/views/ProjectsView.vue +++ b/src/views/ProjectsView.vue @@ -464,8 +464,10 @@ export default class ProjectsView extends Vue { ); this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG); } - } catch (error: any) { - logger.error("Got error loading plans:", error.message || error); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logger.error("Got error loading plans:", errorMessage); this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG); } finally { this.isLoading = false; @@ -578,8 +580,10 @@ export default class ProjectsView extends Vue { ); this.notify.error(NOTIFY_OFFERS_LOAD_ERROR.message, TIMEOUTS.LONG); } - } catch (error: any) { - logger.error("Got error loading offers:", error.message || error); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + logger.error("Got error loading offers:", errorMessage); this.notify.error(NOTIFY_OFFERS_FETCH_ERROR.message, TIMEOUTS.LONG); } finally { this.isLoading = false;