bulltrackers-module 1.0.502 → 1.0.504

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.
@@ -51,8 +51,8 @@ function extractCollectionName(path) {
51
51
  * @param {Firestore} db - Firestore instance
52
52
  * @param {string} newPath - New collection path
53
53
  * @param {string} legacyPath - Legacy collection path
54
- * @param {object} options - Options for reading
55
- * @param {boolean} options.isCollection - If true, reads as collection; if false, reads as document
54
+ * @param {object} [options={}] - Options for reading
55
+ * @param {boolean} [options.isCollection=false] - If true, reads as collection; if false, reads as document
56
56
  * @returns {Promise<object|null>} - Document data or null if not found
57
57
  */
58
58
  async function readWithFallback(db, newPath, legacyPath, options = {}) {
@@ -101,9 +101,9 @@ async function readWithFallback(db, newPath, legacyPath, options = {}) {
101
101
  * @param {string} newPath - New collection path
102
102
  * @param {string} legacyPath - Legacy collection path
103
103
  * @param {object} data - Data to write
104
- * @param {object} options - Write options
105
- * @param {boolean} options.isCollection - If true, writes as collection; if false, writes as document
106
- * @param {boolean} options.merge - If true, merges data instead of overwriting
104
+ * @param {object} [options={}] - Write options
105
+ * @param {boolean} [options.isCollection=false] - If true, writes as collection; if false, writes as document
106
+ * @param {boolean} [options.merge=false] - If true, merges data instead of overwriting
107
107
  * @returns {Promise<void>}
108
108
  */
109
109
  async function writeDual(db, newPath, legacyPath, data, options = {}) {
@@ -72,10 +72,41 @@ function tryDecompress(data) {
72
72
  * Helper function to find the latest available date for signed-in user portfolio data
73
73
  * Searches backwards from today up to 30 days
74
74
  */
75
- async function findLatestPortfolioDate(db, signedInUsersCollection, userCid, maxDaysBack = 30) {
75
+ async function findLatestPortfolioDate(db, signedInUsersCollection, userCid, maxDaysBack = 30, collectionRegistry = null) {
76
76
  const CANARY_BLOCK_ID = '19M';
77
77
  const today = new Date();
78
78
 
79
+ // Try new structure first if collectionRegistry is available
80
+ if (collectionRegistry) {
81
+ try {
82
+ const { getRootDataPortfolioPath } = require('./collection_helpers');
83
+
84
+ for (let i = 0; i < maxDaysBack; i++) {
85
+ const checkDate = new Date(today);
86
+ checkDate.setDate(checkDate.getDate() - i);
87
+ const dateStr = checkDate.toISOString().split('T')[0];
88
+
89
+ try {
90
+ const rootPath = getRootDataPortfolioPath(collectionRegistry, dateStr, userCid);
91
+ const rootDoc = await db.doc(rootPath).get();
92
+
93
+ if (rootDoc.exists) {
94
+ const data = rootDoc.data();
95
+ if (data && (data.AggregatedPositions || data.AggregatedMirrors)) {
96
+ return dateStr; // Found data in new structure
97
+ }
98
+ }
99
+ } catch (error) {
100
+ // Continue to next date if error
101
+ continue;
102
+ }
103
+ }
104
+ } catch (error) {
105
+ // Fall through to legacy check
106
+ }
107
+ }
108
+
109
+ // Fallback to legacy structure
79
110
  for (let i = 0; i < maxDaysBack; i++) {
80
111
  const checkDate = new Date(today);
81
112
  checkDate.setDate(checkDate.getDate() - i);
@@ -637,9 +668,9 @@ async function getUserPortfolio(req, res, dependencies, config) {
637
668
  // 1. Try user-centric latest snapshot
638
669
  if (collectionRegistry) {
639
670
  try {
640
- const { getUserPortfolioPath, extractCollectionName } = require('./collection_helpers');
671
+ const { getUserPortfolioPath, extractCollectionName: extractCollectionNameHelper } = require('./collection_helpers');
641
672
  const latestPath = getUserPortfolioPath(collectionRegistry, effectiveCid);
642
- const collectionName = extractCollectionName(latestPath);
673
+ const collectionName = extractCollectionNameHelper(latestPath);
643
674
 
644
675
  const latestDoc = await db.collection(collectionName)
645
676
  .doc(effectiveCidStr)
@@ -665,7 +696,7 @@ async function getUserPortfolio(req, res, dependencies, config) {
665
696
  // 2. Try root data structure (date-based)
666
697
  if (!portfolioData && collectionRegistry) {
667
698
  try {
668
- const { getRootDataPortfolioPath, extractCollectionName } = require('./collection_helpers');
699
+ const { getRootDataPortfolioPath } = require('./collection_helpers');
669
700
  const rootPath = getRootDataPortfolioPath(collectionRegistry, dataDate, effectiveCid);
670
701
 
671
702
  // Path is: SignedInUserPortfolioData/{date}/{cid}
@@ -2112,9 +2143,11 @@ async function trackProfileView(req, res, dependencies, config) {
2112
2143
  }
2113
2144
 
2114
2145
  // Fallback to legacy check
2146
+ let viewDocId = `${piCid}_${today}`;
2147
+ let viewRef = null;
2148
+
2115
2149
  if (existingUniqueViewers.length === 0) {
2116
- const viewDocId = `${piCid}_${today}`;
2117
- const viewRef = db.collection(profileViewsCollection).doc(viewDocId);
2150
+ viewRef = db.collection(profileViewsCollection).doc(viewDocId);
2118
2151
  const existingDoc = await viewRef.get();
2119
2152
  if (existingDoc.exists) {
2120
2153
  existingData = existingDoc.data();
@@ -2136,9 +2169,6 @@ async function trackProfileView(req, res, dependencies, config) {
2136
2169
  };
2137
2170
 
2138
2171
  // Write to both new and legacy structures
2139
- const { getCollectionPath, writeDual } = require('./collection_helpers');
2140
- const { collectionRegistry } = dependencies;
2141
-
2142
2172
  if (collectionRegistry) {
2143
2173
  const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'profileViews', {
2144
2174
  piCid: String(piCid)
@@ -2146,8 +2176,11 @@ async function trackProfileView(req, res, dependencies, config) {
2146
2176
 
2147
2177
  const legacyPath = `${profileViewsCollection}/${viewDocId}`;
2148
2178
 
2149
- await writeDual(db, newPath, legacyPath, viewData, { merge: true });
2179
+ await writeDual(db, newPath, legacyPath, viewData, { isCollection: false, merge: true });
2150
2180
  } else {
2181
+ if (!viewRef) {
2182
+ viewRef = db.collection(profileViewsCollection).doc(viewDocId);
2183
+ }
2151
2184
  await viewRef.set(viewData, { merge: true });
2152
2185
  }
2153
2186
 
@@ -112,7 +112,7 @@ async function requestPiFetch(req, res, dependencies, config) {
112
112
 
113
113
  const legacyPath = `pi_fetch_requests/${piCidNum}/requests/${requestId}`;
114
114
 
115
- await writeDual(db, newPath, legacyPath, requestData);
115
+ await writeDual(db, newPath, legacyPath, requestData, { isCollection: false, merge: false });
116
116
  } else {
117
117
  // Fallback to legacy only
118
118
  const requestRef = db.collection('pi_fetch_requests')
@@ -139,7 +139,7 @@ async function requestPiFetch(req, res, dependencies, config) {
139
139
 
140
140
  const legacyUserPath = `pi_fetch_requests/${piCidNum}/user_requests/${requestUserCid}`;
141
141
 
142
- await writeDual(db, newUserPath, legacyUserPath, userRequestData, { merge: true });
142
+ await writeDual(db, newUserPath, legacyUserPath, userRequestData, { isCollection: false, merge: true });
143
143
  } else {
144
144
  const userRequestRef = db.collection('pi_fetch_requests')
145
145
  .doc(String(piCidNum))
@@ -165,7 +165,7 @@ async function requestPiFetch(req, res, dependencies, config) {
165
165
 
166
166
  const legacyGlobalPath = `pi_fetch_requests/${piCidNum}/global/latest`;
167
167
 
168
- await writeDual(db, newGlobalPath, legacyGlobalPath, globalData, { merge: true });
168
+ await writeDual(db, newGlobalPath, legacyGlobalPath, globalData, { isCollection: false, merge: true });
169
169
  } else {
170
170
  const globalRef = db.collection('pi_fetch_requests')
171
171
  .doc(String(piCidNum))
@@ -287,7 +287,6 @@ async function getPiFetchStatus(req, res, dependencies, config) {
287
287
 
288
288
  const doc = await docRef.get();
289
289
  if (doc.exists) {
290
- const { tryDecompress } = require('./data_helpers');
291
290
  const data = tryDecompress(doc.data());
292
291
 
293
292
  if (data && data[String(piCidNum)]) {
@@ -143,7 +143,7 @@ async function hasUserCopied(db, userCid, piCid, config) {
143
143
  // If key exists, merge arrays or objects
144
144
  if (Array.isArray(mergedData[key]) && Array.isArray(value)) {
145
145
  mergedData[key] = [...mergedData[key], ...value];
146
- } else if (typeof mergedData[key] === 'object' && typeof value === 'object') {
146
+ } else if (typeof mergedData[key] === 'object' && typeof value === 'object' && mergedData[key] !== null && value !== null) {
147
147
  mergedData[key] = { ...mergedData[key], ...value };
148
148
  } else {
149
149
  mergedData[key] = value;
@@ -374,7 +374,7 @@ async function submitReview(req, res, dependencies, config) {
374
374
 
375
375
  const legacyPath = `${reviewsCollection}/${reviewId}`;
376
376
 
377
- await writeDual(db, newPath, legacyPath, reviewData, { merge: true });
377
+ await writeDual(db, newPath, legacyPath, reviewData, { isCollection: false, merge: true });
378
378
  } else {
379
379
  await db.collection(reviewsCollection).doc(reviewId).set(reviewData, { merge: true });
380
380
  }
@@ -541,7 +541,7 @@ async function getUserReview(req, res, dependencies, config) {
541
541
 
542
542
  const data = reviewData;
543
543
  const review = {
544
- id: reviewDoc.id,
544
+ id: reviewId, // Use reviewId which is already defined
545
545
  rating: data.rating,
546
546
  comment: data.comment || "",
547
547
  isAnonymous: data.isAnonymous || false,
@@ -80,7 +80,7 @@ async function subscribeToAlerts(req, res, dependencies, config) {
80
80
 
81
81
  const legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
82
82
 
83
- await writeDual(db, newPath, legacyPath, subscriptionData, { merge: true });
83
+ await writeDual(db, newPath, legacyPath, subscriptionData, { isCollection: false, merge: true });
84
84
  } else {
85
85
  // Global subscription: signedInUsers/{cid}/subscriptions/{piCid}
86
86
  if (collectionRegistry) {
@@ -90,7 +90,7 @@ async function subscribeToAlerts(req, res, dependencies, config) {
90
90
 
91
91
  const legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
92
92
 
93
- await writeDual(db, newPath, legacyPath, subscriptionData, { merge: true });
93
+ await writeDual(db, newPath, legacyPath, subscriptionData, { isCollection: false, merge: true });
94
94
  } else {
95
95
  // Fallback to legacy only
96
96
  const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
@@ -183,7 +183,7 @@ async function updateSubscription(req, res, dependencies, config) {
183
183
  // Update both structures if using new path, otherwise just legacy
184
184
  if (newPath && collectionRegistry) {
185
185
  const updatedData = { ...subscriptionData, ...updates };
186
- await writeDual(db, newPath, legacyPath, updatedData, { merge: true });
186
+ await writeDual(db, newPath, legacyPath, updatedData, { isCollection: false, merge: true });
187
187
  } else if (subscriptionRef) {
188
188
  await subscriptionRef.update(updates);
189
189
  }
@@ -242,7 +242,6 @@ async function unsubscribeFromAlerts(req, res, dependencies, config) {
242
242
 
243
243
  // Also check per-watchlist subscriptions (need to search all watchlists)
244
244
  if (collectionRegistry && !found) {
245
- const { getCollectionPath } = require('./collection_helpers');
246
245
  const watchlistsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlists', {
247
246
  cid: String(userCid)
248
247
  });
@@ -155,8 +155,8 @@ async function requestUserSync(req, res, dependencies, config) {
155
155
  };
156
156
 
157
157
  // Write to both new and legacy locations during migration
158
- await writeDual(db, newRequestPath, legacyRequestPath, requestData);
159
- await writeDual(db, newStatusPath, legacyStatusPath, statusData, { merge: true });
158
+ await writeDual(db, newRequestPath, legacyRequestPath, requestData, { isCollection: false, merge: false });
159
+ await writeDual(db, newStatusPath, legacyStatusPath, statusData, { isCollection: false, merge: true });
160
160
 
161
161
  // Also update the request ref for later updates (use new path)
162
162
  const requestRef = db.doc(newRequestPath);
@@ -415,6 +415,10 @@ async function getUserSyncStatus(req, res, dependencies, config) {
415
415
  if (referenceTime && (now - referenceTime) > STALE_THRESHOLD_MS) {
416
416
  // Before marking as stale, do one final check for computation results
417
417
  // Sometimes computation completes but status wasn't updated
418
+ const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
419
+ const resultsSub = config.resultsSubcollection || 'results';
420
+ const compsSub = config.computationsSubcollection || 'computations';
421
+
418
422
  const finalCheck = await checkComputationResults(db, targetCidNum, userType, insightsCollection, resultsSub, compsSub, logger);
419
423
  if (!finalCheck) {
420
424
  isStale = true;
@@ -181,14 +181,14 @@ async function finalizeVerification(req, res, dependencies, config) {
181
181
 
182
182
  if (newUserPath && collectionRegistry) {
183
183
  // Write to both during migration
184
- await writeDual(db, newUserPath, legacyUserPath, userData, { merge: true });
184
+ await writeDual(db, newUserPath, legacyUserPath, userData, { isCollection: false, merge: true });
185
185
  } else {
186
186
  // Fallback to legacy only
187
187
  await db.collection(signedInUsersCollection).doc(String(realCID)).set(userData, { merge: true });
188
188
  }
189
189
 
190
190
  // Also store verification data in user-centric path
191
- const verificationData = {
191
+ const newVerificationData = {
192
192
  status: 'VERIFIED',
193
193
  verifiedAt: FieldValue.serverTimestamp(),
194
194
  cid: realCID,
@@ -202,7 +202,7 @@ async function finalizeVerification(req, res, dependencies, config) {
202
202
  : null;
203
203
 
204
204
  if (newVerificationPath && collectionRegistry) {
205
- await db.doc(newVerificationPath).set(verificationData, { merge: true });
205
+ await db.doc(newVerificationPath).set(newVerificationData, { merge: true });
206
206
  }
207
207
 
208
208
  // 3. Trigger Downstream Systems via Pub/Sub
@@ -213,7 +213,7 @@ async function createWatchlist(req, res, dependencies, config) {
213
213
 
214
214
  const legacyPath = `${config.watchlistsCollection || 'watchlists'}/${userCid}/lists/${watchlistId}`;
215
215
 
216
- await writeDual(db, newWatchlistPath, legacyPath, watchlistData);
216
+ await writeDual(db, newWatchlistPath, legacyPath, watchlistData, { isCollection: false, merge: false });
217
217
  } else {
218
218
  // Fallback to legacy only
219
219
  const watchlistsCollection = config.watchlistsCollection || 'watchlists';
@@ -364,7 +364,7 @@ async function updateWatchlist(req, res, dependencies, config) {
364
364
  // Update both structures if using new path
365
365
  if (newPath && collectionRegistry) {
366
366
  const updatedData = { ...existingData, ...updates };
367
- await writeDual(db, newPath, legacyPath, updatedData, { merge: true });
367
+ await writeDual(db, newPath, legacyPath, updatedData, { isCollection: false, merge: true });
368
368
  } else {
369
369
  await watchlistRef.update(updates);
370
370
  }
@@ -406,6 +406,7 @@ async function deleteWatchlist(req, res, dependencies, config) {
406
406
 
407
407
  // Find and delete from both structures
408
408
  let found = false;
409
+ let watchlistData = null; // Store watchlist data for public watchlist check
409
410
 
410
411
  if (collectionRegistry) {
411
412
  try {
@@ -420,7 +421,7 @@ async function deleteWatchlist(req, res, dependencies, config) {
420
421
 
421
422
  const watchlistDoc = await watchlistRef.get();
422
423
  if (watchlistDoc.exists) {
423
- const watchlistData = watchlistDoc.data();
424
+ watchlistData = watchlistDoc.data();
424
425
  // Verify ownership
425
426
  if (watchlistData.createdBy !== Number(userCid)) {
426
427
  return res.status(403).json({ error: "You can only delete your own watchlists" });
@@ -434,21 +435,23 @@ async function deleteWatchlist(req, res, dependencies, config) {
434
435
  }
435
436
 
436
437
  // Also delete from legacy
437
- const watchlistsCollection = config.watchlistsCollection || 'watchlists';
438
- const watchlistRef = db.collection(watchlistsCollection)
439
- .doc(String(userCid))
440
- .collection('lists')
441
- .doc(id);
442
-
443
- const watchlistDoc = await watchlistRef.get();
444
- if (watchlistDoc.exists) {
445
- const watchlistData = watchlistDoc.data();
446
- // Verify ownership
447
- if (watchlistData.createdBy !== Number(userCid)) {
448
- return res.status(403).json({ error: "You can only delete your own watchlists" });
438
+ if (!found || !watchlistData) {
439
+ const watchlistsCollection = config.watchlistsCollection || 'watchlists';
440
+ const watchlistRef = db.collection(watchlistsCollection)
441
+ .doc(String(userCid))
442
+ .collection('lists')
443
+ .doc(id);
444
+
445
+ const watchlistDoc = await watchlistRef.get();
446
+ if (watchlistDoc.exists) {
447
+ watchlistData = watchlistDoc.data();
448
+ // Verify ownership
449
+ if (watchlistData.createdBy !== Number(userCid)) {
450
+ return res.status(403).json({ error: "You can only delete your own watchlists" });
451
+ }
452
+ await watchlistRef.delete();
453
+ found = true;
449
454
  }
450
- await watchlistRef.delete();
451
- found = true;
452
455
  }
453
456
 
454
457
  if (!found) {
@@ -456,7 +459,7 @@ async function deleteWatchlist(req, res, dependencies, config) {
456
459
  }
457
460
 
458
461
  // Remove from public watchlists if it was public
459
- if (watchlistData.visibility === 'public') {
462
+ if (watchlistData && watchlistData.visibility === 'public') {
460
463
  const publicRef = db.collection('public_watchlists').doc(id);
461
464
  await publicRef.delete();
462
465
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.502",
3
+ "version": "1.0.504",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [