bulltrackers-module 1.0.574 → 1.0.576

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.
@@ -9,6 +9,8 @@ const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
9
9
 
10
10
  /**
11
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
12
14
  * Returns { found: boolean, profileData: object | null, computationData: object | null }
13
15
  * @param {object} db - Firestore instance
14
16
  * @param {string} insightsCollection - Insights collection name
@@ -23,73 +25,121 @@ const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
23
25
  */
24
26
  async function checkPiInComputationDate(db, insightsCollection, resultsSub, compsSub, category, computationName, dateStr, cidStr, logger) {
25
27
  try {
26
- const computationRef = db.collection(insightsCollection)
27
- .doc(dateStr)
28
- .collection(resultsSub)
29
- .doc(category)
30
- .collection(compsSub)
31
- .doc(computationName);
28
+ // Check if this computation uses the pages subcollection structure
29
+ const usesPagesStructure = computationName === 'PopularInvestorProfileMetrics' || computationName === 'SignedInUserProfileMetrics';
32
30
 
33
- const computationDoc = await computationRef.get();
34
-
35
- if (!computationDoc.exists) {
36
- return { found: false, profileData: null, computationData: null };
37
- }
38
-
39
- const rawData = computationDoc.data();
40
- let computationData = null;
41
-
42
- // Check if data is sharded
43
- if (rawData._sharded === true && rawData._shardCount) {
44
- const shardsCol = computationRef.collection('_shards');
45
- const shardCount = rawData._shardCount;
31
+ if (usesPagesStructure) {
32
+ // New path format: /unified_insights/YYYY-MM-DD/results/popular-investor/computations/{computationName}/pages/{cid}
33
+ const pageRef = db.collection(insightsCollection)
34
+ .doc(dateStr)
35
+ .collection(resultsSub)
36
+ .doc(category)
37
+ .collection(compsSub)
38
+ .doc(computationName)
39
+ .collection('pages')
40
+ .doc(cidStr);
46
41
 
47
- logger.log('INFO', `[checkPiInComputationDate] Reading ${shardCount} shards for date ${dateStr}`);
42
+ const pageDoc = await pageRef.get();
48
43
 
49
- computationData = {};
50
-
51
- // Read all shards (shard_0, shard_1, ..., shard_N-1)
52
- for (let i = 0; i < shardCount; i++) {
53
- const shardDoc = await shardsCol.doc(`shard_${i}`).get();
54
- if (shardDoc.exists) {
55
- const shardData = shardDoc.data();
56
- Object.assign(computationData, shardData);
57
- } else {
58
- logger.log('WARN', `[checkPiInComputationDate] Shard shard_${i} missing for date ${dateStr}`);
59
- }
44
+ if (!pageDoc.exists) {
45
+ logger.log('INFO', `[checkPiInComputationDate] Page document not found for CID ${cidStr} in date ${dateStr}`);
46
+ return { found: false, profileData: null, computationData: null };
60
47
  }
61
- } else {
62
- // Data is in the main document (compressed or raw)
63
- computationData = tryDecompress(rawData);
48
+
49
+ const rawData = pageDoc.data();
50
+ let profileData = null;
51
+
52
+ // Decompress if needed
53
+ profileData = tryDecompress(rawData);
64
54
 
65
55
  // Handle string decompression result
66
- if (typeof computationData === 'string') {
56
+ if (typeof profileData === 'string') {
67
57
  try {
68
- computationData = JSON.parse(computationData);
58
+ profileData = JSON.parse(profileData);
69
59
  } catch (e) {
70
- logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for date ${dateStr}:`, e.message);
60
+ logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for CID ${cidStr} on date ${dateStr}:`, e.message);
71
61
  return { found: false, profileData: null, computationData: null };
72
62
  }
73
63
  }
74
- }
75
-
76
- // Check if CID exists in the computation data
77
- if (computationData && typeof computationData === 'object' && !Array.isArray(computationData)) {
78
- // Filter out metadata keys that start with underscore
79
- const cids = Object.keys(computationData).filter(key => !key.startsWith('_'));
80
64
 
81
- // Check if the requested CID exists
82
- const profileData = computationData[cidStr];
83
- if (profileData) {
84
- logger.log('INFO', `[checkPiInComputationDate] Found CID ${cidStr} in date ${dateStr} (total CIDs: ${cids.length})`);
85
- return { found: true, profileData, computationData };
65
+ if (profileData && typeof profileData === 'object') {
66
+ logger.log('INFO', `[checkPiInComputationDate] Found profile data for CID ${cidStr} in date ${dateStr}`);
67
+ // Return the profile data - computationData is set to null since we're reading individual pages
68
+ return { found: true, profileData, computationData: null };
86
69
  } else {
87
- logger.log('INFO', `[checkPiInComputationDate] CID ${cidStr} not found in date ${dateStr} (total CIDs: ${cids.length})`);
88
- return { found: false, profileData: null, computationData };
70
+ logger.log('WARN', `[checkPiInComputationDate] Profile data is not an object for CID ${cidStr} on date ${dateStr}`);
71
+ return { found: false, profileData: null, computationData: null };
89
72
  }
90
73
  } else {
91
- logger.log('WARN', `[checkPiInComputationDate] Computation data is not an object for date ${dateStr}`);
92
- return { found: false, profileData: null, computationData: null };
74
+ // Standard path for other computations: read from main computation document
75
+ const computationRef = db.collection(insightsCollection)
76
+ .doc(dateStr)
77
+ .collection(resultsSub)
78
+ .doc(category)
79
+ .collection(compsSub)
80
+ .doc(computationName);
81
+
82
+ const computationDoc = await computationRef.get();
83
+
84
+ if (!computationDoc.exists) {
85
+ return { found: false, profileData: null, computationData: null };
86
+ }
87
+
88
+ const rawData = computationDoc.data();
89
+ let computationData = null;
90
+
91
+ // Check if data is sharded
92
+ if (rawData._sharded === true && rawData._shardCount) {
93
+ const shardsCol = computationRef.collection('_shards');
94
+ const shardCount = rawData._shardCount;
95
+
96
+ logger.log('INFO', `[checkPiInComputationDate] Reading ${shardCount} shards for date ${dateStr}`);
97
+
98
+ computationData = {};
99
+
100
+ // Read all shards (shard_0, shard_1, ..., shard_N-1)
101
+ for (let i = 0; i < shardCount; i++) {
102
+ const shardDoc = await shardsCol.doc(`shard_${i}`).get();
103
+ if (shardDoc.exists) {
104
+ const shardData = shardDoc.data();
105
+ Object.assign(computationData, shardData);
106
+ } else {
107
+ logger.log('WARN', `[checkPiInComputationDate] Shard shard_${i} missing for date ${dateStr}`);
108
+ }
109
+ }
110
+ } else {
111
+ // Data is in the main document (compressed or raw)
112
+ computationData = tryDecompress(rawData);
113
+
114
+ // Handle string decompression result
115
+ if (typeof computationData === 'string') {
116
+ try {
117
+ computationData = JSON.parse(computationData);
118
+ } catch (e) {
119
+ logger.log('WARN', `[checkPiInComputationDate] Failed to parse decompressed string for date ${dateStr}:`, e.message);
120
+ return { found: false, profileData: null, computationData: null };
121
+ }
122
+ }
123
+ }
124
+
125
+ // Check if CID exists in the computation data
126
+ if (computationData && typeof computationData === 'object' && !Array.isArray(computationData)) {
127
+ // Filter out metadata keys that start with underscore
128
+ const cids = Object.keys(computationData).filter(key => !key.startsWith('_'));
129
+
130
+ // Check if the requested CID exists
131
+ const profileData = computationData[cidStr];
132
+ if (profileData) {
133
+ logger.log('INFO', `[checkPiInComputationDate] Found CID ${cidStr} in date ${dateStr} (total CIDs: ${cids.length})`);
134
+ return { found: true, profileData, computationData };
135
+ } else {
136
+ logger.log('INFO', `[checkPiInComputationDate] CID ${cidStr} not found in date ${dateStr} (total CIDs: ${cids.length})`);
137
+ return { found: false, profileData: null, computationData };
138
+ }
139
+ } else {
140
+ logger.log('WARN', `[checkPiInComputationDate] Computation data is not an object for date ${dateStr}`);
141
+ return { found: false, profileData: null, computationData: null };
142
+ }
93
143
  }
94
144
  } catch (error) {
95
145
  logger.log('ERROR', `[checkPiInComputationDate] Error checking PI in computation:`, error);
@@ -174,39 +224,29 @@ async function getUserComputations(req, res, dependencies, config) {
174
224
  const dateStr = checkDate.toISOString().split('T')[0];
175
225
 
176
226
  try {
177
- // Check if computation document exists for this date
178
- const computationRef = db.collection(insightsCollection)
179
- .doc(dateStr)
180
- .collection(resultsSub)
181
- .doc(category)
182
- .collection(compsSub)
183
- .doc(firstCompName);
227
+ // Check if page/computation document exists for this user and date
228
+ // For computations using pages subcollection, checkPiInComputationDate handles it
229
+ // For other computations, check the main document first
230
+ const { found } = await checkPiInComputationDate(
231
+ db,
232
+ insightsCollection,
233
+ resultsSub,
234
+ compsSub,
235
+ category,
236
+ firstCompName,
237
+ dateStr,
238
+ String(effectiveCid),
239
+ logger
240
+ );
184
241
 
185
- const computationDoc = await computationRef.get();
186
-
187
- if (computationDoc.exists) {
188
- // Computation exists, verify user is in it (same as data-status)
189
- const { found } = await checkPiInComputationDate(
190
- db,
191
- insightsCollection,
192
- resultsSub,
193
- compsSub,
194
- category,
195
- firstCompName,
196
- dateStr,
197
- String(effectiveCid),
198
- logger
199
- );
200
-
201
- if (found) {
202
- foundDate = dateStr;
203
- if (dateStr !== today) {
204
- logger.log('INFO', `[getUserComputations] Using fallback date ${foundDate} for effective CID ${effectiveCid} (today: ${today})`);
205
- } else {
206
- logger.log('INFO', `[getUserComputations] Found computation for effective CID ${effectiveCid} on today's date`);
207
- }
208
- break; // Found user, stop searching
242
+ if (found) {
243
+ foundDate = dateStr;
244
+ if (dateStr !== today) {
245
+ logger.log('INFO', `[getUserComputations] Using fallback date ${foundDate} for effective CID ${effectiveCid} (today: ${today})`);
246
+ } else {
247
+ logger.log('INFO', `[getUserComputations] Found computation for effective CID ${effectiveCid} on today's date`);
209
248
  }
249
+ break; // Found user, stop searching
210
250
  }
211
251
  } catch (error) {
212
252
  // Continue to next date if error
@@ -249,94 +289,129 @@ async function getUserComputations(req, res, dependencies, config) {
249
289
 
250
290
  for (const compName of computationNames) {
251
291
  try {
252
- const docRef = db.collection(insightsCollection)
253
- .doc(date)
254
- .collection(resultsSub)
255
- .doc(category)
256
- .collection(compsSub)
257
- .doc(compName);
258
-
259
- const doc = await docRef.get();
292
+ let userResult = null;
260
293
 
261
- if (doc.exists) {
262
- const rawData = doc.data();
263
- let data = tryDecompress(rawData);
294
+ // Special handling for computations that use pages subcollection structure
295
+ // Both PopularInvestorProfileMetrics and SignedInUserProfileMetrics use pages subcollection
296
+ if (compName === 'PopularInvestorProfileMetrics' || compName === 'SignedInUserProfileMetrics') {
297
+ const pageRef = db.collection(insightsCollection)
298
+ .doc(date)
299
+ .collection(resultsSub)
300
+ .doc(category)
301
+ .collection(compsSub)
302
+ .doc(compName)
303
+ .collection('pages')
304
+ .doc(String(effectiveCid));
264
305
 
265
- if (typeof data === 'string') {
266
- try {
267
- data = JSON.parse(data);
268
- } catch (e) {
269
- logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} on ${date}:`, e.message);
270
- data = null;
271
- }
272
- }
306
+ const pageDoc = await pageRef.get();
273
307
 
274
- if (data && data._sharded === true && data._shardCount) {
275
- const shardsCol = docRef.collection('_shards');
276
- const shardsSnapshot = await shardsCol.get();
308
+ if (pageDoc.exists) {
309
+ const rawData = pageDoc.data();
310
+ let data = tryDecompress(rawData);
277
311
 
278
- if (!shardsSnapshot.empty) {
279
- data = {};
280
- for (const shardDoc of shardsSnapshot.docs) {
281
- const shardData = shardDoc.data();
282
- Object.assign(data, shardData);
312
+ if (typeof data === 'string') {
313
+ try {
314
+ data = JSON.parse(data);
315
+ } catch (e) {
316
+ logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} page on ${date}:`, e.message);
317
+ data = null;
283
318
  }
284
- } else {
285
- data = null;
286
319
  }
287
- }
288
-
289
- // [FIX] Handle meta computations (global results) vs standard computations (user-specific)
290
- // Use metadata from schema collection to determine computation type
291
- let userResult = null;
292
- if (data && typeof data === 'object') {
293
- const metadata = computationMetadata[compName];
294
- const isMetaComputation = metadata && metadata.type === 'meta';
295
320
 
296
- if (isMetaComputation) {
297
- // Meta computation: return entire data object (global results)
321
+ if (data && typeof data === 'object') {
298
322
  userResult = data;
299
- } else {
300
- // Standard computation: extract user-specific result
301
- const userCidKey = String(effectiveCid);
302
- userResult = data.hasOwnProperty(userCidKey) ? data[userCidKey] : null;
303
323
  }
304
324
  }
325
+ } else {
326
+ // Standard path for other computations
327
+ const docRef = db.collection(insightsCollection)
328
+ .doc(date)
329
+ .collection(resultsSub)
330
+ .doc(category)
331
+ .collection(compsSub)
332
+ .doc(compName);
305
333
 
306
- if (isDevOverrideActive && (compName === 'SignedInUserProfileMetrics' || compName === 'SignedInUserCopiedPIs')) {
307
- if (compName === 'SignedInUserCopiedPIs') {
308
- userResult = {
309
- current: devOverride.fakeCopiedPIs,
310
- past: [],
311
- all: devOverride.fakeCopiedPIs
312
- };
313
- logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserCopiedPIs for user ${userCid}`);
314
- } else if (compName === 'SignedInUserProfileMetrics' && userResult && userResult.copiedPIs) {
315
- const fakeMirrors = devOverride.fakeCopiedPIs.map(cid => ({
316
- cid: Number(cid),
317
- username: `PI-${cid}`,
318
- invested: 0,
319
- netProfit: 0,
320
- value: 0,
321
- pendingClosure: false,
322
- isRanked: false
323
- }));
334
+ const doc = await docRef.get();
335
+
336
+ if (doc.exists) {
337
+ const rawData = doc.data();
338
+ let data = tryDecompress(rawData);
339
+
340
+ if (typeof data === 'string') {
341
+ try {
342
+ data = JSON.parse(data);
343
+ } catch (e) {
344
+ logger.log('WARN', `[getUserComputations] Failed to parse decompressed string for ${compName} on ${date}:`, e.message);
345
+ data = null;
346
+ }
347
+ }
348
+
349
+ if (data && data._sharded === true && data._shardCount) {
350
+ const shardsCol = docRef.collection('_shards');
351
+ const shardsSnapshot = await shardsCol.get();
324
352
 
325
- userResult = {
326
- ...userResult,
327
- copiedPIs: {
328
- chartType: 'cards',
329
- data: fakeMirrors
353
+ if (!shardsSnapshot.empty) {
354
+ data = {};
355
+ for (const shardDoc of shardsSnapshot.docs) {
356
+ const shardData = shardDoc.data();
357
+ Object.assign(data, shardData);
330
358
  }
331
- };
332
- logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserProfileMetrics.copiedPIs for user ${userCid}`);
359
+ } else {
360
+ data = null;
361
+ }
362
+ }
363
+
364
+ // [FIX] Handle meta computations (global results) vs standard computations (user-specific)
365
+ // Use metadata from schema collection to determine computation type
366
+ if (data && typeof data === 'object') {
367
+ const metadata = computationMetadata[compName];
368
+ const isMetaComputation = metadata && metadata.type === 'meta';
369
+
370
+ if (isMetaComputation) {
371
+ // Meta computation: return entire data object (global results)
372
+ userResult = data;
373
+ } else {
374
+ // Standard computation: extract user-specific result
375
+ const userCidKey = String(effectiveCid);
376
+ userResult = data.hasOwnProperty(userCidKey) ? data[userCidKey] : null;
377
+ }
333
378
  }
334
379
  }
335
-
336
- if (userResult) {
337
- results[date][compName] = userResult;
380
+ }
381
+
382
+ if (isDevOverrideActive && (compName === 'SignedInUserProfileMetrics' || compName === 'SignedInUserCopiedPIs')) {
383
+ if (compName === 'SignedInUserCopiedPIs') {
384
+ userResult = {
385
+ current: devOverride.fakeCopiedPIs,
386
+ past: [],
387
+ all: devOverride.fakeCopiedPIs
388
+ };
389
+ logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserCopiedPIs for user ${userCid}`);
390
+ } else if (compName === 'SignedInUserProfileMetrics' && userResult && userResult.copiedPIs) {
391
+ const fakeMirrors = devOverride.fakeCopiedPIs.map(cid => ({
392
+ cid: Number(cid),
393
+ username: `PI-${cid}`,
394
+ invested: 0,
395
+ netProfit: 0,
396
+ value: 0,
397
+ pendingClosure: false,
398
+ isRanked: false
399
+ }));
400
+
401
+ userResult = {
402
+ ...userResult,
403
+ copiedPIs: {
404
+ chartType: 'cards',
405
+ data: fakeMirrors
406
+ }
407
+ };
408
+ logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserProfileMetrics.copiedPIs for user ${userCid}`);
338
409
  }
339
410
  }
411
+
412
+ if (userResult) {
413
+ results[date][compName] = userResult;
414
+ }
340
415
  } catch (err) {
341
416
  logger.log('WARN', `[getUserComputations] Error fetching ${compName} for ${date}`, err);
342
417
  }
@@ -163,45 +163,33 @@ async function getUserDataStatus(req, res, dependencies, config) {
163
163
  const dateStr = checkDate.toISOString().split('T')[0];
164
164
 
165
165
  try {
166
- // Check if computation document exists for this date
167
- const computationRef = db.collection(insightsCollection)
168
- .doc(dateStr)
169
- .collection(resultsSub)
170
- .doc(category)
171
- .collection(compsSub)
172
- .doc(computationName);
166
+ // Check if page document exists for this user and date (new pages subcollection structure)
167
+ const { found } = await checkPiInComputationDate(
168
+ db,
169
+ insightsCollection,
170
+ resultsSub,
171
+ compsSub,
172
+ category,
173
+ computationName,
174
+ dateStr,
175
+ String(effectiveCid),
176
+ logger
177
+ );
173
178
 
174
- const computationDoc = await computationRef.get();
175
-
176
- if (computationDoc.exists) {
177
- // Computation exists for this date, check if user is in it
178
- const { found } = await checkPiInComputationDate(
179
- db,
180
- insightsCollection,
181
- resultsSub,
182
- compsSub,
183
- category,
184
- computationName,
185
- dateStr,
186
- String(effectiveCid),
187
- logger
188
- );
179
+ if (found) {
180
+ foundDate = dateStr;
181
+ computationAvailable = true;
182
+ computationDate = dateStr;
183
+ isComputationFallback = daysBack > 0;
189
184
 
190
- if (found) {
191
- foundDate = dateStr;
192
- computationAvailable = true;
193
- computationDate = dateStr;
194
- isComputationFallback = daysBack > 0;
195
-
196
- if (isComputationFallback) {
197
- logger.log('INFO', `[getUserDataStatus] Found computation for user ${userCid} on date ${computationDate} (${daysBack} days back from today: ${today})`);
198
- } else {
199
- logger.log('INFO', `[getUserDataStatus] Found computation for user ${userCid} on today's date`);
200
- }
201
- break; // Found user, stop searching
185
+ if (isComputationFallback) {
186
+ logger.log('INFO', `[getUserDataStatus] Found computation for user ${userCid} on date ${computationDate} (${daysBack} days back from today: ${today})`);
202
187
  } else {
203
- logger.log('DEBUG', `[getUserDataStatus] Computation exists for date ${dateStr} but user ${userCid} not found in it, continuing search...`);
188
+ logger.log('INFO', `[getUserDataStatus] Found computation for user ${userCid} on today's date`);
204
189
  }
190
+ break; // Found user, stop searching
191
+ } else {
192
+ logger.log('DEBUG', `[getUserDataStatus] Page document not found for user ${userCid} on date ${dateStr}, continuing search...`);
205
193
  }
206
194
  } catch (error) {
207
195
  // Continue to next date if error
@@ -138,33 +138,14 @@ async function getPiProfile(req, res, dependencies, config) {
138
138
  if (!foundDate || !profileData) {
139
139
  logger.log('WARN', `[getPiProfile] CID ${cid} not found in any checked dates: ${checkedDates.join(', ')}`);
140
140
 
141
- // Try to get sample data from the latest date to show what CIDs are available
142
- const latestResult = await checkPiInComputationDate(
143
- db,
144
- insightsCollection,
145
- resultsSub,
146
- compsSub,
147
- category,
148
- computationName,
149
- latestDate,
150
- cidStr,
151
- logger
152
- );
153
-
154
- const allAvailableCids = latestResult.computationData && typeof latestResult.computationData === 'object' && !Array.isArray(latestResult.computationData)
155
- ? Object.keys(latestResult.computationData)
156
- .filter(key => !key.startsWith('_'))
157
- .sort()
158
- : [];
159
-
141
+ // Note: With the new pages subcollection structure, we can't enumerate all available CIDs
142
+ // from a single document, so we skip that debug information
160
143
  return res.status(404).json({
161
144
  error: "Profile data not found",
162
145
  message: `Popular Investor ${cid} does not exist in computation results for the last ${maxDaysBackForPi + 1} days. This PI may not have been processed recently.`,
163
146
  debug: {
164
147
  searchedCid: cidStr,
165
148
  checkedDates: checkedDates,
166
- totalCidsInLatestDocument: allAvailableCids.length,
167
- sampleAvailableCids: allAvailableCids.slice(0, 20),
168
149
  latestDate: latestDate
169
150
  }
170
151
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.574",
3
+ "version": "1.0.576",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [