bulltrackers-module 1.0.465 → 1.0.467

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.
@@ -20,7 +20,8 @@ async function getAlertTypes(req, res, dependencies, config) {
20
20
  id: type.id,
21
21
  name: type.name,
22
22
  description: type.description,
23
- severity: type.severity
23
+ severity: type.severity,
24
+ computationName: type.computationName // Include computation name for dynamic watchlists
24
25
  }))
25
26
  });
26
27
  } catch (error) {
@@ -30,6 +31,33 @@ async function getAlertTypes(req, res, dependencies, config) {
30
31
  }
31
32
  }
32
33
 
34
+ /**
35
+ * GET /user/me/dynamic-watchlist-computations
36
+ * Get available computations for dynamic watchlists (from alert types)
37
+ */
38
+ async function getDynamicWatchlistComputations(req, res, dependencies, config) {
39
+ try {
40
+ const alertTypes = getAllAlertTypes();
41
+
42
+ // Extract unique computations from alert types
43
+ const computations = alertTypes.map(type => ({
44
+ computationName: type.computationName,
45
+ alertTypeName: type.name,
46
+ description: type.description,
47
+ severity: type.severity
48
+ }));
49
+
50
+ return res.status(200).json({
51
+ success: true,
52
+ computations
53
+ });
54
+ } catch (error) {
55
+ const { logger } = dependencies;
56
+ logger.log('ERROR', '[getDynamicWatchlistComputations] Error fetching computations', error);
57
+ return res.status(500).json({ error: error.message });
58
+ }
59
+ }
60
+
33
61
  /**
34
62
  * GET /user/me/alerts
35
63
  * Get user's alerts (paginated)
@@ -317,6 +345,7 @@ async function deleteAlert(req, res, dependencies, config) {
317
345
 
318
346
  module.exports = {
319
347
  getAlertTypes,
348
+ getDynamicWatchlistComputations,
320
349
  getUserAlerts,
321
350
  getAlertCount,
322
351
  markAlertRead,
@@ -148,20 +148,7 @@ async function createWatchlist(req, res, dependencies, config) {
148
148
 
149
149
  await watchlistRef.set(watchlistData);
150
150
 
151
- // If public, also add to public watchlists collection
152
- if (visibility === 'public') {
153
- const publicRef = db.collection('public_watchlists').doc(watchlistId);
154
- await publicRef.set({
155
- watchlistId,
156
- createdBy: Number(userCid),
157
- name: watchlistData.name,
158
- type,
159
- description: dynamicConfig?.description || '',
160
- copyCount: 0,
161
- createdAt: FieldValue.serverTimestamp(),
162
- updatedAt: FieldValue.serverTimestamp()
163
- });
164
- }
151
+ // Watchlists are always created as private - no public version on creation
165
152
 
166
153
  logger.log('SUCCESS', `[createWatchlist] Created ${type} watchlist "${name}" for user ${userCid}`);
167
154
 
@@ -250,29 +237,12 @@ async function updateWatchlist(req, res, dependencies, config) {
250
237
  updates.hasBeenModified = true;
251
238
  }
252
239
 
253
- if (visibility !== undefined) {
254
- if (visibility !== 'public' && visibility !== 'private') {
255
- return res.status(400).json({ error: "Visibility must be 'public' or 'private'" });
256
- }
257
- updates.visibility = visibility;
258
-
259
- // Update public watchlists collection
260
- const publicRef = db.collection('public_watchlists').doc(id);
261
- if (visibility === 'public') {
262
- await publicRef.set({
263
- watchlistId: id,
264
- createdBy: existingData.createdBy,
265
- name: updates.name || existingData.name,
266
- type: existingData.type,
267
- description: (dynamicConfig?.description || existingData.dynamicConfig?.description || ''),
268
- copyCount: existingData.copyCount || 0,
269
- createdAt: existingData.createdAt,
270
- updatedAt: FieldValue.serverTimestamp()
271
- }, { merge: true });
272
- } else {
273
- // Remove from public if making private
274
- await publicRef.delete();
275
- }
240
+ // Visibility changes are now handled through publish/unpublish endpoints
241
+ // Users can only update their private watchlist
242
+ if (visibility !== undefined && visibility !== 'private') {
243
+ return res.status(400).json({
244
+ error: "Cannot change visibility directly. Use the publish endpoint to create a public version."
245
+ });
276
246
  }
277
247
 
278
248
  if (items !== undefined && existingData.type === 'static') {
@@ -368,7 +338,7 @@ async function copyWatchlist(req, res, dependencies, config) {
368
338
  const { db, logger } = dependencies;
369
339
  const { userCid } = req.query;
370
340
  const { id } = req.params;
371
- const { name } = req.body; // Optional custom name
341
+ const { name, version } = req.body; // Optional custom name and version number
372
342
 
373
343
  if (!userCid || !id) {
374
344
  return res.status(400).json({ error: "Missing userCid or watchlist id" });
@@ -388,20 +358,51 @@ async function copyWatchlist(req, res, dependencies, config) {
388
358
  const publicData = publicDoc.data();
389
359
  const originalCreatorCid = Number(publicData.createdBy);
390
360
 
391
- // Find the original watchlist
392
- const originalRef = db.collection(config.watchlistsCollection || 'watchlists')
393
- .doc(String(originalCreatorCid))
394
- .collection('lists')
395
- .doc(id);
396
-
397
- const originalDoc = await originalRef.get();
361
+ let originalData;
398
362
 
399
- if (!originalDoc.exists) {
400
- return res.status(404).json({ error: "Original watchlist not found" });
363
+ // If version is specified, copy from that specific version snapshot
364
+ if (version) {
365
+ const versionRef = db.collection('public_watchlists')
366
+ .doc(id)
367
+ .collection('versions')
368
+ .doc(String(version));
369
+
370
+ const versionDoc = await versionRef.get();
371
+
372
+ if (!versionDoc.exists) {
373
+ return res.status(404).json({ error: `Version ${version} not found for this watchlist` });
374
+ }
375
+
376
+ originalData = versionDoc.data();
377
+ } else {
378
+ // Copy from latest version (get the latest version)
379
+ const latestVersion = publicData.latestVersion || 1;
380
+ const versionRef = db.collection('public_watchlists')
381
+ .doc(id)
382
+ .collection('versions')
383
+ .doc(String(latestVersion));
384
+
385
+ const versionDoc = await versionRef.get();
386
+
387
+ if (versionDoc.exists) {
388
+ originalData = versionDoc.data();
389
+ } else {
390
+ // Fallback: try to get from original watchlist (for backwards compatibility)
391
+ const originalRef = db.collection(config.watchlistsCollection || 'watchlists')
392
+ .doc(String(originalCreatorCid))
393
+ .collection('lists')
394
+ .doc(id);
395
+
396
+ const originalDoc = await originalRef.get();
397
+
398
+ if (!originalDoc.exists) {
399
+ return res.status(404).json({ error: "Original watchlist not found" });
400
+ }
401
+
402
+ originalData = originalDoc.data();
403
+ }
401
404
  }
402
405
 
403
- const originalData = originalDoc.data();
404
-
405
406
  // Check if user is copying their own watchlist
406
407
  const isCopyingOwn = originalCreatorCid === userCidNum;
407
408
 
@@ -445,6 +446,7 @@ async function copyWatchlist(req, res, dependencies, config) {
445
446
  createdBy: userCidNum,
446
447
  visibility: 'private', // Copied watchlists are always private
447
448
  copiedFrom: id,
449
+ copiedFromVersion: version || (publicData.latestVersion || 1), // Track which version was copied
448
450
  copiedFromCreator: originalCreatorCid,
449
451
  originalName: originalData.name, // Store original name for comparison
450
452
  hasBeenModified: false, // Track if user made meaningful changes
@@ -455,6 +457,11 @@ async function copyWatchlist(req, res, dependencies, config) {
455
457
 
456
458
  // Remove fields that shouldn't be copied
457
459
  delete watchlistData.copyCount;
460
+ delete watchlistData.version;
461
+ delete watchlistData.versionId;
462
+ delete watchlistData.snapshotAt;
463
+ delete watchlistData.isImmutable;
464
+ delete watchlistData.watchlistId;
458
465
 
459
466
  const newWatchlistRef = db.collection(config.watchlistsCollection || 'watchlists')
460
467
  .doc(String(userCidNum))
@@ -525,6 +532,157 @@ async function getPublicWatchlists(req, res, dependencies, config) {
525
532
  }
526
533
  }
527
534
 
535
+ /**
536
+ * POST /user/me/watchlists/:id/publish
537
+ * Publish a version of a private watchlist
538
+ * Creates an immutable snapshot version that can be copied by others
539
+ */
540
+ async function publishWatchlistVersion(req, res, dependencies, config) {
541
+ const { db, logger } = dependencies;
542
+ const { userCid } = req.query;
543
+ const { id } = req.params;
544
+
545
+ if (!userCid || !id) {
546
+ return res.status(400).json({ error: "Missing userCid or watchlist id" });
547
+ }
548
+
549
+ try {
550
+ const watchlistsCollection = config.watchlistsCollection || 'watchlists';
551
+ const watchlistRef = db.collection(watchlistsCollection)
552
+ .doc(String(userCid))
553
+ .collection('lists')
554
+ .doc(id);
555
+
556
+ const watchlistDoc = await watchlistRef.get();
557
+
558
+ if (!watchlistDoc.exists) {
559
+ return res.status(404).json({ error: "Watchlist not found" });
560
+ }
561
+
562
+ const watchlistData = watchlistDoc.data();
563
+
564
+ // Verify ownership
565
+ if (watchlistData.createdBy !== Number(userCid)) {
566
+ return res.status(403).json({ error: "You can only publish your own watchlists" });
567
+ }
568
+
569
+ // Check if this is a copied watchlist that hasn't been modified
570
+ if (watchlistData.copiedFrom && watchlistData.copiedFromCreator && !watchlistData.hasBeenModified) {
571
+ return res.status(400).json({
572
+ error: "Cannot publish copied watchlist without making meaningful changes. Please modify the watchlist items, thresholds, or parameters before publishing."
573
+ });
574
+ }
575
+
576
+ // Get current version number
577
+ const publicRef = db.collection('public_watchlists').doc(id);
578
+ const publicDoc = await publicRef.get();
579
+
580
+ let versionNumber = 1;
581
+ if (publicDoc.exists) {
582
+ const publicData = publicDoc.data();
583
+ versionNumber = (publicData.latestVersion || 0) + 1;
584
+ }
585
+
586
+ // Create version snapshot
587
+ const versionId = `${id}_v${versionNumber}`;
588
+ const versionData = {
589
+ watchlistId: id,
590
+ version: versionNumber,
591
+ createdBy: Number(userCid),
592
+ name: watchlistData.name,
593
+ type: watchlistData.type,
594
+ description: watchlistData.dynamicConfig?.description || '',
595
+ snapshotAt: FieldValue.serverTimestamp(),
596
+ copyCount: 0,
597
+ createdAt: watchlistData.createdAt,
598
+ isImmutable: true // Public versions are immutable
599
+ };
600
+
601
+ // Only include items if it's a static watchlist
602
+ if (watchlistData.type === 'static' && watchlistData.items) {
603
+ versionData.items = JSON.parse(JSON.stringify(watchlistData.items));
604
+ }
605
+
606
+ // Only include dynamicConfig if it's a dynamic watchlist
607
+ if (watchlistData.type === 'dynamic' && watchlistData.dynamicConfig) {
608
+ versionData.dynamicConfig = JSON.parse(JSON.stringify(watchlistData.dynamicConfig));
609
+ }
610
+
611
+ // Store version in versions subcollection
612
+ const versionRef = db.collection('public_watchlists')
613
+ .doc(id)
614
+ .collection('versions')
615
+ .doc(String(versionNumber));
616
+
617
+ await versionRef.set(versionData);
618
+
619
+ // Update or create public watchlist entry (points to latest version)
620
+ await publicRef.set({
621
+ watchlistId: id,
622
+ createdBy: Number(userCid),
623
+ name: watchlistData.name,
624
+ type: watchlistData.type,
625
+ description: watchlistData.dynamicConfig?.description || '',
626
+ latestVersion: versionNumber,
627
+ latestVersionId: versionId,
628
+ copyCount: publicDoc.exists ? (publicDoc.data().copyCount || 0) : 0,
629
+ createdAt: watchlistData.createdAt,
630
+ updatedAt: FieldValue.serverTimestamp()
631
+ }, { merge: true });
632
+
633
+ logger.log('SUCCESS', `[publishWatchlistVersion] User ${userCid} published watchlist ${id} as version ${versionNumber}`);
634
+
635
+ return res.status(201).json({
636
+ success: true,
637
+ version: versionNumber,
638
+ versionId: versionId,
639
+ watchlist: versionData
640
+ });
641
+
642
+ } catch (error) {
643
+ logger.log('ERROR', `[publishWatchlistVersion] Error publishing watchlist ${id} for ${userCid}`, error);
644
+ return res.status(500).json({ error: error.message });
645
+ }
646
+ }
647
+
648
+ /**
649
+ * GET /user/public-watchlists/:id/versions
650
+ * Get version history for a public watchlist
651
+ */
652
+ async function getWatchlistVersions(req, res, dependencies, config) {
653
+ const { db, logger } = dependencies;
654
+ const { id } = req.params;
655
+
656
+ try {
657
+ const versionsRef = db.collection('public_watchlists')
658
+ .doc(id)
659
+ .collection('versions')
660
+ .orderBy('version', 'desc');
661
+
662
+ const snapshot = await versionsRef.get();
663
+ const versions = [];
664
+
665
+ snapshot.forEach(doc => {
666
+ versions.push({
667
+ version: doc.data().version,
668
+ versionId: `${id}_v${doc.data().version}`,
669
+ ...doc.data()
670
+ });
671
+ });
672
+
673
+ return res.status(200).json({
674
+ success: true,
675
+ watchlistId: id,
676
+ versions,
677
+ count: versions.length
678
+ });
679
+
680
+ } catch (error) {
681
+ logger.log('ERROR', `[getWatchlistVersions] Error fetching versions for ${id}`, error);
682
+ return res.status(500).json({ error: error.message });
683
+ }
684
+ }
685
+
528
686
  module.exports = {
529
687
  getUserWatchlists,
530
688
  getWatchlist,
@@ -532,6 +690,8 @@ module.exports = {
532
690
  updateWatchlist,
533
691
  deleteWatchlist,
534
692
  copyWatchlist,
535
- getPublicWatchlists
693
+ getPublicWatchlists,
694
+ publishWatchlistVersion,
695
+ getWatchlistVersions
536
696
  };
537
697
 
@@ -6,10 +6,10 @@ const express = require('express');
6
6
  const { submitReview, getReviews, getUserReview, checkReviewEligibility } = require('./helpers/review_helpers');
7
7
  const { getPiAnalytics, getUserRecommendations, getWatchlist, updateWatchlist, autoGenerateWatchlist, getUserDataStatus, getUserPortfolio, getUserSocialPosts, getUserComputations, getUserVerification, getInstrumentMappings, searchPopularInvestors, requestPiAddition, getWatchlistTriggerCounts, checkPisInRankings, getPiProfile, checkIfUserIsPopularInvestor, trackProfileView, getSignedInUserPIPersonalizedMetrics } = require('./helpers/data_helpers');
8
8
  const { initiateVerification, finalizeVerification } = require('./helpers/verification_helpers');
9
- const { getUserWatchlists, getWatchlist: getWatchlistById, createWatchlist, updateWatchlist: updateWatchlistById, deleteWatchlist, copyWatchlist, getPublicWatchlists } = require('./helpers/watchlist_helpers');
9
+ const { getUserWatchlists, getWatchlist: getWatchlistById, createWatchlist, updateWatchlist: updateWatchlistById, deleteWatchlist, copyWatchlist, getPublicWatchlists, publishWatchlistVersion, getWatchlistVersions } = require('./helpers/watchlist_helpers');
10
10
  const { subscribeToAlerts, updateSubscription, unsubscribeFromAlerts, getUserSubscriptions, subscribeToWatchlist } = require('./helpers/subscription_helpers');
11
11
  const { setDevOverride, getDevOverrideStatus } = require('./helpers/dev_helpers');
12
- const { getAlertTypes, getUserAlerts, getAlertCount, markAlertRead, markAllAlertsRead, deleteAlert } = require('./helpers/alert_helpers');
12
+ const { getAlertTypes, getDynamicWatchlistComputations, getUserAlerts, getAlertCount, markAlertRead, markAllAlertsRead, deleteAlert } = require('./helpers/alert_helpers');
13
13
  const { requestPiFetch, getPiFetchStatus } = require('./helpers/on_demand_fetch_helpers');
14
14
  const { requestUserSync, getUserSyncStatus } = require('./helpers/user_sync_helpers');
15
15
 
@@ -65,9 +65,11 @@ module.exports = (dependencies, config) => {
65
65
  router.put('/me/watchlists/:id', (req, res) => updateWatchlistById(req, res, dependencies, config));
66
66
  router.delete('/me/watchlists/:id', (req, res) => deleteWatchlist(req, res, dependencies, config));
67
67
  router.post('/me/watchlists/:id/copy', (req, res) => copyWatchlist(req, res, dependencies, config));
68
+ router.post('/me/watchlists/:id/publish', (req, res) => publishWatchlistVersion(req, res, dependencies, config));
68
69
 
69
70
  // Public watchlists
70
71
  router.get('/public-watchlists', (req, res) => getPublicWatchlists(req, res, dependencies, config));
72
+ router.get('/public-watchlists/:id/versions', (req, res) => getWatchlistVersions(req, res, dependencies, config));
71
73
 
72
74
  // --- Alert Subscriptions ---
73
75
  router.post('/me/subscriptions', (req, res) => subscribeToAlerts(req, res, dependencies, config));
@@ -88,6 +90,7 @@ module.exports = (dependencies, config) => {
88
90
 
89
91
  // --- Alert Management ---
90
92
  router.get('/me/alert-types', (req, res) => getAlertTypes(req, res, dependencies, config));
93
+ router.get('/me/dynamic-watchlist-computations', (req, res) => getDynamicWatchlistComputations(req, res, dependencies, config));
91
94
  router.get('/me/alerts', (req, res) => getUserAlerts(req, res, dependencies, config));
92
95
  router.get('/me/alerts/count', (req, res) => getAlertCount(req, res, dependencies, config));
93
96
  router.put('/me/alerts/:alertId/read', (req, res) => markAlertRead(req, res, dependencies, config));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.465",
3
+ "version": "1.0.467",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [