bulltrackers-module 1.0.717 → 1.0.718

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.
@@ -513,6 +513,63 @@ const manageUserWatchlist = async (db, userId, instruction, payload = {}) => {
513
513
  // 6. Commit Batch
514
514
  await batch.commit();
515
515
 
516
+ // 7. Write to BigQuery (after Firestore commit to get final values)
517
+ if (process.env.BIGQUERY_ENABLED !== 'false' && (addedItems.length > 0 || removedItems.length > 0)) {
518
+ try {
519
+ const { ensureWatchlistMembershipTable, insertRowsWithMerge } = require('../../../core/utils/bigquery_utils');
520
+ await ensureWatchlistMembershipTable();
521
+
522
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
523
+ const bigqueryRows = [];
524
+
525
+ // Process all affected PIs (both added and removed)
526
+ const affectedPIs = new Set([
527
+ ...addedItems.map(item => String(item.cid)),
528
+ ...removedItems.map(item => String(item.cid))
529
+ ]);
530
+
531
+ // Read updated documents from subcollection for each affected PI
532
+ for (const piId of affectedPIs) {
533
+ const masterRef = db.collection('WatchlistMembershipData')
534
+ .doc(todayStr).collection('popular_investors').doc(piId);
535
+
536
+ const piDoc = await masterRef.get();
537
+ if (piDoc.exists) {
538
+ const piData = piDoc.data();
539
+
540
+ // Transform to BigQuery format
541
+ const lastUpdated = piData.lastUpdated
542
+ ? (piData.lastUpdated.toDate ? piData.lastUpdated.toDate().toISOString() : piData.lastUpdated)
543
+ : new Date().toISOString();
544
+
545
+ bigqueryRows.push({
546
+ date: todayStr,
547
+ pi_id: parseInt(piId, 10),
548
+ total_users: piData.totalUsers || 0,
549
+ public_watchlist_count: piData.publicWatchlistCount || 0,
550
+ private_watchlist_count: piData.privateWatchlistCount || 0,
551
+ users: Array.isArray(piData.users) ? piData.users : [],
552
+ last_updated: lastUpdated
553
+ });
554
+ }
555
+ }
556
+
557
+ if (bigqueryRows.length > 0) {
558
+ // Use MERGE to update existing rows or insert new
559
+ await insertRowsWithMerge(
560
+ datasetId,
561
+ 'watchlist_membership',
562
+ bigqueryRows,
563
+ ['date', 'pi_id'], // Key fields for MERGE
564
+ console // Simple logger
565
+ );
566
+ }
567
+ } catch (bqError) {
568
+ console.error(`[manageUserWatchlist] BigQuery write failed: ${bqError.message}`);
569
+ // Don't throw - Firestore write succeeded, BigQuery is secondary
570
+ }
571
+ }
572
+
516
573
  return {
517
574
  success: true,
518
575
  id: watchlistId,
@@ -1583,6 +1640,51 @@ const trackPopularInvestorView = async (db, piId, viewerId = null, viewerType =
1583
1640
  batch.set(globalDayRef, globalUpdate, { merge: true });
1584
1641
 
1585
1642
  await batch.commit();
1643
+
1644
+ // 5. Write to BigQuery (after Firestore commit to get final values)
1645
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
1646
+ try {
1647
+ const { ensurePIPageViewsTable, insertRowsWithMerge } = require('../../../core/utils/bigquery_utils');
1648
+ await ensurePIPageViewsTable();
1649
+
1650
+ // Read the updated document to get final values (after increments)
1651
+ const updatedDoc = await globalDayRef.get();
1652
+ if (updatedDoc.exists) {
1653
+ const data = updatedDoc.data();
1654
+ const piData = data[piId];
1655
+
1656
+ if (piData) {
1657
+ // Transform to BigQuery format
1658
+ const lastUpdated = piData.lastUpdated
1659
+ ? (piData.lastUpdated.toDate ? piData.lastUpdated.toDate().toISOString() : piData.lastUpdated)
1660
+ : new Date().toISOString();
1661
+
1662
+ const bigqueryRow = {
1663
+ date: todayStr,
1664
+ pi_id: parseInt(piId, 10),
1665
+ total_views: piData.totalViews || 0,
1666
+ unique_viewers: piData.uniqueViewers || 0,
1667
+ views_by_user: piData.viewsByUser || {},
1668
+ last_updated: lastUpdated
1669
+ };
1670
+
1671
+ // Use MERGE to update existing row or insert new
1672
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
1673
+ await insertRowsWithMerge(
1674
+ datasetId,
1675
+ 'pi_page_views',
1676
+ [bigqueryRow],
1677
+ ['date', 'pi_id'], // Key fields for MERGE
1678
+ console // Simple logger
1679
+ );
1680
+ }
1681
+ }
1682
+ } catch (bqError) {
1683
+ console.error(`[trackPopularInvestorView] BigQuery write failed: ${bqError.message}`);
1684
+ // Don't throw - Firestore write succeeded, BigQuery is secondary
1685
+ }
1686
+ }
1687
+
1586
1688
  return { success: true };
1587
1689
  };
1588
1690
  // ==========================================
@@ -3494,6 +3596,152 @@ const subscribeToAllWatchlistPIs = async (db, userId, watchlistId, alertTypes =
3494
3596
  }
3495
3597
  };
3496
3598
 
3599
+ /**
3600
+ * Helper function for BigQuery caching pattern:
3601
+ * 1. Check Firestore cache (with TTL)
3602
+ * 2. If missing/expired, fetch from BigQuery
3603
+ * 3. Cache result in Firestore with 10-minute TTL
3604
+ * @param {Object} firestore - Firestore instance
3605
+ * @param {string} cacheCollection - Firestore collection for cache (e.g., 'PIRatingsData')
3606
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
3607
+ * @param {Function} bigqueryFetcher - Async function that fetches from BigQuery
3608
+ * @param {string} dataType - Data type name for logging
3609
+ * @returns {Promise<Object|null>} Cached or fetched data
3610
+ */
3611
+ async function fetchWithBigQueryCache(firestore, cacheCollection, dateStr, bigqueryFetcher, dataType = 'data') {
3612
+ const CACHE_TTL_MINUTES = 10;
3613
+ const CACHE_TTL_MS = CACHE_TTL_MINUTES * 60 * 1000;
3614
+
3615
+ try {
3616
+ // 1. Check Firestore cache
3617
+ const cacheDocRef = firestore.collection(cacheCollection).doc(dateStr);
3618
+ const cacheDoc = await cacheDocRef.get();
3619
+
3620
+ if (cacheDoc.exists) {
3621
+ const cacheData = cacheDoc.data();
3622
+ const cachedAt = cacheData.cachedAt;
3623
+
3624
+ // Check if cache is still valid
3625
+ if (cachedAt) {
3626
+ const cachedTimestamp = cachedAt.toMillis ? cachedAt.toMillis() : (cachedAt._seconds * 1000);
3627
+ const now = Date.now();
3628
+ const age = now - cachedTimestamp;
3629
+
3630
+ if (age < CACHE_TTL_MS) {
3631
+ // Cache is valid, return cached data
3632
+ const { cachedAt, ...data } = cacheData;
3633
+ return data;
3634
+ }
3635
+ }
3636
+ }
3637
+
3638
+ // 2. Cache missing or expired, fetch from BigQuery
3639
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
3640
+ try {
3641
+ const bigqueryData = await bigqueryFetcher();
3642
+
3643
+ if (bigqueryData) {
3644
+ // 3. Cache the result in Firestore with TTL
3645
+ await cacheDocRef.set({
3646
+ ...bigqueryData,
3647
+ cachedAt: FieldValue.serverTimestamp()
3648
+ }, { merge: true });
3649
+
3650
+ return bigqueryData;
3651
+ }
3652
+ } catch (error) {
3653
+ console.error(`[API] BigQuery fetch failed for ${dataType} (${dateStr}): ${error.message}`);
3654
+ // If BigQuery fails, return cached data if available (even if expired)
3655
+ if (cacheDoc.exists) {
3656
+ const cacheData = cacheDoc.data();
3657
+ const { cachedAt, ...data } = cacheData;
3658
+ return data;
3659
+ }
3660
+ }
3661
+ }
3662
+
3663
+ // 4. Fallback: return cached data even if expired, or null
3664
+ if (cacheDoc.exists) {
3665
+ const cacheData = cacheDoc.data();
3666
+ const { cachedAt, ...data } = cacheData;
3667
+ return data;
3668
+ }
3669
+
3670
+ return null;
3671
+ } catch (error) {
3672
+ console.error(`[API] Error in fetchWithBigQueryCache for ${dataType} (${dateStr}): ${error.message}`);
3673
+ return null;
3674
+ }
3675
+ }
3676
+
3677
+ /**
3678
+ * Fetch PI Ratings data with BigQuery caching
3679
+ * @param {Object} firestore - Firestore instance
3680
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
3681
+ * @returns {Promise<Object|null>} Ratings data
3682
+ */
3683
+ async function fetchPIRatings(firestore, dateStr) {
3684
+ const { queryPIRatings } = require('../../../core/utils/bigquery_utils');
3685
+ return await fetchWithBigQueryCache(
3686
+ firestore,
3687
+ 'PIRatingsData',
3688
+ dateStr,
3689
+ () => queryPIRatings(dateStr),
3690
+ 'PI Ratings'
3691
+ );
3692
+ }
3693
+
3694
+ /**
3695
+ * Fetch PI Page Views data with BigQuery caching
3696
+ * @param {Object} firestore - Firestore instance
3697
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
3698
+ * @returns {Promise<Object|null>} Page views data
3699
+ */
3700
+ async function fetchPIPageViews(firestore, dateStr) {
3701
+ const { queryPIPageViews } = require('../../../core/utils/bigquery_utils');
3702
+ return await fetchWithBigQueryCache(
3703
+ firestore,
3704
+ 'PIPageViewsData',
3705
+ dateStr,
3706
+ () => queryPIPageViews(dateStr),
3707
+ 'PI Page Views'
3708
+ );
3709
+ }
3710
+
3711
+ /**
3712
+ * Fetch Watchlist Membership data with BigQuery caching
3713
+ * @param {Object} firestore - Firestore instance
3714
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
3715
+ * @returns {Promise<Object|null>} Watchlist membership data
3716
+ */
3717
+ async function fetchWatchlistMembership(firestore, dateStr) {
3718
+ const { queryWatchlistMembership } = require('../../../core/utils/bigquery_utils');
3719
+ return await fetchWithBigQueryCache(
3720
+ firestore,
3721
+ 'WatchlistMembershipData',
3722
+ dateStr,
3723
+ () => queryWatchlistMembership(dateStr),
3724
+ 'Watchlist Membership'
3725
+ );
3726
+ }
3727
+
3728
+ /**
3729
+ * Fetch PI Alert History data with BigQuery caching
3730
+ * @param {Object} firestore - Firestore instance
3731
+ * @param {string} dateStr - Date string (YYYY-MM-DD)
3732
+ * @returns {Promise<Object|null>} Alert history data
3733
+ */
3734
+ async function fetchPIAlertHistory(firestore, dateStr) {
3735
+ const { queryPIAlertHistory } = require('../../../core/utils/bigquery_utils');
3736
+ return await fetchWithBigQueryCache(
3737
+ firestore,
3738
+ 'PIAlertHistoryData',
3739
+ dateStr,
3740
+ () => queryPIAlertHistory(dateStr),
3741
+ 'PI Alert History'
3742
+ );
3743
+ }
3744
+
3497
3745
  module.exports = {
3498
3746
  latestUserCentricSnapshot,
3499
3747
  pageCollection,
@@ -3542,5 +3790,9 @@ module.exports = {
3542
3790
  unsubscribeFromAlerts,
3543
3791
  getWatchlistTriggerCounts,
3544
3792
  subscribeToAllWatchlistPIs,
3545
- queryDynamicWatchlistMatches
3793
+ queryDynamicWatchlistMatches,
3794
+ fetchPIRatings,
3795
+ fetchPIPageViews,
3796
+ fetchWatchlistMembership,
3797
+ fetchPIAlertHistory
3546
3798
  };
@@ -376,8 +376,58 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
376
376
  const cached = await tryLoadFromGCS(config, dateString, 'social', logger);
377
377
  if (cached) return cached;
378
378
 
379
- // 2. FIRESTORE FALLBACK
380
- logger.log('INFO', `Loading and partitioning social data for ${dateString}`);
379
+ // 2. BIGQUERY FIRST (if enabled)
380
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
381
+ try {
382
+ const { querySocialData } = require('../../core/utils/bigquery_utils');
383
+ const bigqueryData = await querySocialData(dateString, null, null, logger);
384
+
385
+ if (bigqueryData && Object.keys(bigqueryData).length > 0) {
386
+ logger.log('INFO', `[DataLoader] ✅ Using BigQuery for social data (${dateString}): ${Object.keys(bigqueryData).length} users`);
387
+
388
+ // Transform BigQuery data to expected format: { generic: {}, pi: {}, signedIn: {} }
389
+ // BigQuery returns: { userId: { posts_data: { posts: {...}, postCount: N }, user_type: '...' } }
390
+ const result = { generic: {}, pi: {}, signedIn: {} };
391
+
392
+ for (const [userId, userData] of Object.entries(bigqueryData)) {
393
+ const userType = userData.user_type || 'UNKNOWN';
394
+
395
+ // Handle posts_data - may be object (parsed JSON) or string (needs parsing)
396
+ let postsData = userData.posts_data || {};
397
+ if (typeof postsData === 'string') {
398
+ try {
399
+ postsData = JSON.parse(postsData);
400
+ } catch (e) {
401
+ logger.log('WARN', `[DataLoader] Failed to parse posts_data for user ${userId}: ${e.message}`);
402
+ continue;
403
+ }
404
+ }
405
+
406
+ // Extract posts map from posts_data structure: { posts: {...}, postCount: N }
407
+ const posts = postsData.posts || {};
408
+
409
+ // Partition by user type
410
+ if (userType === 'POPULAR_INVESTOR') {
411
+ result.pi[userId] = posts;
412
+ } else if (userType === 'SIGNED_IN_USER') {
413
+ result.signedIn[userId] = posts;
414
+ } else {
415
+ // Generic/unknown user types go to generic
416
+ result.generic[userId] = posts;
417
+ }
418
+ }
419
+
420
+ logger.log('INFO', `[DataLoader] ✅ Loaded Social Data from BigQuery: ${Object.keys(result.generic).length} Generic, ${Object.keys(result.pi).length} PIs, ${Object.keys(result.signedIn).length} Signed-In`);
421
+ return result;
422
+ }
423
+ } catch (bqError) {
424
+ logger.log('WARN', `[DataLoader] BigQuery social query failed for ${dateString}, falling back to Firestore: ${bqError.message}`);
425
+ // Fall through to Firestore
426
+ }
427
+ }
428
+
429
+ // 3. FIRESTORE FALLBACK
430
+ logger.log('INFO', `Loading and partitioning social data for ${dateString} (Firestore)`);
381
431
 
382
432
  const result = { generic: {}, pi: {}, signedIn: {} };
383
433
 
@@ -874,31 +924,33 @@ async function loadPIRatings(config, deps, dateString) {
874
924
  const cached = await tryLoadFromGCS(config, dateString, 'ratings', logger);
875
925
  if (cached) return cached;
876
926
 
877
- // 2. FIRESTORE FALLBACK
878
- logger.log('INFO', `Loading PI Ratings (Raw Logs) for ${dateString}`);
879
- try {
880
- const shardsColRef = db.collection('PiReviews').doc(dateString).collection('shards');
881
- const shardDocs = await withRetry(() => shardsColRef.listDocuments(), `listRatingShards(${dateString})`);
882
-
883
- if (!shardDocs || shardDocs.length === 0) {
884
- logger.log('WARN', `No rating shards found for ${dateString}`);
885
- return {};
927
+ // 2. BIGQUERY FIRST (if enabled)
928
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
929
+ try {
930
+ const { queryPIRatings } = require('../../core/utils/bigquery_utils');
931
+ const bigqueryData = await queryPIRatings(dateString, logger);
932
+ if (bigqueryData) {
933
+ logger.log('INFO', `[DataLoader] Loaded PI Ratings from BigQuery for ${dateString}`);
934
+ return bigqueryData;
935
+ }
936
+ } catch (error) {
937
+ logger.log('WARN', `[DataLoader] BigQuery PI Ratings query failed, falling back to Firestore: ${error.message}`);
886
938
  }
939
+ }
887
940
 
888
- const rawReviewsByPi = {};
889
- for (const docRef of shardDocs) {
890
- const docSnap = await docRef.get();
891
- if (!docSnap.exists) continue;
892
- const rawData = tryDecompress(docSnap.data());
893
- Object.values(rawData).forEach(entry => {
894
- if (entry && typeof entry === 'object' && entry.piCid && entry.rating !== undefined) {
895
- if (!rawReviewsByPi[entry.piCid]) rawReviewsByPi[entry.piCid] = [];
896
- rawReviewsByPi[entry.piCid].push(entry);
897
- }
898
- });
941
+ // 3. FIRESTORE FALLBACK
942
+ const collectionName = config.piRatingsCollection || 'PIRatingsData';
943
+ logger.log('INFO', `Loading PI Ratings from Firestore for ${dateString}`);
944
+ try {
945
+ const docRef = db.collection(collectionName).doc(dateString);
946
+ const docSnap = await withRetry(() => docRef.get(), `getPIRatings(${dateString})`);
947
+ if (!docSnap.exists) {
948
+ logger.log('WARN', `PI Ratings not found for ${dateString}`);
949
+ return {};
899
950
  }
900
- logger.log('INFO', `Loaded raw reviews for ${Object.keys(rawReviewsByPi).length} PIs.`);
901
- return rawReviewsByPi;
951
+ const data = tryDecompress(docSnap.data());
952
+ const { date, lastUpdated, ...piRatings } = data;
953
+ return piRatings;
902
954
  } catch (error) {
903
955
  logger.log('ERROR', `Failed to load PI Ratings: ${error.message}`);
904
956
  return {};
@@ -914,9 +966,23 @@ async function loadPIPageViews(config, deps, dateString) {
914
966
  const cached = await tryLoadFromGCS(config, dateString, 'page_views', logger);
915
967
  if (cached) return cached;
916
968
 
917
- // 2. FIRESTORE FALLBACK
969
+ // 2. BIGQUERY FIRST (if enabled)
970
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
971
+ try {
972
+ const { queryPIPageViews } = require('../../core/utils/bigquery_utils');
973
+ const bigqueryData = await queryPIPageViews(dateString, logger);
974
+ if (bigqueryData) {
975
+ logger.log('INFO', `[DataLoader] ✅ Loaded PI Page Views from BigQuery for ${dateString}`);
976
+ return bigqueryData;
977
+ }
978
+ } catch (error) {
979
+ logger.log('WARN', `[DataLoader] BigQuery PI Page Views query failed, falling back to Firestore: ${error.message}`);
980
+ }
981
+ }
982
+
983
+ // 3. FIRESTORE FALLBACK
918
984
  const collectionName = config.piPageViewsCollection || 'PIPageViewsData';
919
- logger.log('INFO', `Loading PI Page Views for ${dateString}`);
985
+ logger.log('INFO', `Loading PI Page Views from Firestore for ${dateString}`);
920
986
  try {
921
987
  const docRef = db.collection(collectionName).doc(dateString);
922
988
  const docSnap = await withRetry(() => docRef.get(), `getPIPageViews(${dateString})`);
@@ -939,9 +1005,23 @@ async function loadWatchlistMembership(config, deps, dateString) {
939
1005
  const cached = await tryLoadFromGCS(config, dateString, 'watchlist', logger);
940
1006
  if (cached) return cached;
941
1007
 
942
- // 2. FIRESTORE FALLBACK
1008
+ // 2. BIGQUERY FIRST (if enabled)
1009
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
1010
+ try {
1011
+ const { queryWatchlistMembership } = require('../../core/utils/bigquery_utils');
1012
+ const bigqueryData = await queryWatchlistMembership(dateString, logger);
1013
+ if (bigqueryData) {
1014
+ logger.log('INFO', `[DataLoader] ✅ Loaded Watchlist Membership from BigQuery for ${dateString}`);
1015
+ return bigqueryData;
1016
+ }
1017
+ } catch (error) {
1018
+ logger.log('WARN', `[DataLoader] BigQuery Watchlist Membership query failed, falling back to Firestore: ${error.message}`);
1019
+ }
1020
+ }
1021
+
1022
+ // 3. FIRESTORE FALLBACK
943
1023
  const collectionName = config.watchlistMembershipCollection || 'WatchlistMembershipData';
944
- logger.log('INFO', `Loading Watchlist Membership for ${dateString}`);
1024
+ logger.log('INFO', `Loading Watchlist Membership from Firestore for ${dateString}`);
945
1025
  try {
946
1026
  const docRef = db.collection(collectionName).doc(dateString);
947
1027
  const docSnap = await withRetry(() => docRef.get(), `getWatchlistMembership(${dateString})`);
@@ -964,9 +1044,23 @@ async function loadPIAlertHistory(config, deps, dateString) {
964
1044
  const cached = await tryLoadFromGCS(config, dateString, 'alerts', logger);
965
1045
  if (cached) return cached;
966
1046
 
967
- // 2. FIRESTORE FALLBACK
1047
+ // 2. BIGQUERY FIRST (if enabled)
1048
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
1049
+ try {
1050
+ const { queryPIAlertHistory } = require('../../core/utils/bigquery_utils');
1051
+ const bigqueryData = await queryPIAlertHistory(dateString, logger);
1052
+ if (bigqueryData) {
1053
+ logger.log('INFO', `[DataLoader] ✅ Loaded PI Alert History from BigQuery for ${dateString}`);
1054
+ return bigqueryData;
1055
+ }
1056
+ } catch (error) {
1057
+ logger.log('WARN', `[DataLoader] BigQuery PI Alert History query failed, falling back to Firestore: ${error.message}`);
1058
+ }
1059
+ }
1060
+
1061
+ // 3. FIRESTORE FALLBACK
968
1062
  const collectionName = config.piAlertHistoryCollection || 'PIAlertHistoryData';
969
- logger.log('INFO', `Loading PI Alert History for ${dateString}`);
1063
+ logger.log('INFO', `Loading PI Alert History from Firestore for ${dateString}`);
970
1064
  try {
971
1065
  const docRef = db.collection(collectionName).doc(dateString);
972
1066
  const docSnap = await withRetry(() => docRef.get(), `getPIAlertHistory(${dateString})`);
@@ -630,6 +630,42 @@ const SCHEMAS = {
630
630
  { name: 'instrument_id', type: 'INT64', mode: 'REQUIRED' },
631
631
  { name: 'ticker', type: 'STRING', mode: 'REQUIRED' },
632
632
  { name: 'last_updated', type: 'TIMESTAMP', mode: 'REQUIRED' }
633
+ ],
634
+ pi_ratings: [
635
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
636
+ { name: 'pi_id', type: 'INT64', mode: 'REQUIRED' },
637
+ { name: 'average_rating', type: 'FLOAT64', mode: 'NULLABLE' },
638
+ { name: 'total_ratings', type: 'INT64', mode: 'NULLABLE' },
639
+ { name: 'ratings_by_user', type: 'JSON', mode: 'NULLABLE' },
640
+ { name: 'last_updated', type: 'TIMESTAMP', mode: 'REQUIRED' }
641
+ ],
642
+ pi_page_views: [
643
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
644
+ { name: 'pi_id', type: 'INT64', mode: 'REQUIRED' },
645
+ { name: 'total_views', type: 'INT64', mode: 'NULLABLE' },
646
+ { name: 'unique_viewers', type: 'INT64', mode: 'NULLABLE' },
647
+ { name: 'views_by_user', type: 'JSON', mode: 'NULLABLE' },
648
+ { name: 'last_updated', type: 'TIMESTAMP', mode: 'REQUIRED' }
649
+ ],
650
+ watchlist_membership: [
651
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
652
+ { name: 'pi_id', type: 'INT64', mode: 'REQUIRED' },
653
+ { name: 'total_users', type: 'INT64', mode: 'NULLABLE' },
654
+ { name: 'public_watchlist_count', type: 'INT64', mode: 'NULLABLE' },
655
+ { name: 'private_watchlist_count', type: 'INT64', mode: 'NULLABLE' },
656
+ { name: 'users', type: 'JSON', mode: 'NULLABLE' },
657
+ { name: 'last_updated', type: 'TIMESTAMP', mode: 'REQUIRED' }
658
+ ],
659
+ pi_alert_history: [
660
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
661
+ { name: 'pi_id', type: 'INT64', mode: 'REQUIRED' },
662
+ { name: 'alert_type', type: 'STRING', mode: 'REQUIRED' },
663
+ { name: 'triggered', type: 'BOOLEAN', mode: 'NULLABLE' },
664
+ { name: 'trigger_count', type: 'INT64', mode: 'NULLABLE' },
665
+ { name: 'triggered_for', type: 'JSON', mode: 'NULLABLE' },
666
+ { name: 'metadata', type: 'JSON', mode: 'NULLABLE' },
667
+ { name: 'last_triggered', type: 'TIMESTAMP', mode: 'NULLABLE' },
668
+ { name: 'last_updated', type: 'TIMESTAMP', mode: 'REQUIRED' }
633
669
  ]
634
670
  };
635
671
 
@@ -838,6 +874,94 @@ async function ensureTickerMappingsTable(logger = null) {
838
874
  );
839
875
  }
840
876
 
877
+ /**
878
+ * Ensure pi_ratings table exists
879
+ * @param {object} logger - Logger instance
880
+ * @returns {Promise<Table>}
881
+ */
882
+ async function ensurePIRatingsTable(logger = null) {
883
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
884
+ const tableId = 'pi_ratings';
885
+ const schema = getSchema(tableId);
886
+
887
+ return await ensureTableExists(
888
+ datasetId,
889
+ tableId,
890
+ schema,
891
+ {
892
+ partitionField: 'date',
893
+ clusterFields: ['pi_id']
894
+ },
895
+ logger
896
+ );
897
+ }
898
+
899
+ /**
900
+ * Ensure pi_page_views table exists
901
+ * @param {object} logger - Logger instance
902
+ * @returns {Promise<Table>}
903
+ */
904
+ async function ensurePIPageViewsTable(logger = null) {
905
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
906
+ const tableId = 'pi_page_views';
907
+ const schema = getSchema(tableId);
908
+
909
+ return await ensureTableExists(
910
+ datasetId,
911
+ tableId,
912
+ schema,
913
+ {
914
+ partitionField: 'date',
915
+ clusterFields: ['pi_id']
916
+ },
917
+ logger
918
+ );
919
+ }
920
+
921
+ /**
922
+ * Ensure watchlist_membership table exists
923
+ * @param {object} logger - Logger instance
924
+ * @returns {Promise<Table>}
925
+ */
926
+ async function ensureWatchlistMembershipTable(logger = null) {
927
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
928
+ const tableId = 'watchlist_membership';
929
+ const schema = getSchema(tableId);
930
+
931
+ return await ensureTableExists(
932
+ datasetId,
933
+ tableId,
934
+ schema,
935
+ {
936
+ partitionField: 'date',
937
+ clusterFields: ['pi_id']
938
+ },
939
+ logger
940
+ );
941
+ }
942
+
943
+ /**
944
+ * Ensure pi_alert_history table exists
945
+ * @param {object} logger - Logger instance
946
+ * @returns {Promise<Table>}
947
+ */
948
+ async function ensurePIAlertHistoryTable(logger = null) {
949
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
950
+ const tableId = 'pi_alert_history';
951
+ const schema = getSchema(tableId);
952
+
953
+ return await ensureTableExists(
954
+ datasetId,
955
+ tableId,
956
+ schema,
957
+ {
958
+ partitionField: 'date',
959
+ clusterFields: ['pi_id', 'alert_type']
960
+ },
961
+ logger
962
+ );
963
+ }
964
+
841
965
  /**
842
966
  * Query portfolio data from BigQuery
843
967
  * @param {string} dateStr - Date string (YYYY-MM-DD)
@@ -1082,16 +1206,18 @@ async function querySocialData(dateStr, userIds = null, userTypes = null, logger
1082
1206
  const tablePath = `${datasetId}.social_post_snapshots`;
1083
1207
 
1084
1208
  try {
1085
- const conditions = [`date = '${dateStr}'`];
1209
+ // Build WHERE clause with parameterized queries (SQL injection safe)
1210
+ const conditions = [`date = @dateStr`];
1211
+ const params = { dateStr: dateStr };
1086
1212
 
1087
1213
  if (userIds && userIds.length > 0) {
1088
- const userIdList = userIds.map(id => String(id)).join(',');
1089
- conditions.push(`user_id IN (${userIdList})`);
1214
+ conditions.push(`user_id IN UNNEST(@userIds)`);
1215
+ params.userIds = userIds.map(id => parseInt(id, 10));
1090
1216
  }
1091
1217
 
1092
1218
  if (userTypes && userTypes.length > 0) {
1093
- const typeList = userTypes.map(t => `'${t.toUpperCase()}'`).join(',');
1094
- conditions.push(`user_type IN (${typeList})`);
1219
+ conditions.push(`user_type IN UNNEST(@userTypes)`);
1220
+ params.userTypes = userTypes.map(t => t.toUpperCase());
1095
1221
  }
1096
1222
 
1097
1223
  const whereClause = conditions.join(' AND ');
@@ -1110,7 +1236,7 @@ async function querySocialData(dateStr, userIds = null, userTypes = null, logger
1110
1236
  logger.log('INFO', `[BigQuery] 🔍 Querying social posts from ${tablePath} for date ${dateStr}${userTypes ? ` (types: ${userTypes.join(',')})` : ''}${userIds ? ` (${userIds.length} users)` : ''}`);
1111
1237
  }
1112
1238
 
1113
- const rows = await query(sqlQuery, {}, logger);
1239
+ const rows = await query(sqlQuery, { params }, logger);
1114
1240
 
1115
1241
  if (!rows || rows.length === 0) {
1116
1242
  if (logger) logger.log('INFO', `[BigQuery] No social data found in ${tablePath} for ${dateStr}`);
@@ -1711,6 +1837,285 @@ async function queryTickerMappings(logger = null) {
1711
1837
  }
1712
1838
  }
1713
1839
 
1840
+ /**
1841
+ * Query PI ratings from BigQuery for a specific date
1842
+ * Returns data in format: { piId: { averageRating, totalRatings, ratingsByUser } }
1843
+ * @param {string} dateStr - Date (YYYY-MM-DD)
1844
+ * @param {object} logger - Logger instance
1845
+ * @returns {Promise<object|null>} Ratings data map, or null if not found/error
1846
+ */
1847
+ async function queryPIRatings(dateStr, logger = null) {
1848
+ if (process.env.BIGQUERY_ENABLED === 'false') {
1849
+ if (logger) logger.log('DEBUG', '[BigQuery] PI ratings query skipped (BIGQUERY_ENABLED=false)');
1850
+ return null;
1851
+ }
1852
+
1853
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
1854
+ const tablePath = `${datasetId}.pi_ratings`;
1855
+
1856
+ try {
1857
+ const sqlQuery = `
1858
+ SELECT
1859
+ pi_id,
1860
+ average_rating,
1861
+ total_ratings,
1862
+ ratings_by_user
1863
+ FROM \`${tablePath}\`
1864
+ WHERE date = @dateStr
1865
+ ORDER BY pi_id ASC
1866
+ `;
1867
+
1868
+ if (logger) {
1869
+ logger.log('INFO', `[BigQuery] 🔍 Querying PI ratings from ${tablePath} for ${dateStr}`);
1870
+ }
1871
+
1872
+ const rows = await query(sqlQuery, {
1873
+ params: {
1874
+ dateStr: dateStr
1875
+ }
1876
+ }, logger);
1877
+
1878
+ if (!rows || rows.length === 0) {
1879
+ if (logger) logger.log('INFO', `[BigQuery] No PI ratings found for ${dateStr}`);
1880
+ return null;
1881
+ }
1882
+
1883
+ // Transform to expected format: { piId: { averageRating, totalRatings, ratingsByUser } }
1884
+ const ratings = {};
1885
+ for (const row of rows) {
1886
+ const piId = String(row.pi_id);
1887
+ ratings[piId] = {
1888
+ averageRating: row.average_rating || 0,
1889
+ totalRatings: row.total_ratings || 0,
1890
+ ratingsByUser: row.ratings_by_user || {}
1891
+ };
1892
+ }
1893
+
1894
+ if (logger) {
1895
+ logger.log('INFO', `[BigQuery] ✅ Retrieved ratings for ${Object.keys(ratings).length} PIs for ${dateStr}`);
1896
+ }
1897
+
1898
+ return ratings;
1899
+ } catch (error) {
1900
+ if (logger) {
1901
+ logger.log('WARN', `[BigQuery] PI ratings query failed for ${dateStr}: ${error.message}`);
1902
+ }
1903
+ return null;
1904
+ }
1905
+ }
1906
+
1907
+ /**
1908
+ * Query PI page views from BigQuery for a specific date
1909
+ * Returns data in format: { piId: { totalViews, uniqueViewers, viewsByUser } }
1910
+ * @param {string} dateStr - Date (YYYY-MM-DD)
1911
+ * @param {object} logger - Logger instance
1912
+ * @returns {Promise<object|null>} Page views data map, or null if not found/error
1913
+ */
1914
+ async function queryPIPageViews(dateStr, logger = null) {
1915
+ if (process.env.BIGQUERY_ENABLED === 'false') {
1916
+ if (logger) logger.log('DEBUG', '[BigQuery] PI page views query skipped (BIGQUERY_ENABLED=false)');
1917
+ return null;
1918
+ }
1919
+
1920
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
1921
+ const tablePath = `${datasetId}.pi_page_views`;
1922
+
1923
+ try {
1924
+ const sqlQuery = `
1925
+ SELECT
1926
+ pi_id,
1927
+ total_views,
1928
+ unique_viewers,
1929
+ views_by_user
1930
+ FROM \`${tablePath}\`
1931
+ WHERE date = @dateStr
1932
+ ORDER BY pi_id ASC
1933
+ `;
1934
+
1935
+ if (logger) {
1936
+ logger.log('INFO', `[BigQuery] 🔍 Querying PI page views from ${tablePath} for ${dateStr}`);
1937
+ }
1938
+
1939
+ const rows = await query(sqlQuery, {
1940
+ params: {
1941
+ dateStr: dateStr
1942
+ }
1943
+ }, logger);
1944
+
1945
+ if (!rows || rows.length === 0) {
1946
+ if (logger) logger.log('INFO', `[BigQuery] No PI page views found for ${dateStr}`);
1947
+ return null;
1948
+ }
1949
+
1950
+ // Transform to expected format: { piId: { totalViews, uniqueViewers, viewsByUser } }
1951
+ const pageViews = {};
1952
+ for (const row of rows) {
1953
+ const piId = String(row.pi_id);
1954
+ pageViews[piId] = {
1955
+ totalViews: row.total_views || 0,
1956
+ uniqueViewers: row.unique_viewers || 0,
1957
+ viewsByUser: row.views_by_user || {}
1958
+ };
1959
+ }
1960
+
1961
+ if (logger) {
1962
+ logger.log('INFO', `[BigQuery] ✅ Retrieved page views for ${Object.keys(pageViews).length} PIs for ${dateStr}`);
1963
+ }
1964
+
1965
+ return pageViews;
1966
+ } catch (error) {
1967
+ if (logger) {
1968
+ logger.log('WARN', `[BigQuery] PI page views query failed for ${dateStr}: ${error.message}`);
1969
+ }
1970
+ return null;
1971
+ }
1972
+ }
1973
+
1974
+ /**
1975
+ * Query watchlist membership from BigQuery for a specific date
1976
+ * Returns data in format: { piId: { totalUsers, users, publicWatchlistCount, privateWatchlistCount } }
1977
+ * @param {string} dateStr - Date (YYYY-MM-DD)
1978
+ * @param {object} logger - Logger instance
1979
+ * @returns {Promise<object|null>} Watchlist membership data map, or null if not found/error
1980
+ */
1981
+ async function queryWatchlistMembership(dateStr, logger = null) {
1982
+ if (process.env.BIGQUERY_ENABLED === 'false') {
1983
+ if (logger) logger.log('DEBUG', '[BigQuery] Watchlist membership query skipped (BIGQUERY_ENABLED=false)');
1984
+ return null;
1985
+ }
1986
+
1987
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
1988
+ const tablePath = `${datasetId}.watchlist_membership`;
1989
+
1990
+ try {
1991
+ const sqlQuery = `
1992
+ SELECT
1993
+ pi_id,
1994
+ total_users,
1995
+ public_watchlist_count,
1996
+ private_watchlist_count,
1997
+ users
1998
+ FROM \`${tablePath}\`
1999
+ WHERE date = @dateStr
2000
+ ORDER BY pi_id ASC
2001
+ `;
2002
+
2003
+ if (logger) {
2004
+ logger.log('INFO', `[BigQuery] 🔍 Querying watchlist membership from ${tablePath} for ${dateStr}`);
2005
+ }
2006
+
2007
+ const rows = await query(sqlQuery, {
2008
+ params: {
2009
+ dateStr: dateStr
2010
+ }
2011
+ }, logger);
2012
+
2013
+ if (!rows || rows.length === 0) {
2014
+ if (logger) logger.log('INFO', `[BigQuery] No watchlist membership found for ${dateStr}`);
2015
+ return null;
2016
+ }
2017
+
2018
+ // Transform to expected format: { piId: { totalUsers, users, publicWatchlistCount, privateWatchlistCount } }
2019
+ const membership = {};
2020
+ for (const row of rows) {
2021
+ const piId = String(row.pi_id);
2022
+ membership[piId] = {
2023
+ totalUsers: row.total_users || 0,
2024
+ users: row.users || [],
2025
+ publicWatchlistCount: row.public_watchlist_count || 0,
2026
+ privateWatchlistCount: row.private_watchlist_count || 0
2027
+ };
2028
+ }
2029
+
2030
+ if (logger) {
2031
+ logger.log('INFO', `[BigQuery] ✅ Retrieved watchlist membership for ${Object.keys(membership).length} PIs for ${dateStr}`);
2032
+ }
2033
+
2034
+ return membership;
2035
+ } catch (error) {
2036
+ if (logger) {
2037
+ logger.log('WARN', `[BigQuery] Watchlist membership query failed for ${dateStr}: ${error.message}`);
2038
+ }
2039
+ return null;
2040
+ }
2041
+ }
2042
+
2043
+ /**
2044
+ * Query PI alert history from BigQuery for a specific date
2045
+ * Returns data in format: { piId: { alertType: { triggered, count, triggeredFor, metadata } } }
2046
+ * @param {string} dateStr - Date (YYYY-MM-DD)
2047
+ * @param {object} logger - Logger instance
2048
+ * @returns {Promise<object|null>} Alert history data map, or null if not found/error
2049
+ */
2050
+ async function queryPIAlertHistory(dateStr, logger = null) {
2051
+ if (process.env.BIGQUERY_ENABLED === 'false') {
2052
+ if (logger) logger.log('DEBUG', '[BigQuery] PI alert history query skipped (BIGQUERY_ENABLED=false)');
2053
+ return null;
2054
+ }
2055
+
2056
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
2057
+ const tablePath = `${datasetId}.pi_alert_history`;
2058
+
2059
+ try {
2060
+ const sqlQuery = `
2061
+ SELECT
2062
+ pi_id,
2063
+ alert_type,
2064
+ triggered,
2065
+ trigger_count,
2066
+ triggered_for,
2067
+ metadata
2068
+ FROM \`${tablePath}\`
2069
+ WHERE date = @dateStr
2070
+ ORDER BY pi_id ASC, alert_type ASC
2071
+ `;
2072
+
2073
+ if (logger) {
2074
+ logger.log('INFO', `[BigQuery] 🔍 Querying PI alert history from ${tablePath} for ${dateStr}`);
2075
+ }
2076
+
2077
+ const rows = await query(sqlQuery, {
2078
+ params: {
2079
+ dateStr: dateStr
2080
+ }
2081
+ }, logger);
2082
+
2083
+ if (!rows || rows.length === 0) {
2084
+ if (logger) logger.log('INFO', `[BigQuery] No PI alert history found for ${dateStr}`);
2085
+ return null;
2086
+ }
2087
+
2088
+ // Transform to expected format: { piId: { alertType: { triggered, count, triggeredFor, metadata } } }
2089
+ const alertHistory = {};
2090
+ for (const row of rows) {
2091
+ const piId = String(row.pi_id);
2092
+ const alertType = row.alert_type;
2093
+
2094
+ if (!alertHistory[piId]) {
2095
+ alertHistory[piId] = {};
2096
+ }
2097
+
2098
+ alertHistory[piId][alertType] = {
2099
+ triggered: row.triggered || false,
2100
+ count: row.trigger_count || 0,
2101
+ triggeredFor: row.triggered_for || [],
2102
+ metadata: row.metadata || {}
2103
+ };
2104
+ }
2105
+
2106
+ if (logger) {
2107
+ logger.log('INFO', `[BigQuery] ✅ Retrieved alert history for ${Object.keys(alertHistory).length} PIs for ${dateStr}`);
2108
+ }
2109
+
2110
+ return alertHistory;
2111
+ } catch (error) {
2112
+ if (logger) {
2113
+ logger.log('WARN', `[BigQuery] PI alert history query failed for ${dateStr}: ${error.message}`);
2114
+ }
2115
+ return null;
2116
+ }
2117
+ }
2118
+
1714
2119
  module.exports = {
1715
2120
  getBigQueryClient,
1716
2121
  getOrCreateDataset,
@@ -1728,6 +2133,10 @@ module.exports = {
1728
2133
  ensurePIRankingsTable,
1729
2134
  ensureInstrumentInsightsTable,
1730
2135
  ensureTickerMappingsTable,
2136
+ ensurePIRatingsTable,
2137
+ ensurePIPageViewsTable,
2138
+ ensureWatchlistMembershipTable,
2139
+ ensurePIAlertHistoryTable,
1731
2140
  queryPortfolioData,
1732
2141
  queryHistoryData,
1733
2142
  querySocialData,
@@ -1736,6 +2145,10 @@ module.exports = {
1736
2145
  queryPIRankings,
1737
2146
  queryInstrumentInsights,
1738
2147
  queryTickerMappings,
2148
+ queryPIRatings,
2149
+ queryPIPageViews,
2150
+ queryWatchlistMembership,
2151
+ queryPIAlertHistory,
1739
2152
  queryComputationResult,
1740
2153
  queryComputationResultsRange,
1741
2154
  checkExistingRows,
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @fileoverview Backfill Ticker Mappings from Firestore to BigQuery
3
+ *
4
+ * This function reads the single ticker mappings document from Firestore
5
+ * and writes it to BigQuery table.
6
+ *
7
+ * Usage (Local Node.js script):
8
+ * node index.js
9
+ *
10
+ * Features:
11
+ * - Reads single document from Firestore: instrument_mappings/etoro_to_ticker
12
+ * - Transforms to BigQuery rows (one row per instrument)
13
+ * - Uses insertRowsWithMerge (MERGE operation) for idempotent writes
14
+ * - Does NOT delete any Firestore data
15
+ */
16
+
17
+ const { Firestore } = require('@google-cloud/firestore');
18
+ const {
19
+ ensureTickerMappingsTable,
20
+ insertRowsWithMerge
21
+ } = require('../../core/utils/bigquery_utils');
22
+
23
+ const db = new Firestore();
24
+
25
+ /**
26
+ * Backfill ticker mappings from Firestore to BigQuery
27
+ */
28
+ async function backfillTickerMappings(logger = console) {
29
+ logger.log('INFO', '[Backfill] Starting ticker mappings backfill...');
30
+
31
+ try {
32
+ await ensureTickerMappingsTable(logger);
33
+
34
+ // Read the single document from Firestore
35
+ logger.log('INFO', '[Backfill] Fetching ticker mappings from Firestore...');
36
+ const docRef = db.collection('instrument_mappings').doc('etoro_to_ticker');
37
+ const docSnap = await docRef.get();
38
+
39
+ if (!docSnap.exists) {
40
+ logger.log('WARN', '[Backfill] Ticker mappings document not found in Firestore');
41
+ return { success: false, message: 'Document not found' };
42
+ }
43
+
44
+ const mappingsData = docSnap.data();
45
+ const instrumentIds = Object.keys(mappingsData);
46
+
47
+ if (instrumentIds.length === 0) {
48
+ logger.log('WARN', '[Backfill] Ticker mappings document is empty');
49
+ return { success: false, message: 'Document is empty' };
50
+ }
51
+
52
+ logger.log('INFO', `[Backfill] Found ${instrumentIds.length} ticker mappings`);
53
+
54
+ // Transform to BigQuery rows
55
+ const fetchedAt = new Date().toISOString();
56
+ const bigqueryRows = instrumentIds.map(instrumentId => {
57
+ return {
58
+ instrument_id: parseInt(instrumentId, 10),
59
+ ticker: String(mappingsData[instrumentId]),
60
+ last_updated: fetchedAt
61
+ };
62
+ });
63
+
64
+ // Write to BigQuery using MERGE (idempotent, can re-run safely)
65
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
66
+ const keyFields = ['instrument_id'];
67
+ await insertRowsWithMerge(datasetId, 'ticker_mappings', bigqueryRows, keyFields, logger);
68
+
69
+ logger.log('SUCCESS', `[Backfill] ✅ Ticker mappings backfill complete: ${bigqueryRows.length} mappings`);
70
+
71
+ return { success: true, totalRows: bigqueryRows.length };
72
+ } catch (error) {
73
+ logger.log('ERROR', `[Backfill] Ticker mappings backfill failed: ${error.message}`);
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Main entry point
80
+ */
81
+ async function backfillTickerMappingsMain() {
82
+ const logger = {
83
+ log: (level, message, ...args) => {
84
+ const timestamp = new Date().toISOString();
85
+ console.log(`[${timestamp}] [${level}] ${message}`, ...args);
86
+ }
87
+ };
88
+
89
+ logger.log('INFO', '[Backfill] Starting Ticker Mappings backfill...');
90
+
91
+ try {
92
+ const result = await backfillTickerMappings(logger);
93
+
94
+ logger.log('SUCCESS', '[Backfill] ✅ All backfills completed!');
95
+ return result;
96
+ } catch (error) {
97
+ logger.log('ERROR', `[Backfill] Fatal error: ${error.message}`);
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ // CLI handling
103
+ if (require.main === module) {
104
+ backfillTickerMappingsMain()
105
+ .then(result => {
106
+ console.log('\n✅ Backfill completed successfully!');
107
+ console.log('Results:', JSON.stringify(result, null, 2));
108
+ process.exit(0);
109
+ })
110
+ .catch(error => {
111
+ console.error('\n❌ Backfill failed:', error);
112
+ process.exit(1);
113
+ });
114
+ }
115
+
116
+ module.exports = { backfillTickerMappings, backfillTickerMappingsMain };
package/index.js CHANGED
@@ -67,6 +67,10 @@ const { backfillPIMasterListRankings } = require('./functions
67
67
  const { backfillInstrumentInsights } = require('./functions/maintenance/backfill-instrument-insights/index');
68
68
  const { backfillTickerMappings } = require('./functions/maintenance/backfill-ticker-mappings/index');
69
69
  const { backfillPriceData } = require('./functions/maintenance/backfill-price-data-from-firestore/index');
70
+ const { backfillPIRatings } = require('./functions/maintenance/backfill-pi-ratings/index');
71
+ const { backfillPIPageViews } = require('./functions/maintenance/backfill-pi-page-views/index');
72
+ const { backfillWatchlistMembershipData } = require('./functions/maintenance/backfill-watchlist-membership/index');
73
+ const { backfillPIAlertHistory } = require('./functions/maintenance/backfill-pi-alert-history/index');
70
74
 
71
75
  // Alert System
72
76
  const { handleAlertTrigger, handleComputationResultWrite, checkAndSendAllClearNotifications } = require('./functions/alert-system/index');
@@ -139,7 +143,11 @@ const maintenance = {
139
143
  backfillPIMasterListRankings,
140
144
  backfillInstrumentInsights,
141
145
  backfillTickerMappings,
142
- backfillPriceData
146
+ backfillPriceData,
147
+ backfillPIRatings,
148
+ backfillPIPageViews,
149
+ backfillWatchlistMembershipData,
150
+ backfillPIAlertHistory
143
151
  };
144
152
 
145
153
  const proxy = { handlePost };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.717",
3
+ "version": "1.0.718",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -24,7 +24,8 @@
24
24
  "functions/alert-system/",
25
25
  "functions/maintenance/backfill-instrument-insights",
26
26
  "functions/maintenance/backfill-pi-master-list-rankings",
27
- "functions/maintenance/backfill-task-engine-data"
27
+ "functions/maintenance/backfill-task-engine-data",
28
+ "functions/maintenance/backfill-ticker-mappings"
28
29
  ],
29
30
  "keywords": [
30
31
  "bulltrackers",