bulltrackers-module 1.0.721 → 1.0.723
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/functions/computation-system/data/CachedDataLoader.js +101 -102
- package/functions/computation-system/data/DependencyFetcher.js +48 -8
- package/functions/computation-system/persistence/ResultCommitter.js +158 -573
- package/functions/computation-system/utils/data_loader.js +253 -1088
- package/functions/core/utils/bigquery_utils.js +248 -112
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +4 -1
- package/functions/fetch-insights/helpers/handler_helpers.js +63 -65
- package/functions/fetch-popular-investors/helpers/fetch_helpers.js +143 -458
- package/functions/orchestrator/index.js +108 -141
- package/functions/root-data-indexer/index.js +130 -437
- package/index.js +0 -2
- package/package.json +3 -4
- package/functions/invalid-speculator-handler/helpers/handler_helpers.js +0 -38
- package/functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers.js +0 -101
|
@@ -1,563 +1,256 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Root Data Indexer
|
|
3
3
|
* Runs daily to index exactly what data is available for every date.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* REFACTORED: Simplifies checks by relying on BigQuery for migrated data types.
|
|
5
|
+
* RETAINS: Firestore checks for Verifications, Normal/Speculator users, and Generic Social.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
9
9
|
const pLimit = require('p-limit');
|
|
10
10
|
|
|
11
11
|
const CANARY_BLOCK_ID = '19M';
|
|
12
|
-
const PRICE_SHARD_ID = 'shard_0';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Helper function to check if any part document exists in a parts collection
|
|
16
|
-
*
|
|
17
|
-
* @returns {Promise<boolean>} - True if any part_* document exists
|
|
15
|
+
* Used only for legacy Retail (Normal/Speculator) data.
|
|
18
16
|
*/
|
|
19
17
|
async function checkAnyPartExists(partsCollectionRef) {
|
|
20
18
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (snapshot.empty) return false;
|
|
24
|
-
|
|
25
|
-
// Check if any document ID starts with 'part_'
|
|
26
|
-
for (const doc of snapshot.docs) {
|
|
27
|
-
if (doc.id.startsWith('part_')) {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
19
|
+
const snapshot = await partsCollectionRef.limit(1).get();
|
|
20
|
+
return !snapshot.empty;
|
|
32
21
|
} catch (error) {
|
|
33
22
|
return false;
|
|
34
23
|
}
|
|
35
24
|
}
|
|
36
25
|
|
|
37
|
-
/**
|
|
38
|
-
* Helper function to check if any shard document exists in a collection
|
|
39
|
-
* @param {Firestore.CollectionReference} collectionRef - Reference to the collection
|
|
40
|
-
* @returns {Promise<boolean>} - True if any shard_* document exists
|
|
41
|
-
*/
|
|
42
|
-
async function checkAnyShardExists(collectionRef) {
|
|
43
|
-
try {
|
|
44
|
-
// List all documents in the collection
|
|
45
|
-
const snapshot = await collectionRef.limit(10).get();
|
|
46
|
-
if (snapshot.empty) return false;
|
|
47
|
-
|
|
48
|
-
// Check if any document ID starts with 'shard_'
|
|
49
|
-
for (const doc of snapshot.docs) {
|
|
50
|
-
if (doc.id.startsWith('shard_')) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
26
|
exports.runRootDataIndexer = async (config, dependencies) => {
|
|
61
27
|
const { db, logger } = dependencies;
|
|
62
28
|
const {
|
|
63
29
|
availabilityCollection,
|
|
64
30
|
earliestDate,
|
|
65
31
|
collections = {},
|
|
66
|
-
targetDate
|
|
32
|
+
targetDate
|
|
67
33
|
} = config;
|
|
68
34
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Collection Names (Fail-safe defaults)
|
|
72
|
-
const PI_SOCIAL_COLL_NAME = collections.piSocial || 'pi_social_posts';
|
|
73
|
-
const SIGNED_IN_SOCIAL_COLL_NAME = collections.signedInUserSocialCollection || 'signed_in_users_social';
|
|
74
|
-
|
|
75
|
-
// Ensure all required collections have defaults to prevent "collectionPath is not valid" errors
|
|
35
|
+
// Collection Config (Retained for Firestore-based types)
|
|
76
36
|
const safeCollections = {
|
|
77
37
|
normalPortfolios: collections.normalPortfolios || 'NormalUserPortfolios',
|
|
78
38
|
speculatorPortfolios: collections.speculatorPortfolios || 'SpeculatorPortfolios',
|
|
79
39
|
normalHistory: collections.normalHistory || 'NormalUserTradeHistory',
|
|
80
40
|
speculatorHistory: collections.speculatorHistory || 'SpeculatorTradeHistory',
|
|
81
|
-
|
|
82
|
-
social: collections.social || 'daily_social_insights',
|
|
83
|
-
prices: collections.prices || PRICE_COLLECTION_NAME,
|
|
84
|
-
piRankings: collections.piRankings || 'popular_investor_rankings',
|
|
85
|
-
piPortfolios: collections.piPortfolios || 'pi_portfolios_overall',
|
|
86
|
-
piDeepPortfolios: collections.piDeepPortfolios || 'pi_portfolios_deep',
|
|
87
|
-
piHistory: collections.piHistory || 'pi_trade_history',
|
|
88
|
-
signedInUsers: collections.signedInUsers || 'signed_in_users',
|
|
89
|
-
signedInHistory: collections.signedInHistory || 'signed_in_user_history',
|
|
41
|
+
social: collections.social || 'daily_social_insights', // Generic Social
|
|
90
42
|
verifications: collections.verifications || 'user_verifications',
|
|
91
|
-
|
|
92
|
-
piRatings: collections.piRatings || 'PIRatingsData',
|
|
93
|
-
piPageViews: collections.piPageViews || 'PIPageViewsData',
|
|
94
|
-
watchlistMembership: collections.watchlistMembership || 'WatchlistMembershipData',
|
|
95
|
-
piAlertHistory: collections.piAlertHistory || 'PIAlertHistoryData',
|
|
96
|
-
piMasterList: collections.piMasterList || 'system_state', // [NEW] Collection for master list
|
|
97
|
-
...collections // Allow overrides
|
|
43
|
+
...collections
|
|
98
44
|
};
|
|
99
45
|
|
|
100
46
|
const scanMode = targetDate ? 'SINGLE_DATE' : 'FULL_SCAN';
|
|
101
|
-
logger.log('INFO', `[RootDataIndexer] Starting
|
|
102
|
-
|
|
103
|
-
// 1. Price Availability - Read from date tracking documents
|
|
104
|
-
// Find the latest price tracking document and extract available dates
|
|
105
|
-
const priceAvailabilitySet = new Set();
|
|
47
|
+
logger.log('INFO', `[RootDataIndexer] Starting Scan... Mode: ${scanMode}`, { targetDate });
|
|
106
48
|
|
|
107
|
-
//
|
|
108
|
-
let priceTrackingCollectionName = 'pricedatastoreddates';
|
|
109
|
-
if (dependencies.collectionRegistry && dependencies.collectionRegistry.getCollectionPath) {
|
|
110
|
-
try {
|
|
111
|
-
const trackingPath = dependencies.collectionRegistry.getCollectionPath('rootData', 'priceTracking', { fetchDate: '2025-01-01' });
|
|
112
|
-
priceTrackingCollectionName = trackingPath.split('/')[0];
|
|
113
|
-
} catch (e) {
|
|
114
|
-
logger.log('WARN', `[RootDataIndexer] Failed to get price tracking collection from registry, using default: ${e.message}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
// Get the latest price date tracking document
|
|
120
|
-
const dateTrackingRef = db.collection(priceTrackingCollectionName)
|
|
121
|
-
.orderBy('fetchDate', 'desc')
|
|
122
|
-
.limit(1);
|
|
123
|
-
|
|
124
|
-
const dateTrackingSnapshot = await dateTrackingRef.get();
|
|
125
|
-
|
|
126
|
-
if (!dateTrackingSnapshot.empty) {
|
|
127
|
-
const latestTrackingDoc = dateTrackingSnapshot.docs[0].data();
|
|
128
|
-
const datesAvailable = latestTrackingDoc.datesAvailable || [];
|
|
129
|
-
const fetchDate = latestTrackingDoc.fetchDate;
|
|
130
|
-
|
|
131
|
-
// Add all dates from the tracking document
|
|
132
|
-
datesAvailable.forEach(dateKey => {
|
|
133
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
|
|
134
|
-
priceAvailabilitySet.add(dateKey);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// IMPORTANT: If the tracking document was written for today (fetchDate matches targetDate),
|
|
139
|
-
// we should consider prices available for that date even if the API didn't return that exact date.
|
|
140
|
-
// This is because the price fetcher ran for that date and stored prices (even if they're historical).
|
|
141
|
-
if (targetDate && fetchDate === targetDate) {
|
|
142
|
-
priceAvailabilitySet.add(targetDate);
|
|
143
|
-
logger.log('INFO', `[RootDataIndexer] Added fetchDate (${fetchDate}) to price availability set for target date check`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
logger.log('INFO', `[RootDataIndexer] Loaded ${priceAvailabilitySet.size} price dates from tracking document (fetchDate: ${fetchDate})`);
|
|
147
|
-
|
|
148
|
-
// Debug: Log a sample of dates and check if target date is present
|
|
149
|
-
if (targetDate) {
|
|
150
|
-
const sampleDates = Array.from(priceAvailabilitySet).slice(0, 5);
|
|
151
|
-
const hasTargetDate = priceAvailabilitySet.has(targetDate);
|
|
152
|
-
logger.log('INFO', `[RootDataIndexer] Price availability check for ${targetDate}: ${hasTargetDate ? 'FOUND' : 'NOT FOUND'}. Sample dates: ${sampleDates.join(', ')}`);
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
logger.log('WARN', '[RootDataIndexer] No price date tracking documents found. Falling back to empty set.');
|
|
156
|
-
}
|
|
157
|
-
} catch (e) {
|
|
158
|
-
logger.log('ERROR', '[RootDataIndexer] Failed to load price date tracking document.', { error: e.message });
|
|
159
|
-
// Fallback: try to sample shards if tracking document fails
|
|
160
|
-
if (!targetDate) {
|
|
161
|
-
try {
|
|
162
|
-
const priceCollectionRef = db.collection(PRICE_COLLECTION_NAME);
|
|
163
|
-
const priceShardsSnapshot = await priceCollectionRef.limit(10).get();
|
|
164
|
-
|
|
165
|
-
if (!priceShardsSnapshot.empty) {
|
|
166
|
-
for (const shardDoc of priceShardsSnapshot.docs) {
|
|
167
|
-
if (shardDoc.id.startsWith('shard_')) {
|
|
168
|
-
const data = shardDoc.data();
|
|
169
|
-
Object.values(data).forEach(instrument => {
|
|
170
|
-
if (instrument && instrument.prices) {
|
|
171
|
-
Object.keys(instrument.prices).forEach(dateKey => {
|
|
172
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
|
|
173
|
-
priceAvailabilitySet.add(dateKey);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
logger.log('INFO', `[RootDataIndexer] Fallback: Loaded ${priceAvailabilitySet.size} price dates from shard sampling.`);
|
|
181
|
-
}
|
|
182
|
-
} catch (fallbackError) {
|
|
183
|
-
logger.log('ERROR', '[RootDataIndexer] Fallback shard sampling also failed.', { error: fallbackError.message });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 2. Determine Date Range
|
|
49
|
+
// 1. Determine Date Range
|
|
189
50
|
const datesToScan = [];
|
|
190
51
|
if (targetDate) {
|
|
191
|
-
// [NEW] Single Date Optimization
|
|
192
52
|
datesToScan.push(targetDate);
|
|
193
53
|
} else {
|
|
194
|
-
// [OLD] Full History
|
|
195
54
|
const start = new Date(earliestDate || '2023-01-01');
|
|
196
55
|
const end = new Date();
|
|
197
56
|
end.setDate(end.getDate() + 1);
|
|
198
|
-
|
|
199
57
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
200
58
|
datesToScan.push(d.toISOString().slice(0, 10));
|
|
201
59
|
}
|
|
202
60
|
}
|
|
203
61
|
|
|
204
|
-
// 3. Scan in Parallel
|
|
205
62
|
const limit = pLimit(20);
|
|
206
63
|
let updatesCount = 0;
|
|
207
64
|
|
|
208
65
|
const promises = datesToScan.map(dateStr => limit(async () => {
|
|
209
66
|
try {
|
|
210
|
-
// Define Time Range for Social Query (Full Day UTC)
|
|
211
|
-
// Use UTC methods to ensure correct timezone handling
|
|
212
|
-
const dayStart = new Date(dateStr + 'T00:00:00.000Z');
|
|
213
|
-
const dayEnd = new Date(dateStr + 'T23:59:59.999Z');
|
|
214
|
-
|
|
215
67
|
const availability = {
|
|
216
68
|
date: dateStr,
|
|
217
69
|
lastUpdated: FieldValue.serverTimestamp(),
|
|
70
|
+
// High-level aggregates
|
|
218
71
|
hasPortfolio: false,
|
|
219
72
|
hasHistory: false,
|
|
220
73
|
hasSocial: false,
|
|
221
74
|
hasInsights: false,
|
|
222
75
|
hasPrices: false,
|
|
223
76
|
details: {
|
|
77
|
+
// --- FIRESTORE DRIVEN (Not Migrated) ---
|
|
224
78
|
normalPortfolio: false,
|
|
225
79
|
speculatorPortfolio: false,
|
|
226
80
|
normalHistory: false,
|
|
227
81
|
speculatorHistory: false,
|
|
82
|
+
signedInUserVerification: false, // Explicitly Firestore
|
|
83
|
+
|
|
84
|
+
// --- BIGQUERY DRIVEN (Migrated) ---
|
|
228
85
|
piRankings: false,
|
|
229
86
|
piPortfolios: false,
|
|
230
|
-
piDeepPortfolios: false,
|
|
87
|
+
piDeepPortfolios: false, // Deprecated/Merged into piPortfolios
|
|
231
88
|
piHistory: false,
|
|
232
|
-
piSocial: false,
|
|
89
|
+
piSocial: false,
|
|
233
90
|
signedInUserPortfolio: false,
|
|
234
91
|
signedInUserHistory: false,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
92
|
+
signedInSocial: false,
|
|
93
|
+
|
|
94
|
+
// Profile Metrics (BigQuery)
|
|
238
95
|
piRatings: false,
|
|
239
96
|
piPageViews: false,
|
|
240
97
|
watchlistMembership: false,
|
|
241
98
|
piAlertHistory: false,
|
|
242
|
-
piMasterList: false
|
|
99
|
+
piMasterList: false
|
|
243
100
|
}
|
|
244
101
|
};
|
|
245
102
|
|
|
246
|
-
// --- Define Refs & Check Paths ---
|
|
247
|
-
|
|
248
103
|
// =========================================================================
|
|
249
|
-
// BIGQUERY
|
|
104
|
+
// 1. BIGQUERY CHECKS (Primary for most data)
|
|
250
105
|
// =========================================================================
|
|
251
|
-
let
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
let bigqueryHasPIMasterList = false;
|
|
258
|
-
|
|
106
|
+
let bqData = {
|
|
107
|
+
portfolio: {}, history: {}, social: {},
|
|
108
|
+
insights: [], prices: {}, rankings: [], masterList: {},
|
|
109
|
+
ratings: {}, pageViews: {}, watchlists: {}, alerts: {}
|
|
110
|
+
};
|
|
111
|
+
|
|
259
112
|
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
260
113
|
try {
|
|
261
114
|
const {
|
|
262
|
-
queryPortfolioData,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
queryAssetPrices,
|
|
267
|
-
queryPIRankings,
|
|
268
|
-
queryPIMasterList
|
|
115
|
+
queryPortfolioData, queryHistoryData, querySocialData,
|
|
116
|
+
queryInstrumentInsights, queryAssetPrices, queryPIRankings,
|
|
117
|
+
queryPIMasterList, queryPIRatings, queryPIPageViews,
|
|
118
|
+
queryWatchlistMembership, queryPIAlertHistory
|
|
269
119
|
} = require('../core/utils/bigquery_utils');
|
|
270
120
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
121
|
+
const [p, h, s, i, pr, r, ml, rt, pv, wl, ah] = await Promise.all([
|
|
122
|
+
queryPortfolioData(dateStr, null, null, logger).catch(() => ({})),
|
|
123
|
+
queryHistoryData(dateStr, null, null, logger).catch(() => ({})),
|
|
124
|
+
querySocialData(dateStr, null, null, logger).catch(() => ({})),
|
|
125
|
+
queryInstrumentInsights(dateStr, logger).catch(() => []),
|
|
126
|
+
queryAssetPrices(dateStr, dateStr, null, logger).catch(() => ({})),
|
|
127
|
+
queryPIRankings(dateStr, logger).catch(() => []),
|
|
128
|
+
queryPIMasterList(logger).catch(() => ({})),
|
|
129
|
+
queryPIRatings(dateStr, logger).catch(() => ({})),
|
|
130
|
+
queryPIPageViews(dateStr, logger).catch(() => ({})),
|
|
131
|
+
queryWatchlistMembership(dateStr, logger).catch(() => ({})),
|
|
132
|
+
queryPIAlertHistory(dateStr, logger).catch(() => ({}))
|
|
280
133
|
]);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
bigqueryHasPIMasterList = masterListData && Object.keys(masterListData).length > 0;
|
|
289
|
-
|
|
290
|
-
if (bigqueryHasPortfolio || bigqueryHasHistory || bigqueryHasSocial || bigqueryHasInsights || bigqueryHasPrices || bigqueryHasPIRankings) {
|
|
291
|
-
logger.log('INFO', `[RootDataIndexer/${dateStr}] ✅ Found data in BigQuery: portfolio=${bigqueryHasPortfolio}, history=${bigqueryHasHistory}, social=${bigqueryHasSocial}, insights=${bigqueryHasInsights}, prices=${bigqueryHasPrices}, rankings=${bigqueryHasPIRankings}`);
|
|
292
|
-
}
|
|
293
|
-
} catch (bqError) {
|
|
294
|
-
logger.log('WARN', `[RootDataIndexer/${dateStr}] BigQuery check failed, using Firestore fallback: ${bqError.message}`);
|
|
134
|
+
bqData = {
|
|
135
|
+
portfolio: p || {}, history: h || {}, social: s || {},
|
|
136
|
+
insights: i || [], prices: pr || {}, rankings: r || [], masterList: ml || {},
|
|
137
|
+
ratings: rt || {}, pageViews: pv || {}, watchlists: wl || {}, alerts: ah || {}
|
|
138
|
+
};
|
|
139
|
+
} catch (e) {
|
|
140
|
+
logger.log('WARN', `[RootDataIndexer/${dateStr}] BigQuery check failed: ${e.message}`);
|
|
295
141
|
}
|
|
296
142
|
}
|
|
143
|
+
|
|
144
|
+
// Helper to check user types in BQ result maps
|
|
145
|
+
const hasType = (map, type) => Object.values(map).some(u => u.user_type === type);
|
|
146
|
+
|
|
147
|
+
// --- Populate Migrated Flags from BQ ---
|
|
148
|
+
availability.details.piPortfolios = hasType(bqData.portfolio, 'POPULAR_INVESTOR');
|
|
149
|
+
availability.details.signedInUserPortfolio = hasType(bqData.portfolio, 'SIGNED_IN_USER');
|
|
297
150
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const normPortPartsRef = db.collection(safeCollections.normalPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
151
|
+
availability.details.piHistory = hasType(bqData.history, 'POPULAR_INVESTOR');
|
|
152
|
+
availability.details.signedInUserHistory = hasType(bqData.history, 'SIGNED_IN_USER');
|
|
301
153
|
|
|
302
|
-
|
|
303
|
-
|
|
154
|
+
availability.details.piSocial = hasType(bqData.social, 'POPULAR_INVESTOR');
|
|
155
|
+
availability.details.signedInSocial = hasType(bqData.social, 'SIGNED_IN_USER');
|
|
156
|
+
|
|
157
|
+
availability.details.piRankings = bqData.rankings.length > 0 || (bqData.rankings.Items && bqData.rankings.Items.length > 0);
|
|
158
|
+
availability.details.piMasterList = Object.keys(bqData.masterList).length > 0;
|
|
159
|
+
|
|
160
|
+
// Profile Metrics
|
|
161
|
+
availability.details.piRatings = Object.keys(bqData.ratings).length > 0;
|
|
162
|
+
availability.details.piPageViews = Object.keys(bqData.pageViews).length > 0;
|
|
163
|
+
availability.details.watchlistMembership = Object.keys(bqData.watchlists).length > 0;
|
|
164
|
+
availability.details.piAlertHistory = Object.keys(bqData.alerts).length > 0;
|
|
165
|
+
|
|
166
|
+
// Global Flags based on BQ
|
|
167
|
+
availability.hasInsights = bqData.insights.length > 0;
|
|
168
|
+
availability.hasPrices = Object.keys(bqData.prices).length > 0;
|
|
169
|
+
|
|
170
|
+
// =========================================================================
|
|
171
|
+
// 2. FIRESTORE CHECKS (Only for Non-Migrated Data)
|
|
172
|
+
// =========================================================================
|
|
304
173
|
|
|
305
|
-
//
|
|
174
|
+
// A. Normal/Speculator Portfolios & History (Legacy Structure)
|
|
175
|
+
// Path: {collection}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
|
|
176
|
+
const normPortPartsRef = db.collection(safeCollections.normalPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
177
|
+
const specPortPartsRef = db.collection(safeCollections.speculatorPortfolios).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
306
178
|
const normHistPartsRef = db.collection(safeCollections.normalHistory).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
307
|
-
|
|
308
|
-
// Path: {speculatorHistory}/19M/snapshots/{YYYY-MM-DD}/parts/part_*
|
|
309
179
|
const specHistPartsRef = db.collection(safeCollections.speculatorHistory).doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
const insightsRef = db.collection(safeCollections.insights).doc(dateStr);
|
|
313
|
-
|
|
314
|
-
// Generic Asset Posts
|
|
180
|
+
|
|
181
|
+
// B. Generic Social (Instrument Feed)
|
|
315
182
|
// Path: {social}/{YYYY-MM-DD}/posts (Limit 1)
|
|
316
183
|
const socialPostsRef = db.collection(safeCollections.social).doc(dateStr).collection('posts');
|
|
317
184
|
|
|
318
|
-
//
|
|
319
|
-
// Path: {piRankings}/{YYYY-MM-DD}
|
|
320
|
-
const piRankingsRef = db.collection(safeCollections.piRankings).doc(dateStr);
|
|
321
|
-
|
|
322
|
-
// Path: PopularInvestorPortfolioData/{YYYY-MM-DD}/{cid}
|
|
323
|
-
const piPortfoliosCollectionRef = db.collection('PopularInvestorPortfolioData').doc(dateStr);
|
|
324
|
-
|
|
325
|
-
// Path: PopularInvestorTradeHistoryData/{YYYY-MM-DD}/{cid}
|
|
326
|
-
const piHistoryCollectionRef = db.collection('PopularInvestorTradeHistoryData').doc(dateStr);
|
|
327
|
-
|
|
328
|
-
// Path: PopularInvestorSocialPostData/{YYYY-MM-DD}/{cid}
|
|
329
|
-
const piSocialCollectionRef = db.collection('PopularInvestorSocialPostData').doc(dateStr);
|
|
330
|
-
|
|
331
|
-
// Legacy paths (for backward compatibility during migration)
|
|
332
|
-
const piPortfoliosPartsRef = db.collection(safeCollections.piPortfolios)
|
|
333
|
-
.doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
334
|
-
|
|
335
|
-
const piDeepPartsRef = db.collection(safeCollections.piDeepPortfolios)
|
|
336
|
-
.doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
337
|
-
|
|
338
|
-
const piHistoryPartsRef = db.collection(safeCollections.piHistory)
|
|
339
|
-
.doc(CANARY_BLOCK_ID).collection('snapshots').doc(dateStr).collection('parts');
|
|
340
|
-
|
|
341
|
-
// 3. Signed-In Users (NEW STRUCTURE)
|
|
342
|
-
// Path: SignedInUserPortfolioData/{YYYY-MM-DD}/{cid}
|
|
343
|
-
const signedInPortCollectionRef = db.collection('SignedInUserPortfolioData').doc(dateStr);
|
|
344
|
-
|
|
345
|
-
// Path: SignedInUserTradeHistoryData/{YYYY-MM-DD}/{cid}
|
|
346
|
-
const signedInHistCollectionRef = db.collection('SignedInUserTradeHistoryData').doc(dateStr);
|
|
347
|
-
|
|
348
|
-
// Path: SignedInUserSocialPostData/{YYYY-MM-DD}/{cid}
|
|
349
|
-
const signedInSocialCollectionRef = db.collection('SignedInUserSocialPostData').doc(dateStr);
|
|
350
|
-
|
|
351
|
-
// Path: {verifications} (Limit 1) - Checks if collection is non-empty generally
|
|
185
|
+
// C. Verifications
|
|
352
186
|
const verificationsRef = db.collection(safeCollections.verifications);
|
|
353
|
-
|
|
354
|
-
// 4. New Root Data Types for Profile Metrics (Global documents per date)
|
|
355
|
-
// Path: PIRatingsData/{YYYY-MM-DD}
|
|
356
|
-
const piRatingsRef = db.collection(safeCollections.piRatings).doc(dateStr);
|
|
357
|
-
|
|
358
|
-
// Path: PIPageViewsData/{YYYY-MM-DD}
|
|
359
|
-
const piPageViewsRef = db.collection(safeCollections.piPageViews).doc(dateStr);
|
|
360
|
-
|
|
361
|
-
// Path: WatchlistMembershipData/{YYYY-MM-DD}
|
|
362
|
-
const watchlistMembershipRef = db.collection(safeCollections.watchlistMembership).doc(dateStr);
|
|
363
|
-
|
|
364
|
-
// Path: PIAlertHistoryData/{YYYY-MM-DD}
|
|
365
|
-
const piAlertHistoryRef = db.collection(safeCollections.piAlertHistory).doc(dateStr);
|
|
366
187
|
|
|
367
|
-
//
|
|
368
|
-
// Path: system_state/popular_investor_master_list
|
|
369
|
-
const piMasterListRef = db.collection(safeCollections.piMasterList).doc('popular_investor_master_list');
|
|
370
|
-
|
|
371
|
-
// 4. Social Data Checks - Use date tracking documents (NEW STRUCTURE)
|
|
372
|
-
// Single tracking documents at root level:
|
|
373
|
-
// - PopularInvestorSocialPostData/_dates -> fetchedDates.{date}
|
|
374
|
-
// - SignedInUserSocialPostData/_dates -> fetchedDates.{date}
|
|
375
|
-
// For generic social (InstrumentFeedSocialPostData), check the posts subcollection
|
|
376
|
-
const [piSocialTrackingDoc, signedInSocialTrackingDoc, genericSocialSnap] = await Promise.all([
|
|
377
|
-
db.collection('PopularInvestorSocialPostData').doc('_dates').get(),
|
|
378
|
-
db.collection('SignedInUserSocialPostData').doc('_dates').get(),
|
|
379
|
-
db.collection('InstrumentFeedSocialPostData').doc(dateStr).collection('posts').limit(1).get()
|
|
380
|
-
]);
|
|
381
|
-
|
|
382
|
-
// Check if date exists in tracking documents
|
|
383
|
-
// The _dates document uses dot notation: fetchedDates.2025-12-29: true
|
|
384
|
-
// When read, this becomes: { fetchedDates: { "2025-12-29": true } }
|
|
385
|
-
let foundPISocial = false;
|
|
386
|
-
let foundSignedInSocial = false;
|
|
387
|
-
|
|
388
|
-
if (piSocialTrackingDoc.exists) {
|
|
389
|
-
const data = piSocialTrackingDoc.data();
|
|
390
|
-
// Check both nested structure and flat dot-notation structure
|
|
391
|
-
if (data.fetchedDates && typeof data.fetchedDates === 'object') {
|
|
392
|
-
if (data.fetchedDates[dateStr] === true) {
|
|
393
|
-
foundPISocial = true;
|
|
394
|
-
}
|
|
395
|
-
} else if (data[`fetchedDates.${dateStr}`] === true) {
|
|
396
|
-
// Handle flat dot-notation structure (if Firestore stores it that way)
|
|
397
|
-
foundPISocial = true;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (signedInSocialTrackingDoc.exists) {
|
|
402
|
-
const data = signedInSocialTrackingDoc.data();
|
|
403
|
-
// Check both nested structure and flat dot-notation structure
|
|
404
|
-
if (data.fetchedDates && typeof data.fetchedDates === 'object') {
|
|
405
|
-
if (data.fetchedDates[dateStr] === true) {
|
|
406
|
-
foundSignedInSocial = true;
|
|
407
|
-
}
|
|
408
|
-
} else if (data[`fetchedDates.${dateStr}`] === true) {
|
|
409
|
-
// Handle flat dot-notation structure (if Firestore stores it that way)
|
|
410
|
-
foundSignedInSocial = true;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// --- Execute Checks ---
|
|
415
|
-
// Helper to check if any documents exist in a date collection (new structure)
|
|
416
|
-
const checkDateCollectionHasDocs = async (collectionRef) => {
|
|
417
|
-
try {
|
|
418
|
-
// For new structure: collection/{date} is a document, check if it has subcollections (CIDs)
|
|
419
|
-
const subcollections = await collectionRef.listCollections();
|
|
420
|
-
if (subcollections.length === 0) return false;
|
|
421
|
-
// Check if any subcollection (CID) has a document
|
|
422
|
-
for (const subcol of subcollections) {
|
|
423
|
-
const docs = await subcol.limit(1).get();
|
|
424
|
-
if (!docs.empty) return true;
|
|
425
|
-
}
|
|
426
|
-
return false;
|
|
427
|
-
} catch (e) {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
|
|
188
|
+
// Execute Firestore Queries
|
|
432
189
|
const [
|
|
433
190
|
normPortExists, specPortExists,
|
|
434
191
|
normHistExists, specHistExists,
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
piPortExists,
|
|
438
|
-
piDeepExists,
|
|
439
|
-
piHistExists,
|
|
440
|
-
signedInPortExists, signedInHistExists,
|
|
441
|
-
verificationsQuery,
|
|
442
|
-
// New Root Data Types
|
|
443
|
-
piRatingsSnap,
|
|
444
|
-
piPageViewsSnap,
|
|
445
|
-
watchlistMembershipSnap,
|
|
446
|
-
piAlertHistorySnap,
|
|
447
|
-
piMasterListSnap // [NEW]
|
|
192
|
+
genericSocialDocs,
|
|
193
|
+
verificationsQuery
|
|
448
194
|
] = await Promise.all([
|
|
449
195
|
checkAnyPartExists(normPortPartsRef),
|
|
450
196
|
checkAnyPartExists(specPortPartsRef),
|
|
451
197
|
checkAnyPartExists(normHistPartsRef),
|
|
452
198
|
checkAnyPartExists(specHistPartsRef),
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
piRankingsRef.get(),
|
|
456
|
-
// Check new structure first, fallback to legacy
|
|
457
|
-
checkDateCollectionHasDocs(piPortfoliosCollectionRef).then(exists => exists || checkAnyPartExists(piPortfoliosPartsRef)),
|
|
458
|
-
checkAnyPartExists(piDeepPartsRef), // Legacy only
|
|
459
|
-
// Check new structure first, fallback to legacy
|
|
460
|
-
checkDateCollectionHasDocs(piHistoryCollectionRef).then(exists => exists || checkAnyPartExists(piHistoryPartsRef)),
|
|
461
|
-
// Check new structure for signed-in users
|
|
462
|
-
checkDateCollectionHasDocs(signedInPortCollectionRef),
|
|
463
|
-
checkDateCollectionHasDocs(signedInHistCollectionRef),
|
|
464
|
-
verificationsRef.limit(1).get(),
|
|
465
|
-
// New Root Data Types - check if document exists
|
|
466
|
-
piRatingsRef.get(),
|
|
467
|
-
piPageViewsRef.get(),
|
|
468
|
-
watchlistMembershipRef.get(),
|
|
469
|
-
piAlertHistoryRef.get(),
|
|
470
|
-
piMasterListRef.get() // [NEW] - Moved to end to match destructuring order
|
|
471
|
-
]);
|
|
472
|
-
|
|
473
|
-
// Also check social collections directly (new structure)
|
|
474
|
-
const [piSocialExists, signedInSocialExists] = await Promise.all([
|
|
475
|
-
checkDateCollectionHasDocs(piSocialCollectionRef),
|
|
476
|
-
checkDateCollectionHasDocs(signedInSocialCollectionRef)
|
|
199
|
+
socialPostsRef.limit(1).get(),
|
|
200
|
+
verificationsRef.limit(1).get()
|
|
477
201
|
]);
|
|
478
|
-
|
|
479
|
-
// Update social flags if found in new structure
|
|
480
|
-
if (piSocialExists) foundPISocial = true;
|
|
481
|
-
if (signedInSocialExists) foundSignedInSocial = true;
|
|
482
|
-
|
|
483
|
-
// Social data checks are done above using tracking documents
|
|
484
|
-
// foundPISocial and foundSignedInSocial are already set from the tracking document checks
|
|
485
|
-
logger.log('INFO', `[RootDataIndexer/${dateStr}] Social check results - PI: ${foundPISocial}, Signed-in: ${foundSignedInSocial}`);
|
|
486
202
|
|
|
487
|
-
// ---
|
|
488
|
-
|
|
489
|
-
availability.details.
|
|
490
|
-
availability.details.
|
|
491
|
-
availability.details.
|
|
492
|
-
availability.details.speculatorHistory = bigqueryHasHistory || specHistExists;
|
|
493
|
-
availability.details.piRankings = bigqueryHasPIRankings || piRankingsSnap.exists;
|
|
494
|
-
|
|
495
|
-
availability.details.piPortfolios = bigqueryHasPortfolio || piPortExists;
|
|
496
|
-
availability.details.piDeepPortfolios = piDeepExists; // Legacy only, no BigQuery equivalent
|
|
497
|
-
availability.details.piHistory = bigqueryHasHistory || piHistExists;
|
|
498
|
-
|
|
499
|
-
// PI & Signed-In Social Flags (Strict)
|
|
500
|
-
// Use BigQuery if available, otherwise use Firestore tracking documents
|
|
501
|
-
const finalPISocial = bigqueryHasSocial || foundPISocial;
|
|
502
|
-
const finalSignedInSocial = bigqueryHasSocial || foundSignedInSocial;
|
|
503
|
-
availability.details.piSocial = finalPISocial;
|
|
504
|
-
availability.details.hasPISocial = finalPISocial;
|
|
505
|
-
availability.details.signedInSocial = finalSignedInSocial;
|
|
506
|
-
availability.details.hasSignedInSocial = finalSignedInSocial;
|
|
507
|
-
|
|
508
|
-
// Signed-In Flags
|
|
509
|
-
availability.details.signedInUserPortfolio = bigqueryHasPortfolio || signedInPortExists;
|
|
510
|
-
availability.details.signedInUserHistory = bigqueryHasHistory || signedInHistExists;
|
|
203
|
+
// --- Populate Non-Migrated Flags ---
|
|
204
|
+
availability.details.normalPortfolio = normPortExists;
|
|
205
|
+
availability.details.speculatorPortfolio = specPortExists;
|
|
206
|
+
availability.details.normalHistory = normHistExists;
|
|
207
|
+
availability.details.speculatorHistory = specHistExists;
|
|
511
208
|
availability.details.signedInUserVerification = !verificationsQuery.empty;
|
|
512
|
-
|
|
513
|
-
// New Root Data Types for Profile Metrics
|
|
514
|
-
availability.details.piRatings = piRatingsSnap.exists;
|
|
515
|
-
availability.details.piPageViews = piPageViewsSnap.exists;
|
|
516
|
-
availability.details.watchlistMembership = watchlistMembershipSnap.exists;
|
|
517
|
-
availability.details.piAlertHistory = piAlertHistorySnap.exists;
|
|
518
|
-
|
|
519
|
-
// Aggregates (use BigQuery if available, otherwise Firestore)
|
|
520
|
-
availability.hasPortfolio = bigqueryHasPortfolio || normPortExists || specPortExists || piPortExists || signedInPortExists;
|
|
521
|
-
availability.hasHistory = bigqueryHasHistory || normHistExists || specHistExists || piHistExists || signedInHistExists;
|
|
522
|
-
availability.hasInsights = bigqueryHasInsights || insightsSnap.exists;
|
|
523
|
-
availability.hasSocial = bigqueryHasSocial || finalPISocial || finalSignedInSocial || genericSocialExists;
|
|
524
209
|
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// Price Check (BigQuery first, then Firestore tracking document)
|
|
529
|
-
const hasPriceForDate = bigqueryHasPrices || priceAvailabilitySet.has(dateStr);
|
|
530
|
-
availability.hasPrices = hasPriceForDate;
|
|
210
|
+
// =========================================================================
|
|
211
|
+
// 3. AGGREGATE FLAGS
|
|
212
|
+
// =========================================================================
|
|
531
213
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
214
|
+
availability.hasPortfolio =
|
|
215
|
+
availability.details.piPortfolios ||
|
|
216
|
+
availability.details.signedInUserPortfolio ||
|
|
217
|
+
normPortExists ||
|
|
218
|
+
specPortExists;
|
|
219
|
+
|
|
220
|
+
availability.hasHistory =
|
|
221
|
+
availability.details.piHistory ||
|
|
222
|
+
availability.details.signedInUserHistory ||
|
|
223
|
+
normHistExists ||
|
|
224
|
+
specHistExists;
|
|
225
|
+
|
|
226
|
+
availability.hasSocial =
|
|
227
|
+
availability.details.piSocial ||
|
|
228
|
+
availability.details.signedInSocial ||
|
|
229
|
+
!genericSocialDocs.empty;
|
|
535
230
|
|
|
231
|
+
// Store Result
|
|
536
232
|
await db.collection(availabilityCollection).doc(dateStr).set(availability);
|
|
537
233
|
updatesCount++;
|
|
538
234
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
.
|
|
542
|
-
.
|
|
543
|
-
|
|
235
|
+
const summary = [
|
|
236
|
+
availability.hasPortfolio ? 'Port' : '',
|
|
237
|
+
availability.hasHistory ? 'Hist' : '',
|
|
238
|
+
availability.hasSocial ? 'Soc' : '',
|
|
239
|
+
availability.hasInsights ? 'Ins' : '',
|
|
240
|
+
availability.hasPrices ? 'Prc' : ''
|
|
241
|
+
].filter(Boolean).join('+');
|
|
242
|
+
|
|
243
|
+
logger.log('INFO', `[RootDataIndexer/${dateStr}] Indexed. Found: [${summary || 'NONE'}]`);
|
|
544
244
|
|
|
545
245
|
} catch (e) {
|
|
546
|
-
logger.log('ERROR', `[RootDataIndexer] Failed to index ${dateStr}`, {
|
|
547
|
-
message: e.message,
|
|
548
|
-
code: e.code
|
|
549
|
-
});
|
|
246
|
+
logger.log('ERROR', `[RootDataIndexer] Failed to index ${dateStr}`, { message: e.message });
|
|
550
247
|
}
|
|
551
248
|
}));
|
|
552
249
|
|
|
553
250
|
await Promise.all(promises);
|
|
554
251
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
logger.log('WARN', `[RootDataIndexer] Indexing complete but NO dates were updated. This may indicate a failure. Mode: ${scanMode}`, { targetDate });
|
|
558
|
-
} else {
|
|
559
|
-
logger.log('SUCCESS', `[RootDataIndexer] Indexing complete. Updated ${updatesCount} dates. Mode: ${scanMode}`, { targetDate, updatesCount });
|
|
560
|
-
}
|
|
252
|
+
if (updatesCount === 0) logger.log('WARN', `[RootDataIndexer] No dates updated. Mode: ${scanMode}`);
|
|
253
|
+
else logger.log('SUCCESS', `[RootDataIndexer] Updated ${updatesCount} dates. Mode: ${scanMode}`);
|
|
561
254
|
|
|
562
|
-
return { success: updatesCount > 0, count: updatesCount
|
|
255
|
+
return { success: updatesCount > 0, count: updatesCount };
|
|
563
256
|
};
|