bulltrackers-module 1.0.462 → 1.0.464
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Data Feeder Pipeline (
|
|
2
|
-
# Orchestrates data fetching with UTC-alignment and
|
|
1
|
+
# Data Feeder Pipeline (Final Verified Version)
|
|
2
|
+
# Orchestrates data fetching with UTC-alignment, Test Mode, and extended Timeouts.
|
|
3
3
|
|
|
4
4
|
main:
|
|
5
5
|
params: [input]
|
|
@@ -11,7 +11,7 @@ main:
|
|
|
11
11
|
- market_date: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
12
12
|
|
|
13
13
|
# --- TEST MODE / SELECTIVE EXECUTION ---
|
|
14
|
-
#
|
|
14
|
+
# Static routing to allow manual testing of specific steps via Input JSON.
|
|
15
15
|
- check_test_mode:
|
|
16
16
|
switch:
|
|
17
17
|
- condition: '${input != null and "target_step" in input}'
|
|
@@ -38,6 +38,8 @@ main:
|
|
|
38
38
|
args:
|
|
39
39
|
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
|
|
40
40
|
auth: { type: OIDC }
|
|
41
|
+
timeout: 300 # Fixed: Prevents timeout errors for long-running functions
|
|
42
|
+
retry: ${http.default_retry}
|
|
41
43
|
- insights_fetch:
|
|
42
44
|
steps:
|
|
43
45
|
- call_insights_fetcher:
|
|
@@ -45,6 +47,8 @@ main:
|
|
|
45
47
|
args:
|
|
46
48
|
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
|
|
47
49
|
auth: { type: OIDC }
|
|
50
|
+
timeout: 300 # Fixed: Prevents timeout errors for long-running functions
|
|
51
|
+
retry: ${http.default_retry}
|
|
48
52
|
|
|
49
53
|
- index_market_data:
|
|
50
54
|
call: http.post
|
|
@@ -53,8 +57,10 @@ main:
|
|
|
53
57
|
body:
|
|
54
58
|
targetDate: '${market_date}'
|
|
55
59
|
auth: { type: OIDC }
|
|
60
|
+
timeout: 300
|
|
56
61
|
|
|
57
62
|
# --- PHASE 2: ALIGN TO MIDNIGHT ---
|
|
63
|
+
# Dynamically calculates seconds remaining until exactly 00:00 UTC.
|
|
58
64
|
- wait_for_midnight:
|
|
59
65
|
assign:
|
|
60
66
|
- now_sec: '${int(sys.now())}'
|
|
@@ -72,6 +78,7 @@ main:
|
|
|
72
78
|
args:
|
|
73
79
|
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
|
|
74
80
|
auth: { type: OIDC }
|
|
81
|
+
timeout: 300
|
|
75
82
|
except:
|
|
76
83
|
as: e
|
|
77
84
|
steps:
|
|
@@ -86,7 +93,9 @@ main:
|
|
|
86
93
|
args:
|
|
87
94
|
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
88
95
|
auth: { type: OIDC }
|
|
96
|
+
timeout: 300
|
|
89
97
|
|
|
98
|
+
# Fixed: Split assign and call into two separate steps
|
|
90
99
|
- prepare_midnight_index:
|
|
91
100
|
assign:
|
|
92
101
|
- current_date: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
@@ -97,12 +106,14 @@ main:
|
|
|
97
106
|
body:
|
|
98
107
|
targetDate: '${current_date}'
|
|
99
108
|
auth: { type: OIDC }
|
|
109
|
+
timeout: 300
|
|
100
110
|
|
|
101
111
|
- run_global_indexer:
|
|
102
112
|
call: http.post
|
|
103
113
|
args:
|
|
104
114
|
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
105
115
|
auth: { type: OIDC }
|
|
116
|
+
timeout: 300
|
|
106
117
|
|
|
107
118
|
# --- PHASE 4: RECURRING SOCIAL FETCH (UTC Aligned 3hr) ---
|
|
108
119
|
- init_social_loop:
|
|
@@ -116,7 +127,7 @@ main:
|
|
|
116
127
|
- calculate_next_window:
|
|
117
128
|
assign:
|
|
118
129
|
- now_sec_loop: '${int(sys.now())}'
|
|
119
|
-
- window_size: 10800
|
|
130
|
+
- window_size: 10800 # Fixed: Aligns to 03:00, 06:00, 09:00 UTC...
|
|
120
131
|
- sleep_loop: '${window_size - (now_sec_loop % window_size)}'
|
|
121
132
|
|
|
122
133
|
- wait_for_3hr_window:
|
|
@@ -129,7 +140,9 @@ main:
|
|
|
129
140
|
args:
|
|
130
141
|
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
131
142
|
auth: { type: OIDC }
|
|
143
|
+
timeout: 300
|
|
132
144
|
|
|
145
|
+
# Fixed: Split assign and call into two separate steps
|
|
133
146
|
- prepare_recurring_index:
|
|
134
147
|
assign:
|
|
135
148
|
- cur_date_rec: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
@@ -140,6 +153,7 @@ main:
|
|
|
140
153
|
body:
|
|
141
154
|
targetDate: '${cur_date_rec}'
|
|
142
155
|
auth: { type: OIDC }
|
|
156
|
+
timeout: 300
|
|
143
157
|
|
|
144
158
|
- increment:
|
|
145
159
|
assign:
|
|
@@ -213,6 +213,30 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
213
213
|
return res.status(403).json({ error: "You can only modify your own watchlists" });
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
// Check if this is a copied watchlist
|
|
217
|
+
const isCopiedWatchlist = existingData.copiedFrom && existingData.copiedFromCreator;
|
|
218
|
+
const wasModified = existingData.hasBeenModified || false;
|
|
219
|
+
|
|
220
|
+
// Determine if meaningful changes are being made (not just name)
|
|
221
|
+
let hasMeaningfulChanges = wasModified;
|
|
222
|
+
if (items !== undefined && existingData.type === 'static') {
|
|
223
|
+
// Check if items actually changed
|
|
224
|
+
const itemsChanged = JSON.stringify(items) !== JSON.stringify(existingData.items || []);
|
|
225
|
+
hasMeaningfulChanges = hasMeaningfulChanges || itemsChanged;
|
|
226
|
+
}
|
|
227
|
+
if (dynamicConfig !== undefined && existingData.type === 'dynamic') {
|
|
228
|
+
// Check if dynamicConfig actually changed
|
|
229
|
+
const configChanged = JSON.stringify(dynamicConfig) !== JSON.stringify(existingData.dynamicConfig || {});
|
|
230
|
+
hasMeaningfulChanges = hasMeaningfulChanges || configChanged;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// If trying to make a copied watchlist public, check if meaningful changes were made
|
|
234
|
+
if (visibility === 'public' && isCopiedWatchlist && !hasMeaningfulChanges) {
|
|
235
|
+
return res.status(400).json({
|
|
236
|
+
error: "Cannot publish copied watchlist without making meaningful changes. Please modify the watchlist items, thresholds, or parameters before publishing."
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
216
240
|
const updates = {
|
|
217
241
|
updatedAt: FieldValue.serverTimestamp()
|
|
218
242
|
};
|
|
@@ -221,6 +245,11 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
221
245
|
updates.name = name.trim();
|
|
222
246
|
}
|
|
223
247
|
|
|
248
|
+
// Track if watchlist has been modified (for copied watchlists)
|
|
249
|
+
if (isCopiedWatchlist && hasMeaningfulChanges) {
|
|
250
|
+
updates.hasBeenModified = true;
|
|
251
|
+
}
|
|
252
|
+
|
|
224
253
|
if (visibility !== undefined) {
|
|
225
254
|
if (visibility !== 'public' && visibility !== 'private') {
|
|
226
255
|
return res.status(400).json({ error: "Visibility must be 'public' or 'private'" });
|
|
@@ -235,7 +264,7 @@ async function updateWatchlist(req, res, dependencies, config) {
|
|
|
235
264
|
createdBy: existingData.createdBy,
|
|
236
265
|
name: updates.name || existingData.name,
|
|
237
266
|
type: existingData.type,
|
|
238
|
-
description: dynamicConfig?.description || '',
|
|
267
|
+
description: (dynamicConfig?.description || existingData.dynamicConfig?.description || ''),
|
|
239
268
|
copyCount: existingData.copyCount || 0,
|
|
240
269
|
createdAt: existingData.createdAt,
|
|
241
270
|
updatedAt: FieldValue.serverTimestamp()
|
|
@@ -345,6 +374,8 @@ async function copyWatchlist(req, res, dependencies, config) {
|
|
|
345
374
|
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
346
375
|
}
|
|
347
376
|
|
|
377
|
+
const userCidNum = Number(userCid);
|
|
378
|
+
|
|
348
379
|
try {
|
|
349
380
|
// First, try to find in public watchlists
|
|
350
381
|
const publicRef = db.collection('public_watchlists').doc(id);
|
|
@@ -355,10 +386,11 @@ async function copyWatchlist(req, res, dependencies, config) {
|
|
|
355
386
|
}
|
|
356
387
|
|
|
357
388
|
const publicData = publicDoc.data();
|
|
389
|
+
const originalCreatorCid = Number(publicData.createdBy);
|
|
358
390
|
|
|
359
391
|
// Find the original watchlist
|
|
360
392
|
const originalRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
361
|
-
.doc(String(
|
|
393
|
+
.doc(String(originalCreatorCid))
|
|
362
394
|
.collection('lists')
|
|
363
395
|
.doc(id);
|
|
364
396
|
|
|
@@ -370,15 +402,52 @@ async function copyWatchlist(req, res, dependencies, config) {
|
|
|
370
402
|
|
|
371
403
|
const originalData = originalDoc.data();
|
|
372
404
|
|
|
405
|
+
// Check if user is copying their own watchlist
|
|
406
|
+
const isCopyingOwn = originalCreatorCid === userCidNum;
|
|
407
|
+
|
|
408
|
+
// If copying own watchlist, find existing copies to determine number
|
|
409
|
+
let copyNumber = 1;
|
|
410
|
+
let newName = name;
|
|
411
|
+
|
|
412
|
+
if (isCopyingOwn) {
|
|
413
|
+
// Get all watchlists by this user to find existing copies
|
|
414
|
+
const userWatchlistsRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
415
|
+
.doc(String(userCidNum))
|
|
416
|
+
.collection('lists');
|
|
417
|
+
|
|
418
|
+
const userWatchlistsSnapshot = await userWatchlistsRef.get();
|
|
419
|
+
const baseName = originalData.name.replace(/\s*#\d+$/, ''); // Remove existing #N suffix
|
|
420
|
+
|
|
421
|
+
// Count existing copies (including original)
|
|
422
|
+
const existingCopies = [];
|
|
423
|
+
userWatchlistsSnapshot.forEach(doc => {
|
|
424
|
+
const data = doc.data();
|
|
425
|
+
const docName = data.name.replace(/\s*#\d+$/, '');
|
|
426
|
+
if (docName === baseName || docName === originalData.name) {
|
|
427
|
+
existingCopies.push(data);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Determine next copy number
|
|
432
|
+
copyNumber = existingCopies.length + 1;
|
|
433
|
+
newName = name || `${baseName} #${copyNumber}`;
|
|
434
|
+
} else {
|
|
435
|
+
// Copying someone else's watchlist
|
|
436
|
+
newName = name || `${originalData.name} (Copy)`;
|
|
437
|
+
}
|
|
438
|
+
|
|
373
439
|
// Create new watchlist for the copying user
|
|
374
440
|
const newWatchlistId = generateWatchlistId();
|
|
375
441
|
const watchlistData = {
|
|
376
442
|
...originalData,
|
|
377
443
|
id: newWatchlistId,
|
|
378
|
-
name:
|
|
379
|
-
createdBy:
|
|
444
|
+
name: newName,
|
|
445
|
+
createdBy: userCidNum,
|
|
380
446
|
visibility: 'private', // Copied watchlists are always private
|
|
381
447
|
copiedFrom: id,
|
|
448
|
+
copiedFromCreator: originalCreatorCid,
|
|
449
|
+
originalName: originalData.name, // Store original name for comparison
|
|
450
|
+
hasBeenModified: false, // Track if user made meaningful changes
|
|
382
451
|
createdAt: FieldValue.serverTimestamp(),
|
|
383
452
|
updatedAt: FieldValue.serverTimestamp(),
|
|
384
453
|
isAutoGenerated: false
|
|
@@ -388,19 +457,21 @@ async function copyWatchlist(req, res, dependencies, config) {
|
|
|
388
457
|
delete watchlistData.copyCount;
|
|
389
458
|
|
|
390
459
|
const newWatchlistRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
391
|
-
.doc(String(
|
|
460
|
+
.doc(String(userCidNum))
|
|
392
461
|
.collection('lists')
|
|
393
462
|
.doc(newWatchlistId);
|
|
394
463
|
|
|
395
464
|
await newWatchlistRef.set(watchlistData);
|
|
396
465
|
|
|
397
|
-
// Increment copy count on original
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
466
|
+
// Increment copy count on original (only if copying someone else's)
|
|
467
|
+
if (!isCopyingOwn) {
|
|
468
|
+
await publicRef.update({
|
|
469
|
+
copyCount: FieldValue.increment(1),
|
|
470
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
471
|
+
});
|
|
472
|
+
}
|
|
402
473
|
|
|
403
|
-
logger.log('SUCCESS', `[copyWatchlist] User ${userCid} copied watchlist ${id} as ${newWatchlistId}`);
|
|
474
|
+
logger.log('SUCCESS', `[copyWatchlist] User ${userCid} copied watchlist ${id} as ${newWatchlistId}${isCopyingOwn ? ' (own watchlist)' : ''}`);
|
|
404
475
|
|
|
405
476
|
return res.status(201).json({
|
|
406
477
|
success: true,
|