bulltrackers-module 1.0.502 → 1.0.503
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/helpers/collection_helpers.js +5 -5
- package/functions/generic-api/user-api/helpers/data_helpers.js +43 -10
- package/functions/generic-api/user-api/helpers/on_demand_fetch_helpers.js +3 -4
- package/functions/generic-api/user-api/helpers/review_helpers.js +3 -3
- package/functions/generic-api/user-api/helpers/subscription_helpers.js +3 -4
- package/functions/generic-api/user-api/helpers/user_sync_helpers.js +6 -2
- package/functions/generic-api/user-api/helpers/verification_helpers.js +3 -3
- package/functions/generic-api/user-api/helpers/watchlist_helpers.js +21 -18
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
}
|