diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts index 3dfa6e1b..cfb88798 100644 --- a/src/db/tables/contacts.ts +++ b/src/db/tables/contacts.ts @@ -32,6 +32,17 @@ export type ContactWithJsonStrings = Omit & { contactMethods?: string; }; +/** + * This is for those cases (eg. with a DB) where field values may be all primitives or may be JSON values. + * See src/db/databaseUtil.ts parseJsonField for more details. + * + * This is so that we can reuse most of the type and don't have to maintain another copy. + * Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2 + */ +export type ContactMaybeWithJsonStrings = Omit & { + contactMethods?: string | Array; +}; + export const ContactSchema = { contacts: "&did, name", // no need to key by other things }; diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 6baa8cc6..47a943c0 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -50,7 +50,7 @@ import { type SettingsWithJsonStrings, } from "@/db/tables/settings"; import { logger } from "@/utils/logger"; -import { Contact } from "@/db/tables/contacts"; +import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts"; import { Account } from "@/db/tables/accounts"; import { Temp } from "@/db/tables/temp"; import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database"; @@ -642,15 +642,81 @@ export const PlatformServiceMixin = { // CACHED SPECIALIZED SHORTCUTS (massive performance boost) // ================================================= + /** + * Normalize contact data by parsing JSON strings into proper objects + * Handles the contactMethods field which can be either a JSON string or an array + * @param rawContacts Raw contact data from database + * @returns Normalized Contact[] array + */ + $normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[] { + return rawContacts.map((contact) => { + // Create a new contact object with proper typing + const normalizedContact: Contact = { + did: contact.did, + iViewContent: contact.iViewContent, + name: contact.name, + nextPubKeyHashB64: contact.nextPubKeyHashB64, + notes: contact.notes, + profileImageUrl: contact.profileImageUrl, + publicKeyBase64: contact.publicKeyBase64, + seesMe: contact.seesMe, + registered: contact.registered, + }; + + // Handle contactMethods field which can be a JSON string or an array + if (contact.contactMethods !== undefined) { + if (typeof contact.contactMethods === "string") { + // Parse JSON string into array + normalizedContact.contactMethods = this._parseJsonField( + contact.contactMethods, + [], + ); + } else if (Array.isArray(contact.contactMethods)) { + // Validate that each item in the array is a proper ContactMethod object + normalizedContact.contactMethods = contact.contactMethods.filter( + (method) => { + const isValid = + method && + typeof method === "object" && + typeof method.label === "string" && + typeof method.type === "string" && + typeof method.value === "string"; + + if (!isValid && method !== undefined) { + console.warn( + "[ContactNormalization] Invalid contact method:", + method, + ); + } + + return isValid; + }, + ); + } else { + // Invalid data, use empty array + normalizedContact.contactMethods = []; + } + } else { + // No contactMethods, use empty array + normalizedContact.contactMethods = []; + } + + return normalizedContact; + }); + }, + /** * Load all contacts (always fresh) - $contacts() * Always fetches fresh data from database for consistency - * @returns Promise Array of contact objects + * Handles JSON string/object duality for contactMethods field + * @returns Promise Array of normalized contact objects */ async $contacts(): Promise { - return (await this.$query( + const rawContacts = (await this.$query( "SELECT * FROM contacts ORDER BY name", - )) as Contact[]; + )) as ContactMaybeWithJsonStrings[]; + + return this.$normalizeContacts(rawContacts); }, /** @@ -1026,7 +1092,13 @@ export const PlatformServiceMixin = { Object.entries(changes).forEach(([key, value]) => { if (value !== undefined) { setParts.push(`${key} = ?`); - params.push(value); + + // Handle contactMethods field - convert array to JSON string + if (key === "contactMethods" && Array.isArray(value)) { + params.push(JSON.stringify(value)); + } else { + params.push(value); + } } }); @@ -1048,45 +1120,36 @@ export const PlatformServiceMixin = { /** * Get all contacts as typed objects - $getAllContacts() * Eliminates verbose query + mapping patterns - * @returns Promise Array of contact objects + * Handles JSON string/object duality for contactMethods field + * @returns Promise Array of normalized contact objects */ async $getAllContacts(): Promise { - const results = await this.$dbQuery( - "SELECT did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl FROM contacts ORDER BY name", - ); + const rawContacts = (await this.$query( + "SELECT * FROM contacts ORDER BY name", + )) as ContactMaybeWithJsonStrings[]; - return this.$mapResults(results, (row: unknown[]) => ({ - did: row[0] as string, - name: row[1] as string, - publicKeyBase64: row[2] as string, - seesMe: Boolean(row[3]), - registered: Boolean(row[4]), - nextPubKeyHashB64: row[5] as string, - profileImageUrl: row[6] as string, - })); + return this.$normalizeContacts(rawContacts); }, /** * Get single contact by DID - $getContact() * Eliminates verbose single contact query patterns + * Handles JSON string/object duality for contactMethods field * @param did Contact DID to retrieve - * @returns Promise Contact object or null if not found + * @returns Promise Normalized contact object or null if not found */ async $getContact(did: string): Promise { - const results = await this.$dbQuery( + const rawContacts = (await this.$query( "SELECT * FROM contacts WHERE did = ?", [did], - ); + )) as ContactMaybeWithJsonStrings[]; - if (!results || !results.values || results.values.length === 0) { + if (rawContacts.length === 0) { return null; } - const contactData = this._mapColumnsToValues( - results.columns, - results.values, - ); - return contactData.length > 0 ? (contactData[0] as Contact) : null; + const normalizedContacts = this.$normalizeContacts(rawContacts); + return normalizedContacts[0]; }, /** @@ -1681,6 +1744,7 @@ declare module "@vue/runtime-core" { $contactCount(): Promise; $settings(defaults?: Settings): Promise; $accountSettings(did?: string, defaults?: Settings): Promise; + $normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[]; // Settings update shortcuts (eliminate 90% boilerplate) $saveSettings(changes: Partial): Promise; diff --git a/src/views/ContactEditView.vue b/src/views/ContactEditView.vue index 5b6f63a5..2c4cebfd 100644 --- a/src/views/ContactEditView.vue +++ b/src/views/ContactEditView.vue @@ -239,7 +239,21 @@ export default class ContactEditView extends Vue { this.contact = contact; this.contactName = contact.name || ""; this.contactNotes = contact.notes || ""; - this.contactMethods = contact.contactMethods || []; + + // Ensure contactMethods is a valid array of ContactMethod objects + if (Array.isArray(contact.contactMethods)) { + this.contactMethods = contact.contactMethods.filter((method) => { + return ( + method && + typeof method === "object" && + typeof method.label === "string" && + typeof method.type === "string" && + typeof method.value === "string" + ); + }); + } else { + this.contactMethods = []; + } } else { this.notify.error( `${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,