bulltrackers-module 1.0.504 → 1.0.506

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +345 -0
  2. package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +320 -0
  3. package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +116 -0
  4. package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +171 -0
  5. package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +710 -0
  6. package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +109 -0
  7. package/functions/generic-api/user-api/MIGRATION_PLAN.md +499 -0
  8. package/functions/generic-api/user-api/README_MIGRATION.md +152 -0
  9. package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +106 -0
  10. package/functions/generic-api/user-api/REFACTORING_STATUS.md +85 -0
  11. package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +206 -0
  12. package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +126 -0
  13. package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
  14. package/functions/generic-api/user-api/helpers/{test_alert_helpers.js → alerts/test_alert_helpers.js} +1 -1
  15. package/functions/generic-api/user-api/helpers/collection_helpers.js +23 -45
  16. package/functions/generic-api/user-api/helpers/core/compression_helpers.js +68 -0
  17. package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +213 -0
  18. package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +486 -0
  19. package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +77 -0
  20. package/functions/generic-api/user-api/helpers/data/computation_helpers.js +299 -0
  21. package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
  22. package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +238 -0
  23. package/functions/generic-api/user-api/helpers/data/social_helpers.js +55 -0
  24. package/functions/generic-api/user-api/helpers/data_helpers.js +85 -2750
  25. package/functions/generic-api/user-api/helpers/{dev_helpers.js → dev/dev_helpers.js} +0 -1
  26. package/functions/generic-api/user-api/helpers/{on_demand_fetch_helpers.js → fetch/on_demand_fetch_helpers.js} +33 -115
  27. package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +360 -0
  28. package/functions/generic-api/user-api/helpers/{notification_helpers.js → notifications/notification_helpers.js} +0 -1
  29. package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +200 -0
  30. package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +125 -0
  31. package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +178 -0
  32. package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +65 -0
  33. package/functions/generic-api/user-api/helpers/{review_helpers.js → reviews/review_helpers.js} +23 -107
  34. package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +177 -0
  35. package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +70 -0
  36. package/functions/generic-api/user-api/helpers/{user_sync_helpers.js → sync/user_sync_helpers.js} +54 -127
  37. package/functions/generic-api/user-api/helpers/{verification_helpers.js → verification/verification_helpers.js} +4 -43
  38. package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +95 -0
  39. package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +139 -0
  40. package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +306 -0
  41. package/functions/generic-api/user-api/helpers/{watchlist_helpers.js → watchlist/watchlist_management_helpers.js} +62 -213
  42. package/functions/generic-api/user-api/index.js +9 -9
  43. package/functions/task-engine/handler_creator.js +7 -6
  44. package/package.json +1 -1
  45. package/functions/generic-api/API_MIGRATION_PLAN.md +0 -436
  46. package/functions/generic-api/user-api/helpers/FALLBACK_CONDITIONS.md +0 -98
  47. package/functions/generic-api/user-api/helpers/HISTORY_STORAGE_LOCATION.md +0 -66
  48. package/functions/generic-api/user-api/helpers/subscription_helpers.js +0 -512
  49. /package/functions/generic-api/user-api/helpers/{alert_helpers.js → alerts/alert_helpers.js} +0 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * @fileoverview Alert Subscription Management Helpers
3
+ * Handles subscriptions for watchlist alerts (static and dynamic)
4
+ */
5
+
6
+ const { FieldValue } = require('@google-cloud/firestore');
7
+
8
+ /**
9
+ * POST /user/me/subscriptions
10
+ * Subscribe to alerts for a PI in a watchlist
11
+ */
12
+ async function subscribeToAlerts(req, res, dependencies, config) {
13
+ const { db, logger } = dependencies;
14
+ const { userCid, watchlistId, piCid, alertTypes, thresholds } = req.body;
15
+
16
+ if (!userCid || !watchlistId || !piCid) {
17
+ return res.status(400).json({ error: "Missing required fields: userCid, watchlistId, piCid" });
18
+ }
19
+
20
+ try {
21
+ // Verify watchlist exists and belongs to user
22
+ const watchlistsCollection = config.watchlistsCollection || 'watchlists';
23
+ const watchlistRef = db.collection(watchlistsCollection)
24
+ .doc(String(userCid))
25
+ .collection('lists')
26
+ .doc(watchlistId);
27
+
28
+ const watchlistDoc = await watchlistRef.get();
29
+
30
+ if (!watchlistDoc.exists) {
31
+ return res.status(404).json({ error: "Watchlist not found" });
32
+ }
33
+
34
+ const watchlistData = watchlistDoc.data();
35
+
36
+ // Verify PI is in the watchlist
37
+ let piInWatchlist = false;
38
+ if (watchlistData.type === 'static') {
39
+ piInWatchlist = watchlistData.items?.some(item => item.cid === Number(piCid));
40
+ } else if (watchlistData.type === 'dynamic') {
41
+ // For dynamic watchlists, we'll check if the PI is in the current computation result
42
+ // This is a simplified check - in production, you'd fetch the latest computation result
43
+ piInWatchlist = true; // Allow subscriptions for dynamic watchlists
44
+ }
45
+
46
+ if (!piInWatchlist && watchlistData.type === 'static') {
47
+ return res.status(400).json({ error: "PI is not in this watchlist" });
48
+ }
49
+
50
+ // Default alert types (all enabled) if not provided
51
+ const defaultAlertTypes = {
52
+ newPositions: true,
53
+ volatilityChanges: true,
54
+ increasedRisk: true,
55
+ newSector: true,
56
+ increasedPositionSize: true,
57
+ newSocialPost: true
58
+ };
59
+
60
+ const subscriptionData = {
61
+ userCid: Number(userCid),
62
+ piCid: Number(piCid),
63
+ watchlistId: watchlistId,
64
+ alertTypes: alertTypes || defaultAlertTypes,
65
+ thresholds: thresholds || {},
66
+ subscribedAt: FieldValue.serverTimestamp(),
67
+ lastAlertAt: null
68
+ };
69
+
70
+ // Store subscription
71
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
72
+ const subscriptionRef = db.collection(subscriptionsCollection)
73
+ .doc(String(userCid))
74
+ .collection('alerts')
75
+ .doc(String(piCid));
76
+
77
+ await subscriptionRef.set(subscriptionData, { merge: true });
78
+
79
+ logger.log('SUCCESS', `[subscribeToAlerts] User ${userCid} subscribed to alerts for PI ${piCid} in watchlist ${watchlistId}`);
80
+
81
+ return res.status(200).json({
82
+ success: true,
83
+ subscription: subscriptionData
84
+ });
85
+
86
+ } catch (error) {
87
+ logger.log('ERROR', `[subscribeToAlerts] Error creating subscription for user ${userCid}`, error);
88
+ return res.status(500).json({ error: error.message });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * PUT /user/me/subscriptions/:piCid
94
+ * Update alert subscription settings
95
+ */
96
+ async function updateSubscription(req, res, dependencies, config) {
97
+ const { db, logger } = dependencies;
98
+ const { userCid } = req.query;
99
+ const { piCid } = req.params;
100
+ const { alertTypes, thresholds } = req.body;
101
+
102
+ if (!userCid || !piCid) {
103
+ return res.status(400).json({ error: "Missing userCid or piCid" });
104
+ }
105
+
106
+ try {
107
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
108
+ const subscriptionRef = db.collection(subscriptionsCollection)
109
+ .doc(String(userCid))
110
+ .collection('alerts')
111
+ .doc(String(piCid));
112
+
113
+ const subscriptionDoc = await subscriptionRef.get();
114
+
115
+ if (!subscriptionDoc.exists) {
116
+ return res.status(404).json({ error: "Subscription not found" });
117
+ }
118
+
119
+ const updates = {};
120
+
121
+ if (alertTypes !== undefined) {
122
+ updates.alertTypes = alertTypes;
123
+ }
124
+
125
+ if (thresholds !== undefined) {
126
+ updates.thresholds = thresholds;
127
+ }
128
+
129
+ if (Object.keys(updates).length === 0) {
130
+ return res.status(400).json({ error: "No updates provided" });
131
+ }
132
+
133
+ updates.updatedAt = FieldValue.serverTimestamp();
134
+
135
+ await subscriptionRef.update(updates);
136
+
137
+ logger.log('SUCCESS', `[updateSubscription] Updated subscription for user ${userCid}, PI ${piCid}`);
138
+
139
+ const updatedDoc = await subscriptionRef.get();
140
+ return res.status(200).json({
141
+ success: true,
142
+ subscription: {
143
+ id: updatedDoc.id,
144
+ ...updatedDoc.data()
145
+ }
146
+ });
147
+
148
+ } catch (error) {
149
+ logger.log('ERROR', `[updateSubscription] Error updating subscription for user ${userCid}`, error);
150
+ return res.status(500).json({ error: error.message });
151
+ }
152
+ }
153
+
154
+ /**
155
+ * DELETE /user/me/subscriptions/:piCid
156
+ * Unsubscribe from alerts for a PI
157
+ */
158
+ async function unsubscribeFromAlerts(req, res, dependencies, config) {
159
+ const { db, logger } = dependencies;
160
+ const { userCid } = req.query;
161
+ const { piCid } = req.params;
162
+
163
+ if (!userCid || !piCid) {
164
+ return res.status(400).json({ error: "Missing userCid or piCid" });
165
+ }
166
+
167
+ try {
168
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
169
+ const subscriptionRef = db.collection(subscriptionsCollection)
170
+ .doc(String(userCid))
171
+ .collection('alerts')
172
+ .doc(String(piCid));
173
+
174
+ const subscriptionDoc = await subscriptionRef.get();
175
+
176
+ if (!subscriptionDoc.exists) {
177
+ return res.status(404).json({ error: "Subscription not found" });
178
+ }
179
+
180
+ await subscriptionRef.delete();
181
+
182
+ logger.log('SUCCESS', `[unsubscribeFromAlerts] User ${userCid} unsubscribed from alerts for PI ${piCid}`);
183
+
184
+ return res.status(200).json({
185
+ success: true,
186
+ message: "Unsubscribed successfully"
187
+ });
188
+
189
+ } catch (error) {
190
+ logger.log('ERROR', `[unsubscribeFromAlerts] Error unsubscribing user ${userCid} from PI ${piCid}`, error);
191
+ return res.status(500).json({ error: error.message });
192
+ }
193
+ }
194
+
195
+ /**
196
+ * GET /user/me/subscriptions
197
+ * Get all subscriptions for a user
198
+ */
199
+ async function getUserSubscriptions(req, res, dependencies, config) {
200
+ const { db, logger } = dependencies;
201
+ const { userCid } = req.query;
202
+
203
+ if (!userCid) {
204
+ return res.status(400).json({ error: "Missing userCid" });
205
+ }
206
+
207
+ try {
208
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
209
+ const subscriptionsRef = db.collection(subscriptionsCollection)
210
+ .doc(String(userCid))
211
+ .collection('alerts');
212
+
213
+ const snapshot = await subscriptionsRef.get();
214
+
215
+ const subscriptions = [];
216
+ snapshot.forEach(doc => {
217
+ subscriptions.push({
218
+ piCid: Number(doc.id),
219
+ ...doc.data()
220
+ });
221
+ });
222
+
223
+ return res.status(200).json({
224
+ subscriptions,
225
+ count: subscriptions.length
226
+ });
227
+
228
+ } catch (error) {
229
+ logger.log('ERROR', `[getUserSubscriptions] Error fetching subscriptions for user ${userCid}`, error);
230
+ return res.status(500).json({ error: error.message });
231
+ }
232
+ }
233
+
234
+ /**
235
+ * POST /user/me/watchlists/:id/subscribe-all
236
+ * Subscribe to all PIs in a watchlist with default alert settings
237
+ */
238
+ async function subscribeToWatchlist(req, res, dependencies, config) {
239
+ const { db, logger } = dependencies;
240
+ const { userCid } = req.query;
241
+ const { id } = req.params;
242
+ const { alertTypes, thresholds } = req.body;
243
+
244
+ if (!userCid || !id) {
245
+ return res.status(400).json({ error: "Missing userCid or watchlist id" });
246
+ }
247
+
248
+ try {
249
+ // Get watchlist
250
+ const watchlistsCollection = config.watchlistsCollection || 'watchlists';
251
+ const watchlistRef = db.collection(watchlistsCollection)
252
+ .doc(String(userCid))
253
+ .collection('lists')
254
+ .doc(id);
255
+
256
+ const watchlistDoc = await watchlistRef.get();
257
+
258
+ if (!watchlistDoc.exists) {
259
+ return res.status(404).json({ error: "Watchlist not found" });
260
+ }
261
+
262
+ const watchlistData = watchlistDoc.data();
263
+
264
+ // Default alert types
265
+ const defaultAlertTypes = alertTypes || {
266
+ newPositions: true,
267
+ volatilityChanges: true,
268
+ increasedRisk: true,
269
+ newSector: true,
270
+ increasedPositionSize: true,
271
+ newSocialPost: true
272
+ };
273
+
274
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
275
+ const subscriptionsRef = db.collection(subscriptionsCollection)
276
+ .doc(String(userCid))
277
+ .collection('alerts');
278
+
279
+ let subscribedCount = 0;
280
+
281
+ if (watchlistData.type === 'static') {
282
+ // Subscribe to all PIs in static watchlist
283
+ const items = watchlistData.items || [];
284
+
285
+ for (const item of items) {
286
+ const subscriptionData = {
287
+ userCid: Number(userCid),
288
+ piCid: item.cid,
289
+ watchlistId: id,
290
+ alertTypes: item.alertConfig || defaultAlertTypes,
291
+ thresholds: thresholds || {},
292
+ subscribedAt: FieldValue.serverTimestamp(),
293
+ lastAlertAt: null
294
+ };
295
+
296
+ await subscriptionsRef.doc(String(item.cid)).set(subscriptionData, { merge: true });
297
+ subscribedCount++;
298
+ }
299
+ } else if (watchlistData.type === 'dynamic') {
300
+ // For dynamic watchlists, we'd need to fetch the current computation result
301
+ // For now, we'll just set up the subscription structure
302
+ // The actual PIs will be determined when the computation runs
303
+ logger.log('INFO', `[subscribeToWatchlist] Dynamic watchlist subscription setup for ${id} (will be populated by computation)`);
304
+ }
305
+
306
+ logger.log('SUCCESS', `[subscribeToWatchlist] Subscribed user ${userCid} to ${subscribedCount} PIs in watchlist ${id}`);
307
+
308
+ return res.status(200).json({
309
+ success: true,
310
+ subscribed: subscribedCount,
311
+ watchlistId: id,
312
+ watchlistType: watchlistData.type
313
+ });
314
+
315
+ } catch (error) {
316
+ logger.log('ERROR', `[subscribeToWatchlist] Error subscribing to watchlist ${id} for user ${userCid}`, error);
317
+ return res.status(500).json({ error: error.message });
318
+ }
319
+ }
320
+
321
+ module.exports = {
322
+ subscribeToAlerts,
323
+ updateSubscription,
324
+ unsubscribeFromAlerts,
325
+ getUserSubscriptions,
326
+ subscribeToWatchlist
327
+ };
@@ -5,7 +5,7 @@
5
5
 
6
6
  const { FieldValue } = require('@google-cloud/firestore');
7
7
  const { getAllAlertTypes, getAlertTypeByComputation } = require('../../../alert-system/helpers/alert_type_registry');
8
- const { isDeveloperAccount, getDevOverride } = require('./dev_helpers');
8
+ const { isDeveloperAccount, getDevOverride } = require('../dev/dev_helpers');
9
9
 
10
10
  /**
11
11
  * POST /user/dev/test-alert
@@ -1,41 +1,11 @@
1
1
  /**
2
- * @fileoverview Collection Path Helpers
3
- * Utilities for resolving collection paths using the collection registry
4
- * and handling CID lookups, dual reads/writes for migration
2
+ * @fileoverview Collection Path Helpers (Backward Compatibility)
3
+ * Re-exports from core/path_resolution_helpers.js for backward compatibility
4
+ * This file maintains the old API while using the new migration-enabled helpers
5
5
  */
6
6
 
7
- /**
8
- * Get eToro CID from Firebase UID
9
- * @param {Firestore} db - Firestore instance
10
- * @param {string} firebaseUid - Firebase authentication UID
11
- * @returns {Promise<number|null>} - eToro CID or null if not found
12
- */
13
- async function getCidFromFirebaseUid(db, firebaseUid) {
14
- try {
15
- const userDoc = await db.collection('signedInUsers').doc(firebaseUid).get();
16
- if (!userDoc.exists) return null;
17
- const data = userDoc.data();
18
- return data.etoroCID || data.cid || null;
19
- } catch (error) {
20
- console.error('[getCidFromFirebaseUid] Error fetching CID:', error);
21
- return null;
22
- }
23
- }
24
-
25
- /**
26
- * Get collection path from registry
27
- * @param {object} collectionRegistry - Collection registry instance
28
- * @param {string} category - Registry category (e.g., 'signedInUsers', 'popularInvestors')
29
- * @param {string} subcategory - Subcategory name (e.g., 'portfolio', 'watchlists')
30
- * @param {object} params - Dynamic segment values (e.g., { cid: '123', date: '2025-01-01' })
31
- * @returns {string} - Resolved collection/document path
32
- */
33
- function getCollectionPath(collectionRegistry, category, subcategory, params = {}) {
34
- if (!collectionRegistry || !collectionRegistry.getCollectionPath) {
35
- throw new Error('Collection registry not available or missing getCollectionPath method');
36
- }
37
- return collectionRegistry.getCollectionPath(category, subcategory, params);
38
- }
7
+ // Re-export from core helpers for backward compatibility
8
+ const coreHelpers = require('./core/path_resolution_helpers');
39
9
 
40
10
  /**
41
11
  * Extract collection name from a full path
@@ -48,7 +18,8 @@ function extractCollectionName(path) {
48
18
 
49
19
  /**
50
20
  * Dual read helper - tries new structure first, falls back to legacy
51
- * @param {Firestore} db - Firestore instance
21
+ * @deprecated Use readWithMigration from core/path_resolution_helpers instead
22
+ * @param {object} db - Firestore instance
52
23
  * @param {string} newPath - New collection path
53
24
  * @param {string} legacyPath - Legacy collection path
54
25
  * @param {object} [options={}] - Options for reading
@@ -97,7 +68,8 @@ async function readWithFallback(db, newPath, legacyPath, options = {}) {
97
68
 
98
69
  /**
99
70
  * Dual write helper - writes to both new and legacy locations during migration
100
- * @param {Firestore} db - Firestore instance
71
+ * @deprecated Use writeWithMigration from core/path_resolution_helpers instead
72
+ * @param {object} db - Firestore instance
101
73
  * @param {string} newPath - New collection path
102
74
  * @param {string} legacyPath - Legacy collection path
103
75
  * @param {object} data - Data to write
@@ -155,7 +127,7 @@ async function writeDual(db, newPath, legacyPath, data, options = {}) {
155
127
  * @returns {string} - Path to latest portfolio snapshot
156
128
  */
157
129
  function getUserPortfolioPath(collectionRegistry, cid) {
158
- return getCollectionPath(collectionRegistry, 'signedInUsers', 'portfolio', { cid: String(cid) });
130
+ return coreHelpers.getCollectionPath(collectionRegistry, 'signedInUsers', 'portfolio', { cid: String(cid) });
159
131
  }
160
132
 
161
133
  /**
@@ -165,7 +137,7 @@ function getUserPortfolioPath(collectionRegistry, cid) {
165
137
  * @returns {string} - Path to latest trade history snapshot
166
138
  */
167
139
  function getUserTradeHistoryPath(collectionRegistry, cid) {
168
- return getCollectionPath(collectionRegistry, 'signedInUsers', 'tradeHistory', { cid: String(cid) });
140
+ return coreHelpers.getCollectionPath(collectionRegistry, 'signedInUsers', 'tradeHistory', { cid: String(cid) });
169
141
  }
170
142
 
171
143
  /**
@@ -175,7 +147,7 @@ function getUserTradeHistoryPath(collectionRegistry, cid) {
175
147
  * @returns {string} - Path to social posts collection
176
148
  */
177
149
  function getUserSocialPostsPath(collectionRegistry, cid) {
178
- return getCollectionPath(collectionRegistry, 'signedInUsers', 'socialPosts', { cid: String(cid) });
150
+ return coreHelpers.getCollectionPath(collectionRegistry, 'signedInUsers', 'socialPosts', { cid: String(cid) });
179
151
  }
180
152
 
181
153
  /**
@@ -186,7 +158,7 @@ function getUserSocialPostsPath(collectionRegistry, cid) {
186
158
  * @returns {string} - Path to date-based portfolio data
187
159
  */
188
160
  function getRootDataPortfolioPath(collectionRegistry, date, cid) {
189
- return getCollectionPath(collectionRegistry, 'rootData', 'signedInUserPortfolio', { date, cid: String(cid) });
161
+ return coreHelpers.getCollectionPath(collectionRegistry, 'rootData', 'signedInUserPortfolio', { date, cid: String(cid) });
190
162
  }
191
163
 
192
164
  /**
@@ -197,12 +169,13 @@ function getRootDataPortfolioPath(collectionRegistry, date, cid) {
197
169
  * @returns {string} - Path to date-based trade history data
198
170
  */
199
171
  function getRootDataTradeHistoryPath(collectionRegistry, date, cid) {
200
- return getCollectionPath(collectionRegistry, 'rootData', 'signedInUserTradeHistory', { date, cid: String(cid) });
172
+ return coreHelpers.getCollectionPath(collectionRegistry, 'rootData', 'signedInUserTradeHistory', { date, cid: String(cid) });
201
173
  }
202
174
 
175
+ // Re-export core helpers
203
176
  module.exports = {
204
- getCidFromFirebaseUid,
205
- getCollectionPath,
177
+ getCidFromFirebaseUid: coreHelpers.getCidFromFirebaseUid,
178
+ getCollectionPath: coreHelpers.getCollectionPath,
206
179
  extractCollectionName,
207
180
  readWithFallback,
208
181
  writeDual,
@@ -210,6 +183,11 @@ module.exports = {
210
183
  getUserTradeHistoryPath,
211
184
  getUserSocialPostsPath,
212
185
  getRootDataPortfolioPath,
213
- getRootDataTradeHistoryPath
186
+ getRootDataTradeHistoryPath,
187
+ // Also export new migration helpers
188
+ readWithMigration: coreHelpers.readWithMigration,
189
+ writeWithMigration: coreHelpers.writeWithMigration,
190
+ getNewPath: coreHelpers.getNewPath,
191
+ getLegacyPath: coreHelpers.getLegacyPath
214
192
  };
215
193
 
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @fileoverview Compression/Decompression Utilities
3
+ * Handles decompression of computation results stored as compressed byte strings
4
+ */
5
+
6
+ const zlib = require('zlib');
7
+
8
+ /**
9
+ * Decompress computation results if they are stored as compressed byte strings
10
+ * @param {object} data - The raw Firestore document data
11
+ * @returns {object} The decompressed JSON object or original data if not compressed
12
+ */
13
+ function tryDecompress(data) {
14
+ if (data && data._compressed === true && data.payload) {
15
+ try {
16
+ let buffer;
17
+
18
+ // Handle different payload types from Firestore
19
+ if (Buffer.isBuffer(data.payload)) {
20
+ // Already a Buffer - use directly
21
+ buffer = data.payload;
22
+ } else if (typeof data.payload === 'string') {
23
+ // If it's a string, try base64 first, then direct
24
+ try {
25
+ buffer = Buffer.from(data.payload, 'base64');
26
+ } catch (e) {
27
+ // If not base64, might already be decompressed JSON string
28
+ try {
29
+ const parsed = JSON.parse(data.payload);
30
+ // If parsing succeeds, return it (data was already decompressed)
31
+ return parsed;
32
+ } catch (e2) {
33
+ // Not JSON either, try as raw buffer
34
+ buffer = Buffer.from(data.payload);
35
+ }
36
+ }
37
+ } else {
38
+ // Try to convert to Buffer
39
+ buffer = Buffer.from(data.payload);
40
+ }
41
+
42
+ // Decompress the buffer
43
+ const decompressed = zlib.gunzipSync(buffer);
44
+ const jsonString = decompressed.toString('utf8');
45
+
46
+ // Parse the JSON string
47
+ const parsed = JSON.parse(jsonString);
48
+
49
+ // Verify it's an object, not a string
50
+ if (typeof parsed === 'string') {
51
+ // Might be double-encoded JSON string
52
+ return JSON.parse(parsed);
53
+ }
54
+
55
+ return parsed;
56
+ } catch (e) {
57
+ console.error('[tryDecompress] Decompression failed:', e.message);
58
+ // Return empty object on failure to avoid crashing
59
+ return {};
60
+ }
61
+ }
62
+ return data;
63
+ }
64
+
65
+ module.exports = {
66
+ tryDecompress
67
+ };
68
+