From 73fc32b75dc959b6583d2ed4082e9c330da35c4b Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sat, 7 Jun 2025 06:01:17 +0000 Subject: [PATCH] fix(import): ensure contact import works for both Dexie and absurd-sql backends - Refactor importContacts to handle both Dexie and absurd-sql (SQLite) storage - Add ContactDbRecord interface with all string fields strictly typed (never null) - Add helper functions to coerce null/undefined to empty string for all string fields - Guarantee contactMethods is always stored as a JSON string (never null) - Add runtime validation for required fields (e.g., did) - Ensure imported/updated contacts are type-safe and compatible with both backends - Improve code documentation and maintainability Security: - No sensitive data exposed - All fields validated and sanitized before database write - Consistent data structure across storage backends Testing: - Import tested with both Dexie and absurd-sql backends - Null/undefined fields correctly handled and coerced - No linter/type errors remain --- src/views/ContactImportView.vue | 96 ++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/src/views/ContactImportView.vue b/src/views/ContactImportView.vue index ebbc21da..004b7223 100644 --- a/src/views/ContactImportView.vue +++ b/src/views/ContactImportView.vue @@ -223,6 +223,75 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { decodeEndorserJwt } from "../libs/crypto/vc"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +/** + * Interface for contact data as stored in the database + * Differs from Contact interface in that contactMethods is stored as a JSON string + */ +interface ContactDbRecord { + did: string; + contactMethods: string; + name: string; + notes: string; + profileImageUrl: string; + publicKeyBase64: string; + nextPubKeyHashB64: string; + seesMe: boolean; + registered: boolean; +} + +/** + * Ensures a value is a string, never null or undefined + */ +function safeString(val: unknown): string { + return typeof val === 'string' ? val : (val == null ? '' : String(val)); +} + +/** + * Converts a Contact object to a ContactDbRecord for database storage + * @param contact The contact object to convert + * @returns A ContactDbRecord with contactMethods as a JSON string + * @throws Error if contact.did is missing or invalid + */ +function contactToDbRecord(contact: Contact): ContactDbRecord { + if (!contact.did) { + throw new Error("Contact must have a DID"); + } + + // Convert contactMethods array to JSON string, defaulting to empty array + const contactMethodsStr = (contact.contactMethods != null + ? JSON.stringify(contact.contactMethods) + : "[]"); + + return { + did: safeString(contact.did), // Required field, must be present + contactMethods: contactMethodsStr, + name: safeString(contact.name), + notes: safeString(contact.notes), + profileImageUrl: safeString(contact.profileImageUrl), + publicKeyBase64: safeString(contact.publicKeyBase64), + nextPubKeyHashB64: safeString(contact.nextPubKeyHashB64), + seesMe: contact.seesMe ?? false, + registered: contact.registered ?? false + }; +} + +/** + * Converts a ContactDbRecord back to a Contact object + * @param record The database record to convert + * @returns A Contact object with parsed contactMethods array + */ +function dbRecordToContact(record: ContactDbRecord): Contact { + return { + ...record, + name: safeString(record.name), + notes: safeString(record.notes), + profileImageUrl: safeString(record.profileImageUrl), + publicKeyBase64: safeString(record.publicKeyBase64), + nextPubKeyHashB64: safeString(record.nextPubKeyHashB64), + contactMethods: JSON.parse(record.contactMethods || "[]") + }; +} + /** * Contact Import View Component * @author Matthew Raymer @@ -533,25 +602,36 @@ export default class ContactImportView extends Vue { if (this.contactsSelected[i]) { const contact = this.contactsImporting[i]; const existingContact = this.contactsExisting[contact.did]; + const platformService = PlatformServiceFactory.getInstance(); + + // Convert contact to database record format + const contactToStore = contactToDbRecord(contact); + if (existingContact) { - const platformService = PlatformServiceFactory.getInstance(); - // @ts-expect-error because we're just using the value to store to the DB - contact.contactMethods = JSON.stringify(contact.contactMethods); + // Update existing contact const { sql, params } = databaseUtil.generateUpdateStatement( - contact as unknown as Record, + contactToStore as unknown as Record, "contacts", "did = ?", [contact.did], ); await platformService.dbExec(sql, params); if (USE_DEXIE_DB) { - await db.contacts.update(contact.did, contact); + // For Dexie, we need to parse the contactMethods back to an array + await db.contacts.update(contact.did, dbRecordToContact(contactToStore)); } updatedCount++; } else { - // without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': # could not be cloned. - // DataError: Failed to execute 'add' on 'IDBObjectStore': Evaluating the object store's key path yielded a value that is not a valid key. - await db.contacts.add(R.clone(contact)); + // Add new contact + const { sql, params } = databaseUtil.generateInsertStatement( + contactToStore as unknown as Record, + "contacts", + ); + await platformService.dbExec(sql, params); + if (USE_DEXIE_DB) { + // For Dexie, we need to parse the contactMethods back to an array + await db.contacts.add(dbRecordToContact(contactToStore)); + } importedCount++; } }