bulltrackers-module 1.0.727 → 1.0.729

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.
@@ -18,7 +18,7 @@ const errorHandler = require('./middleware/error_handler.js');
18
18
  const allowedOrigins = [
19
19
  'https://bulltrackers.web.app',
20
20
  'http://172.26.96.1:8000',
21
- 'http://127.0.0.1:8000/'
21
+ 'http://127.0.0.1:8000'
22
22
  ];
23
23
 
24
24
  /**
@@ -142,7 +142,7 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
142
142
  return JSON.parse(fileContent.toString('utf8'));
143
143
  }
144
144
  } catch (e) {
145
- log.error(`[DependencyFetcher] ❌ GCS Fetch Failed for ${name}: ${e.message}`);
145
+ log.log('ERROR', `[DependencyFetcher] ❌ GCS Fetch Failed for ${name}: ${e.message}`);
146
146
  // Depending on strictness, we might return null here or allow it to fail hard.
147
147
  // Returning null allows 'isDataEmpty' to catch it as "MISSING"
148
148
  return null;
@@ -158,7 +158,7 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
158
158
  data = { ...data, ...realData }; // Merge payload into base
159
159
  delete data.payload;
160
160
  } catch (e) {
161
- log.error(`[DependencyFetcher] ❌ ${e.message} at ${docRef.path}`);
161
+ log.log('ERROR', `[DependencyFetcher] ❌ ${e.message} at ${docRef.path}`);
162
162
  return null;
163
163
  }
164
164
  }
@@ -169,7 +169,7 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
169
169
  if (data._sharded) {
170
170
  const shardSnaps = await docRef.collection('_shards').get();
171
171
  if (shardSnaps.empty) {
172
- log.error(`[DependencyFetcher] ❌ Sharded doc has no shards: ${docRef.path}`);
172
+ log.log('ERROR', `[DependencyFetcher] ❌ Sharded doc has no shards: ${docRef.path}`);
173
173
  return null;
174
174
  }
175
175
 
@@ -186,7 +186,7 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
186
186
  const decomp = tryDecompress(sData.payload);
187
187
  sData = (typeof decomp === 'string') ? JSON.parse(decomp) : decomp;
188
188
  } catch (e) {
189
- log.error(`[DependencyFetcher] ❌ Shard decompression failed (${shard.id}): ${e.message}`);
189
+ log.log('ERROR', `[DependencyFetcher] ❌ Shard decompression failed (${shard.id}): ${e.message}`);
190
190
  continue;
191
191
  }
192
192
  }
@@ -321,4 +321,251 @@ async function loadRetailFirestore(db, collectionName, dateStr) {
321
321
  } catch (e) {
322
322
  return {};
323
323
  }
324
- }
324
+ }
325
+
326
+ // =============================================================================
327
+ // 8. STREAMING DATA (For StandardExecutor)
328
+ // =============================================================================
329
+
330
+ /**
331
+ * Get portfolio part references for streaming.
332
+ * In BigQuery mode, returns virtual refs that signal to load from BQ.
333
+ * @param {object} config - Configuration object
334
+ * @param {object} deps - Dependencies (db, logger)
335
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
336
+ * @param {Array<string>|null} userTypes - User types to filter (null = all)
337
+ * @returns {Promise<Array>} Array of part references
338
+ */
339
+ exports.getPortfolioPartRefs = async (config, deps, dateStr, userTypes = null) => {
340
+ const { logger } = deps;
341
+
342
+ // In BigQuery mode, return virtual refs with metadata
343
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
344
+ logger.log('INFO', `[DataLoader] Using BigQuery virtual refs for portfolios (${dateStr})`);
345
+ return [{
346
+ _bigquery: true,
347
+ type: 'portfolio',
348
+ date: dateStr,
349
+ userTypes: userTypes
350
+ }];
351
+ }
352
+
353
+ // Firestore fallback - return actual document references
354
+ const { db } = deps;
355
+ const refs = [];
356
+
357
+ // Check both PI and SignedIn collections
358
+ const collections = [
359
+ { name: 'PopularInvestorPortfolios', userType: 'POPULAR_INVESTOR' },
360
+ { name: 'SignedInUserPortfolios', userType: 'SIGNED_IN_USER' }
361
+ ];
362
+
363
+ for (const col of collections) {
364
+ if (userTypes && !userTypes.includes(col.userType)) continue;
365
+
366
+ try {
367
+ const partsSnap = await db.collection(col.name)
368
+ .doc('latest')
369
+ .collection('snapshots')
370
+ .doc(dateStr)
371
+ .collection('parts')
372
+ .listDocuments();
373
+
374
+ for (const docRef of partsSnap) {
375
+ refs.push({ ref: docRef, userType: col.userType, collection: col.name });
376
+ }
377
+ } catch (e) {
378
+ logger.log('WARN', `[DataLoader] Failed to get portfolio refs from ${col.name}: ${e.message}`);
379
+ }
380
+ }
381
+
382
+ return refs;
383
+ };
384
+
385
+ /**
386
+ * Get history part references for streaming.
387
+ * @param {object} config - Configuration object
388
+ * @param {object} deps - Dependencies (db, logger)
389
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
390
+ * @param {Array<string>|null} userTypes - User types to filter (null = all)
391
+ * @returns {Promise<Array>} Array of part references
392
+ */
393
+ exports.getHistoryPartRefs = async (config, deps, dateStr, userTypes = null) => {
394
+ const { logger } = deps;
395
+
396
+ // In BigQuery mode, return virtual refs with metadata
397
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
398
+ logger.log('INFO', `[DataLoader] Using BigQuery virtual refs for history (${dateStr})`);
399
+ return [{
400
+ _bigquery: true,
401
+ type: 'history',
402
+ date: dateStr,
403
+ userTypes: userTypes
404
+ }];
405
+ }
406
+
407
+ // Firestore fallback - return actual document references
408
+ const { db } = deps;
409
+ const refs = [];
410
+
411
+ const collections = [
412
+ { name: 'PopularInvestorTradeHistory', userType: 'POPULAR_INVESTOR' },
413
+ { name: 'SignedInUserTradeHistory', userType: 'SIGNED_IN_USER' }
414
+ ];
415
+
416
+ for (const col of collections) {
417
+ if (userTypes && !userTypes.includes(col.userType)) continue;
418
+
419
+ try {
420
+ const partsSnap = await db.collection(col.name)
421
+ .doc('latest')
422
+ .collection('snapshots')
423
+ .doc(dateStr)
424
+ .collection('parts')
425
+ .listDocuments();
426
+
427
+ for (const docRef of partsSnap) {
428
+ refs.push({ ref: docRef, userType: col.userType, collection: col.name });
429
+ }
430
+ } catch (e) {
431
+ logger.log('WARN', `[DataLoader] Failed to get history refs from ${col.name}: ${e.message}`);
432
+ }
433
+ }
434
+
435
+ return refs;
436
+ };
437
+
438
+ /**
439
+ * Stream portfolio data in chunks (async generator).
440
+ * Yields objects of { cid: portfolioData } for memory efficiency.
441
+ * @param {object} config - Configuration object
442
+ * @param {object} deps - Dependencies (db, logger)
443
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
444
+ * @param {Array} refs - Part references from getPortfolioPartRefs
445
+ * @param {Array<string>|null} userTypes - User types to filter
446
+ * @yields {object} Chunk of portfolio data { cid: data }
447
+ */
448
+ exports.streamPortfolioData = async function* (config, deps, dateStr, refs, userTypes = null) {
449
+ const { logger } = deps;
450
+
451
+ if (!refs || refs.length === 0) {
452
+ logger.log('WARN', `[DataLoader] No portfolio refs provided for streaming (${dateStr})`);
453
+ return;
454
+ }
455
+
456
+ // BigQuery mode - load all at once and yield in chunks
457
+ const isBigQuery = refs.some(r => r && r._bigquery === true);
458
+
459
+ if (isBigQuery && process.env.BIGQUERY_ENABLED !== 'false') {
460
+ try {
461
+ const types = userTypes || ['POPULAR_INVESTOR', 'SIGNED_IN_USER'];
462
+ const data = await queryPortfolioData(dateStr, null, types, logger);
463
+
464
+ if (data && Object.keys(data).length > 0) {
465
+ logger.log('INFO', `[DataLoader] ✅ Streaming ${Object.keys(data).length} portfolios from BigQuery (${dateStr})`);
466
+
467
+ // Yield in chunks of 100 users for memory efficiency
468
+ const CHUNK_SIZE = 100;
469
+ const entries = Object.entries(data);
470
+
471
+ for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
472
+ const chunk = {};
473
+ entries.slice(i, i + CHUNK_SIZE).forEach(([cid, portfolio]) => {
474
+ chunk[cid] = portfolio;
475
+ });
476
+ yield chunk;
477
+ }
478
+ return;
479
+ }
480
+ } catch (e) {
481
+ logger.log('ERROR', `[DataLoader] BigQuery portfolio stream failed: ${e.message}`);
482
+ }
483
+ }
484
+
485
+ // Firestore fallback - stream from refs
486
+ for (const refInfo of refs) {
487
+ if (!refInfo.ref) continue;
488
+
489
+ try {
490
+ const snap = await refInfo.ref.get();
491
+ if (snap.exists) {
492
+ const data = snap.data();
493
+ // Add user type metadata
494
+ const enriched = {};
495
+ for (const [cid, portfolio] of Object.entries(data)) {
496
+ enriched[cid] = { ...portfolio, _userType: refInfo.userType };
497
+ }
498
+ yield enriched;
499
+ }
500
+ } catch (e) {
501
+ logger.log('WARN', `[DataLoader] Failed to stream portfolio part: ${e.message}`);
502
+ }
503
+ }
504
+ };
505
+
506
+ /**
507
+ * Stream history data in chunks (async generator).
508
+ * @param {object} config - Configuration object
509
+ * @param {object} deps - Dependencies (db, logger)
510
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
511
+ * @param {Array} refs - Part references from getHistoryPartRefs
512
+ * @param {Array<string>|null} userTypes - User types to filter
513
+ * @yields {object} Chunk of history data { cid: data }
514
+ */
515
+ exports.streamHistoryData = async function* (config, deps, dateStr, refs, userTypes = null) {
516
+ const { logger } = deps;
517
+
518
+ if (!refs || refs.length === 0) {
519
+ logger.log('WARN', `[DataLoader] No history refs provided for streaming (${dateStr})`);
520
+ return;
521
+ }
522
+
523
+ // BigQuery mode - load all at once and yield in chunks
524
+ const isBigQuery = refs.some(r => r && r._bigquery === true);
525
+
526
+ if (isBigQuery && process.env.BIGQUERY_ENABLED !== 'false') {
527
+ try {
528
+ const types = userTypes || ['POPULAR_INVESTOR', 'SIGNED_IN_USER'];
529
+ const data = await queryHistoryData(dateStr, null, types, logger);
530
+
531
+ if (data && Object.keys(data).length > 0) {
532
+ logger.log('INFO', `[DataLoader] ✅ Streaming ${Object.keys(data).length} history records from BigQuery (${dateStr})`);
533
+
534
+ // Yield in chunks of 100 users
535
+ const CHUNK_SIZE = 100;
536
+ const entries = Object.entries(data);
537
+
538
+ for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
539
+ const chunk = {};
540
+ entries.slice(i, i + CHUNK_SIZE).forEach(([cid, history]) => {
541
+ chunk[cid] = history;
542
+ });
543
+ yield chunk;
544
+ }
545
+ return;
546
+ }
547
+ } catch (e) {
548
+ logger.log('ERROR', `[DataLoader] BigQuery history stream failed: ${e.message}`);
549
+ }
550
+ }
551
+
552
+ // Firestore fallback - stream from refs
553
+ for (const refInfo of refs) {
554
+ if (!refInfo.ref) continue;
555
+
556
+ try {
557
+ const snap = await refInfo.ref.get();
558
+ if (snap.exists) {
559
+ const data = snap.data();
560
+ // Add user type metadata
561
+ const enriched = {};
562
+ for (const [cid, history] of Object.entries(data)) {
563
+ enriched[cid] = { ...history, _userType: refInfo.userType };
564
+ }
565
+ yield enriched;
566
+ }
567
+ } catch (e) {
568
+ logger.log('WARN', `[DataLoader] Failed to stream history part: ${e.message}`);
569
+ }
570
+ }
571
+ };
@@ -55,11 +55,32 @@ async function withRetry(fn, operationName, maxRetries = 3) {
55
55
 
56
56
  /** * Recursively sanitizes data for Firestore compatibility.
57
57
  * Converts 'undefined' to 'null' to prevent validation errors.
58
+ * Converts invalid Dates to null to prevent Timestamp serialization errors.
59
+ * Converts NaN numbers to null to prevent serialization errors.
58
60
  */
59
61
  function sanitizeForFirestore(data) {
60
62
  if (data === undefined) return null;
61
63
  if (data === null) return null;
62
- if (data instanceof Date) return data;
64
+
65
+ // Handle Date objects - validate they're not Invalid Date (NaN timestamp)
66
+ if (data instanceof Date) {
67
+ if (isNaN(data.getTime())) {
68
+ // Invalid Date - return null instead of crashing Firestore
69
+ return null;
70
+ }
71
+ return data;
72
+ }
73
+
74
+ // Handle NaN numbers (would cause serialization issues)
75
+ if (typeof data === 'number' && isNaN(data)) {
76
+ return null;
77
+ }
78
+
79
+ // Handle Infinity (also not valid for Firestore)
80
+ if (data === Infinity || data === -Infinity) {
81
+ return null;
82
+ }
83
+
63
84
  // Handle Firestore Types if necessary (e.g. GeoPoint, Timestamp), passed through as objects usually.
64
85
 
65
86
  if (Array.isArray(data)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.727",
3
+ "version": "1.0.729",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [