forked from trent_larson/crowd-funder-for-time-pwa
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
This commit is contained in:
@@ -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];
|
||||||
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
// Convert contact to database record format
|
||||||
|
const contactToStore = contactToDbRecord(contact);
|
||||||
|
|
||||||
if (existingContact) {
|
if (existingContact) {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
// Update existing contact
|
||||||
// @ts-expect-error because we're just using the value to store to the DB
|
|
||||||
contact.contactMethods = JSON.stringify(contact.contactMethods);
|
|
||||||
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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user