bulltrackers-module 1.0.548 → 1.0.549
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/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 +105 -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
|
};
|
|
@@ -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,28 @@ 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 || !readResult.exists) {
|
|
205
229
|
return res.status(404).json({ error: "Watchlist not found" });
|
|
206
230
|
}
|
|
207
231
|
|
|
208
|
-
const existingData =
|
|
232
|
+
const existingData = readResult.data();
|
|
209
233
|
|
|
210
234
|
// Verify ownership
|
|
211
235
|
if (existingData.createdBy !== Number(userCid)) {
|
|
@@ -262,7 +286,7 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
262
286
|
|
|
263
287
|
// Update global rootdata collection for computation system
|
|
264
288
|
// Compare old and new items to find added/removed PIs
|
|
265
|
-
const { updateWatchlistMembershipRootData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
289
|
+
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
266
290
|
const oldItems = existingData.items || [];
|
|
267
291
|
const newItems = items || [];
|
|
268
292
|
const oldPiCids = new Set(oldItems.map(item => String(item.cid)));
|
|
@@ -274,7 +298,10 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
274
298
|
for (const newItem of newItems) {
|
|
275
299
|
const piCid = String(newItem.cid);
|
|
276
300
|
if (!oldPiCids.has(piCid)) {
|
|
301
|
+
// Update global WatchlistMembershipData/{date} document
|
|
277
302
|
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'add');
|
|
303
|
+
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
304
|
+
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'add');
|
|
278
305
|
}
|
|
279
306
|
}
|
|
280
307
|
|
|
@@ -282,7 +309,10 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
282
309
|
for (const oldItem of oldItems) {
|
|
283
310
|
const piCid = String(oldItem.cid);
|
|
284
311
|
if (!newPiCids.has(piCid)) {
|
|
312
|
+
// Update global WatchlistMembershipData/{date} document
|
|
285
313
|
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'remove');
|
|
314
|
+
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
315
|
+
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'remove');
|
|
286
316
|
}
|
|
287
317
|
}
|
|
288
318
|
}
|
|
@@ -291,16 +321,35 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
291
321
|
updates.dynamicConfig = dynamicConfig;
|
|
292
322
|
}
|
|
293
323
|
|
|
294
|
-
|
|
324
|
+
// Merge updates with existing data
|
|
325
|
+
const updatedData = { ...existingData, ...updates };
|
|
326
|
+
|
|
327
|
+
// Write to new SignedInUsers path with dual-write to legacy path
|
|
328
|
+
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
329
|
+
await writeWithMigration(
|
|
330
|
+
db,
|
|
331
|
+
'signedInUsers',
|
|
332
|
+
'watchlists',
|
|
333
|
+
{ cid: userCid },
|
|
334
|
+
updatedData,
|
|
335
|
+
{
|
|
336
|
+
isCollection: true,
|
|
337
|
+
merge: true,
|
|
338
|
+
dataType: 'watchlists',
|
|
339
|
+
config,
|
|
340
|
+
collectionRegistry: dependencies.collectionRegistry,
|
|
341
|
+
documentId: id,
|
|
342
|
+
dualWrite: true
|
|
343
|
+
}
|
|
344
|
+
);
|
|
295
345
|
|
|
296
346
|
logger.log('SUCCESS', `[updateWatchlist] Updated watchlist ${id} for user ${userCid}`);
|
|
297
347
|
|
|
298
|
-
const updatedDoc = await watchlistRef.get();
|
|
299
348
|
return res.status(200).json({
|
|
300
349
|
success: true,
|
|
301
350
|
watchlist: {
|
|
302
|
-
id:
|
|
303
|
-
...
|
|
351
|
+
id: id,
|
|
352
|
+
...updatedData
|
|
304
353
|
}
|
|
305
354
|
});
|
|
306
355
|
|
|
@@ -343,8 +392,41 @@ async function deleteWatchlist(req, res, dependencies, config) {
|
|
|
343
392
|
return res.status(403).json({ error: "You can only delete your own watchlists" });
|
|
344
393
|
}
|
|
345
394
|
|
|
346
|
-
//
|
|
347
|
-
|
|
395
|
+
// Update global rootdata collections before deleting
|
|
396
|
+
// Remove all PIs from global tracking if this is a static watchlist with items
|
|
397
|
+
if (watchlistData.type === 'static' && watchlistData.items && watchlistData.items.length > 0) {
|
|
398
|
+
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
399
|
+
const today = new Date().toISOString().split('T')[0];
|
|
400
|
+
const isPublic = watchlistData.visibility === 'public';
|
|
401
|
+
|
|
402
|
+
for (const item of watchlistData.items) {
|
|
403
|
+
const piCid = String(item.cid);
|
|
404
|
+
// Remove from global WatchlistMembershipData/{date} document
|
|
405
|
+
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'remove');
|
|
406
|
+
// Remove from PI-centric PopularInvestors/{piCid}/watchlistData
|
|
407
|
+
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'remove');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Delete watchlist from both new and legacy paths
|
|
412
|
+
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
413
|
+
// Delete from new path
|
|
414
|
+
const newPathRef = db.collection('SignedInUsers')
|
|
415
|
+
.doc(String(userCid))
|
|
416
|
+
.collection('watchlists')
|
|
417
|
+
.doc(id);
|
|
418
|
+
await newPathRef.delete().catch(err => {
|
|
419
|
+
logger.log('WARN', `[deleteWatchlist] Failed to delete from new path: ${err.message}`);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Delete from legacy path if it exists
|
|
423
|
+
const legacyPathRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
424
|
+
.doc(String(userCid))
|
|
425
|
+
.collection('lists')
|
|
426
|
+
.doc(id);
|
|
427
|
+
await legacyPathRef.delete().catch(err => {
|
|
428
|
+
logger.log('WARN', `[deleteWatchlist] Failed to delete from legacy path: ${err.message}`);
|
|
429
|
+
});
|
|
348
430
|
|
|
349
431
|
// Remove from public watchlists if it was public
|
|
350
432
|
if (watchlistData.visibility === 'public') {
|