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
|
-
//
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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));
|