bulltrackers-module 1.0.592 → 1.0.593
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/old-generic-api/admin-api/index.js +895 -0
- package/functions/old-generic-api/helpers/api_helpers.js +457 -0
- package/functions/old-generic-api/index.js +204 -0
- package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +355 -0
- package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
- package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +212 -0
- package/functions/old-generic-api/user-api/helpers/collection_helpers.js +193 -0
- package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +68 -0
- package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +256 -0
- package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +640 -0
- package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +195 -0
- package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +503 -0
- package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
- package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +245 -0
- package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +174 -0
- package/functions/old-generic-api/user-api/helpers/data_helpers.js +87 -0
- package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +336 -0
- package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +615 -0
- package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +231 -0
- package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +641 -0
- package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +182 -0
- package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +137 -0
- package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +190 -0
- package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +66 -0
- package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +550 -0
- package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +378 -0
- package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +295 -0
- package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +162 -0
- package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +677 -0
- package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +323 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +96 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +141 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +310 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +829 -0
- package/functions/old-generic-api/user-api/index.js +109 -0
- package/package.json +2 -2
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Computation Data Helpers
|
|
3
|
+
* Handles computation results endpoints with migration support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { findLatestComputationDate } = require('../core/data_lookup_helpers');
|
|
7
|
+
const { tryDecompress } = require('../core/compression_helpers');
|
|
8
|
+
const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a PI exists in a computation date
|
|
12
|
+
* For PopularInvestorProfileMetrics and SignedInUserProfileMetrics: reads from pages subcollection
|
|
13
|
+
* For other computations: reads from main computation document
|
|
14
|
+
* Returns { found: boolean, profileData: object | null, computationData: object | null }
|
|
15
|
+
* @param {object} db - Firestore instance
|
|
16
|
+
* @param {string} insightsCollection - Insights collection name
|
|
17
|
+
* @param {string} resultsSub - Results subcollection name
|
|
18
|
+
* @param {string} compsSub - Computations subcollection name
|
|
19
|
+
* @param {string} category - Category name
|
|
20
|
+
* @param {string} computationName - Computation name
|
|
21
|
+
* @param {string} dateStr - Date string (YYYY-MM-DD)
|
|
22
|
+
* @param {string} cidStr - CID as string
|
|
23
|
+
* @param {object} logger - Logger instance
|
|
24
|
+
* @returns {Promise<{found: boolean, profileData: object|null, computationData: object|null}>}
|
|
25
|
+
*/
|
|
26
|
+
async function checkPiInComputationDate(db, insightsCollection, resultsSub, compsSub, category, computationName, dateStr, cidStr, logger) {
|
|
27
|
+
try {
|
|
28
|
+
// Check if this computation uses the pages subcollection structure
|
|
29
|
+
const usesPagesStructure = computationName === 'PopularInvestorProfileMetrics' ||
|
|
30
|
+
computationName === 'SignedInUserProfileMetrics' ||
|
|
31
|
+
computationName === 'SignedInUserPIPersonalizedMetrics';
|
|
32
|
+
|
|
33
|
+
if (usesPagesStructure) {
|
|
34
|
+
// New path format: /unified_insights/YYYY-MM-DD/results/popular-investor/computations/{computationName}/pages/{cid}
|
|
35
|
+
const pageRef = db.collection(insightsCollection)
|
|
36
|
+
.doc(dateStr)
|
|
37
|
+
.collection(resultsSub)
|
|
38
|
+
.doc(category)
|
|
39
|
+
.collection(compsSub)
|
|
40
|
+
.doc(computationName)
|
|
41
|
+
.collection('pages')
|
|
42
|
+
.doc(cidStr);
|
|
43
|
+
|
|
44
|
+
const pageDoc = await pageRef.get();
|
|
45
|
+
|
|
46
|
+
if (!pageDoc.exists) {
|
|
47
|
+
logger.log('INFO', `[checkPiInComputationDate] Page document not found for CID ${cidStr} in date ${dateStr}`);
|
|
48
|
+
return { found: false, profileData: null, computationData: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rawData = pageDoc.data();
|
|
52
|
+
let profileData = null;
|
|
53
|
+
|
|
54
|
+
// Decompress if needed
|
|
55
|
+
profileData = tryDecompress(rawData);
|
|
56
|
+
|
|
57
|
+
// Handle string decompression result
|
|
58
|
+
if (typeof profileData === 'string') {
|
|
59
|
+
try {
|
|
60
|
+
profileData = JSON.parse(profileData);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for CID ${cidStr} on date ${dateStr}:`, e.message);
|
|
63
|
+
return { found: false, profileData: null, computationData: null };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (profileData && typeof profileData === 'object') {
|
|
68
|
+
logger.log('INFO', `[checkPiInComputationDate] Found profile data for CID ${cidStr} in date ${dateStr}`);
|
|
69
|
+
// Return the profile data - computationData is set to null since we're reading individual pages
|
|
70
|
+
return { found: true, profileData, computationData: null };
|
|
71
|
+
} else {
|
|
72
|
+
logger.log('WARN', `[checkPiInComputationDate] Profile data is not an object for CID ${cidStr} on date ${dateStr}`);
|
|
73
|
+
return { found: false, profileData: null, computationData: null };
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// Standard path for other computations: read from main computation document
|
|
77
|
+
const computationRef = db.collection(insightsCollection)
|
|
78
|
+
.doc(dateStr)
|
|
79
|
+
.collection(resultsSub)
|
|
80
|
+
.doc(category)
|
|
81
|
+
.collection(compsSub)
|
|
82
|
+
.doc(computationName);
|
|
83
|
+
|
|
84
|
+
const computationDoc = await computationRef.get();
|
|
85
|
+
|
|
86
|
+
if (!computationDoc.exists) {
|
|
87
|
+
return { found: false, profileData: null, computationData: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rawData = computationDoc.data();
|
|
91
|
+
let computationData = null;
|
|
92
|
+
|
|
93
|
+
// Check if data is sharded
|
|
94
|
+
if (rawData._sharded === true && rawData._shardCount) {
|
|
95
|
+
const shardsCol = computationRef.collection('_shards');
|
|
96
|
+
const shardCount = rawData._shardCount;
|
|
97
|
+
|
|
98
|
+
logger.log('INFO', `[checkPiInComputationDate] Reading ${shardCount} shards for date ${dateStr}`);
|
|
99
|
+
|
|
100
|
+
computationData = {};
|
|
101
|
+
|
|
102
|
+
// Read all shards (shard_0, shard_1, ..., shard_N-1)
|
|
103
|
+
for (let i = 0; i < shardCount; i++) {
|
|
104
|
+
const shardDoc = await shardsCol.doc(`shard_${i}`).get();
|
|
105
|
+
if (shardDoc.exists) {
|
|
106
|
+
const shardData = shardDoc.data();
|
|
107
|
+
Object.assign(computationData, shardData);
|
|
108
|
+
} else {
|
|
109
|
+
logger.log('WARN', `[checkPiInComputationDate] Shard shard_${i} missing for date ${dateStr}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Data is in the main document (compressed or raw)
|
|
114
|
+
computationData = tryDecompress(rawData);
|
|
115
|
+
|
|
116
|
+
// Handle string decompression result
|
|
117
|
+
if (typeof computationData === 'string') {
|
|
118
|
+
try {
|
|
119
|
+
computationData = JSON.parse(computationData);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for date ${dateStr}:`, e.message);
|
|
122
|
+
return { found: false, profileData: null, computationData: null };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check if CID exists in the computation data
|
|
128
|
+
if (computationData && typeof computationData === 'object' && !Array.isArray(computationData)) {
|
|
129
|
+
// Filter out metadata keys that start with underscore
|
|
130
|
+
const cids = Object.keys(computationData).filter(key => !key.startsWith('_'));
|
|
131
|
+
|
|
132
|
+
// Check if the requested CID exists
|
|
133
|
+
const profileData = computationData[cidStr];
|
|
134
|
+
if (profileData) {
|
|
135
|
+
logger.log('INFO', `[checkPiInComputationDate] Found CID ${cidStr} in date ${dateStr} (total CIDs: ${cids.length})`);
|
|
136
|
+
return { found: true, profileData, computationData };
|
|
137
|
+
} else {
|
|
138
|
+
logger.log('INFO', `[checkPiInComputationDate] CID ${cidStr} not found in date ${dateStr} (total CIDs: ${cids.length})`);
|
|
139
|
+
return { found: false, profileData: null, computationData };
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
logger.log('WARN', `[checkPiInComputationDate] Computation data is not an object for date ${dateStr}`);
|
|
143
|
+
return { found: false, profileData: null, computationData: null };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.log('ERROR', `[checkPiInComputationDate] Error checking PI in computation:`, error);
|
|
148
|
+
return { found: false, profileData: null, computationData: null };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Helper: Get computation metadata from schema collection
|
|
154
|
+
* @param {object} db - Firestore instance
|
|
155
|
+
* @param {string} computationName - Computation name
|
|
156
|
+
* @param {object} config - Config object
|
|
157
|
+
* @returns {Promise<object|null>} Metadata object or null if not found
|
|
158
|
+
*/
|
|
159
|
+
async function getComputationMetadata(db, computationName, config) {
|
|
160
|
+
try {
|
|
161
|
+
const schemaCollection = config.schemaCollection || 'computation_schemas';
|
|
162
|
+
const schemaDoc = await db.collection(schemaCollection).doc(computationName).get();
|
|
163
|
+
if (schemaDoc.exists) {
|
|
164
|
+
return schemaDoc.data().metadata || null;
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// Non-critical, return null
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* GET /user/me/computations
|
|
174
|
+
* Fetches computation results for a specific signed-in user
|
|
175
|
+
*/
|
|
176
|
+
async function getUserComputations(req, res, dependencies, config) {
|
|
177
|
+
const { db, logger } = dependencies;
|
|
178
|
+
const { userCid, computation, mode = 'latest', limit = 30 } = req.query;
|
|
179
|
+
|
|
180
|
+
if (!userCid) {
|
|
181
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// Check for dev override impersonation
|
|
186
|
+
const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
|
|
187
|
+
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
188
|
+
const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
|
|
189
|
+
|
|
190
|
+
const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
|
|
191
|
+
const resultsSub = config.resultsSubcollection || 'results';
|
|
192
|
+
const compsSub = config.computationsSubcollection || 'computations';
|
|
193
|
+
|
|
194
|
+
const category = 'popular-investor';
|
|
195
|
+
const today = new Date().toISOString().split('T')[0];
|
|
196
|
+
|
|
197
|
+
const computationNames = computation ? computation.split(',') : [];
|
|
198
|
+
|
|
199
|
+
if (computationNames.length === 0) {
|
|
200
|
+
return res.status(400).json({ error: "Please specify at least one computation name" });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// [NEW] Pre-fetch metadata for all computations to determine which are meta vs standard
|
|
204
|
+
const computationMetadata = {};
|
|
205
|
+
for (const compName of computationNames) {
|
|
206
|
+
const metadata = await getComputationMetadata(db, compName, config);
|
|
207
|
+
if (metadata) {
|
|
208
|
+
computationMetadata[compName] = metadata;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const isDevOverrideActive = devOverride && devOverride.enabled && devOverride.fakeCopiedPIs.length > 0;
|
|
213
|
+
|
|
214
|
+
// Check if any of the requested computations are meta computations
|
|
215
|
+
const firstCompName = computationNames[0];
|
|
216
|
+
const firstCompMetadata = computationMetadata[firstCompName];
|
|
217
|
+
const isMetaComputation = firstCompMetadata && firstCompMetadata.type === 'meta';
|
|
218
|
+
|
|
219
|
+
let datesToCheck = [today];
|
|
220
|
+
|
|
221
|
+
if (mode === 'latest') {
|
|
222
|
+
let foundDate = null;
|
|
223
|
+
|
|
224
|
+
if (isMetaComputation) {
|
|
225
|
+
// For meta computations: check today first, then look back 7 days
|
|
226
|
+
const maxDaysBack = 7;
|
|
227
|
+
|
|
228
|
+
for (let daysBack = 0; daysBack < maxDaysBack; daysBack++) {
|
|
229
|
+
const checkDate = new Date();
|
|
230
|
+
checkDate.setDate(checkDate.getDate() - daysBack);
|
|
231
|
+
const dateStr = checkDate.toISOString().split('T')[0];
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
// For meta computations, just check if the document exists
|
|
235
|
+
const computationRef = db.collection(insightsCollection)
|
|
236
|
+
.doc(dateStr)
|
|
237
|
+
.collection(resultsSub)
|
|
238
|
+
.doc(category)
|
|
239
|
+
.collection(compsSub)
|
|
240
|
+
.doc(firstCompName);
|
|
241
|
+
|
|
242
|
+
const computationDoc = await computationRef.get();
|
|
243
|
+
|
|
244
|
+
if (computationDoc.exists) {
|
|
245
|
+
foundDate = dateStr;
|
|
246
|
+
if (dateStr !== today) {
|
|
247
|
+
logger.log('INFO', `[getUserComputations] Meta computation ${firstCompName} found on fallback date ${foundDate} (today: ${today})`);
|
|
248
|
+
} else {
|
|
249
|
+
logger.log('INFO', `[getUserComputations] Meta computation ${firstCompName} found on today's date`);
|
|
250
|
+
}
|
|
251
|
+
break; // Found document, stop searching
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// Continue to next date if error
|
|
255
|
+
logger.log('DEBUG', `[getUserComputations] Error checking date ${dateStr} for meta computation:`, error.message);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// For user-specific computations: use same logic as data-status
|
|
261
|
+
// Search backwards and verify user exists
|
|
262
|
+
const maxDaysBack = 30; // Match data-status search window
|
|
263
|
+
|
|
264
|
+
for (let daysBack = 0; daysBack < maxDaysBack; daysBack++) {
|
|
265
|
+
const checkDate = new Date();
|
|
266
|
+
checkDate.setDate(checkDate.getDate() - daysBack);
|
|
267
|
+
const dateStr = checkDate.toISOString().split('T')[0];
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
// Check if page/computation document exists for this user and date
|
|
271
|
+
// For computations using pages subcollection, checkPiInComputationDate handles it
|
|
272
|
+
// For other computations, check the main document first
|
|
273
|
+
const { found } = await checkPiInComputationDate(
|
|
274
|
+
db,
|
|
275
|
+
insightsCollection,
|
|
276
|
+
resultsSub,
|
|
277
|
+
compsSub,
|
|
278
|
+
category,
|
|
279
|
+
firstCompName,
|
|
280
|
+
dateStr,
|
|
281
|
+
String(effectiveCid),
|
|
282
|
+
logger
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (found) {
|
|
286
|
+
foundDate = dateStr;
|
|
287
|
+
if (dateStr !== today) {
|
|
288
|
+
logger.log('INFO', `[getUserComputations] Using fallback date ${foundDate} for effective CID ${effectiveCid} (today: ${today})`);
|
|
289
|
+
} else {
|
|
290
|
+
logger.log('INFO', `[getUserComputations] Found computation for effective CID ${effectiveCid} on today's date`);
|
|
291
|
+
}
|
|
292
|
+
break; // Found user, stop searching
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
// Continue to next date if error
|
|
296
|
+
logger.log('DEBUG', `[getUserComputations] Error checking date ${dateStr}:`, error.message);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (foundDate) {
|
|
303
|
+
datesToCheck = [foundDate];
|
|
304
|
+
} else {
|
|
305
|
+
const maxDaysBack = isMetaComputation ? 7 : 30;
|
|
306
|
+
logger.log('WARN', `[getUserComputations] No computation data found for ${isMetaComputation ? 'meta computation' : `CID ${effectiveCid}`} in last ${maxDaysBack} days. Frontend will use fallback.`);
|
|
307
|
+
return res.status(200).json({
|
|
308
|
+
status: 'success',
|
|
309
|
+
userCid: String(effectiveCid),
|
|
310
|
+
mode,
|
|
311
|
+
computations: computationNames,
|
|
312
|
+
data: {},
|
|
313
|
+
isFallback: true,
|
|
314
|
+
requestedDate: today,
|
|
315
|
+
isImpersonating: isImpersonating || false,
|
|
316
|
+
actualCid: Number(userCid)
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
} else if (mode === 'series') {
|
|
320
|
+
const limitNum = parseInt(limit) || 30;
|
|
321
|
+
datesToCheck = [];
|
|
322
|
+
for (let i = 0; i < limitNum; i++) {
|
|
323
|
+
const date = new Date();
|
|
324
|
+
date.setDate(date.getDate() - i);
|
|
325
|
+
datesToCheck.push(date.toISOString().split('T')[0]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const results = {};
|
|
330
|
+
let isFallback = false;
|
|
331
|
+
|
|
332
|
+
for (const date of datesToCheck) {
|
|
333
|
+
results[date] = {};
|
|
334
|
+
|
|
335
|
+
for (const compName of computationNames) {
|
|
336
|
+
try {
|
|
337
|
+
let userResult = null;
|
|
338
|
+
|
|
339
|
+
// Special handling for computations that use pages subcollection structure
|
|
340
|
+
// PopularInvestorProfileMetrics, SignedInUserProfileMetrics, and SignedInUserPIPersonalizedMetrics use pages subcollection
|
|
341
|
+
if (compName === 'PopularInvestorProfileMetrics' ||
|
|
342
|
+
compName === 'SignedInUserProfileMetrics' ||
|
|
343
|
+
compName === 'SignedInUserPIPersonalizedMetrics') {
|
|
344
|
+
const pageRef = db.collection(insightsCollection)
|
|
345
|
+
.doc(date)
|
|
346
|
+
.collection(resultsSub)
|
|
347
|
+
.doc(category)
|
|
348
|
+
.collection(compsSub)
|
|
349
|
+
.doc(compName)
|
|
350
|
+
.collection('pages')
|
|
351
|
+
.doc(String(effectiveCid));
|
|
352
|
+
|
|
353
|
+
const pageDoc = await pageRef.get();
|
|
354
|
+
|
|
355
|
+
if (pageDoc.exists) {
|
|
356
|
+
const rawData = pageDoc.data();
|
|
357
|
+
let data = tryDecompress(rawData);
|
|
358
|
+
|
|
359
|
+
if (typeof data === 'string') {
|
|
360
|
+
try {
|
|
361
|
+
data = JSON.parse(data);
|
|
362
|
+
} catch (e) {
|
|
363
|
+
logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} page on ${date}:`, e.message);
|
|
364
|
+
data = null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (data && typeof data === 'object') {
|
|
369
|
+
userResult = data;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
// Standard path for other computations
|
|
374
|
+
const docRef = db.collection(insightsCollection)
|
|
375
|
+
.doc(date)
|
|
376
|
+
.collection(resultsSub)
|
|
377
|
+
.doc(category)
|
|
378
|
+
.collection(compsSub)
|
|
379
|
+
.doc(compName);
|
|
380
|
+
|
|
381
|
+
const doc = await docRef.get();
|
|
382
|
+
|
|
383
|
+
if (doc.exists) {
|
|
384
|
+
const rawData = doc.data();
|
|
385
|
+
let data = tryDecompress(rawData);
|
|
386
|
+
|
|
387
|
+
if (typeof data === 'string') {
|
|
388
|
+
try {
|
|
389
|
+
data = JSON.parse(data);
|
|
390
|
+
} catch (e) {
|
|
391
|
+
logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} on ${date}:`, e.message);
|
|
392
|
+
data = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (data && data._sharded === true && data._shardCount) {
|
|
397
|
+
const shardsCol = docRef.collection('_shards');
|
|
398
|
+
const shardsSnapshot = await shardsCol.get();
|
|
399
|
+
|
|
400
|
+
if (!shardsSnapshot.empty) {
|
|
401
|
+
data = {};
|
|
402
|
+
for (const shardDoc of shardsSnapshot.docs) {
|
|
403
|
+
const shardData = shardDoc.data();
|
|
404
|
+
Object.assign(data, shardData);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
data = null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// [FIX] Handle meta computations (global results) vs standard computations (user-specific)
|
|
412
|
+
// Use metadata from schema collection to determine computation type
|
|
413
|
+
if (data && typeof data === 'object') {
|
|
414
|
+
const metadata = computationMetadata[compName];
|
|
415
|
+
const isMetaComputation = metadata && metadata.type === 'meta';
|
|
416
|
+
|
|
417
|
+
if (isMetaComputation) {
|
|
418
|
+
// Meta computation: return entire data object (global results)
|
|
419
|
+
userResult = data;
|
|
420
|
+
} else {
|
|
421
|
+
// Standard computation: extract user-specific result
|
|
422
|
+
const userCidKey = String(effectiveCid);
|
|
423
|
+
userResult = data.hasOwnProperty(userCidKey) ? data[userCidKey] : null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (isDevOverrideActive && (compName === 'SignedInUserProfileMetrics' || compName === 'SignedInUserCopiedPIs')) {
|
|
430
|
+
if (compName === 'SignedInUserCopiedPIs') {
|
|
431
|
+
userResult = {
|
|
432
|
+
current: devOverride.fakeCopiedPIs,
|
|
433
|
+
past: [],
|
|
434
|
+
all: devOverride.fakeCopiedPIs
|
|
435
|
+
};
|
|
436
|
+
logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserCopiedPIs for user ${userCid}`);
|
|
437
|
+
} else if (compName === 'SignedInUserProfileMetrics' && userResult && userResult.copiedPIs) {
|
|
438
|
+
const fakeMirrors = devOverride.fakeCopiedPIs.map(cid => ({
|
|
439
|
+
cid: Number(cid),
|
|
440
|
+
username: `PI-${cid}`,
|
|
441
|
+
invested: 0,
|
|
442
|
+
netProfit: 0,
|
|
443
|
+
value: 0,
|
|
444
|
+
pendingClosure: false,
|
|
445
|
+
isRanked: false
|
|
446
|
+
}));
|
|
447
|
+
|
|
448
|
+
userResult = {
|
|
449
|
+
...userResult,
|
|
450
|
+
copiedPIs: {
|
|
451
|
+
chartType: 'cards',
|
|
452
|
+
data: fakeMirrors
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserProfileMetrics.copiedPIs for user ${userCid}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (userResult) {
|
|
460
|
+
results[date][compName] = userResult;
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
logger.log('WARN', `[getUserComputations] Error fetching ${compName} for ${date}`, err);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (mode === 'latest' && Object.keys(results[date]).length > 0) {
|
|
468
|
+
isFallback = date !== today;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const cleanedResults = {};
|
|
474
|
+
for (const [date, data] of Object.entries(results)) {
|
|
475
|
+
if (Object.keys(data).length > 0) {
|
|
476
|
+
cleanedResults[date] = data;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return res.status(200).json({
|
|
481
|
+
status: 'success',
|
|
482
|
+
userCid: String(effectiveCid),
|
|
483
|
+
mode,
|
|
484
|
+
computations: computationNames,
|
|
485
|
+
data: cleanedResults,
|
|
486
|
+
isFallback: isFallback,
|
|
487
|
+
requestedDate: today,
|
|
488
|
+
devOverrideActive: isDevOverrideActive,
|
|
489
|
+
isImpersonating: isImpersonating || false,
|
|
490
|
+
actualCid: Number(userCid)
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
} catch (error) {
|
|
494
|
+
logger.log('ERROR', `[getUserComputations] Error fetching computations for ${userCid}`, error);
|
|
495
|
+
return res.status(500).json({ error: error.message });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
module.exports = {
|
|
500
|
+
getUserComputations,
|
|
501
|
+
checkPiInComputationDate
|
|
502
|
+
};
|
|
503
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Instrument Mappings Helpers
|
|
3
|
+
* Handles instrument ID to ticker and sector mappings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /user/me/instrument-mappings
|
|
8
|
+
* Fetches instrument ID to ticker and sector mappings for the frontend
|
|
9
|
+
*/
|
|
10
|
+
async function getInstrumentMappings(req, res, dependencies, config) {
|
|
11
|
+
const { db, logger } = dependencies;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Fetch from Firestore (same source as computation system)
|
|
15
|
+
const [tickerToIdDoc, tickerToSectorDoc] = await Promise.all([
|
|
16
|
+
db.collection('instrument_mappings').doc('etoro_to_ticker').get(),
|
|
17
|
+
db.collection('instrument_sector_mappings').doc('sector_mappings').get()
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
if (!tickerToIdDoc.exists) {
|
|
21
|
+
return res.status(404).json({ error: "Instrument mappings not found" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tickerToId = tickerToIdDoc.data();
|
|
25
|
+
const tickerToSector = tickerToSectorDoc.exists ? tickerToSectorDoc.data() : {};
|
|
26
|
+
|
|
27
|
+
// Convert to ID -> Ticker mapping (reverse the mapping)
|
|
28
|
+
const idToTicker = {};
|
|
29
|
+
const idToSector = {};
|
|
30
|
+
|
|
31
|
+
for (const [id, ticker] of Object.entries(tickerToId)) {
|
|
32
|
+
idToTicker[String(id)] = ticker;
|
|
33
|
+
// Map ID -> Sector via ticker
|
|
34
|
+
if (tickerToSector[ticker]) {
|
|
35
|
+
idToSector[String(id)] = tickerToSector[ticker];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return res.status(200).json({
|
|
40
|
+
instrumentToTicker: idToTicker,
|
|
41
|
+
instrumentToSector: idToSector,
|
|
42
|
+
count: Object.keys(idToTicker).length,
|
|
43
|
+
sectorCount: Object.keys(idToSector).length
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.log('ERROR', `[getInstrumentMappings] Error fetching mappings`, error);
|
|
48
|
+
return res.status(500).json({ error: error.message });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
getInstrumentMappings
|
|
54
|
+
};
|
|
55
|
+
|