forked from jsnbuchanan/crowd-funder-for-time-pwa
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.
This commit is contained in:
@@ -98,3 +98,81 @@ export interface VerifiableCredentialClaim {
|
|||||||
credentialSubject: ClaimObject;
|
credentialSubject: ClaimObject;
|
||||||
[key: string]: unknown;
|
[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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ import { logger } from "../utils/logger";
|
|||||||
// No longer needed - using PlatformServiceMixin methods
|
// No longer needed - using PlatformServiceMixin methods
|
||||||
// import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
// import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
|
import { isDatabaseError } from "@/interfaces/common";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { APP_SERVER } from "@/constants/app";
|
import { APP_SERVER } from "@/constants/app";
|
||||||
import {
|
import {
|
||||||
@@ -376,7 +377,11 @@ export default class ContactsView extends Vue {
|
|||||||
"",
|
"",
|
||||||
async (name) => {
|
async (name) => {
|
||||||
await this.addContact({
|
await this.addContact({
|
||||||
did: (registration.vc.credentialSubject.agent as any).identifier,
|
did: (
|
||||||
|
registration.vc.credentialSubject.agent as {
|
||||||
|
identifier: string;
|
||||||
|
}
|
||||||
|
).identifier,
|
||||||
name: name,
|
name: name,
|
||||||
registered: true,
|
registered: true,
|
||||||
});
|
});
|
||||||
@@ -387,7 +392,11 @@ export default class ContactsView extends Vue {
|
|||||||
async () => {
|
async () => {
|
||||||
// on cancel, will still add the contact
|
// on cancel, will still add the contact
|
||||||
await this.addContact({
|
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)",
|
name: "(person who invited you)",
|
||||||
registered: true,
|
registered: true,
|
||||||
});
|
});
|
||||||
@@ -396,8 +405,7 @@ export default class ContactsView extends Vue {
|
|||||||
this.showOnboardingInfo();
|
this.showOnboardingInfo();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
} catch (error: unknown) {
|
||||||
} catch (error: any) {
|
|
||||||
const fullError = "Error redeeming invite: " + errorStringForLog(error);
|
const fullError = "Error redeeming invite: " + errorStringForLog(error);
|
||||||
this.$logAndConsole(fullError, true);
|
this.$logAndConsole(fullError, true);
|
||||||
let message = "Got an error sending the invite.";
|
let message = "Got an error sending the invite.";
|
||||||
@@ -881,20 +889,21 @@ export default class ContactsView extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Handle errors during contact addition
|
* Handle errors during contact addition
|
||||||
*/
|
*/
|
||||||
private handleContactAddError(err: any): void {
|
private handleContactAddError(err: unknown): void {
|
||||||
const fullError =
|
const fullError =
|
||||||
"Error when adding contact to storage: " + errorStringForLog(err);
|
"Error when adding contact to storage: " + errorStringForLog(err);
|
||||||
this.$logAndConsole(fullError, true);
|
this.$logAndConsole(fullError, true);
|
||||||
|
|
||||||
let message = NOTIFY_CONTACT_IMPORT_ERROR.message;
|
let message = NOTIFY_CONTACT_IMPORT_ERROR.message;
|
||||||
if (
|
|
||||||
(err as any).message?.indexOf("Key already exists in the object store.") >
|
// Use type-safe error checking with our new type guards
|
||||||
-1
|
if (isDatabaseError(err)) {
|
||||||
) {
|
if (err.message.includes("Key already exists in the object store")) {
|
||||||
message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
|
message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
|
||||||
}
|
}
|
||||||
if ((err as any).name === "ConstraintError") {
|
if (err.name === "ConstraintError") {
|
||||||
message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
|
message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notify.error(message, TIMEOUTS.LONG);
|
this.notify.error(message, TIMEOUTS.LONG);
|
||||||
|
|||||||
@@ -234,7 +234,9 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
{
|
{
|
||||||
did,
|
did,
|
||||||
settingsKeys: Object.keys(newSettings).filter(
|
settingsKeys: Object.keys(newSettings).filter(
|
||||||
(k) => (newSettings as any)[k] !== undefined,
|
(k) =>
|
||||||
|
k in newSettings &&
|
||||||
|
newSettings[k as keyof typeof newSettings] !== undefined,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -464,8 +464,10 @@ export default class ProjectsView extends Vue {
|
|||||||
);
|
);
|
||||||
this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG);
|
this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
logger.error("Got error loading plans:", error.message || error);
|
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);
|
this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -578,8 +580,10 @@ export default class ProjectsView extends Vue {
|
|||||||
);
|
);
|
||||||
this.notify.error(NOTIFY_OFFERS_LOAD_ERROR.message, TIMEOUTS.LONG);
|
this.notify.error(NOTIFY_OFFERS_LOAD_ERROR.message, TIMEOUTS.LONG);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
logger.error("Got error loading offers:", error.message || error);
|
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);
|
this.notify.error(NOTIFY_OFFERS_FETCH_ERROR.message, TIMEOUTS.LONG);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user