feat(export): Replace CSV export with standardized JSON format

- Add contactsToExportJson utility function for standardized data export
- Replace CSV export with JSON format in DataExportSection
- Update file extension and MIME type to application/json
- Remove Dexie-specific export logic in favor of unified SQLite/Dexie approach
- Update success notifications to reflect JSON format
- Add TypeScript interfaces for export data structure

This change improves data portability and standardization by:
- Using a consistent JSON format for data export/import
- Supporting both SQLite and Dexie databases
- Including all contact fields in export
- Properly handling contactMethods as stringified JSON
- Maintaining backward compatibility with existing import tools

Security: No sensitive data exposure, maintains existing access controls
This commit is contained in:
Matthew Raymer
2025-06-07 05:02:33 +00:00
parent cfb186a04e
commit b9223d7fe2
9 changed files with 230 additions and 126 deletions

View File

@@ -63,13 +63,18 @@ backup and database export, with platform-specific download instructions. * *
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import {
PlatformService,
PlatformCapabilities,
} from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util";
import { db } from "../db/index";
/**
* @vue-component
@@ -131,24 +136,25 @@ export default class DataExportSection extends Vue {
*/
public async exportDatabase() {
try {
if (!USE_DEXIE_DB) {
throw new Error("Not implemented");
let allContacts: Contact[] = [];
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
allContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
const blob = await db.export({
prettyJson: true,
transform: (table, value, key) => {
if (table === "contacts") {
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
Object.keys(value).forEach((prop) => {
if (value[prop] === undefined) {
delete value[prop];
}
});
}
return { value, key };
},
});
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup.json`;
// if (USE_DEXIE_DB) {
// await db.open();
// allContacts = await db.contacts.toArray();
// }
// Convert contacts to export format
const exportData = contactsToExportJson(allContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: "application/json" });
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link
@@ -160,8 +166,7 @@ export default class DataExportSection extends Vue {
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory
const content = await blob.text();
await this.platformService.writeAndShareFile(fileName, content);
await this.platformService.writeAndShareFile(fileName, jsonStr);
} else {
throw new Error("This platform does not support file downloads.");
}
@@ -172,10 +177,10 @@ export default class DataExportSection extends Vue {
type: "success",
title: "Export Successful",
text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup. It is in the Dexie format."
: "You should have been prompted to save your backup file.",
? "See your downloads directory for the backup. It is in JSON format."
: "The backup file has been saved.",
},
-1,
3000,
);
} catch (error) {
logger.error("Export Error:", error);