bulltrackers-module 1.0.629 → 1.0.631
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/alert-system/helpers/alert_helpers.js +69 -77
- package/functions/alert-system/index.js +19 -29
- package/functions/api-v2/helpers/notification_helpers.js +187 -0
- package/functions/computation-system/helpers/computation_worker.js +1 -1
- package/functions/task-engine/helpers/popular_investor_helpers.js +11 -7
- package/index.js +0 -5
- package/package.json +1 -2
- package/functions/old-generic-api/admin-api/index.js +0 -895
- package/functions/old-generic-api/helpers/api_helpers.js +0 -457
- package/functions/old-generic-api/index.js +0 -204
- package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
- package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
- package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
- package/functions/old-generic-api/user-api/helpers/collection_helpers.js +0 -193
- package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +0 -68
- package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
- package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
- package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
- package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +0 -503
- package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
- package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
- package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +0 -174
- package/functions/old-generic-api/user-api/helpers/data_helpers.js +0 -87
- package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
- package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
- package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
- package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
- package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
- package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
- package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
- package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
- package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
- package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
- package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
- package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
- package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
- package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
- package/functions/old-generic-api/user-api/index.js +0 -109
package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js
DELETED
|
@@ -1,829 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Watchlist Management Helpers
|
|
3
|
-
* Handles CRUD operations for user watchlists (static and dynamic)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
|
-
const crypto = require('crypto');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Generate unique watchlist ID
|
|
11
|
-
*/
|
|
12
|
-
function generateWatchlistId() {
|
|
13
|
-
return `watchlist_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* GET /user/me/watchlists
|
|
18
|
-
* List all watchlists for a user
|
|
19
|
-
*/
|
|
20
|
-
async function getUserWatchlists(req, res, dependencies, config) {
|
|
21
|
-
const { db, logger } = dependencies;
|
|
22
|
-
const { userCid } = req.query;
|
|
23
|
-
|
|
24
|
-
if (!userCid) {
|
|
25
|
-
return res.status(400).json({ error: "Missing userCid" });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
30
|
-
const userWatchlistsRef = db.collection(watchlistsCollection)
|
|
31
|
-
.doc(String(userCid))
|
|
32
|
-
.collection('lists');
|
|
33
|
-
|
|
34
|
-
const snapshot = await userWatchlistsRef.get();
|
|
35
|
-
|
|
36
|
-
const watchlists = [];
|
|
37
|
-
snapshot.forEach(doc => {
|
|
38
|
-
watchlists.push({
|
|
39
|
-
id: doc.id,
|
|
40
|
-
...doc.data()
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Sort by creation date (newest first)
|
|
45
|
-
watchlists.sort((a, b) => {
|
|
46
|
-
const aTime = a.createdAt?.seconds || 0;
|
|
47
|
-
const bTime = b.createdAt?.seconds || 0;
|
|
48
|
-
return bTime - aTime;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return res.status(200).json({
|
|
52
|
-
watchlists,
|
|
53
|
-
count: watchlists.length
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
} catch (error) {
|
|
57
|
-
logger.log('ERROR', `[getUserWatchlists] Error fetching watchlists for ${userCid}`, error);
|
|
58
|
-
return res.status(500).json({ error: error.message });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* GET /user/me/watchlists/:id
|
|
64
|
-
* Get a specific watchlist
|
|
65
|
-
*/
|
|
66
|
-
async function getWatchlist(req, res, dependencies, config) {
|
|
67
|
-
const { db, logger } = dependencies;
|
|
68
|
-
const { userCid } = req.query;
|
|
69
|
-
const { id } = req.params;
|
|
70
|
-
|
|
71
|
-
if (!userCid || !id) {
|
|
72
|
-
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
77
|
-
const watchlistRef = db.collection(watchlistsCollection)
|
|
78
|
-
.doc(String(userCid))
|
|
79
|
-
.collection('lists')
|
|
80
|
-
.doc(id);
|
|
81
|
-
|
|
82
|
-
const watchlistDoc = await watchlistRef.get();
|
|
83
|
-
|
|
84
|
-
if (!watchlistDoc.exists) {
|
|
85
|
-
return res.status(404).json({ error: "Watchlist not found" });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return res.status(200).json({
|
|
89
|
-
id: watchlistDoc.id,
|
|
90
|
-
...watchlistDoc.data()
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
} catch (error) {
|
|
94
|
-
logger.log('ERROR', `[getWatchlist] Error fetching watchlist ${id} for ${userCid}`, error);
|
|
95
|
-
return res.status(500).json({ error: error.message });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* POST /user/me/watchlists
|
|
101
|
-
* Create a new watchlist
|
|
102
|
-
*/
|
|
103
|
-
async function createWatchlist(req, res, dependencies, config) {
|
|
104
|
-
const { db, logger } = dependencies;
|
|
105
|
-
const { userCid, name, type, visibility = 'private', items, dynamicConfig } = req.body;
|
|
106
|
-
|
|
107
|
-
if (!userCid || !name || !type) {
|
|
108
|
-
return res.status(400).json({ error: "Missing required fields: userCid, name, type" });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (type !== 'static' && type !== 'dynamic') {
|
|
112
|
-
return res.status(400).json({ error: "Type must be 'static' or 'dynamic'" });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (visibility !== 'public' && visibility !== 'private') {
|
|
116
|
-
return res.status(400).json({ error: "Visibility must be 'public' or 'private'" });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
121
|
-
const watchlistId = generateWatchlistId();
|
|
122
|
-
|
|
123
|
-
const watchlistData = {
|
|
124
|
-
id: watchlistId,
|
|
125
|
-
name: name.trim(),
|
|
126
|
-
type,
|
|
127
|
-
visibility,
|
|
128
|
-
createdBy: Number(userCid),
|
|
129
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
130
|
-
updatedAt: FieldValue.serverTimestamp(),
|
|
131
|
-
isAutoGenerated: false,
|
|
132
|
-
copyCount: 0
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
if (type === 'static') {
|
|
136
|
-
watchlistData.items = items || [];
|
|
137
|
-
} else if (type === 'dynamic') {
|
|
138
|
-
if (!dynamicConfig || !dynamicConfig.computationName) {
|
|
139
|
-
return res.status(400).json({ error: "Dynamic watchlists require dynamicConfig with computationName" });
|
|
140
|
-
}
|
|
141
|
-
watchlistData.dynamicConfig = dynamicConfig;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Write to new SignedInUsers path with dual-write to legacy path
|
|
145
|
-
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
146
|
-
await writeWithMigration(
|
|
147
|
-
db,
|
|
148
|
-
'signedInUsers',
|
|
149
|
-
'watchlists',
|
|
150
|
-
{ cid: userCid },
|
|
151
|
-
watchlistData,
|
|
152
|
-
{
|
|
153
|
-
isCollection: true,
|
|
154
|
-
merge: false,
|
|
155
|
-
dataType: 'watchlists',
|
|
156
|
-
config,
|
|
157
|
-
collectionRegistry: dependencies.collectionRegistry,
|
|
158
|
-
documentId: watchlistId,
|
|
159
|
-
dualWrite: true
|
|
160
|
-
}
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
// Update global rootdata collection for computation system if static watchlist has items
|
|
164
|
-
if (type === 'static' && items && items.length > 0) {
|
|
165
|
-
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
166
|
-
const today = new Date().toISOString().split('T')[0];
|
|
167
|
-
const isPublic = visibility === 'public';
|
|
168
|
-
|
|
169
|
-
for (const item of items) {
|
|
170
|
-
const piCid = String(item.cid);
|
|
171
|
-
// Update global WatchlistMembershipData/{date} document
|
|
172
|
-
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'add');
|
|
173
|
-
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
174
|
-
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'add');
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Watchlists are always created as private - no public version on creation
|
|
179
|
-
|
|
180
|
-
logger.log('SUCCESS', `[createWatchlist] Created ${type} watchlist "${name}" for user ${userCid}`);
|
|
181
|
-
|
|
182
|
-
return res.status(201).json({
|
|
183
|
-
success: true,
|
|
184
|
-
watchlist: {
|
|
185
|
-
id: watchlistId,
|
|
186
|
-
...watchlistData
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
} catch (error) {
|
|
191
|
-
logger.log('ERROR', `[createWatchlist] Error creating watchlist for ${userCid}`, error);
|
|
192
|
-
return res.status(500).json({ error: error.message });
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* PUT /user/me/watchlists/:id
|
|
198
|
-
* Update a watchlist
|
|
199
|
-
*/
|
|
200
|
-
async function updateWatchlist(req, res, dependencies, config) {
|
|
201
|
-
const { db, logger } = dependencies;
|
|
202
|
-
const { userCid } = req.query;
|
|
203
|
-
const { id } = req.params;
|
|
204
|
-
const { name, visibility, items, dynamicConfig } = req.body;
|
|
205
|
-
|
|
206
|
-
if (!userCid || !id) {
|
|
207
|
-
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
// Read from new path with migration fallback
|
|
212
|
-
const { readWithMigration } = require('../core/path_resolution_helpers');
|
|
213
|
-
const readResult = await readWithMigration(
|
|
214
|
-
db,
|
|
215
|
-
'signedInUsers',
|
|
216
|
-
'watchlists',
|
|
217
|
-
{ cid: userCid },
|
|
218
|
-
{
|
|
219
|
-
isCollection: true,
|
|
220
|
-
dataType: 'watchlists',
|
|
221
|
-
config,
|
|
222
|
-
logger,
|
|
223
|
-
collectionRegistry: dependencies.collectionRegistry,
|
|
224
|
-
documentId: id
|
|
225
|
-
}
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
if (!readResult) {
|
|
229
|
-
return res.status(404).json({ error: "Watchlist not found" });
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Handle both document read (with data) and collection read (with snapshot)
|
|
233
|
-
let existingData;
|
|
234
|
-
if (readResult.data) {
|
|
235
|
-
existingData = readResult.data;
|
|
236
|
-
} else if (readResult.snapshot && !readResult.snapshot.empty) {
|
|
237
|
-
// If we got a snapshot, find the specific document
|
|
238
|
-
const doc = readResult.snapshot.docs.find(d => d.id === id);
|
|
239
|
-
if (!doc) {
|
|
240
|
-
return res.status(404).json({ error: "Watchlist not found" });
|
|
241
|
-
}
|
|
242
|
-
existingData = doc.data();
|
|
243
|
-
} else {
|
|
244
|
-
return res.status(404).json({ error: "Watchlist not found" });
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Verify ownership
|
|
248
|
-
if (existingData.createdBy !== Number(userCid)) {
|
|
249
|
-
return res.status(403).json({ error: "You can only modify your own watchlists" });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Check if this is a copied watchlist
|
|
253
|
-
const isCopiedWatchlist = existingData.copiedFrom && existingData.copiedFromCreator;
|
|
254
|
-
const wasModified = existingData.hasBeenModified || false;
|
|
255
|
-
|
|
256
|
-
// Determine if meaningful changes are being made (not just name)
|
|
257
|
-
let hasMeaningfulChanges = wasModified;
|
|
258
|
-
if (items !== undefined && existingData.type === 'static') {
|
|
259
|
-
// Check if items actually changed
|
|
260
|
-
const itemsChanged = JSON.stringify(items) !== JSON.stringify(existingData.items || []);
|
|
261
|
-
hasMeaningfulChanges = hasMeaningfulChanges || itemsChanged;
|
|
262
|
-
}
|
|
263
|
-
if (dynamicConfig !== undefined && existingData.type === 'dynamic') {
|
|
264
|
-
// Check if dynamicConfig actually changed
|
|
265
|
-
const configChanged = JSON.stringify(dynamicConfig) !== JSON.stringify(existingData.dynamicConfig || {});
|
|
266
|
-
hasMeaningfulChanges = hasMeaningfulChanges || configChanged;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// If trying to make a copied watchlist public, check if meaningful changes were made
|
|
270
|
-
if (visibility === 'public' && isCopiedWatchlist && !hasMeaningfulChanges) {
|
|
271
|
-
return res.status(400).json({
|
|
272
|
-
error: "Cannot publish copied watchlist without making meaningful changes. Please modify the watchlist items, thresholds, or parameters before publishing."
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const updates = {
|
|
277
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
if (name !== undefined) {
|
|
281
|
-
updates.name = name.trim();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Track if watchlist has been modified (for copied watchlists)
|
|
285
|
-
if (isCopiedWatchlist && hasMeaningfulChanges) {
|
|
286
|
-
updates.hasBeenModified = true;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Visibility changes are now handled through publish/unpublish endpoints
|
|
290
|
-
// Users can only update their private watchlist
|
|
291
|
-
if (visibility !== undefined && visibility !== 'private') {
|
|
292
|
-
return res.status(400).json({
|
|
293
|
-
error: "Cannot change visibility directly. Use the publish endpoint to create a public version."
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (items !== undefined && existingData.type === 'static') {
|
|
298
|
-
updates.items = items;
|
|
299
|
-
|
|
300
|
-
// Update global rootdata collection for computation system
|
|
301
|
-
// Compare old and new items to find added/removed PIs
|
|
302
|
-
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
303
|
-
const oldItems = existingData.items || [];
|
|
304
|
-
const newItems = items || [];
|
|
305
|
-
const oldPiCids = new Set(oldItems.map(item => String(item.cid)));
|
|
306
|
-
const newPiCids = new Set(newItems.map(item => String(item.cid)));
|
|
307
|
-
const today = new Date().toISOString().split('T')[0];
|
|
308
|
-
const isPublic = existingData.visibility === 'public';
|
|
309
|
-
|
|
310
|
-
// Find added PIs
|
|
311
|
-
for (const newItem of newItems) {
|
|
312
|
-
const piCid = String(newItem.cid);
|
|
313
|
-
if (!oldPiCids.has(piCid)) {
|
|
314
|
-
// Update global WatchlistMembershipData/{date} document
|
|
315
|
-
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'add');
|
|
316
|
-
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
317
|
-
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'add');
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Find removed PIs
|
|
322
|
-
for (const oldItem of oldItems) {
|
|
323
|
-
const piCid = String(oldItem.cid);
|
|
324
|
-
if (!newPiCids.has(piCid)) {
|
|
325
|
-
// Update global WatchlistMembershipData/{date} document
|
|
326
|
-
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'remove');
|
|
327
|
-
// Update PI-centric PopularInvestors/{piCid}/watchlistData
|
|
328
|
-
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'remove');
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (dynamicConfig !== undefined && existingData.type === 'dynamic') {
|
|
334
|
-
updates.dynamicConfig = dynamicConfig;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Merge updates with existing data
|
|
338
|
-
const updatedData = { ...existingData, ...updates };
|
|
339
|
-
|
|
340
|
-
// Write to new SignedInUsers path with dual-write to legacy path
|
|
341
|
-
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
342
|
-
await writeWithMigration(
|
|
343
|
-
db,
|
|
344
|
-
'signedInUsers',
|
|
345
|
-
'watchlists',
|
|
346
|
-
{ cid: userCid },
|
|
347
|
-
updatedData,
|
|
348
|
-
{
|
|
349
|
-
isCollection: true,
|
|
350
|
-
merge: true,
|
|
351
|
-
dataType: 'watchlists',
|
|
352
|
-
config,
|
|
353
|
-
collectionRegistry: dependencies.collectionRegistry,
|
|
354
|
-
documentId: id,
|
|
355
|
-
dualWrite: true
|
|
356
|
-
}
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
logger.log('SUCCESS', `[updateWatchlist] Updated watchlist ${id} for user ${userCid}`);
|
|
360
|
-
|
|
361
|
-
return res.status(200).json({
|
|
362
|
-
success: true,
|
|
363
|
-
watchlist: {
|
|
364
|
-
id: id,
|
|
365
|
-
...updatedData
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
} catch (error) {
|
|
370
|
-
logger.log('ERROR', `[updateWatchlist] Error updating watchlist ${id} for ${userCid}`, error);
|
|
371
|
-
return res.status(500).json({ error: error.message });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* DELETE /user/me/watchlists/:id
|
|
377
|
-
* Delete a watchlist
|
|
378
|
-
*/
|
|
379
|
-
async function deleteWatchlist(req, res, dependencies, config) {
|
|
380
|
-
const { db, logger } = dependencies;
|
|
381
|
-
const { userCid } = req.query;
|
|
382
|
-
const { id } = req.params;
|
|
383
|
-
|
|
384
|
-
if (!userCid || !id) {
|
|
385
|
-
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
390
|
-
const watchlistRef = db.collection(watchlistsCollection)
|
|
391
|
-
.doc(String(userCid))
|
|
392
|
-
.collection('lists')
|
|
393
|
-
.doc(id);
|
|
394
|
-
|
|
395
|
-
const watchlistDoc = await watchlistRef.get();
|
|
396
|
-
|
|
397
|
-
if (!watchlistDoc.exists) {
|
|
398
|
-
return res.status(404).json({ error: "Watchlist not found" });
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const watchlistData = watchlistDoc.data();
|
|
402
|
-
|
|
403
|
-
// Verify ownership
|
|
404
|
-
if (watchlistData.createdBy !== Number(userCid)) {
|
|
405
|
-
return res.status(403).json({ error: "You can only delete your own watchlists" });
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Update global rootdata collections before deleting
|
|
409
|
-
// Remove all PIs from global tracking if this is a static watchlist with items
|
|
410
|
-
if (watchlistData.type === 'static' && watchlistData.items && watchlistData.items.length > 0) {
|
|
411
|
-
const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
|
|
412
|
-
const today = new Date().toISOString().split('T')[0];
|
|
413
|
-
const isPublic = watchlistData.visibility === 'public';
|
|
414
|
-
|
|
415
|
-
for (const item of watchlistData.items) {
|
|
416
|
-
const piCid = String(item.cid);
|
|
417
|
-
// Remove from global WatchlistMembershipData/{date} document
|
|
418
|
-
await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'remove');
|
|
419
|
-
// Remove from PI-centric PopularInvestors/{piCid}/watchlistData
|
|
420
|
-
await updatePIWatchlistData(db, logger, piCid, userCid, today, 'remove');
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Delete watchlist from both new and legacy paths
|
|
425
|
-
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
426
|
-
// Delete from new path
|
|
427
|
-
const newPathRef = db.collection('SignedInUsers')
|
|
428
|
-
.doc(String(userCid))
|
|
429
|
-
.collection('watchlists')
|
|
430
|
-
.doc(id);
|
|
431
|
-
await newPathRef.delete().catch(err => {
|
|
432
|
-
logger.log('WARN', `[deleteWatchlist] Failed to delete from new path: ${err.message}`);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
// Delete from legacy path if it exists
|
|
436
|
-
const legacyPathRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
437
|
-
.doc(String(userCid))
|
|
438
|
-
.collection('lists')
|
|
439
|
-
.doc(id);
|
|
440
|
-
await legacyPathRef.delete().catch(err => {
|
|
441
|
-
logger.log('WARN', `[deleteWatchlist] Failed to delete from legacy path: ${err.message}`);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Remove from public watchlists if it was public
|
|
445
|
-
if (watchlistData.visibility === 'public') {
|
|
446
|
-
const publicRef = db.collection('public_watchlists').doc(id);
|
|
447
|
-
await publicRef.delete();
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// TODO: Clean up subscriptions for this watchlist
|
|
451
|
-
// This would require deleting entries in watchlist_subscriptions collection
|
|
452
|
-
|
|
453
|
-
logger.log('SUCCESS', `[deleteWatchlist] Deleted watchlist ${id} for user ${userCid}`);
|
|
454
|
-
|
|
455
|
-
return res.status(200).json({
|
|
456
|
-
success: true,
|
|
457
|
-
message: "Watchlist deleted successfully"
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
} catch (error) {
|
|
461
|
-
logger.log('ERROR', `[deleteWatchlist] Error deleting watchlist ${id} for ${userCid}`, error);
|
|
462
|
-
return res.status(500).json({ error: error.message });
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* POST /user/me/watchlists/:id/copy
|
|
468
|
-
* Copy a public watchlist
|
|
469
|
-
*/
|
|
470
|
-
async function copyWatchlist(req, res, dependencies, config) {
|
|
471
|
-
const { db, logger } = dependencies;
|
|
472
|
-
const { userCid } = req.query;
|
|
473
|
-
const { id } = req.params;
|
|
474
|
-
const { name, version } = req.body; // Optional custom name and version number
|
|
475
|
-
|
|
476
|
-
if (!userCid || !id) {
|
|
477
|
-
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const userCidNum = Number(userCid);
|
|
481
|
-
|
|
482
|
-
try {
|
|
483
|
-
// First, try to find in public watchlists
|
|
484
|
-
const publicRef = db.collection('public_watchlists').doc(id);
|
|
485
|
-
const publicDoc = await publicRef.get();
|
|
486
|
-
|
|
487
|
-
if (!publicDoc.exists) {
|
|
488
|
-
return res.status(404).json({ error: "Public watchlist not found" });
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const publicData = publicDoc.data();
|
|
492
|
-
const originalCreatorCid = Number(publicData.createdBy);
|
|
493
|
-
|
|
494
|
-
let originalData;
|
|
495
|
-
|
|
496
|
-
// If version is specified, copy from that specific version snapshot
|
|
497
|
-
if (version) {
|
|
498
|
-
const versionRef = db.collection('public_watchlists')
|
|
499
|
-
.doc(id)
|
|
500
|
-
.collection('versions')
|
|
501
|
-
.doc(String(version));
|
|
502
|
-
|
|
503
|
-
const versionDoc = await versionRef.get();
|
|
504
|
-
|
|
505
|
-
if (!versionDoc.exists) {
|
|
506
|
-
return res.status(404).json({ error: `Version ${version} not found for this watchlist` });
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
originalData = versionDoc.data();
|
|
510
|
-
} else {
|
|
511
|
-
// Copy from latest version (get the latest version)
|
|
512
|
-
const latestVersion = publicData.latestVersion || 1;
|
|
513
|
-
const versionRef = db.collection('public_watchlists')
|
|
514
|
-
.doc(id)
|
|
515
|
-
.collection('versions')
|
|
516
|
-
.doc(String(latestVersion));
|
|
517
|
-
|
|
518
|
-
const versionDoc = await versionRef.get();
|
|
519
|
-
|
|
520
|
-
if (versionDoc.exists) {
|
|
521
|
-
originalData = versionDoc.data();
|
|
522
|
-
} else {
|
|
523
|
-
// Fallback: try to get from original watchlist (for backwards compatibility)
|
|
524
|
-
const originalRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
525
|
-
.doc(String(originalCreatorCid))
|
|
526
|
-
.collection('lists')
|
|
527
|
-
.doc(id);
|
|
528
|
-
|
|
529
|
-
const originalDoc = await originalRef.get();
|
|
530
|
-
|
|
531
|
-
if (!originalDoc.exists) {
|
|
532
|
-
return res.status(404).json({ error: "Original watchlist not found" });
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
originalData = originalDoc.data();
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Check if user is copying their own watchlist
|
|
540
|
-
const isCopyingOwn = originalCreatorCid === userCidNum;
|
|
541
|
-
|
|
542
|
-
// If copying own watchlist, find existing copies to determine number
|
|
543
|
-
let copyNumber = 1;
|
|
544
|
-
let newName = name;
|
|
545
|
-
|
|
546
|
-
if (isCopyingOwn) {
|
|
547
|
-
// Get all watchlists by this user to find existing copies
|
|
548
|
-
const userWatchlistsRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
549
|
-
.doc(String(userCidNum))
|
|
550
|
-
.collection('lists');
|
|
551
|
-
|
|
552
|
-
const userWatchlistsSnapshot = await userWatchlistsRef.get();
|
|
553
|
-
const baseName = originalData.name.replace(/\s*#\d+$/, ''); // Remove existing #N suffix
|
|
554
|
-
|
|
555
|
-
// Count existing copies (including original)
|
|
556
|
-
const existingCopies = [];
|
|
557
|
-
userWatchlistsSnapshot.forEach(doc => {
|
|
558
|
-
const data = doc.data();
|
|
559
|
-
const docName = data.name.replace(/\s*#\d+$/, '');
|
|
560
|
-
if (docName === baseName || docName === originalData.name) {
|
|
561
|
-
existingCopies.push(data);
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// Determine next copy number
|
|
566
|
-
copyNumber = existingCopies.length + 1;
|
|
567
|
-
newName = name || `${baseName} #${copyNumber}`;
|
|
568
|
-
} else {
|
|
569
|
-
// Copying someone else's watchlist
|
|
570
|
-
newName = name || `${originalData.name} (Copy)`;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Create new watchlist for the copying user
|
|
574
|
-
const newWatchlistId = generateWatchlistId();
|
|
575
|
-
const watchlistData = {
|
|
576
|
-
...originalData,
|
|
577
|
-
id: newWatchlistId,
|
|
578
|
-
name: newName,
|
|
579
|
-
createdBy: userCidNum,
|
|
580
|
-
visibility: 'private', // Copied watchlists are always private
|
|
581
|
-
copiedFrom: id,
|
|
582
|
-
copiedFromVersion: version || (publicData.latestVersion || 1), // Track which version was copied
|
|
583
|
-
copiedFromCreator: originalCreatorCid,
|
|
584
|
-
originalName: originalData.name, // Store original name for comparison
|
|
585
|
-
hasBeenModified: false, // Track if user made meaningful changes
|
|
586
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
587
|
-
updatedAt: FieldValue.serverTimestamp(),
|
|
588
|
-
isAutoGenerated: false
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
// Remove fields that shouldn't be copied
|
|
592
|
-
delete watchlistData.copyCount;
|
|
593
|
-
delete watchlistData.version;
|
|
594
|
-
delete watchlistData.versionId;
|
|
595
|
-
delete watchlistData.snapshotAt;
|
|
596
|
-
delete watchlistData.isImmutable;
|
|
597
|
-
delete watchlistData.watchlistId;
|
|
598
|
-
|
|
599
|
-
const newWatchlistRef = db.collection(config.watchlistsCollection || 'watchlists')
|
|
600
|
-
.doc(String(userCidNum))
|
|
601
|
-
.collection('lists')
|
|
602
|
-
.doc(newWatchlistId);
|
|
603
|
-
|
|
604
|
-
await newWatchlistRef.set(watchlistData);
|
|
605
|
-
|
|
606
|
-
// Increment copy count on original (only if copying someone else's)
|
|
607
|
-
if (!isCopyingOwn) {
|
|
608
|
-
await publicRef.update({
|
|
609
|
-
copyCount: FieldValue.increment(1),
|
|
610
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
logger.log('SUCCESS', `[copyWatchlist] User ${userCid} copied watchlist ${id} as ${newWatchlistId}${isCopyingOwn ? ' (own watchlist)' : ''}`);
|
|
615
|
-
|
|
616
|
-
return res.status(201).json({
|
|
617
|
-
success: true,
|
|
618
|
-
watchlist: {
|
|
619
|
-
id: newWatchlistId,
|
|
620
|
-
...watchlistData
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
} catch (error) {
|
|
625
|
-
logger.log('ERROR', `[copyWatchlist] Error copying watchlist ${id} for ${userCid}`, error);
|
|
626
|
-
return res.status(500).json({ error: error.message });
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* GET /user/public-watchlists
|
|
632
|
-
* Browse public watchlists
|
|
633
|
-
*/
|
|
634
|
-
async function getPublicWatchlists(req, res, dependencies, config) {
|
|
635
|
-
const { db, logger } = dependencies;
|
|
636
|
-
const { limit = 50, offset = 0 } = req.query;
|
|
637
|
-
|
|
638
|
-
try {
|
|
639
|
-
const publicRef = db.collection('public_watchlists')
|
|
640
|
-
.orderBy('copyCount', 'desc')
|
|
641
|
-
.orderBy('createdAt', 'desc')
|
|
642
|
-
.limit(parseInt(limit))
|
|
643
|
-
.offset(parseInt(offset));
|
|
644
|
-
|
|
645
|
-
const snapshot = await publicRef.get();
|
|
646
|
-
|
|
647
|
-
const watchlists = [];
|
|
648
|
-
snapshot.forEach(doc => {
|
|
649
|
-
watchlists.push({
|
|
650
|
-
id: doc.id,
|
|
651
|
-
...doc.data()
|
|
652
|
-
});
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
return res.status(200).json({
|
|
656
|
-
watchlists,
|
|
657
|
-
count: watchlists.length,
|
|
658
|
-
limit: parseInt(limit),
|
|
659
|
-
offset: parseInt(offset)
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
} catch (error) {
|
|
663
|
-
logger.log('ERROR', `[getPublicWatchlists] Error fetching public watchlists`, error);
|
|
664
|
-
return res.status(500).json({ error: error.message });
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* POST /user/me/watchlists/:id/publish
|
|
670
|
-
* Publish a version of a private watchlist
|
|
671
|
-
* Creates an immutable snapshot version that can be copied by others
|
|
672
|
-
*/
|
|
673
|
-
async function publishWatchlistVersion(req, res, dependencies, config) {
|
|
674
|
-
const { db, logger } = dependencies;
|
|
675
|
-
const { userCid } = req.query;
|
|
676
|
-
const { id } = req.params;
|
|
677
|
-
|
|
678
|
-
if (!userCid || !id) {
|
|
679
|
-
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
try {
|
|
683
|
-
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
684
|
-
const watchlistRef = db.collection(watchlistsCollection)
|
|
685
|
-
.doc(String(userCid))
|
|
686
|
-
.collection('lists')
|
|
687
|
-
.doc(id);
|
|
688
|
-
|
|
689
|
-
const watchlistDoc = await watchlistRef.get();
|
|
690
|
-
|
|
691
|
-
if (!watchlistDoc.exists) {
|
|
692
|
-
return res.status(404).json({ error: "Watchlist not found" });
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
const watchlistData = watchlistDoc.data();
|
|
696
|
-
|
|
697
|
-
// Verify ownership
|
|
698
|
-
if (watchlistData.createdBy !== Number(userCid)) {
|
|
699
|
-
return res.status(403).json({ error: "You can only publish your own watchlists" });
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Check if this is a copied watchlist that hasn't been modified
|
|
703
|
-
if (watchlistData.copiedFrom && watchlistData.copiedFromCreator && !watchlistData.hasBeenModified) {
|
|
704
|
-
return res.status(400).json({
|
|
705
|
-
error: "Cannot publish copied watchlist without making meaningful changes. Please modify the watchlist items, thresholds, or parameters before publishing."
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Get current version number
|
|
710
|
-
const publicRef = db.collection('public_watchlists').doc(id);
|
|
711
|
-
const publicDoc = await publicRef.get();
|
|
712
|
-
|
|
713
|
-
let versionNumber = 1;
|
|
714
|
-
if (publicDoc.exists) {
|
|
715
|
-
const publicData = publicDoc.data();
|
|
716
|
-
versionNumber = (publicData.latestVersion || 0) + 1;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Create version snapshot
|
|
720
|
-
const versionId = `${id}_v${versionNumber}`;
|
|
721
|
-
const versionData = {
|
|
722
|
-
watchlistId: id,
|
|
723
|
-
version: versionNumber,
|
|
724
|
-
createdBy: Number(userCid),
|
|
725
|
-
name: watchlistData.name,
|
|
726
|
-
type: watchlistData.type,
|
|
727
|
-
description: watchlistData.dynamicConfig?.description || '',
|
|
728
|
-
snapshotAt: FieldValue.serverTimestamp(),
|
|
729
|
-
copyCount: 0,
|
|
730
|
-
createdAt: watchlistData.createdAt,
|
|
731
|
-
isImmutable: true // Public versions are immutable
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
// Only include items if it's a static watchlist
|
|
735
|
-
if (watchlistData.type === 'static' && watchlistData.items) {
|
|
736
|
-
versionData.items = JSON.parse(JSON.stringify(watchlistData.items));
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// Only include dynamicConfig if it's a dynamic watchlist
|
|
740
|
-
if (watchlistData.type === 'dynamic' && watchlistData.dynamicConfig) {
|
|
741
|
-
versionData.dynamicConfig = JSON.parse(JSON.stringify(watchlistData.dynamicConfig));
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Store version in versions subcollection
|
|
745
|
-
const versionRef = db.collection('public_watchlists')
|
|
746
|
-
.doc(id)
|
|
747
|
-
.collection('versions')
|
|
748
|
-
.doc(String(versionNumber));
|
|
749
|
-
|
|
750
|
-
await versionRef.set(versionData);
|
|
751
|
-
|
|
752
|
-
// Update or create public watchlist entry (points to latest version)
|
|
753
|
-
await publicRef.set({
|
|
754
|
-
watchlistId: id,
|
|
755
|
-
createdBy: Number(userCid),
|
|
756
|
-
name: watchlistData.name,
|
|
757
|
-
type: watchlistData.type,
|
|
758
|
-
description: watchlistData.dynamicConfig?.description || '',
|
|
759
|
-
latestVersion: versionNumber,
|
|
760
|
-
latestVersionId: versionId,
|
|
761
|
-
copyCount: publicDoc.exists ? (publicDoc.data().copyCount || 0) : 0,
|
|
762
|
-
createdAt: watchlistData.createdAt,
|
|
763
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
764
|
-
}, { merge: true });
|
|
765
|
-
|
|
766
|
-
logger.log('SUCCESS', `[publishWatchlistVersion] User ${userCid} published watchlist ${id} as version ${versionNumber}`);
|
|
767
|
-
|
|
768
|
-
return res.status(201).json({
|
|
769
|
-
success: true,
|
|
770
|
-
version: versionNumber,
|
|
771
|
-
versionId: versionId,
|
|
772
|
-
watchlist: versionData
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
} catch (error) {
|
|
776
|
-
logger.log('ERROR', `[publishWatchlistVersion] Error publishing watchlist ${id} for ${userCid}`, error);
|
|
777
|
-
return res.status(500).json({ error: error.message });
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* GET /user/public-watchlists/:id/versions
|
|
783
|
-
* Get version history for a public watchlist
|
|
784
|
-
*/
|
|
785
|
-
async function getWatchlistVersions(req, res, dependencies, config) {
|
|
786
|
-
const { db, logger } = dependencies;
|
|
787
|
-
const { id } = req.params;
|
|
788
|
-
|
|
789
|
-
try {
|
|
790
|
-
const versionsRef = db.collection('public_watchlists')
|
|
791
|
-
.doc(id)
|
|
792
|
-
.collection('versions')
|
|
793
|
-
.orderBy('version', 'desc');
|
|
794
|
-
|
|
795
|
-
const snapshot = await versionsRef.get();
|
|
796
|
-
const versions = [];
|
|
797
|
-
|
|
798
|
-
snapshot.forEach(doc => {
|
|
799
|
-
versions.push({
|
|
800
|
-
version: doc.data().version,
|
|
801
|
-
versionId: `${id}_v${doc.data().version}`,
|
|
802
|
-
...doc.data()
|
|
803
|
-
});
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
return res.status(200).json({
|
|
807
|
-
success: true,
|
|
808
|
-
watchlistId: id,
|
|
809
|
-
versions,
|
|
810
|
-
count: versions.length
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
} catch (error) {
|
|
814
|
-
logger.log('ERROR', `[getWatchlistVersions] Error fetching versions for ${id}`, error);
|
|
815
|
-
return res.status(500).json({ error: error.message });
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
module.exports = {
|
|
820
|
-
getUserWatchlists,
|
|
821
|
-
getWatchlist,
|
|
822
|
-
createWatchlist,
|
|
823
|
-
updateWatchlist,
|
|
824
|
-
deleteWatchlist,
|
|
825
|
-
copyWatchlist,
|
|
826
|
-
getPublicWatchlists,
|
|
827
|
-
publishWatchlistVersion,
|
|
828
|
-
getWatchlistVersions
|
|
829
|
-
};
|