Browse Source

fix: resolve cross-platform SQLite JSON parsing inconsistencies

- Add platform-agnostic parseJsonField utility to handle different SQLite implementations
- Web SQLite (wa-sqlite/absurd-sql) auto-parses JSON strings to objects
- Capacitor SQLite returns raw strings requiring manual parsing
- Update searchBoxes parsing to use new utility for consistent behavior
- Fixes "[object Object] is not valid JSON" error when switching platforms
- Ensures compatibility between web and mobile SQLite implementations

Fixes: searchBoxes parsing errors in databaseUtil.ts
Related: contactMethods field has similar issue (needs same treatment)
pull/138/head
Matthew Raymer 4 days ago
parent
commit
c1aa522e6c
  1. 892
      package-lock.json
  2. 131
      src/db/databaseUtil.ts
  3. 4
      src/views/SearchAreaView.vue

892
package-lock.json

File diff suppressed because it is too large

131
src/db/databaseUtil.ts

@ -135,6 +135,19 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
result.columns, result.columns,
result.values, result.values,
)[0] as Settings; )[0] as Settings;
// Debug: Check the actual data types from SQLite
logConsoleAndDb(
`[DEBUG] Raw SQLite data types for ${defaultSettings.activeDid}:`,
false,
);
Object.entries(overrideSettings).forEach(([key, value]) => {
logConsoleAndDb(
`[DEBUG] - ${key}: ${typeof value} = ${JSON.stringify(value)}`,
false,
);
});
const overrideSettingsFiltered = Object.fromEntries( const overrideSettingsFiltered = Object.fromEntries(
Object.entries(overrideSettings).filter(([_, v]) => v !== null), Object.entries(overrideSettings).filter(([_, v]) => v !== null),
); );
@ -144,17 +157,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
// Handle searchBoxes parsing // Handle searchBoxes parsing
if (settings.searchBoxes) { if (settings.searchBoxes) {
try { settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
true,
);
// Reset to empty array on parse failure
settings.searchBoxes = [];
}
} }
return settings; return settings;
@ -326,3 +329,109 @@ export function mapColumnsToValues(
return obj; return obj;
}); });
} }
/**
* Debug function to inspect raw settings data in the database
* This helps diagnose issues with data corruption or malformed JSON
* @param did Optional DID to inspect specific account settings
* @author Matthew Raymer
*/
export async function debugSettingsData(did?: string): Promise<void> {
try {
const platform = PlatformServiceFactory.getInstance();
// Get all settings records
const allSettings = await platform.dbQuery("SELECT * FROM settings");
logConsoleAndDb(`[DEBUG] Total settings records: ${allSettings?.values?.length || 0}`, false);
if (allSettings?.values?.length) {
allSettings.values.forEach((row, index) => {
const settings = mapColumnsToValues(allSettings.columns, [row])[0];
logConsoleAndDb(`[DEBUG] Settings record ${index + 1}:`, false);
logConsoleAndDb(`[DEBUG] - ID: ${settings.id}`, false);
logConsoleAndDb(`[DEBUG] - accountDid: ${settings.accountDid}`, false);
logConsoleAndDb(`[DEBUG] - activeDid: ${settings.activeDid}`, false);
if (settings.searchBoxes) {
logConsoleAndDb(`[DEBUG] - searchBoxes type: ${typeof settings.searchBoxes}`, false);
logConsoleAndDb(`[DEBUG] - searchBoxes value: ${String(settings.searchBoxes)}`, false);
// Try to parse it
try {
const parsed = JSON.parse(String(settings.searchBoxes));
logConsoleAndDb(`[DEBUG] - searchBoxes parsed successfully: ${JSON.stringify(parsed)}`, false);
} catch (parseError) {
logConsoleAndDb(`[DEBUG] - searchBoxes parse error: ${parseError}`, true);
}
}
logConsoleAndDb(`[DEBUG] - Full record: ${JSON.stringify(settings, null, 2)}`, false);
});
}
// If specific DID provided, also check accounts table
if (did) {
const account = await platform.dbQuery("SELECT * FROM accounts WHERE did = ?", [did]);
logConsoleAndDb(`[DEBUG] Account for ${did}: ${JSON.stringify(account, null, 2)}`, false);
}
} catch (error) {
logConsoleAndDb(`[DEBUG] Error inspecting settings data: ${error}`, true);
}
}
/**
* Platform-agnostic JSON parsing utility
* Handles different SQLite implementations:
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
* - Capacitor SQLite: Returns raw strings that need manual parsing
*
* @param value The value to parse (could be string or already parsed object)
* @param defaultValue Default value if parsing fails
* @returns Parsed object or default value
* @author Matthew Raymer
*/
export function parseJsonField<T>(
value: unknown,
defaultValue: T
): T {
try {
// If already an object (web SQLite auto-parsed), return as-is
if (typeof value === 'object' && value !== null) {
logConsoleAndDb(
`[DEBUG] JSON field is already an object (auto-parsed by web SQLite), skipping parse`,
false,
);
return value as T;
}
// If it's a string (Capacitor SQLite or fallback), parse it
if (typeof value === 'string') {
logConsoleAndDb(
`[DEBUG] JSON field is a string, parsing JSON (Capacitor SQLite or web fallback)`,
false,
);
return JSON.parse(value) as T;
}
// If it's null/undefined, return default
if (value === null || value === undefined) {
return defaultValue;
}
// Unexpected type, log and return default
logConsoleAndDb(
`[DEBUG] JSON field has unexpected type: ${typeof value}, returning default`,
true,
);
return defaultValue;
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse JSON field: ${error}`,
true,
);
return defaultValue;
}
}

4
src/views/SearchAreaView.vue

@ -215,7 +215,7 @@ export default class SearchAreaView extends Vue {
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [newSearchBox], searchBoxes: searchBoxes as any, // Type assertion for Dexie compatibility
}); });
} }
this.searchBox = newSearchBox; this.searchBox = newSearchBox;
@ -269,7 +269,7 @@ export default class SearchAreaView extends Vue {
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await db.open(); await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [], searchBoxes: "[]" as any, // Type assertion for Dexie compatibility
filterFeedByNearby: false, filterFeedByNearby: false,
}); });
} }

Loading…
Cancel
Save