bulltrackers-module 1.0.501 → 1.0.502
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 +65 -15
- package/functions/computation-system/workflows/datafeederpipelineinstructions.md +6 -6
- package/functions/generic-api/API_MIGRATION_PLAN.md +436 -0
- package/functions/generic-api/user-api/helpers/collection_helpers.js +215 -0
- package/functions/generic-api/user-api/helpers/data_helpers.js +203 -57
- package/functions/generic-api/user-api/helpers/on_demand_fetch_helpers.js +109 -27
- package/functions/generic-api/user-api/helpers/review_helpers.js +99 -15
- package/functions/generic-api/user-api/helpers/subscription_helpers.js +220 -35
- package/functions/generic-api/user-api/helpers/user_sync_helpers.js +117 -46
- package/functions/generic-api/user-api/helpers/verification_helpers.js +42 -3
- package/functions/generic-api/user-api/helpers/watchlist_helpers.js +202 -55
- package/package.json +1 -1
|
@@ -89,13 +89,10 @@ async function requestPiFetch(req, res, dependencies, config) {
|
|
|
89
89
|
const requestId = `req_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
90
90
|
const now = new Date();
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
.doc(String(piCidNum))
|
|
95
|
-
.collection('requests')
|
|
96
|
-
.doc(requestId);
|
|
92
|
+
const { getCollectionPath, writeDual, extractCollectionName } = require('./collection_helpers');
|
|
93
|
+
const { collectionRegistry } = dependencies;
|
|
97
94
|
|
|
98
|
-
|
|
95
|
+
const requestData = {
|
|
99
96
|
requestId,
|
|
100
97
|
userCid: requestUserCid, // Use effective CID
|
|
101
98
|
actualUserCid: Number(userCid), // Track actual developer CID for audit
|
|
@@ -105,35 +102,92 @@ async function requestPiFetch(req, res, dependencies, config) {
|
|
|
105
102
|
isImpersonating: isImpersonating || false,
|
|
106
103
|
createdAt: FieldValue.serverTimestamp(),
|
|
107
104
|
updatedAt: FieldValue.serverTimestamp()
|
|
108
|
-
}
|
|
105
|
+
};
|
|
109
106
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
// Store request in new structure: popularInvestors/{piCid}/fetchRequests/{requestId}
|
|
108
|
+
if (collectionRegistry) {
|
|
109
|
+
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'fetchRequests', {
|
|
110
|
+
piCid: String(piCidNum)
|
|
111
|
+
}) + `/${requestId}`;
|
|
112
|
+
|
|
113
|
+
const legacyPath = `pi_fetch_requests/${piCidNum}/requests/${requestId}`;
|
|
114
|
+
|
|
115
|
+
await writeDual(db, newPath, legacyPath, requestData);
|
|
116
|
+
} else {
|
|
117
|
+
// Fallback to legacy only
|
|
118
|
+
const requestRef = db.collection('pi_fetch_requests')
|
|
119
|
+
.doc(String(piCidNum))
|
|
120
|
+
.collection('requests')
|
|
121
|
+
.doc(requestId);
|
|
122
|
+
|
|
123
|
+
await requestRef.set(requestData);
|
|
124
|
+
}
|
|
115
125
|
|
|
116
|
-
|
|
126
|
+
// Update user rate limit (use effective CID)
|
|
127
|
+
const userRequestData = {
|
|
117
128
|
userCid: requestUserCid,
|
|
118
129
|
piCid: piCidNum,
|
|
119
130
|
lastRequestedAt: FieldValue.serverTimestamp(),
|
|
120
131
|
requestCount: FieldValue.increment(1),
|
|
121
132
|
updatedAt: FieldValue.serverTimestamp()
|
|
122
|
-
}
|
|
133
|
+
};
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
if (collectionRegistry) {
|
|
136
|
+
const newUserPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'userFetchRequests', {
|
|
137
|
+
piCid: String(piCidNum)
|
|
138
|
+
}) + `/${requestUserCid}`;
|
|
139
|
+
|
|
140
|
+
const legacyUserPath = `pi_fetch_requests/${piCidNum}/user_requests/${requestUserCid}`;
|
|
141
|
+
|
|
142
|
+
await writeDual(db, newUserPath, legacyUserPath, userRequestData, { merge: true });
|
|
143
|
+
} else {
|
|
144
|
+
const userRequestRef = db.collection('pi_fetch_requests')
|
|
145
|
+
.doc(String(piCidNum))
|
|
146
|
+
.collection('user_requests')
|
|
147
|
+
.doc(String(requestUserCid));
|
|
148
|
+
|
|
149
|
+
await userRequestRef.set(userRequestData, { merge: true });
|
|
150
|
+
}
|
|
129
151
|
|
|
130
|
-
|
|
152
|
+
// Update global rate limit
|
|
153
|
+
const globalData = {
|
|
131
154
|
piCid: piCidNum,
|
|
132
155
|
lastRequestedAt: FieldValue.serverTimestamp(),
|
|
133
156
|
lastRequestedBy: requestUserCid, // Use effective CID
|
|
134
157
|
totalRequests: FieldValue.increment(1),
|
|
135
158
|
updatedAt: FieldValue.serverTimestamp()
|
|
136
|
-
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (collectionRegistry) {
|
|
162
|
+
const newGlobalPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'fetchStatus', {
|
|
163
|
+
piCid: String(piCidNum)
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const legacyGlobalPath = `pi_fetch_requests/${piCidNum}/global/latest`;
|
|
167
|
+
|
|
168
|
+
await writeDual(db, newGlobalPath, legacyGlobalPath, globalData, { merge: true });
|
|
169
|
+
} else {
|
|
170
|
+
const globalRef = db.collection('pi_fetch_requests')
|
|
171
|
+
.doc(String(piCidNum))
|
|
172
|
+
.collection('global')
|
|
173
|
+
.doc('latest');
|
|
174
|
+
|
|
175
|
+
await globalRef.set(globalData, { merge: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get request ref for later updates
|
|
179
|
+
let requestRef;
|
|
180
|
+
if (collectionRegistry) {
|
|
181
|
+
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'fetchRequests', {
|
|
182
|
+
piCid: String(piCidNum)
|
|
183
|
+
}) + `/${requestId}`;
|
|
184
|
+
requestRef = db.doc(newPath);
|
|
185
|
+
} else {
|
|
186
|
+
requestRef = db.collection('pi_fetch_requests')
|
|
187
|
+
.doc(String(piCidNum))
|
|
188
|
+
.collection('requests')
|
|
189
|
+
.doc(requestId);
|
|
190
|
+
}
|
|
137
191
|
|
|
138
192
|
// Publish to task engine - use on-demand topic for API requests
|
|
139
193
|
const topicName = config.taskEngine?.PUBSUB_TOPIC_USER_FETCH_ONDEMAND ||
|
|
@@ -268,13 +322,41 @@ async function getPiFetchStatus(req, res, dependencies, config) {
|
|
|
268
322
|
}
|
|
269
323
|
|
|
270
324
|
// Data doesn't exist, check for pending requests
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
.collection('requests')
|
|
274
|
-
.orderBy('createdAt', 'desc')
|
|
275
|
-
.limit(1);
|
|
325
|
+
const { getCollectionPath, extractCollectionName, readWithFallback } = require('./collection_helpers');
|
|
326
|
+
const { collectionRegistry } = dependencies;
|
|
276
327
|
|
|
277
|
-
|
|
328
|
+
let requestsSnapshot = null;
|
|
329
|
+
|
|
330
|
+
// Try new structure first
|
|
331
|
+
if (collectionRegistry) {
|
|
332
|
+
try {
|
|
333
|
+
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'fetchRequests', {
|
|
334
|
+
piCid: String(piCidNum)
|
|
335
|
+
});
|
|
336
|
+
const collectionName = extractCollectionName(newPath);
|
|
337
|
+
|
|
338
|
+
const requestsRef = db.collection(collectionName)
|
|
339
|
+
.doc(String(piCidNum))
|
|
340
|
+
.collection('fetchRequests')
|
|
341
|
+
.orderBy('createdAt', 'desc')
|
|
342
|
+
.limit(1);
|
|
343
|
+
|
|
344
|
+
requestsSnapshot = await requestsRef.get();
|
|
345
|
+
} catch (newError) {
|
|
346
|
+
logger.log('WARN', `[getPiFetchStatus] New structure failed, trying legacy: ${newError.message}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Fallback to legacy structure
|
|
351
|
+
if (!requestsSnapshot || requestsSnapshot.empty) {
|
|
352
|
+
const requestsRef = db.collection('pi_fetch_requests')
|
|
353
|
+
.doc(String(piCidNum))
|
|
354
|
+
.collection('requests')
|
|
355
|
+
.orderBy('createdAt', 'desc')
|
|
356
|
+
.limit(1);
|
|
357
|
+
|
|
358
|
+
requestsSnapshot = await requestsRef.get();
|
|
359
|
+
}
|
|
278
360
|
|
|
279
361
|
if (!requestsSnapshot.empty) {
|
|
280
362
|
const latestRequest = requestsSnapshot.docs[0].data();
|
|
@@ -321,8 +321,33 @@ async function submitReview(req, res, dependencies, config) {
|
|
|
321
321
|
|
|
322
322
|
// 3. Check if review already exists (for update) - use effective CID for review ID
|
|
323
323
|
const reviewId = `${effectiveCid}_${piCid}`;
|
|
324
|
-
|
|
325
|
-
const
|
|
324
|
+
|
|
325
|
+
const { getCollectionPath, writeDual, readWithFallback } = require('./collection_helpers');
|
|
326
|
+
const { collectionRegistry } = dependencies;
|
|
327
|
+
|
|
328
|
+
// Check existing review in new or legacy structure
|
|
329
|
+
let existingReview = null;
|
|
330
|
+
let isUpdate = false;
|
|
331
|
+
|
|
332
|
+
if (collectionRegistry) {
|
|
333
|
+
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
|
|
334
|
+
piCid: String(piCid)
|
|
335
|
+
}) + `/${reviewId}`;
|
|
336
|
+
|
|
337
|
+
const legacyPath = `${reviewsCollection}/${reviewId}`;
|
|
338
|
+
|
|
339
|
+
const result = await readWithFallback(db, newPath, legacyPath);
|
|
340
|
+
if (result && result.data) {
|
|
341
|
+
existingReview = { exists: true, data: () => result.data };
|
|
342
|
+
isUpdate = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Fallback to legacy check
|
|
347
|
+
if (!existingReview) {
|
|
348
|
+
existingReview = await db.collection(reviewsCollection).doc(reviewId).get();
|
|
349
|
+
isUpdate = existingReview.exists;
|
|
350
|
+
}
|
|
326
351
|
|
|
327
352
|
// 4. Store/Update Review (store effective CID, but track actual CID for audit)
|
|
328
353
|
const reviewData = {
|
|
@@ -341,7 +366,18 @@ async function submitReview(req, res, dependencies, config) {
|
|
|
341
366
|
reviewData.createdAt = FieldValue.serverTimestamp();
|
|
342
367
|
}
|
|
343
368
|
|
|
344
|
-
|
|
369
|
+
// Write to both new and legacy structures
|
|
370
|
+
if (collectionRegistry) {
|
|
371
|
+
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
|
|
372
|
+
piCid: String(piCid)
|
|
373
|
+
}) + `/${reviewId}`;
|
|
374
|
+
|
|
375
|
+
const legacyPath = `${reviewsCollection}/${reviewId}`;
|
|
376
|
+
|
|
377
|
+
await writeDual(db, newPath, legacyPath, reviewData, { merge: true });
|
|
378
|
+
} else {
|
|
379
|
+
await db.collection(reviewsCollection).doc(reviewId).set(reviewData, { merge: true });
|
|
380
|
+
}
|
|
345
381
|
|
|
346
382
|
logger.log('INFO', `[Review] User ${userCid} ${isUpdate ? 'updated' : 'submitted'} review for PI ${piCid}`);
|
|
347
383
|
return res.status(200).json({
|
|
@@ -368,13 +404,39 @@ async function getReviews(req, res, dependencies, config) {
|
|
|
368
404
|
|
|
369
405
|
try {
|
|
370
406
|
const piCidNum = Number(piCid);
|
|
407
|
+
const { getCollectionPath, extractCollectionName } = require('./collection_helpers');
|
|
408
|
+
const { collectionRegistry } = dependencies;
|
|
371
409
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
410
|
+
let snapshot = null;
|
|
411
|
+
|
|
412
|
+
// Try new structure first
|
|
413
|
+
if (collectionRegistry) {
|
|
414
|
+
try {
|
|
415
|
+
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
|
|
416
|
+
piCid: String(piCidNum)
|
|
417
|
+
});
|
|
418
|
+
const collectionName = extractCollectionName(newPath);
|
|
419
|
+
|
|
420
|
+
const reviewsRef = db.collection(collectionName)
|
|
421
|
+
.doc(String(piCidNum))
|
|
422
|
+
.collection('reviews')
|
|
423
|
+
.orderBy('createdAt', 'desc')
|
|
424
|
+
.limit(100);
|
|
425
|
+
|
|
426
|
+
snapshot = await reviewsRef.get();
|
|
427
|
+
} catch (newError) {
|
|
428
|
+
logger.log('WARN', `[getReviews] New structure failed, trying legacy: ${newError.message}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Fallback to legacy structure
|
|
433
|
+
if (!snapshot || snapshot.empty) {
|
|
434
|
+
snapshot = await db.collection(reviewsCollection)
|
|
435
|
+
.where('piCid', '==', piCidNum)
|
|
436
|
+
.orderBy('createdAt', 'desc')
|
|
437
|
+
.limit(100)
|
|
438
|
+
.get();
|
|
439
|
+
}
|
|
378
440
|
|
|
379
441
|
const reviews = [];
|
|
380
442
|
let totalStars = 0;
|
|
@@ -448,14 +510,36 @@ async function getUserReview(req, res, dependencies, config) {
|
|
|
448
510
|
}
|
|
449
511
|
|
|
450
512
|
try {
|
|
513
|
+
const { getCollectionPath, readWithFallback } = require('./collection_helpers');
|
|
514
|
+
const { collectionRegistry } = dependencies;
|
|
451
515
|
const reviewId = `${userCid}_${piCid}`;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
516
|
+
|
|
517
|
+
let reviewData = null;
|
|
518
|
+
|
|
519
|
+
// Try new structure first
|
|
520
|
+
if (collectionRegistry) {
|
|
521
|
+
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
|
|
522
|
+
piCid: String(piCid)
|
|
523
|
+
}) + `/${reviewId}`;
|
|
524
|
+
|
|
525
|
+
const legacyPath = `${reviewsCollection}/${reviewId}`;
|
|
526
|
+
|
|
527
|
+
const result = await readWithFallback(db, newPath, legacyPath);
|
|
528
|
+
if (result && result.data) {
|
|
529
|
+
reviewData = result.data;
|
|
530
|
+
}
|
|
456
531
|
}
|
|
457
|
-
|
|
458
|
-
|
|
532
|
+
|
|
533
|
+
// Fallback to legacy
|
|
534
|
+
if (!reviewData) {
|
|
535
|
+
const reviewDoc = await db.collection(reviewsCollection).doc(reviewId).get();
|
|
536
|
+
if (!reviewDoc.exists) {
|
|
537
|
+
return res.status(200).json({ exists: false, review: null });
|
|
538
|
+
}
|
|
539
|
+
reviewData = reviewDoc.data();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const data = reviewData;
|
|
459
543
|
const review = {
|
|
460
544
|
id: reviewDoc.id,
|
|
461
545
|
rating: data.rating,
|
|
@@ -67,14 +67,41 @@ async function subscribeToAlerts(req, res, dependencies, config) {
|
|
|
67
67
|
lastAlertAt: null
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
const { getCollectionPath, writeDual } = require('./collection_helpers');
|
|
71
|
+
const { collectionRegistry } = dependencies;
|
|
72
|
+
|
|
73
|
+
// Determine storage location based on whether watchlistId is present
|
|
74
|
+
if (watchlistId && collectionRegistry) {
|
|
75
|
+
// Per-watchlist subscription: signedInUsers/{cid}/watchlists/{watchlistId}/subscriptions/{piCid}
|
|
76
|
+
const newPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlistSubscriptions', {
|
|
77
|
+
cid: String(userCid),
|
|
78
|
+
watchlistId: watchlistId
|
|
79
|
+
}) + `/${piCid}`;
|
|
80
|
+
|
|
81
|
+
const legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
|
|
82
|
+
|
|
83
|
+
await writeDual(db, newPath, legacyPath, subscriptionData, { merge: true });
|
|
84
|
+
} else {
|
|
85
|
+
// Global subscription: signedInUsers/{cid}/subscriptions/{piCid}
|
|
86
|
+
if (collectionRegistry) {
|
|
87
|
+
const newPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'subscriptions', {
|
|
88
|
+
cid: String(userCid)
|
|
89
|
+
}) + `/${piCid}`;
|
|
90
|
+
|
|
91
|
+
const legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
|
|
92
|
+
|
|
93
|
+
await writeDual(db, newPath, legacyPath, subscriptionData, { merge: true });
|
|
94
|
+
} else {
|
|
95
|
+
// Fallback to legacy only
|
|
96
|
+
const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
|
|
97
|
+
const subscriptionRef = db.collection(subscriptionsCollection)
|
|
98
|
+
.doc(String(userCid))
|
|
99
|
+
.collection('alerts')
|
|
100
|
+
.doc(String(piCid));
|
|
101
|
+
|
|
102
|
+
await subscriptionRef.set(subscriptionData, { merge: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
78
105
|
|
|
79
106
|
logger.log('SUCCESS', `[subscribeToAlerts] User ${userCid} subscribed to alerts for PI ${piCid} in watchlist ${watchlistId}`);
|
|
80
107
|
|
|
@@ -104,44 +131,72 @@ async function updateSubscription(req, res, dependencies, config) {
|
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
try {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
const { getCollectionPath, readWithFallback, writeDual } = require('./collection_helpers');
|
|
135
|
+
const { collectionRegistry } = dependencies;
|
|
136
|
+
|
|
137
|
+
// Try to find subscription in new structure first, then legacy
|
|
138
|
+
let subscriptionData = null;
|
|
139
|
+
let subscriptionRef = null;
|
|
140
|
+
let newPath = null;
|
|
141
|
+
let legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
|
|
142
|
+
|
|
143
|
+
// Check global subscriptions first
|
|
144
|
+
if (collectionRegistry) {
|
|
145
|
+
newPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'subscriptions', {
|
|
146
|
+
cid: String(userCid)
|
|
147
|
+
}) + `/${piCid}`;
|
|
148
|
+
|
|
149
|
+
const result = await readWithFallback(db, newPath, legacyPath);
|
|
150
|
+
if (result && result.data) {
|
|
151
|
+
subscriptionData = result.data;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
114
154
|
|
|
115
|
-
if
|
|
116
|
-
|
|
155
|
+
// Fallback to legacy if not found
|
|
156
|
+
if (!subscriptionData) {
|
|
157
|
+
const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
|
|
158
|
+
subscriptionRef = db.collection(subscriptionsCollection)
|
|
159
|
+
.doc(String(userCid))
|
|
160
|
+
.collection('alerts')
|
|
161
|
+
.doc(String(piCid));
|
|
162
|
+
|
|
163
|
+
const subscriptionDoc = await subscriptionRef.get();
|
|
164
|
+
if (!subscriptionDoc.exists) {
|
|
165
|
+
return res.status(404).json({ error: "Subscription not found" });
|
|
166
|
+
}
|
|
167
|
+
subscriptionData = subscriptionDoc.data();
|
|
117
168
|
}
|
|
118
169
|
|
|
119
170
|
const updates = {};
|
|
120
|
-
|
|
121
171
|
if (alertTypes !== undefined) {
|
|
122
172
|
updates.alertTypes = alertTypes;
|
|
123
173
|
}
|
|
124
|
-
|
|
125
174
|
if (thresholds !== undefined) {
|
|
126
175
|
updates.thresholds = thresholds;
|
|
127
176
|
}
|
|
128
|
-
|
|
129
177
|
if (Object.keys(updates).length === 0) {
|
|
130
178
|
return res.status(400).json({ error: "No updates provided" });
|
|
131
179
|
}
|
|
132
180
|
|
|
133
181
|
updates.updatedAt = FieldValue.serverTimestamp();
|
|
134
182
|
|
|
135
|
-
|
|
183
|
+
// Update both structures if using new path, otherwise just legacy
|
|
184
|
+
if (newPath && collectionRegistry) {
|
|
185
|
+
const updatedData = { ...subscriptionData, ...updates };
|
|
186
|
+
await writeDual(db, newPath, legacyPath, updatedData, { merge: true });
|
|
187
|
+
} else if (subscriptionRef) {
|
|
188
|
+
await subscriptionRef.update(updates);
|
|
189
|
+
}
|
|
136
190
|
|
|
137
191
|
logger.log('SUCCESS', `[updateSubscription] Updated subscription for user ${userCid}, PI ${piCid}`);
|
|
138
192
|
|
|
139
|
-
|
|
193
|
+
// Return updated data
|
|
194
|
+
const finalData = { ...subscriptionData, ...updates };
|
|
140
195
|
return res.status(200).json({
|
|
141
196
|
success: true,
|
|
142
197
|
subscription: {
|
|
143
|
-
|
|
144
|
-
...
|
|
198
|
+
piCid: Number(piCid),
|
|
199
|
+
...finalData
|
|
145
200
|
}
|
|
146
201
|
});
|
|
147
202
|
|
|
@@ -165,6 +220,54 @@ async function unsubscribeFromAlerts(req, res, dependencies, config) {
|
|
|
165
220
|
}
|
|
166
221
|
|
|
167
222
|
try {
|
|
223
|
+
const { getCollectionPath, readWithFallback } = require('./collection_helpers');
|
|
224
|
+
const { collectionRegistry } = dependencies;
|
|
225
|
+
|
|
226
|
+
const legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
|
|
227
|
+
let newPath = null;
|
|
228
|
+
let found = false;
|
|
229
|
+
|
|
230
|
+
// Check global subscriptions first
|
|
231
|
+
if (collectionRegistry) {
|
|
232
|
+
newPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'subscriptions', {
|
|
233
|
+
cid: String(userCid)
|
|
234
|
+
}) + `/${piCid}`;
|
|
235
|
+
|
|
236
|
+
const newDoc = await db.doc(newPath).get();
|
|
237
|
+
if (newDoc.exists) {
|
|
238
|
+
await db.doc(newPath).delete();
|
|
239
|
+
found = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Also check per-watchlist subscriptions (need to search all watchlists)
|
|
244
|
+
if (collectionRegistry && !found) {
|
|
245
|
+
const { getCollectionPath } = require('./collection_helpers');
|
|
246
|
+
const watchlistsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlists', {
|
|
247
|
+
cid: String(userCid)
|
|
248
|
+
});
|
|
249
|
+
const watchlistsRef = db.collection(watchlistsPath.split('/')[0])
|
|
250
|
+
.doc(String(userCid))
|
|
251
|
+
.collection('watchlists');
|
|
252
|
+
|
|
253
|
+
const watchlistsSnapshot = await watchlistsRef.get();
|
|
254
|
+
for (const watchlistDoc of watchlistsSnapshot.docs) {
|
|
255
|
+
const watchlistId = watchlistDoc.id;
|
|
256
|
+
const watchlistSubPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlistSubscriptions', {
|
|
257
|
+
cid: String(userCid),
|
|
258
|
+
watchlistId: watchlistId
|
|
259
|
+
}) + `/${piCid}`;
|
|
260
|
+
|
|
261
|
+
const subDoc = await db.doc(watchlistSubPath).get();
|
|
262
|
+
if (subDoc.exists) {
|
|
263
|
+
await db.doc(watchlistSubPath).delete();
|
|
264
|
+
found = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Delete from legacy structure
|
|
168
271
|
const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
|
|
169
272
|
const subscriptionRef = db.collection(subscriptionsCollection)
|
|
170
273
|
.doc(String(userCid))
|
|
@@ -172,13 +275,15 @@ async function unsubscribeFromAlerts(req, res, dependencies, config) {
|
|
|
172
275
|
.doc(String(piCid));
|
|
173
276
|
|
|
174
277
|
const subscriptionDoc = await subscriptionRef.get();
|
|
278
|
+
if (subscriptionDoc.exists) {
|
|
279
|
+
await subscriptionRef.delete();
|
|
280
|
+
found = true;
|
|
281
|
+
}
|
|
175
282
|
|
|
176
|
-
if (!
|
|
283
|
+
if (!found) {
|
|
177
284
|
return res.status(404).json({ error: "Subscription not found" });
|
|
178
285
|
}
|
|
179
286
|
|
|
180
|
-
await subscriptionRef.delete();
|
|
181
|
-
|
|
182
287
|
logger.log('SUCCESS', `[unsubscribeFromAlerts] User ${userCid} unsubscribed from alerts for PI ${piCid}`);
|
|
183
288
|
|
|
184
289
|
return res.status(200).json({
|
|
@@ -205,19 +310,99 @@ async function getUserSubscriptions(req, res, dependencies, config) {
|
|
|
205
310
|
}
|
|
206
311
|
|
|
207
312
|
try {
|
|
313
|
+
const { getCollectionPath, extractCollectionName } = require('./collection_helpers');
|
|
314
|
+
const { collectionRegistry } = dependencies;
|
|
315
|
+
|
|
316
|
+
const subscriptions = [];
|
|
317
|
+
const seenPiCids = new Set();
|
|
318
|
+
|
|
319
|
+
// 1. Read from global subscriptions (new structure)
|
|
320
|
+
if (collectionRegistry) {
|
|
321
|
+
try {
|
|
322
|
+
const globalSubsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'subscriptions', {
|
|
323
|
+
cid: String(userCid)
|
|
324
|
+
});
|
|
325
|
+
const collectionName = extractCollectionName(globalSubsPath);
|
|
326
|
+
|
|
327
|
+
const globalSubsRef = db.collection(collectionName)
|
|
328
|
+
.doc(String(userCid))
|
|
329
|
+
.collection('subscriptions');
|
|
330
|
+
|
|
331
|
+
const globalSnapshot = await globalSubsRef.get();
|
|
332
|
+
globalSnapshot.forEach(doc => {
|
|
333
|
+
const data = doc.data();
|
|
334
|
+
subscriptions.push({
|
|
335
|
+
piCid: Number(doc.id),
|
|
336
|
+
...data
|
|
337
|
+
});
|
|
338
|
+
seenPiCids.add(Number(doc.id));
|
|
339
|
+
});
|
|
340
|
+
} catch (newError) {
|
|
341
|
+
logger.log('WARN', `[getUserSubscriptions] Failed to read global subscriptions from new structure: ${newError.message}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 2. Read from per-watchlist subscriptions
|
|
345
|
+
try {
|
|
346
|
+
const watchlistsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlists', {
|
|
347
|
+
cid: String(userCid)
|
|
348
|
+
});
|
|
349
|
+
const watchlistsCollectionName = extractCollectionName(watchlistsPath);
|
|
350
|
+
|
|
351
|
+
const watchlistsRef = db.collection(watchlistsCollectionName)
|
|
352
|
+
.doc(String(userCid))
|
|
353
|
+
.collection('watchlists');
|
|
354
|
+
|
|
355
|
+
const watchlistsSnapshot = await watchlistsRef.get();
|
|
356
|
+
|
|
357
|
+
for (const watchlistDoc of watchlistsSnapshot.docs) {
|
|
358
|
+
const watchlistId = watchlistDoc.id;
|
|
359
|
+
const watchlistSubsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlistSubscriptions', {
|
|
360
|
+
cid: String(userCid),
|
|
361
|
+
watchlistId: watchlistId
|
|
362
|
+
});
|
|
363
|
+
const watchlistSubsCollectionName = extractCollectionName(watchlistSubsPath);
|
|
364
|
+
|
|
365
|
+
const watchlistSubsRef = db.collection(watchlistSubsCollectionName)
|
|
366
|
+
.doc(String(userCid))
|
|
367
|
+
.collection('watchlists')
|
|
368
|
+
.doc(watchlistId)
|
|
369
|
+
.collection('subscriptions');
|
|
370
|
+
|
|
371
|
+
const watchlistSubsSnapshot = await watchlistSubsRef.get();
|
|
372
|
+
watchlistSubsSnapshot.forEach(doc => {
|
|
373
|
+
const piCid = Number(doc.id);
|
|
374
|
+
if (!seenPiCids.has(piCid)) {
|
|
375
|
+
const data = doc.data();
|
|
376
|
+
subscriptions.push({
|
|
377
|
+
piCid,
|
|
378
|
+
watchlistId,
|
|
379
|
+
...data
|
|
380
|
+
});
|
|
381
|
+
seenPiCids.add(piCid);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
} catch (watchlistError) {
|
|
386
|
+
logger.log('WARN', `[getUserSubscriptions] Failed to read watchlist subscriptions: ${watchlistError.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 3. Fallback to legacy structure for any missing subscriptions
|
|
208
391
|
const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
|
|
209
392
|
const subscriptionsRef = db.collection(subscriptionsCollection)
|
|
210
393
|
.doc(String(userCid))
|
|
211
394
|
.collection('alerts');
|
|
212
395
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
396
|
+
const legacySnapshot = await subscriptionsRef.get();
|
|
397
|
+
legacySnapshot.forEach(doc => {
|
|
398
|
+
const piCid = Number(doc.id);
|
|
399
|
+
if (!seenPiCids.has(piCid)) {
|
|
400
|
+
subscriptions.push({
|
|
401
|
+
piCid,
|
|
402
|
+
...doc.data()
|
|
403
|
+
});
|
|
404
|
+
seenPiCids.add(piCid);
|
|
405
|
+
}
|
|
221
406
|
});
|
|
222
407
|
|
|
223
408
|
return res.status(200).json({
|