contacts-storage.js
1 /** 2 * Filesystem-backed contacts_json storage for sites. 3 * 4 * Stores the full contacts JSON blob in: 5 * data/contacts/{site_id}.json 6 * 7 * This reduces DB size by ~38 MB and improves query performance 8 * on the sites table. 9 * 10 * Pattern: read from filesystem first, fall back to DB column for 11 * sites that haven't been extracted yet. 12 */ 13 14 import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from 'fs'; 15 import { join, dirname } from 'path'; 16 17 const DATA_DIR = join(process.cwd(), 'data', 'contacts'); 18 19 /** Get file path for a site's contacts JSON */ 20 function sitePath(siteId) { 21 const id = Number(siteId); 22 if (!Number.isInteger(id) || id <= 0) { 23 throw new Error(`Invalid siteId: ${siteId}`); 24 } 25 const dir = process.env.CONTACTS_STORAGE_BASE 26 ? join(process.env.CONTACTS_STORAGE_BASE, 'contacts') 27 : DATA_DIR; 28 return join(dir, `${id}.json`); 29 } 30 31 /** 32 * Read contacts_json from filesystem. 33 * @param {number} siteId 34 * @returns {string|null} Raw JSON string, or null if not on filesystem 35 */ 36 function getContactsJson(siteId) { 37 try { 38 return readFileSync(sitePath(siteId), 'utf8') || null; 39 } catch { 40 return null; 41 } 42 } 43 44 /** 45 * Read and parse contacts_json from filesystem. 46 * @param {number} siteId 47 * @returns {Object|null} Parsed contacts data, or null if not on filesystem 48 */ 49 function getContactsData(siteId) { 50 const raw = getContactsJson(siteId); 51 if (!raw) return null; 52 try { 53 return JSON.parse(raw); 54 } catch { 55 return null; 56 } 57 } 58 59 /** 60 * Write contacts_json to filesystem. 61 * @param {number} siteId 62 * @param {string|Object} contactsData - JSON string or object to store 63 */ 64 function setContactsJson(siteId, contactsData) { 65 if (!contactsData) return; 66 const filePath = sitePath(siteId); 67 try { 68 mkdirSync(dirname(filePath), { recursive: true }); 69 const json = typeof contactsData === 'string' ? contactsData : JSON.stringify(contactsData); 70 writeFileSync(filePath, json, 'utf8'); 71 } catch (err) { 72 throw new Error(`Failed to write contacts storage for site ${siteId}: ${err.message}`); 73 } 74 } 75 76 /** 77 * Delete contacts_json from filesystem. 78 * @param {number} siteId 79 * @returns {boolean} true if file existed and was removed 80 */ 81 function deleteContactsJson(siteId) { 82 try { 83 unlinkSync(sitePath(siteId)); 84 return true; 85 } catch { 86 return false; 87 } 88 } 89 90 /** 91 * Check if contacts_json exists on filesystem. 92 * @param {number} siteId 93 * @returns {boolean} 94 */ 95 function hasContactsJson(siteId) { 96 return existsSync(sitePath(siteId)); 97 } 98 99 /** 100 * Read contacts_json with DB fallback. 101 * Checks filesystem first; if missing, reads from the DB row's contacts_json column. 102 * @param {number} siteId 103 * @param {Object} [dbRow] - Optional DB row with contacts_json column (avoids extra query) 104 * @returns {string|null} Raw JSON string 105 */ 106 function getContactsJsonWithFallback(siteId, dbRow) { 107 // Filesystem first 108 const fsData = getContactsJson(siteId); 109 if (fsData) return fsData; 110 111 // Fall back to DB column (skip sentinel value from extraction) 112 if (dbRow && dbRow.contacts_json && !dbRow.contacts_json.includes('"_fs"')) 113 return dbRow.contacts_json; 114 115 return null; 116 } 117 118 /** 119 * Read and parse contacts_json with DB fallback. 120 * @param {number} siteId 121 * @param {Object} [dbRow] - Optional DB row with contacts_json column 122 * @returns {Object|null} Parsed contacts data 123 */ 124 function getContactsDataWithFallback(siteId, dbRow) { 125 const raw = getContactsJsonWithFallback(siteId, dbRow); 126 if (!raw) return null; 127 try { 128 return JSON.parse(raw); 129 } catch { 130 return null; 131 } 132 } 133 134 export { 135 getContactsJson, 136 getContactsData, 137 setContactsJson, 138 deleteContactsJson, 139 hasContactsJson, 140 getContactsJsonWithFallback, 141 getContactsDataWithFallback, 142 DATA_DIR, 143 };