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.
- package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +345 -0
- package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +320 -0
- package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +116 -0
- package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +171 -0
- package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +710 -0
- package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +109 -0
- package/functions/generic-api/user-api/MIGRATION_PLAN.md +499 -0
- package/functions/generic-api/user-api/README_MIGRATION.md +152 -0
- package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +106 -0
- package/functions/generic-api/user-api/REFACTORING_STATUS.md +85 -0
- package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +206 -0
- package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +126 -0
- package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
- package/functions/generic-api/user-api/helpers/{test_alert_helpers.js → alerts/test_alert_helpers.js} +1 -1
- package/functions/generic-api/user-api/helpers/collection_helpers.js +23 -45
- package/functions/generic-api/user-api/helpers/core/compression_helpers.js +68 -0
- package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +213 -0
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +486 -0
- package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +77 -0
- package/functions/generic-api/user-api/helpers/data/computation_helpers.js +299 -0
- package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
- package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +238 -0
- package/functions/generic-api/user-api/helpers/data/social_helpers.js +55 -0
- package/functions/generic-api/user-api/helpers/data_helpers.js +85 -2750
- package/functions/generic-api/user-api/helpers/{dev_helpers.js → dev/dev_helpers.js} +0 -1
- package/functions/generic-api/user-api/helpers/{on_demand_fetch_helpers.js → fetch/on_demand_fetch_helpers.js} +33 -115
- package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +360 -0
- package/functions/generic-api/user-api/helpers/{notification_helpers.js → notifications/notification_helpers.js} +0 -1
- package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +200 -0
- package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +125 -0
- package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +178 -0
- package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +65 -0
- package/functions/generic-api/user-api/helpers/{review_helpers.js → reviews/review_helpers.js} +23 -107
- package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +177 -0
- package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +70 -0
- package/functions/generic-api/user-api/helpers/{user_sync_helpers.js → sync/user_sync_helpers.js} +54 -127
- package/functions/generic-api/user-api/helpers/{verification_helpers.js → verification/verification_helpers.js} +4 -43
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +95 -0
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +139 -0
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +306 -0
- package/functions/generic-api/user-api/helpers/{watchlist_helpers.js → watchlist/watchlist_management_helpers.js} +62 -213
- package/functions/generic-api/user-api/index.js +9 -9
- package/functions/task-engine/handler_creator.js +7 -6
- package/package.json +1 -1
- package/functions/generic-api/API_MIGRATION_PLAN.md +0 -436
- package/functions/generic-api/user-api/helpers/FALLBACK_CONDITIONS.md +0 -98
- package/functions/generic-api/user-api/helpers/HISTORY_STORAGE_LOCATION.md +0 -66
- package/functions/generic-api/user-api/helpers/subscription_helpers.js +0 -512
- /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('
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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
|
+
|