export labels within contacts

This commit is contained in:
2026-01-13 20:46:03 -07:00
parent b6704b348b
commit 84cad0e169
7 changed files with 175 additions and 118 deletions

View File

@@ -39,6 +39,8 @@
* @updated 2025-06-25 - Added high-level entity operations for code reduction
*/
import * as R from "ramda";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import type {
PlatformService,
@@ -49,7 +51,11 @@ import {
type SettingsWithJsonStrings,
} from "@/db/tables/settings";
import { logger } from "@/utils/logger";
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
import {
Contact,
ContactMaybeWithJsonStrings,
ContactWithLabels,
} from "@/db/tables/contacts";
import { Account } from "@/db/tables/accounts";
import { Temp } from "@/db/tables/temp";
import {
@@ -61,6 +67,7 @@ import {
generateInsertStatement,
generateUpdateStatement,
} from "@/utils/sqlHelpers";
import { AppString } from "../constants/app";
// =================================================
// TYPESCRIPT INTERFACES
@@ -86,6 +93,23 @@ interface VueComponentWithMixin {
platformService(): PlatformService;
}
/**
* Interface for the JSON export format of database tables
*/
export interface TableExportData {
tableName: string;
rows: Array<Record<string, unknown>>;
}
/**
* Interface for the complete database export format
*/
export interface DatabaseExport {
data: {
data: Array<TableExportData>;
};
}
// /**
// * Global cache store for mixin instances
// * Uses WeakMap to avoid memory leaks when components are destroyed
@@ -1501,7 +1525,7 @@ export const PlatformServiceMixin = {
* @param did Contact DID
* @returns Promise<string[]> Array of labels
*/
async $getContactLabels(did: string): Promise<string[]> {
async $getContactLabelsForDid(did: string): Promise<string[]> {
try {
const results = (await this.$dbQuery(
"SELECT label FROM contact_labels WHERE did = ? ORDER BY label",
@@ -1952,6 +1976,133 @@ export const PlatformServiceMixin = {
);
},
/**
* Converts an array of contacts to the export JSON format.
* This format is used for data migration and backup purposes.
*
* @param contacts - Array of Contact objects to convert
* @returns DatabaseExport object in the standardized format
*/
$contactsToExportJson(contacts: ContactWithLabels[]): DatabaseExport {
return {
data: {
data: [
{
tableName: "contacts",
rows: contacts,
},
],
},
};
},
/**
* Prepares contact and label data for export to a JSON file.
* Handles normalization of contact data and generates a timestamped filename.
*
* @param appName - Application name for filename (defaults to "TimeSafari")
* @returns Object containing the JSON string and filename
*
* @example
* ```typescript
* const contacts = await $contacts();
* const labelsResult = await $dbQuery("SELECT did, label FROM contact_labels");
* const labels = mapQueryResultToValues(labelsResult);
* const { jsonString, fileName } = saveContactExport(contacts, labels);
* await platformService.writeAndShareFile(fileName, jsonString);
* ```
*/
async $saveContactExport(): Promise<string> {
const contacts = await this.$contacts();
// Fetch all contact labels from database
const labelsResult = await this.$dbQuery(
"SELECT did, label FROM contact_labels ORDER BY did, label",
);
// create a map of did to labels
const contactToLabelsMap = new Map<string, string[]>();
// iterate over the labelsResult and accumulate the labels for each did
labelsResult?.values?.forEach((contactLabel: [string, string]) => {
const did = contactLabel[0];
const label = contactLabel[1];
if (!contactToLabelsMap.has(did)) {
contactToLabelsMap.set(did, []);
}
contactToLabelsMap.get(did)?.push(label);
});
// Process contacts to normalize contactMethods field
// Handle both array format (from normalized contacts) and string format (legacy/database)
const processedContacts: ContactWithLabels[] = contacts.map((contact) => {
// Remove contactMethods field temporarily to get a clean type
const exContact: ContactWithLabels = R.omit(
["contactMethods"],
contact,
);
// Add contactMethods as a proper array of ContactMethod objects
if (contact.contactMethods) {
if (Array.isArray(contact.contactMethods)) {
// Already an array, use it directly
exContact.contactMethods = contact.contactMethods;
} else {
// Check if it's a string that needs parsing
const contactMethodsValue = contact.contactMethods as unknown;
if (
typeof contactMethodsValue === "string" &&
contactMethodsValue.trim() !== ""
) {
try {
// String that needs parsing
exContact.contactMethods = JSON.parse(contactMethodsValue);
} catch (error) {
// Invalid JSON, use empty array
logger.warn(
`Invalid contactMethods JSON for contact ${contact.did}:`,
error,
);
exContact.contactMethods = [];
}
} else {
// Invalid data, use empty array
exContact.contactMethods = [];
}
}
} else {
// No contactMethods, use empty array
exContact.contactMethods = [];
}
// add the labels to the contact
exContact.labels = contactToLabelsMap.get(contact.did) || [];
return exContact;
});
// Build export data with contacts
const exportData = this.$contactsToExportJson(processedContacts);
// Generate JSON string
const jsonString = JSON.stringify(exportData, null, 2);
// Generate filename with current date
const today = new Date();
const dateString = today.toISOString().split("T")[0]; // YYYY-MM-DD format
const appName = AppString.APP_NAME_NO_SPACES;
const fileName = `${appName}-backup-contacts-${dateString}.json`;
// Use platform service to handle export
await (
this as unknown as IPlatformServiceMixin
).platformService.writeAndShareFile(fileName, jsonString);
return fileName;
},
// =================================================
// DEBUGGING
// =================================================
/**
* Debug method to verify settings for a specific DID
* Useful for troubleshooting settings propagation issues
@@ -2086,7 +2237,7 @@ export interface IPlatformServiceMixin {
$getContact(did: string): Promise<Contact | null>;
$deleteContact(did: string): Promise<boolean>;
$contactCount(): Promise<number>;
$getContactLabels(did: string): Promise<string[]>;
$getContactLabelsForDid(did: string): Promise<string[]>;
$getContactIdsWithAllLabels(labels: string[]): Promise<string[]>;
$addContactLabel(did: string, label: string): Promise<boolean>;
$deleteContactLabel(did: string, label: string): Promise<boolean>;
@@ -2146,6 +2297,9 @@ export interface IPlatformServiceMixin {
values: unknown[][],
): Array<Record<string, unknown>>;
// Contact export methods
$saveContactExport(): Promise<string>;
// Debug methods
$debugDidSettings(did: string): Promise<Settings | null>;
$debugMergedSettings(did: string): Promise<void>;
@@ -2233,7 +2387,7 @@ declare module "@vue/runtime-core" {
$getContact(did: string): Promise<Contact | null>;
$deleteContact(did: string): Promise<boolean>;
$contactCount(): Promise<number>;
$getContactLabels(did: string): Promise<string[]>;
$getContactLabelsForDid(did: string): Promise<string[]>;
$getContactIdsWithAllLabels(labels: string[]): Promise<string[]>;
$addContactLabel(did: string, label: string): Promise<boolean>;
$deleteContactLabel(did: string, label: string): Promise<boolean>;
@@ -2293,6 +2447,9 @@ declare module "@vue/runtime-core" {
values: unknown[][],
): Array<Record<string, unknown>>;
// Contact export methods
$saveContactExport(): Promise<string>;
// Debug methods
$debugDidSettings(did: string): Promise<Settings | null>;
$debugMergedSettings(did: string): Promise<void>;