bulltrackers-module 1.0.548 → 1.0.550
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/computation-system/data/CachedDataLoader.js +13 -0
- package/functions/computation-system/layers/extractors.js +97 -0
- package/functions/computation-system/utils/data_loader.js +34 -1
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +43 -15
- package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +87 -1
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +15 -0
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +118 -23
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
loadPIPageViews, // [NEW]
|
|
16
16
|
loadWatchlistMembership: loadWatchlistMembershipData, // [NEW] Renamed to avoid conflict
|
|
17
17
|
loadPIAlertHistory, // [NEW]
|
|
18
|
+
loadPIWatchlistData, // [NEW] PI-centric watchlist data
|
|
18
19
|
loadPopularInvestorMasterList // [NEW]
|
|
19
20
|
} = require('../utils/data_loader');
|
|
20
21
|
const zlib = require('zlib');
|
|
@@ -33,6 +34,7 @@ class CachedDataLoader {
|
|
|
33
34
|
pageViews: new Map(), // [NEW]
|
|
34
35
|
watchlistMembership: new Map(), // [NEW]
|
|
35
36
|
alertHistory: new Map(),// [NEW]
|
|
37
|
+
piWatchlistData: new Map(), // [NEW] PI-centric watchlist data cache (keyed by piCid)
|
|
36
38
|
piMasterList: null // [NEW] Singleton cache (not date dependent)
|
|
37
39
|
};
|
|
38
40
|
}
|
|
@@ -125,6 +127,17 @@ class CachedDataLoader {
|
|
|
125
127
|
return promise;
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
// [NEW] Load PI-Centric Watchlist Data
|
|
131
|
+
// Loads watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
132
|
+
// [FIX] Cache promises to prevent race conditions when multiple users request same data
|
|
133
|
+
async loadPIWatchlistData(piCid) {
|
|
134
|
+
const piCidStr = String(piCid);
|
|
135
|
+
if (this.cache.piWatchlistData.has(piCidStr)) return this.cache.piWatchlistData.get(piCidStr);
|
|
136
|
+
const promise = loadPIWatchlistData(this.config, this.deps, piCidStr);
|
|
137
|
+
this.cache.piWatchlistData.set(piCidStr, promise);
|
|
138
|
+
return promise;
|
|
139
|
+
}
|
|
140
|
+
|
|
128
141
|
async getPriceShardReferences() {
|
|
129
142
|
return getPriceShardRefs(this.config, this.deps);
|
|
130
143
|
}
|
|
@@ -808,6 +808,102 @@ class WatchlistMembershipExtractor {
|
|
|
808
808
|
}
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
+
/**
|
|
812
|
+
* [NEW] Extractor for PI-Centric Watchlist Data
|
|
813
|
+
* Access via: loader.loadPIWatchlistData(piCid) or context-specific loading
|
|
814
|
+
* Schema: { totalUsers, userCids: [], dailyAdditions: { date: { count, userCids: [], timestamp } }, lastUpdated }
|
|
815
|
+
*/
|
|
816
|
+
class PIWatchlistDataExtractor {
|
|
817
|
+
/**
|
|
818
|
+
* Get total users who have this PI in their watchlist
|
|
819
|
+
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
820
|
+
* @returns {number} Total users count or 0 if not available
|
|
821
|
+
*/
|
|
822
|
+
static getTotalUsers(piWatchlistData) {
|
|
823
|
+
if (!piWatchlistData) return 0;
|
|
824
|
+
return piWatchlistData.totalUsers || 0;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Get array of user CIDs who have this PI in their watchlist
|
|
829
|
+
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
830
|
+
* @returns {string[]} Array of user CIDs, or empty array if not available
|
|
831
|
+
*/
|
|
832
|
+
static getUserCids(piWatchlistData) {
|
|
833
|
+
if (!piWatchlistData || !Array.isArray(piWatchlistData.userCids)) return [];
|
|
834
|
+
return piWatchlistData.userCids.map(cid => String(cid));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Check if a specific user has this PI in their watchlist
|
|
839
|
+
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
840
|
+
* @param {string|number} userId - User CID
|
|
841
|
+
* @returns {boolean} True if user has PI in watchlist
|
|
842
|
+
*/
|
|
843
|
+
static hasUser(piWatchlistData, userId) {
|
|
844
|
+
const userCids = this.getUserCids(piWatchlistData);
|
|
845
|
+
return userCids.includes(String(userId));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Get daily additions data for a specific date
|
|
850
|
+
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
851
|
+
* @param {string} date - Date string in YYYY-MM-DD format
|
|
852
|
+
* @returns {Object|null} Daily addition data { count, userCids: [], timestamp } or null if not found
|
|
853
|
+
*/
|
|
854
|
+
static getDailyAdditions(piWatchlistData, date) {
|
|
855
|
+
if (!piWatchlistData || !piWatchlistData.dailyAdditions || typeof piWatchlistData.dailyAdditions !== 'object') {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
return piWatchlistData.dailyAdditions[date] || null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Get count of users who added this PI to their watchlist on a specific date
|
|
863
|
+
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
864
|
+
* @param {string} date - Date string in YYYY-MM-DD format
|
|
865
|
+
* @returns {number} Count of users who added on this date, or 0 if not available
|
|
866
|
+
*/
|
|
867
|
+
static getDailyAdditionCount(piWatchlistData, date) {
|
|
868
|
+
const dailyData = this.getDailyAdditions(piWatchlistData, date);
|
|
869
|
+
return dailyData?.count || 0;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Get user CIDs who added this PI to their watchlist on a specific date
|
|
874
|
+
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
875
|
+
* @param {string} date - Date string in YYYY-MM-DD format
|
|
876
|
+
* @returns {string[]} Array of user CIDs who added on this date, or empty array if not available
|
|
877
|
+
*/
|
|
878
|
+
static getDailyAdditionUserCids(piWatchlistData, date) {
|
|
879
|
+
const dailyData = this.getDailyAdditions(piWatchlistData, date);
|
|
880
|
+
if (!dailyData || !Array.isArray(dailyData.userCids)) return [];
|
|
881
|
+
return dailyData.userCids.map(cid => String(cid));
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Get all dates with daily addition data
|
|
886
|
+
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
887
|
+
* @returns {string[]} Array of date strings (YYYY-MM-DD format), or empty array if not available
|
|
888
|
+
*/
|
|
889
|
+
static getAllDates(piWatchlistData) {
|
|
890
|
+
if (!piWatchlistData || !piWatchlistData.dailyAdditions || typeof piWatchlistData.dailyAdditions !== 'object') {
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
return Object.keys(piWatchlistData.dailyAdditions);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Get last updated timestamp
|
|
898
|
+
* @param {Object} piWatchlistData - The PI watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
899
|
+
* @returns {Object|null} Timestamp object or null if not available
|
|
900
|
+
*/
|
|
901
|
+
static getLastUpdated(piWatchlistData) {
|
|
902
|
+
if (!piWatchlistData) return null;
|
|
903
|
+
return piWatchlistData.lastUpdated || null;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
811
907
|
/**
|
|
812
908
|
* [NEW] Extractor for PI Alert History Data
|
|
813
909
|
* Access via: context.globalData.alertHistory
|
|
@@ -995,6 +1091,7 @@ module.exports = {
|
|
|
995
1091
|
RatingsExtractor,
|
|
996
1092
|
PageViewsExtractor,
|
|
997
1093
|
WatchlistMembershipExtractor,
|
|
1094
|
+
PIWatchlistDataExtractor,
|
|
998
1095
|
AlertHistoryExtractor,
|
|
999
1096
|
PIMasterListExtractor
|
|
1000
1097
|
};
|
|
@@ -774,6 +774,38 @@ async function loadPIAlertHistory(config, deps, dateString) {
|
|
|
774
774
|
}
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
+
/** Stage 17: Load PI-Centric Watchlist Data
|
|
778
|
+
* Loads watchlist data from PopularInvestors/{piCid}/watchlistData/current
|
|
779
|
+
* This provides time-series data of watchlist additions per PI
|
|
780
|
+
*/
|
|
781
|
+
async function loadPIWatchlistData(config, deps, piCid) {
|
|
782
|
+
const { db, logger, calculationUtils } = deps;
|
|
783
|
+
const { withRetry } = calculationUtils;
|
|
784
|
+
const piCidStr = String(piCid);
|
|
785
|
+
|
|
786
|
+
logger.log('INFO', `Loading PI Watchlist Data for PI ${piCid}`);
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
const docRef = db.collection('PopularInvestors')
|
|
790
|
+
.doc(piCidStr)
|
|
791
|
+
.collection('watchlistData')
|
|
792
|
+
.doc('current');
|
|
793
|
+
|
|
794
|
+
const docSnap = await withRetry(() => docRef.get(), `getPIWatchlistData(${piCidStr})`);
|
|
795
|
+
|
|
796
|
+
if (!docSnap.exists) {
|
|
797
|
+
logger.log('WARN', `PI Watchlist Data not found for PI ${piCidStr}`);
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const data = tryDecompress(docSnap.data());
|
|
802
|
+
return data; // Returns { totalUsers, userCids: [], dailyAdditions: { date: { count, userCids, timestamp } }, lastUpdated }
|
|
803
|
+
} catch (error) {
|
|
804
|
+
logger.log('ERROR', `Failed to load PI Watchlist Data for PI ${piCidStr}: ${error.message}`);
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
777
809
|
// [NEW] Load Popular Investor Master List
|
|
778
810
|
async function loadPopularInvestorMasterList(config, deps) {
|
|
779
811
|
const { db, logger, calculationUtils } = deps;
|
|
@@ -823,5 +855,6 @@ module.exports = {
|
|
|
823
855
|
loadPIPageViews,
|
|
824
856
|
loadWatchlistMembership,
|
|
825
857
|
loadPIAlertHistory,
|
|
826
|
-
loadPopularInvestorMasterList // [NEW]
|
|
858
|
+
loadPopularInvestorMasterList, // [NEW]
|
|
859
|
+
loadPIWatchlistData,
|
|
827
860
|
};
|
|
@@ -285,11 +285,22 @@ async function readWithMigration(db, category, subcategory, params, options = {}
|
|
|
285
285
|
try {
|
|
286
286
|
if (isCollection) {
|
|
287
287
|
// Collection path must have odd number of segments
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
if (documentId) {
|
|
289
|
+
// Read specific document from collection
|
|
290
|
+
const docRef = db.collection(newPath).doc(documentId);
|
|
291
|
+
const doc = await docRef.get();
|
|
292
|
+
if (doc.exists) {
|
|
293
|
+
if (logger) logger.log('INFO', `[readWithMigration] Found document in new path: ${docRef.path}`);
|
|
294
|
+
return { data: doc.data(), exists: true, source: 'new', path: docRef.path };
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
// Read entire collection
|
|
298
|
+
const collectionRef = db.collection(newPath);
|
|
299
|
+
const snapshot = await collectionRef.get();
|
|
300
|
+
if (!snapshot.empty) {
|
|
301
|
+
if (logger) logger.log('INFO', `[readWithMigration] Found data in new path: ${newPath}`);
|
|
302
|
+
return { snapshot, source: 'new', path: newPath };
|
|
303
|
+
}
|
|
293
304
|
}
|
|
294
305
|
} else {
|
|
295
306
|
// Document path: if newPath has even segments, it's already a document path
|
|
@@ -345,17 +356,34 @@ async function readWithMigration(db, category, subcategory, params, options = {}
|
|
|
345
356
|
legacyPath = legacyPath.replace(/{version}/g, params.version || '{version}');
|
|
346
357
|
|
|
347
358
|
if (isCollection) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
359
|
+
if (documentId) {
|
|
360
|
+
// Read specific document from legacy collection
|
|
361
|
+
const legacyDocRef = db.collection(legacyPath).doc(documentId);
|
|
362
|
+
const legacyDoc = await legacyDocRef.get();
|
|
363
|
+
if (legacyDoc.exists) {
|
|
364
|
+
if (logger) logger.log('INFO', `[readWithMigration] Found document in legacy path: ${legacyDocRef.path}, will migrate`);
|
|
365
|
+
|
|
366
|
+
// Auto-migrate if new path is available
|
|
367
|
+
if (newPath) {
|
|
368
|
+
await migrateDocumentData(db, legacyDoc, newPath, documentId, logger);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { data: legacyDoc.data(), exists: true, source: 'legacy', path: legacyDocRef.path, migrated: !!newPath };
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
// Read entire legacy collection
|
|
375
|
+
const legacyCollectionRef = db.collection(legacyPath);
|
|
376
|
+
const legacySnapshot = await legacyCollectionRef.get();
|
|
377
|
+
if (!legacySnapshot.empty) {
|
|
378
|
+
if (logger) logger.log('INFO', `[readWithMigration] Found data in legacy path: ${legacyPath}, will migrate`);
|
|
379
|
+
|
|
380
|
+
// Auto-migrate if new path is available
|
|
381
|
+
if (newPath) {
|
|
382
|
+
await migrateCollectionData(db, legacySnapshot, newPath, dataType, logger);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { snapshot: legacySnapshot, source: 'legacy', path: legacyPath, migrated: !!newPath };
|
|
356
386
|
}
|
|
357
|
-
|
|
358
|
-
return { snapshot: legacySnapshot, source: 'legacy', path: legacyPath, migrated: !!newPath };
|
|
359
387
|
}
|
|
360
388
|
} else {
|
|
361
389
|
const legacyDocRef = documentId
|
|
@@ -116,6 +116,7 @@ async function updatePageViewsRootData(db, logger, piCid, userCid, date) {
|
|
|
116
116
|
* Update Watchlist Membership rootdata collection
|
|
117
117
|
* Aggregates watchlist membership from SignedInUsers/{cid}/watchlists into WatchlistMembershipData/{date}
|
|
118
118
|
* This should be called when a PI is added/removed from a watchlist
|
|
119
|
+
* This provides a single point of reference for all watchlists made in that day
|
|
119
120
|
*/
|
|
120
121
|
async function updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, date, action = 'add') {
|
|
121
122
|
try {
|
|
@@ -161,8 +162,9 @@ async function updateWatchlistMembershipRootData(db, logger, piCid, userCid, isP
|
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
// Update the data
|
|
165
|
+
// Update the data - ensure date field is set for rootdata indexer
|
|
165
166
|
const updateData = {
|
|
167
|
+
date: date, // Ensure date field exists for rootdata indexer
|
|
166
168
|
[piCidStr]: {
|
|
167
169
|
totalUsers: piMembership.totalUsers,
|
|
168
170
|
users: piMembership.users,
|
|
@@ -182,6 +184,89 @@ async function updateWatchlistMembershipRootData(db, logger, piCid, userCid, isP
|
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Update Popular Investor watchlist data
|
|
189
|
+
* Tracks watchlist additions per PI in PopularInvestors/{piCid}/watchlistData
|
|
190
|
+
* This provides a time series of watchlist additions over time per popular investor
|
|
191
|
+
*/
|
|
192
|
+
async function updatePIWatchlistData(db, logger, piCid, userCid, date, action = 'add') {
|
|
193
|
+
try {
|
|
194
|
+
const piCidStr = String(piCid);
|
|
195
|
+
const userCidStr = String(userCid);
|
|
196
|
+
const piWatchlistRef = db.collection('PopularInvestors').doc(piCidStr).collection('watchlistData').doc('current');
|
|
197
|
+
|
|
198
|
+
const piWatchlistDoc = await piWatchlistRef.get();
|
|
199
|
+
const existingData = piWatchlistDoc.exists ? piWatchlistDoc.data() : {};
|
|
200
|
+
|
|
201
|
+
// Initialize structure if needed
|
|
202
|
+
const watchlistData = existingData || {
|
|
203
|
+
totalUsers: 0,
|
|
204
|
+
userCids: [],
|
|
205
|
+
dailyAdditions: {},
|
|
206
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (action === 'add') {
|
|
210
|
+
// Add user CID if not already present
|
|
211
|
+
if (!watchlistData.userCids || !Array.isArray(watchlistData.userCids)) {
|
|
212
|
+
watchlistData.userCids = [];
|
|
213
|
+
}
|
|
214
|
+
if (!watchlistData.userCids.includes(userCidStr)) {
|
|
215
|
+
watchlistData.userCids.push(userCidStr);
|
|
216
|
+
watchlistData.totalUsers = watchlistData.userCids.length;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Track daily additions with timestamp
|
|
220
|
+
if (!watchlistData.dailyAdditions || typeof watchlistData.dailyAdditions !== 'object') {
|
|
221
|
+
watchlistData.dailyAdditions = {};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Initialize date entry if needed
|
|
225
|
+
if (!watchlistData.dailyAdditions[date]) {
|
|
226
|
+
watchlistData.dailyAdditions[date] = {
|
|
227
|
+
count: 0,
|
|
228
|
+
userCids: [],
|
|
229
|
+
timestamp: FieldValue.serverTimestamp()
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Add user to this date's additions if not already present
|
|
234
|
+
const dateEntry = watchlistData.dailyAdditions[date];
|
|
235
|
+
if (!dateEntry.userCids.includes(userCidStr)) {
|
|
236
|
+
dateEntry.userCids.push(userCidStr);
|
|
237
|
+
dateEntry.count = dateEntry.userCids.length;
|
|
238
|
+
dateEntry.timestamp = FieldValue.serverTimestamp();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
} else if (action === 'remove') {
|
|
242
|
+
// Remove user CID
|
|
243
|
+
if (watchlistData.userCids && Array.isArray(watchlistData.userCids)) {
|
|
244
|
+
watchlistData.userCids = watchlistData.userCids.filter(cid => cid !== userCidStr);
|
|
245
|
+
watchlistData.totalUsers = watchlistData.userCids.length;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Update daily additions for this date
|
|
249
|
+
if (watchlistData.dailyAdditions && watchlistData.dailyAdditions[date]) {
|
|
250
|
+
const dateEntry = watchlistData.dailyAdditions[date];
|
|
251
|
+
if (dateEntry.userCids && Array.isArray(dateEntry.userCids)) {
|
|
252
|
+
dateEntry.userCids = dateEntry.userCids.filter(cid => cid !== userCidStr);
|
|
253
|
+
dateEntry.count = dateEntry.userCids.length;
|
|
254
|
+
dateEntry.timestamp = FieldValue.serverTimestamp();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Update the document
|
|
260
|
+
watchlistData.lastUpdated = FieldValue.serverTimestamp();
|
|
261
|
+
await piWatchlistRef.set(watchlistData, { merge: true });
|
|
262
|
+
|
|
263
|
+
logger.log('INFO', `[updatePIWatchlistData] Updated watchlist data for PI ${piCid} (action: ${action}, user: ${userCid})`);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
logger.log('ERROR', `[updatePIWatchlistData] Error updating PI watchlist data for PI ${piCid}:`, error);
|
|
266
|
+
// Don't throw - this is a non-critical aggregation
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
185
270
|
/**
|
|
186
271
|
* Update PI Alert History rootdata collection
|
|
187
272
|
* Records alert triggers in PIAlertHistoryData/{date}
|
|
@@ -286,6 +371,7 @@ module.exports = {
|
|
|
286
371
|
updateRatingsRootData,
|
|
287
372
|
updatePageViewsRootData,
|
|
288
373
|
updateWatchlistMembershipRootData,
|
|
374
|
+
updatePIWatchlistData,
|
|
289
375
|
updateAlertHistoryRootData,
|
|
290
376
|
updateAllClearAlertHistory
|
|
291
377
|
};
|
|
@@ -272,6 +272,21 @@ async function autoGenerateWatchlist(req, res, dependencies, config) {
|
|
|
272
272
|
}
|
|
273
273
|
);
|
|
274
274
|
|
|
275
|
+
// Update global rootdata collections for computation system
|
|
276
|
+
if (watchlistItems && watchlistItems.length > 0) {
|
|
277
|
+
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
278
|
+
const today = new Date().toISOString().split('T')[0];
|
|
279
|
+
const isPublic = false; // Auto-generated watchlists are always private
|
|
280
|
+
|
|
281
|
+
for (const item of watchlistItems) {
|
|
282
|
+
const piCid = String(item.cid);
|
|
283
|
+
// Update global WatchlistMembershipData/{date} document
|
|
284
|
+
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'add');
|
|
285
|
+
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
286
|
+
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'add');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
275
290
|
logger.log('SUCCESS', `[autoGenerateWatchlist] Created auto-generated watchlist ${watchlistId} with ${watchlistItems.length} items for user ${userCid}`);
|
|
276
291
|
|
|
277
292
|
return res.status(200).json({
|
|
@@ -141,22 +141,37 @@ async function createWatchlist(req, res, dependencies, config) {
|
|
|
141
141
|
watchlistData.dynamicConfig = dynamicConfig;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
// Write to new SignedInUsers path with dual-write to legacy path
|
|
145
|
+
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
146
|
+
await writeWithMigration(
|
|
147
|
+
db,
|
|
148
|
+
'signedInUsers',
|
|
149
|
+
'watchlists',
|
|
150
|
+
{ cid: userCid },
|
|
151
|
+
watchlistData,
|
|
152
|
+
{
|
|
153
|
+
isCollection: true,
|
|
154
|
+
merge: false,
|
|
155
|
+
dataType: 'watchlists',
|
|
156
|
+
config,
|
|
157
|
+
collectionRegistry: dependencies.collectionRegistry,
|
|
158
|
+
documentId: watchlistId,
|
|
159
|
+
dualWrite: true
|
|
160
|
+
}
|
|
161
|
+
);
|
|
150
162
|
|
|
151
163
|
// Update global rootdata collection for computation system if static watchlist has items
|
|
152
164
|
if (type === 'static' && items && items.length > 0) {
|
|
153
|
-
const { updateWatchlistMembershipRootData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
165
|
+
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
154
166
|
const today = new Date().toISOString().split('T')[0];
|
|
155
167
|
const isPublic = visibility === 'public';
|
|
156
168
|
|
|
157
169
|
for (const item of items) {
|
|
158
170
|
const piCid = String(item.cid);
|
|
171
|
+
// Update global WatchlistMembershipData/{date} document
|
|
159
172
|
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'add');
|
|
173
|
+
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
174
|
+
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'add');
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
177
|
|
|
@@ -193,19 +208,41 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
193
208
|
}
|
|
194
209
|
|
|
195
210
|
try {
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
// Read from new path with migration fallback
|
|
212
|
+
const { readWithMigration } = require('../core/path_resolution_helpers');
|
|
213
|
+
const readResult = await readWithMigration(
|
|
214
|
+
db,
|
|
215
|
+
'signedInUsers',
|
|
216
|
+
'watchlists',
|
|
217
|
+
{ cid: userCid },
|
|
218
|
+
{
|
|
219
|
+
isCollection: true,
|
|
220
|
+
dataType: 'watchlists',
|
|
221
|
+
config,
|
|
222
|
+
logger,
|
|
223
|
+
collectionRegistry: dependencies.collectionRegistry,
|
|
224
|
+
documentId: id
|
|
225
|
+
}
|
|
226
|
+
);
|
|
203
227
|
|
|
204
|
-
if (!
|
|
228
|
+
if (!readResult) {
|
|
205
229
|
return res.status(404).json({ error: "Watchlist not found" });
|
|
206
230
|
}
|
|
207
231
|
|
|
208
|
-
|
|
232
|
+
// Handle both document read (with data) and collection read (with snapshot)
|
|
233
|
+
let existingData;
|
|
234
|
+
if (readResult.data) {
|
|
235
|
+
existingData = readResult.data;
|
|
236
|
+
} else if (readResult.snapshot && !readResult.snapshot.empty) {
|
|
237
|
+
// If we got a snapshot, find the specific document
|
|
238
|
+
const doc = readResult.snapshot.docs.find(d => d.id === id);
|
|
239
|
+
if (!doc) {
|
|
240
|
+
return res.status(404).json({ error: "Watchlist not found" });
|
|
241
|
+
}
|
|
242
|
+
existingData = doc.data();
|
|
243
|
+
} else {
|
|
244
|
+
return res.status(404).json({ error: "Watchlist not found" });
|
|
245
|
+
}
|
|
209
246
|
|
|
210
247
|
// Verify ownership
|
|
211
248
|
if (existingData.createdBy !== Number(userCid)) {
|
|
@@ -262,7 +299,7 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
262
299
|
|
|
263
300
|
// Update global rootdata collection for computation system
|
|
264
301
|
// Compare old and new items to find added/removed PIs
|
|
265
|
-
const { updateWatchlistMembershipRootData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
302
|
+
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
266
303
|
const oldItems = existingData.items || [];
|
|
267
304
|
const newItems = items || [];
|
|
268
305
|
const oldPiCids = new Set(oldItems.map(item => String(item.cid)));
|
|
@@ -274,7 +311,10 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
274
311
|
for (const newItem of newItems) {
|
|
275
312
|
const piCid = String(newItem.cid);
|
|
276
313
|
if (!oldPiCids.has(piCid)) {
|
|
314
|
+
// Update global WatchlistMembershipData/{date} document
|
|
277
315
|
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'add');
|
|
316
|
+
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
317
|
+
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'add');
|
|
278
318
|
}
|
|
279
319
|
}
|
|
280
320
|
|
|
@@ -282,7 +322,10 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
282
322
|
for (const oldItem of oldItems) {
|
|
283
323
|
const piCid = String(oldItem.cid);
|
|
284
324
|
if (!newPiCids.has(piCid)) {
|
|
325
|
+
// Update global WatchlistMembershipData/{date} document
|
|
285
326
|
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'remove');
|
|
327
|
+
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
328
|
+
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'remove');
|
|
286
329
|
}
|
|
287
330
|
}
|
|
288
331
|
}
|
|
@@ -291,16 +334,35 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
291
334
|
updates.dynamicConfig = dynamicConfig;
|
|
292
335
|
}
|
|
293
336
|
|
|
294
|
-
|
|
337
|
+
// Merge updates with existing data
|
|
338
|
+
const updatedData = { ...existingData, ...updates };
|
|
339
|
+
|
|
340
|
+
// Write to new SignedInUsers path with dual-write to legacy path
|
|
341
|
+
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
342
|
+
await writeWithMigration(
|
|
343
|
+
db,
|
|
344
|
+
'signedInUsers',
|
|
345
|
+
'watchlists',
|
|
346
|
+
{ cid: userCid },
|
|
347
|
+
updatedData,
|
|
348
|
+
{
|
|
349
|
+
isCollection: true,
|
|
350
|
+
merge: true,
|
|
351
|
+
dataType: 'watchlists',
|
|
352
|
+
config,
|
|
353
|
+
collectionRegistry: dependencies.collectionRegistry,
|
|
354
|
+
documentId: id,
|
|
355
|
+
dualWrite: true
|
|
356
|
+
}
|
|
357
|
+
);
|
|
295
358
|
|
|
296
359
|
logger.log('SUCCESS', `[updateWatchlist] Updated watchlist ${id} for user ${userCid}`);
|
|
297
360
|
|
|
298
|
-
const updatedDoc = await watchlistRef.get();
|
|
299
361
|
return res.status(200).json({
|
|
300
362
|
success: true,
|
|
301
363
|
watchlist: {
|
|
302
|
-
id:
|
|
303
|
-
...
|
|
364
|
+
id: id,
|
|
365
|
+
...updatedData
|
|
304
366
|
}
|
|
305
367
|
});
|
|
306
368
|
|
|
@@ -343,8 +405,41 @@ async function deleteWatchlist(req, res, dependencies, config) {
|
|
|
343
405
|
return res.status(403).json({ error: "You can only delete your own watchlists" });
|
|
344
406
|
}
|
|
345
407
|
|
|
346
|
-
//
|
|
347
|
-
|
|
408
|
+
// Update global rootdata collections before deleting
|
|
409
|
+
// Remove all PIs from global tracking if this is a static watchlist with items
|
|
410
|
+
if (watchlistData.type === 'static' && watchlistData.items && watchlistData.items.length > 0) {
|
|
411
|
+
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
412
|
+
const today = new Date().toISOString().split('T')[0];
|
|
413
|
+
const isPublic = watchlistData.visibility === 'public';
|
|
414
|
+
|
|
415
|
+
for (const item of watchlistData.items) {
|
|
416
|
+
const piCid = String(item.cid);
|
|
417
|
+
// Remove from global WatchlistMembershipData/{date} document
|
|
418
|
+
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'remove');
|
|
419
|
+
// Remove from PI-centric PopularInvestors/{piCid}/watchlistData
|
|
420
|
+
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'remove');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Delete watchlist from both new and legacy paths
|
|
425
|
+
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
426
|
+
// Delete from new path
|
|
427
|
+
const newPathRef = db.collection('SignedInUsers')
|
|
428
|
+
.doc(String(userCid))
|
|
429
|
+
.collection('watchlists')
|
|
430
|
+
.doc(id);
|
|
431
|
+
await newPathRef.delete().catch(err => {
|
|
432
|
+
logger.log('WARN', `[deleteWatchlist] Failed to delete from new path: ${err.message}`);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Delete from legacy path if it exists
|
|
436
|
+
const legacyPathRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
437
|
+
.doc(String(userCid))
|
|
438
|
+
.collection('lists')
|
|
439
|
+
.doc(id);
|
|
440
|
+
await legacyPathRef.delete().catch(err => {
|
|
441
|
+
logger.log('WARN', `[deleteWatchlist] Failed to delete from legacy path: ${err.message}`);
|
|
442
|
+
});
|
|
348
443
|
|
|
349
444
|
// Remove from public watchlists if it was public
|
|
350
445
|
if (watchlistData.visibility === 'public') {
|