bulltrackers-module 1.0.464 → 1.0.466
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/workflows/data_feeder_pipeline.yaml +74 -55
- package/functions/generic-api/user-api/helpers/alert_helpers.js +30 -1
- package/functions/generic-api/user-api/helpers/watchlist_helpers.js +203 -50
- package/functions/generic-api/user-api/index.js +5 -2
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Data Feeder Pipeline (
|
|
2
|
-
# Orchestrates data fetching with UTC-alignment, Test Mode, and
|
|
1
|
+
# Data Feeder Pipeline (V2.3 - Try/Retry Syntax Fixed)
|
|
2
|
+
# Orchestrates data fetching with UTC-alignment, Test Mode, and Reliability.
|
|
3
3
|
|
|
4
4
|
main:
|
|
5
5
|
params: [input]
|
|
@@ -9,9 +9,16 @@ main:
|
|
|
9
9
|
- project: '${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}'
|
|
10
10
|
- location: "europe-west1"
|
|
11
11
|
- market_date: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
12
|
+
# Define a central retry policy to reuse across all HTTP calls
|
|
13
|
+
- default_retry:
|
|
14
|
+
predicate: ${http.default_retry_predicate}
|
|
15
|
+
max_retries: 5
|
|
16
|
+
backoff:
|
|
17
|
+
initial_delay: 2
|
|
18
|
+
max_delay: 60
|
|
19
|
+
multiplier: 2
|
|
12
20
|
|
|
13
21
|
# --- TEST MODE / SELECTIVE EXECUTION ---
|
|
14
|
-
# Static routing to allow manual testing of specific steps via Input JSON.
|
|
15
22
|
- check_test_mode:
|
|
16
23
|
switch:
|
|
17
24
|
- condition: '${input != null and "target_step" in input}'
|
|
@@ -34,33 +41,36 @@ main:
|
|
|
34
41
|
- price_fetch:
|
|
35
42
|
steps:
|
|
36
43
|
- call_price_fetcher:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
try:
|
|
45
|
+
call: http.post
|
|
46
|
+
args:
|
|
47
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
|
|
48
|
+
auth: { type: OIDC }
|
|
49
|
+
timeout: 300
|
|
50
|
+
retry: ${default_retry} # Fixed: Moved retry to a Try Step
|
|
43
51
|
- insights_fetch:
|
|
44
52
|
steps:
|
|
45
53
|
- call_insights_fetcher:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
try:
|
|
55
|
+
call: http.post
|
|
56
|
+
args:
|
|
57
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
|
|
58
|
+
auth: { type: OIDC }
|
|
59
|
+
timeout: 300
|
|
60
|
+
retry: ${default_retry} # Fixed: Moved retry to a Try Step
|
|
52
61
|
|
|
53
62
|
- index_market_data:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
try:
|
|
64
|
+
call: http.post
|
|
65
|
+
args:
|
|
66
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
67
|
+
body:
|
|
68
|
+
targetDate: '${market_date}'
|
|
69
|
+
auth: { type: OIDC }
|
|
70
|
+
timeout: 300
|
|
71
|
+
retry: ${default_retry}
|
|
61
72
|
|
|
62
73
|
# --- PHASE 2: ALIGN TO MIDNIGHT ---
|
|
63
|
-
# Dynamically calculates seconds remaining until exactly 00:00 UTC.
|
|
64
74
|
- wait_for_midnight:
|
|
65
75
|
assign:
|
|
66
76
|
- now_sec: '${int(sys.now())}'
|
|
@@ -79,6 +89,7 @@ main:
|
|
|
79
89
|
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
|
|
80
90
|
auth: { type: OIDC }
|
|
81
91
|
timeout: 300
|
|
92
|
+
retry: ${default_retry}
|
|
82
93
|
except:
|
|
83
94
|
as: e
|
|
84
95
|
steps:
|
|
@@ -89,31 +100,36 @@ main:
|
|
|
89
100
|
text: '${"Rankings Fetch Failed: " + json.encode(e)}'
|
|
90
101
|
|
|
91
102
|
- run_social_midnight:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
try:
|
|
104
|
+
call: http.post
|
|
105
|
+
args:
|
|
106
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
107
|
+
auth: { type: OIDC }
|
|
108
|
+
timeout: 300
|
|
109
|
+
retry: ${default_retry}
|
|
97
110
|
|
|
98
|
-
# Fixed: Split assign and call into two separate steps
|
|
99
111
|
- prepare_midnight_index:
|
|
100
112
|
assign:
|
|
101
113
|
- current_date: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
102
114
|
- index_midnight_data:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
try:
|
|
116
|
+
call: http.post
|
|
117
|
+
args:
|
|
118
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
119
|
+
body:
|
|
120
|
+
targetDate: '${current_date}'
|
|
121
|
+
auth: { type: OIDC }
|
|
122
|
+
timeout: 300
|
|
123
|
+
retry: ${default_retry}
|
|
110
124
|
|
|
111
125
|
- run_global_indexer:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
try:
|
|
127
|
+
call: http.post
|
|
128
|
+
args:
|
|
129
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
130
|
+
auth: { type: OIDC }
|
|
131
|
+
timeout: 300
|
|
132
|
+
retry: ${default_retry}
|
|
117
133
|
|
|
118
134
|
# --- PHASE 4: RECURRING SOCIAL FETCH (UTC Aligned 3hr) ---
|
|
119
135
|
- init_social_loop:
|
|
@@ -127,7 +143,7 @@ main:
|
|
|
127
143
|
- calculate_next_window:
|
|
128
144
|
assign:
|
|
129
145
|
- now_sec_loop: '${int(sys.now())}'
|
|
130
|
-
- window_size: 10800
|
|
146
|
+
- window_size: 10800
|
|
131
147
|
- sleep_loop: '${window_size - (now_sec_loop % window_size)}'
|
|
132
148
|
|
|
133
149
|
- wait_for_3hr_window:
|
|
@@ -136,24 +152,27 @@ main:
|
|
|
136
152
|
seconds: '${sleep_loop}'
|
|
137
153
|
|
|
138
154
|
- run_social_recurring:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
try:
|
|
156
|
+
call: http.post
|
|
157
|
+
args:
|
|
158
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
159
|
+
auth: { type: OIDC }
|
|
160
|
+
timeout: 300
|
|
161
|
+
retry: ${default_retry}
|
|
144
162
|
|
|
145
|
-
# Fixed: Split assign and call into two separate steps
|
|
146
163
|
- prepare_recurring_index:
|
|
147
164
|
assign:
|
|
148
165
|
- cur_date_rec: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
149
166
|
- index_recurring:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
try:
|
|
168
|
+
call: http.post
|
|
169
|
+
args:
|
|
170
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
171
|
+
body:
|
|
172
|
+
targetDate: '${cur_date_rec}'
|
|
173
|
+
auth: { type: OIDC }
|
|
174
|
+
timeout: 300
|
|
175
|
+
retry: ${default_retry}
|
|
157
176
|
|
|
158
177
|
- increment:
|
|
159
178
|
assign:
|
|
@@ -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,150 @@ 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
|
+
// Snapshot the current state
|
|
596
|
+
items: watchlistData.items ? JSON.parse(JSON.stringify(watchlistData.items)) : undefined,
|
|
597
|
+
dynamicConfig: watchlistData.dynamicConfig ? JSON.parse(JSON.stringify(watchlistData.dynamicConfig)) : undefined,
|
|
598
|
+
snapshotAt: FieldValue.serverTimestamp(),
|
|
599
|
+
copyCount: 0,
|
|
600
|
+
createdAt: watchlistData.createdAt,
|
|
601
|
+
isImmutable: true // Public versions are immutable
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// Store version in versions subcollection
|
|
605
|
+
const versionRef = db.collection('public_watchlists')
|
|
606
|
+
.doc(id)
|
|
607
|
+
.collection('versions')
|
|
608
|
+
.doc(String(versionNumber));
|
|
609
|
+
|
|
610
|
+
await versionRef.set(versionData);
|
|
611
|
+
|
|
612
|
+
// Update or create public watchlist entry (points to latest version)
|
|
613
|
+
await publicRef.set({
|
|
614
|
+
watchlistId: id,
|
|
615
|
+
createdBy: Number(userCid),
|
|
616
|
+
name: watchlistData.name,
|
|
617
|
+
type: watchlistData.type,
|
|
618
|
+
description: watchlistData.dynamicConfig?.description || '',
|
|
619
|
+
latestVersion: versionNumber,
|
|
620
|
+
latestVersionId: versionId,
|
|
621
|
+
copyCount: publicDoc.exists ? (publicDoc.data().copyCount || 0) : 0,
|
|
622
|
+
createdAt: watchlistData.createdAt,
|
|
623
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
624
|
+
}, { merge: true });
|
|
625
|
+
|
|
626
|
+
logger.log('SUCCESS', `[publishWatchlistVersion] User ${userCid} published watchlist ${id} as version ${versionNumber}`);
|
|
627
|
+
|
|
628
|
+
return res.status(201).json({
|
|
629
|
+
success: true,
|
|
630
|
+
version: versionNumber,
|
|
631
|
+
versionId: versionId,
|
|
632
|
+
watchlist: versionData
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
} catch (error) {
|
|
636
|
+
logger.log('ERROR', `[publishWatchlistVersion] Error publishing watchlist ${id} for ${userCid}`, error);
|
|
637
|
+
return res.status(500).json({ error: error.message });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* GET /user/public-watchlists/:id/versions
|
|
643
|
+
* Get version history for a public watchlist
|
|
644
|
+
*/
|
|
645
|
+
async function getWatchlistVersions(req, res, dependencies, config) {
|
|
646
|
+
const { db, logger } = dependencies;
|
|
647
|
+
const { id } = req.params;
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
const versionsRef = db.collection('public_watchlists')
|
|
651
|
+
.doc(id)
|
|
652
|
+
.collection('versions')
|
|
653
|
+
.orderBy('version', 'desc');
|
|
654
|
+
|
|
655
|
+
const snapshot = await versionsRef.get();
|
|
656
|
+
const versions = [];
|
|
657
|
+
|
|
658
|
+
snapshot.forEach(doc => {
|
|
659
|
+
versions.push({
|
|
660
|
+
version: doc.data().version,
|
|
661
|
+
versionId: `${id}_v${doc.data().version}`,
|
|
662
|
+
...doc.data()
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
return res.status(200).json({
|
|
667
|
+
success: true,
|
|
668
|
+
watchlistId: id,
|
|
669
|
+
versions,
|
|
670
|
+
count: versions.length
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
} catch (error) {
|
|
674
|
+
logger.log('ERROR', `[getWatchlistVersions] Error fetching versions for ${id}`, error);
|
|
675
|
+
return res.status(500).json({ error: error.message });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
528
679
|
module.exports = {
|
|
529
680
|
getUserWatchlists,
|
|
530
681
|
getWatchlist,
|
|
@@ -532,6 +683,8 @@ module.exports = {
|
|
|
532
683
|
updateWatchlist,
|
|
533
684
|
deleteWatchlist,
|
|
534
685
|
copyWatchlist,
|
|
535
|
-
getPublicWatchlists
|
|
686
|
+
getPublicWatchlists,
|
|
687
|
+
publishWatchlistVersion,
|
|
688
|
+
getWatchlistVersions
|
|
536
689
|
};
|
|
537
690
|
|
|
@@ -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));
|