bulltrackers-module 1.0.554 → 1.0.555

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.
@@ -18,7 +18,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
18
18
 
19
19
  // 2. Find all users subscribed to this PI and alert type
20
20
  // Use computationName (e.g., 'RiskScoreIncrease') to map to alertConfig keys
21
- const subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName);
21
+ const subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName, computationDate, dependencies);
22
22
 
23
23
  if (subscriptions.length === 0) {
24
24
  logger.log('INFO', `[processAlertForPI] No subscriptions found for PI ${piCid}, alert type ${alertType.id}`);
@@ -65,7 +65,6 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
65
65
  };
66
66
 
67
67
  // Use writeWithMigration to write to new path (with legacy fallback)
68
- // notifications is a subcollection, so we need isCollection: true and documentId
69
68
  const writePromise = writeWithMigration(
70
69
  db,
71
70
  'signedInUsers',
@@ -92,10 +91,18 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
92
91
  // Wait for all notifications to be written
93
92
  await Promise.all(notificationPromises);
94
93
 
95
- // 7. Update global rootdata collection for computation system
96
- const { updateAlertHistoryRootData } = require('../../generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers');
97
- const triggeredUserCids = subscriptions.map(s => s.userCid);
98
- await updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids);
94
+ // 5. Notify the PI themselves if they are a signed-in user (Optional feature)
95
+ await notifyPIIfSignedIn(db, logger, piCid, alertType, subscriptions.length);
96
+
97
+ // 6. Update global rootdata collection for computation system
98
+ // (Wrap in try-catch to prevent crashing the alert if metrics fail)
99
+ try {
100
+ const { updateAlertHistoryRootData } = require('../../generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers');
101
+ const triggeredUserCids = subscriptions.map(s => s.userCid);
102
+ await updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids);
103
+ } catch (e) {
104
+ logger.log('WARN', `[processAlertForPI] Failed to update history rootdata: ${e.message}`);
105
+ }
99
106
 
100
107
  logger.log('SUCCESS', `[processAlertForPI] Created ${notificationPromises.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
101
108
 
@@ -107,20 +114,19 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
107
114
 
108
115
  /**
109
116
  * Find all users who should receive alerts for a PI and alert type
110
- * Reads from actual watchlist structure: watchlists/{userCid}/lists/{watchlistId}
117
+ * Uses WatchlistMembershipData/{date} to find users, then reads watchlists from SignedInUsers/{cid}/watchlists
111
118
  */
112
- async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
119
+ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computationDate, dependencies = {}) {
113
120
  const subscriptions = [];
114
121
 
115
122
  // Map computation names to watchlist alertConfig keys
116
- // The alertTypeId is actually the computation name (e.g., 'RiskScoreIncrease')
117
123
  const computationToConfigKey = {
118
124
  'RiskScoreIncrease': 'increasedRisk',
119
125
  'SignificantVolatility': 'volatilityChanges',
120
126
  'NewSectorExposure': 'newSector',
121
127
  'PositionInvestedIncrease': 'increasedPositionSize',
122
128
  'NewSocialPost': 'newSocialPost',
123
- 'TestSystemProbe': 'increasedRisk' // Hack: Map to 'increasedRisk' so you receive it if you have Risk alerts on
129
+ 'TestSystemProbe': 'increasedRisk' // Hack: Map to 'increasedRisk' key
124
130
  };
125
131
 
126
132
  const configKey = computationToConfigKey[alertTypeId];
@@ -129,7 +135,121 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
129
135
  return subscriptions;
130
136
  }
131
137
 
132
- // Get all watchlists
138
+ // Step 1: Load WatchlistMembershipData/{date} to find which users have this PI in their watchlist
139
+ const piCidStr = String(piCid);
140
+ let userCids = [];
141
+
142
+ try {
143
+ const membershipRef = db.collection('WatchlistMembershipData').doc(computationDate);
144
+ const membershipDoc = await membershipRef.get();
145
+
146
+ if (membershipDoc.exists) {
147
+ const membershipData = membershipDoc.data();
148
+ const piMembership = membershipData[piCidStr];
149
+
150
+ if (piMembership && piMembership.users && Array.isArray(piMembership.users)) {
151
+ userCids = piMembership.users.map(cid => String(cid));
152
+ logger.log('INFO', `[findSubscriptionsForPI] Found ${userCids.length} users with PI ${piCid} in watchlist from WatchlistMembershipData/${computationDate}`);
153
+ } else {
154
+ logger.log('INFO', `[findSubscriptionsForPI] No users found for PI ${piCid} in WatchlistMembershipData/${computationDate}`);
155
+ return subscriptions;
156
+ }
157
+ } else {
158
+ logger.log('WARN', `[findSubscriptionsForPI] WatchlistMembershipData/${computationDate} not found, falling back to scanning all watchlists`);
159
+ // Fallback to legacy method if membership data doesn't exist
160
+ return await findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey);
161
+ }
162
+ } catch (error) {
163
+ logger.log('ERROR', `[findSubscriptionsForPI] Error loading WatchlistMembershipData: ${error.message}`);
164
+ // Fallback to legacy method on error
165
+ return await findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey);
166
+ }
167
+
168
+ // Step 2: For each user, read their watchlists from SignedInUsers/{cid}/watchlists
169
+ const { readWithMigration } = require('../../generic-api/user-api/helpers/core/path_resolution_helpers');
170
+ const { collectionRegistry } = dependencies;
171
+ const config = dependencies.config || {};
172
+
173
+ for (const userCidStr of userCids) {
174
+ try {
175
+ const userCid = Number(userCidStr);
176
+
177
+ // Read all watchlists for this user from new path
178
+ const watchlistsResult = await readWithMigration(
179
+ db,
180
+ 'signedInUsers',
181
+ 'watchlists',
182
+ { cid: userCid },
183
+ {
184
+ isCollection: true,
185
+ dataType: 'watchlists',
186
+ config,
187
+ logger,
188
+ collectionRegistry: collectionRegistry
189
+ }
190
+ );
191
+
192
+ if (!watchlistsResult) {
193
+ continue;
194
+ }
195
+
196
+ // Get watchlists from snapshot or data
197
+ let watchlists = [];
198
+ if (watchlistsResult.snapshot && !watchlistsResult.snapshot.empty) {
199
+ watchlists = watchlistsResult.snapshot.docs.map(doc => ({
200
+ id: doc.id,
201
+ ...doc.data()
202
+ }));
203
+ } else if (watchlistsResult.data) {
204
+ // If it's a single document, wrap it
205
+ watchlists = [watchlistsResult.data];
206
+ }
207
+
208
+ // Step 3: Check each watchlist for the PI and alert config
209
+ for (const watchlistData of watchlists) {
210
+ if (watchlistData.type === 'static' && watchlistData.items && Array.isArray(watchlistData.items)) {
211
+ for (const item of watchlistData.items) {
212
+ if (Number(item.cid) === Number(piCid)) {
213
+ // Check if this alert type is enabled
214
+ const isTestProbe = alertTypeId === 'TestSystemProbe';
215
+ const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
216
+
217
+ // If it's the probe OR the user explicitly enabled it
218
+ if (isTestProbe || isEnabled) {
219
+ subscriptions.push({
220
+ userCid: userCid,
221
+ piCid: piCid,
222
+ piUsername: item.username || `PI-${piCid}`,
223
+ watchlistId: watchlistData.id || watchlistData.watchlistId,
224
+ watchlistName: watchlistData.name || 'Unnamed Watchlist',
225
+ alertConfig: item.alertConfig
226
+ });
227
+ break; // Found in this watchlist, no need to check other items
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ } catch (error) {
234
+ logger.log('WARN', `[findSubscriptionsForPI] Error reading watchlists for user ${userCidStr}: ${error.message}`);
235
+ // Continue with next user
236
+ continue;
237
+ }
238
+ }
239
+
240
+ logger.log('INFO', `[findSubscriptionsForPI] Found ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
241
+ return subscriptions;
242
+ }
243
+
244
+ /**
245
+ * Legacy fallback method - scans all watchlists (used if WatchlistMembershipData is not available)
246
+ */
247
+ async function findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey) {
248
+ const subscriptions = [];
249
+
250
+ logger.log('INFO', `[findSubscriptionsForPILegacy] Using legacy method to find subscriptions for PI ${piCid}`);
251
+
252
+ // Get all watchlists from legacy path
133
253
  const watchlistsCollection = db.collection('watchlists');
134
254
  const watchlistsSnapshot = await watchlistsCollection.get();
135
255
 
@@ -144,8 +264,10 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
144
264
  if (listData.type === 'static' && listData.items && Array.isArray(listData.items)) {
145
265
  for (const item of listData.items) {
146
266
  if (Number(item.cid) === Number(piCid)) {
147
- // Check if this alert type is enabled for this PI in this watchlist
148
- if (item.alertConfig && item.alertConfig[configKey] === true) {
267
+ const isTestProbe = alertTypeId === 'TestSystemProbe';
268
+ const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
269
+
270
+ if (isTestProbe || isEnabled) {
149
271
  subscriptions.push({
150
272
  userCid: userCid,
151
273
  piCid: piCid,
@@ -154,19 +276,14 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
154
276
  watchlistName: listData.name || 'Unnamed Watchlist',
155
277
  alertConfig: item.alertConfig
156
278
  });
157
- break; // Found in this watchlist, no need to check other items
279
+ break;
158
280
  }
159
281
  }
160
282
  }
161
283
  }
162
-
163
- // Check dynamic watchlists - if the PI matches the computation result, check alertConfig
164
- // Note: Dynamic watchlists are handled separately as they're based on computation results
165
- // For now, we only process static watchlists here
166
284
  }
167
285
  }
168
286
 
169
- logger.log('INFO', `[findSubscriptionsForPI] Found ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
170
287
  return subscriptions;
171
288
  }
172
289
 
@@ -178,10 +295,6 @@ function shouldTriggerAlert(subscription, alertTypeId) {
178
295
  if (!subscription.thresholds || Object.keys(subscription.thresholds).length === 0) {
179
296
  return true;
180
297
  }
181
-
182
- // Threshold checking logic would go here
183
- // For now, we'll implement basic threshold checks in the trigger function
184
- // This is a placeholder for future enhancement
185
298
  return true;
186
299
  }
187
300
 
@@ -195,9 +308,7 @@ async function getPIUsername(db, piCid) {
195
308
  const { getPIUsernameFromMasterList } = require('../../generic-api/user-api/helpers/core/user_status_helpers');
196
309
  const username = await getPIUsernameFromMasterList(db, piCid, null, null);
197
310
 
198
- if (username) {
199
- return username;
200
- }
311
+ if (username) return username;
201
312
 
202
313
  // Fallback: try to get from any subscription
203
314
  const subscriptionsSnapshot = await db.collection('watchlist_subscriptions')
@@ -221,18 +332,14 @@ async function getPIUsername(db, piCid) {
221
332
  */
222
333
  async function notifyPIIfSignedIn(db, logger, piCid, alertType, alertCount) {
223
334
  try {
224
- // Check if this PI is a signed-in user
225
- const userRef = db.collection('signed_in_users').doc(String(piCid));
335
+ const userRef = db.collection('signedInUsers').doc(String(piCid));
226
336
  const userDoc = await userRef.get();
227
337
 
228
- if (!userDoc.exists) {
229
- return; // Not a signed-in user
230
- }
231
-
232
- // Create a notification for the PI
233
- const notificationRef = db.collection('user_alerts')
338
+ if (!userDoc.exists) return;
339
+
340
+ const notificationRef = db.collection('signedInUsers')
234
341
  .doc(String(piCid))
235
- .collection('triggered')
342
+ .collection('user_alerts_metrics') // Changed from user_alerts/triggered to avoid collision
236
343
  .doc(`trigger_${Date.now()}_${alertType.id}`);
237
344
 
238
345
  await notificationRef.set({
@@ -246,8 +353,8 @@ async function notifyPIIfSignedIn(db, logger, piCid, alertType, alertCount) {
246
353
 
247
354
  logger.log('INFO', `[notifyPIIfSignedIn] Notified PI ${piCid} that ${alertCount} users received ${alertType.id} alert`);
248
355
  } catch (error) {
249
- logger.log('WARN', `[notifyPIIfSignedIn] Error notifying PI ${piCid}`, error);
250
- // Don't throw - this is optional
356
+ // Silent fail - non-critical
357
+ logger.log('DEBUG', `[notifyPIIfSignedIn] Skipped for PI ${piCid}`);
251
358
  }
252
359
  }
253
360
 
@@ -290,25 +397,17 @@ function tryDecompress(data) {
290
397
 
291
398
  /**
292
399
  * Read and decompress computation results
293
- * Handles two result formats:
294
- * 1. { cids: [...], metadata: {...} } - Global metadata
295
- * 2. { cids: [...], userId1: {...}, userId2: {...} } - Per-user metadata
296
400
  */
297
401
  function readComputationResults(docData) {
298
402
  try {
299
- // Try to decompress if needed
300
403
  const decompressed = tryDecompress(docData);
301
404
 
302
- // If decompressed is an object with cids, return it
303
405
  if (decompressed && typeof decompressed === 'object') {
304
- // Handle structure: { cids: [...], metadata: {...} }
305
406
  if (decompressed.cids && Array.isArray(decompressed.cids)) {
306
- // Check if there's per-user data (keys that are numbers)
307
407
  const userDataKeys = Object.keys(decompressed)
308
408
  .filter(key => key !== 'cids' && key !== 'metadata' && /^\d+$/.test(key));
309
409
 
310
410
  if (userDataKeys.length > 0) {
311
- // Format 2: Per-user metadata stored as { cids: [...], userId: {...} }
312
411
  return {
313
412
  cids: decompressed.cids,
314
413
  perUserData: userDataKeys.reduce((acc, key) => {
@@ -318,7 +417,6 @@ function readComputationResults(docData) {
318
417
  globalMetadata: decompressed.metadata || {}
319
418
  };
320
419
  } else {
321
- // Format 1: Global metadata
322
420
  return {
323
421
  cids: decompressed.cids,
324
422
  metadata: decompressed.metadata || {}
@@ -326,7 +424,6 @@ function readComputationResults(docData) {
326
424
  }
327
425
  }
328
426
 
329
- // If it's a flat object with CID keys but no cids array, extract them
330
427
  const cids = Object.keys(decompressed)
331
428
  .filter(key => /^\d+$/.test(key))
332
429
  .map(key => Number(key));
@@ -344,7 +441,6 @@ function readComputationResults(docData) {
344
441
  };
345
442
  }
346
443
  }
347
-
348
444
  return { cids: [], metadata: {}, perUserData: {} };
349
445
  } catch (error) {
350
446
  console.error('[readComputationResults] Error reading results', error);
@@ -357,9 +453,7 @@ function readComputationResults(docData) {
357
453
  */
358
454
  async function readComputationResultsWithShards(db, docData, docRef) {
359
455
  try {
360
- // Check if data is sharded
361
456
  if (docData._sharded === true && docData._shardCount) {
362
- // Data is stored in shards - read all shards and merge
363
457
  const shardsCol = docRef.collection('_shards');
364
458
  const shardsSnapshot = await shardsCol.get();
365
459
 
@@ -373,8 +467,6 @@ async function readComputationResultsWithShards(db, docData, docRef) {
373
467
  return readComputationResults(mergedData);
374
468
  }
375
469
  }
376
-
377
- // Data is in the main document (compressed or not)
378
470
  return readComputationResults(docData);
379
471
  } catch (error) {
380
472
  console.error('[readComputationResultsWithShards] Error reading sharded results', error);
@@ -387,6 +479,6 @@ module.exports = {
387
479
  findSubscriptionsForPI,
388
480
  getPIUsername,
389
481
  readComputationResults,
390
- readComputationResultsWithShards
391
- };
392
-
482
+ readComputationResultsWithShards,
483
+ notifyPIIfSignedIn // Exporting this as well since it's defined
484
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.554",
3
+ "version": "1.0.555",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [