Browse Source

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
pull/137/head
Matthew Raymer 2 days ago
parent
commit
73fc32b75d
  1. 96
      src/views/ContactImportView.vue

96
src/views/ContactImportView.vue

@ -223,6 +223,75 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc"; import { decodeEndorserJwt } from "../libs/crypto/vc";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; 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 * Contact Import View Component
* @author Matthew Raymer * @author Matthew Raymer
@ -533,25 +602,36 @@ export default class ContactImportView extends Vue {
if (this.contactsSelected[i]) { if (this.contactsSelected[i]) {
const contact = this.contactsImporting[i]; const contact = this.contactsImporting[i];
const existingContact = this.contactsExisting[contact.did]; const existingContact = this.contactsExisting[contact.did];
if (existingContact) {
const platformService = PlatformServiceFactory.getInstance(); 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); // Convert contact to database record format
const contactToStore = contactToDbRecord(contact);
if (existingContact) {
// Update existing contact
const { sql, params } = databaseUtil.generateUpdateStatement( const { sql, params } = databaseUtil.generateUpdateStatement(
contact as unknown as Record<string, unknown>, contactToStore as unknown as Record<string, unknown>,
"contacts", "contacts",
"did = ?", "did = ?",
[contact.did], [contact.did],
); );
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) { 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++; updatedCount++;
} else { } else {
// without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': #<Object> could not be cloned. // Add new contact
// DataError: Failed to execute 'add' on 'IDBObjectStore': Evaluating the object store's key path yielded a value that is not a valid key. const { sql, params } = databaseUtil.generateInsertStatement(
await db.contacts.add(R.clone(contact)); contactToStore as unknown as Record<string, unknown>,
"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++; importedCount++;
} }
} }

Loading…
Cancel
Save