bulltrackers-module 1.0.130 → 1.0.132
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.
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* It reads its pass number from the config and executes only those calculations.
|
|
7
7
|
* This file contains the high-level "manual" of steps. The "how-to" logic
|
|
8
8
|
* is extracted into 'computation_system_utils.js'.
|
|
9
|
+
* --- MODIFIED: To use getEarliestDataDates and pass the date map to the orchestrator helpers. ---
|
|
9
10
|
*/
|
|
10
11
|
const { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
|
|
11
|
-
|
|
12
|
+
// --- MODIFIED: Import getEarliestDataDates ---
|
|
13
|
+
const { getExpectedDateStrings, getEarliestDataDates } = require('../utils/utils.js');
|
|
12
14
|
|
|
13
15
|
async function runComputationPass(config, dependencies, computationManifest) {
|
|
14
16
|
const { logger } = dependencies;
|
|
@@ -17,7 +19,12 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
17
19
|
|
|
18
20
|
const yesterday = new Date(); yesterday.setUTCDate(yesterday.getUTCDate()-1);
|
|
19
21
|
const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
// --- MODIFIED: Call new date function ---
|
|
24
|
+
const earliestDates = await getEarliestDataDates(config, dependencies);
|
|
25
|
+
const firstDate = earliestDates.absoluteEarliest; // Use the absolute earliest for the loop
|
|
26
|
+
// --- END MODIFICATION ---
|
|
27
|
+
|
|
21
28
|
const startDateUTC = firstDate ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate())) : new Date(config.earliestComputationDate+'T00:00:00Z');
|
|
22
29
|
const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
|
|
23
30
|
|
|
@@ -30,7 +37,10 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
30
37
|
for (const dateStr of allExpectedDates) {
|
|
31
38
|
const dateToProcess = new Date(dateStr+'T00:00:00Z');
|
|
32
39
|
try {
|
|
33
|
-
|
|
40
|
+
// --- MODIFIED: Pass earliestDates map to checkRootDataAvailability ---
|
|
41
|
+
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates); if (!rootData) continue;
|
|
42
|
+
// --- END MODIFICATION ---
|
|
43
|
+
|
|
34
44
|
const fetchedDeps = await fetchDependenciesForPass(dateStr, calcsInThisPass, computationManifest, config, dependencies);
|
|
35
45
|
const { standardCalcsToRun, metaCalcsToRun } = filterCalculations(standardCalcs, metaCalcs, rootData.status, fetchedDeps, passToRun, dateStr, logger);
|
|
36
46
|
if (standardCalcsToRun.length) await runStandardComputationPass(dateToProcess, standardCalcsToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData);
|
|
@@ -44,4 +54,4 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
44
54
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
module.exports = { runComputationPass };
|
|
57
|
+
module.exports = { runComputationPass };
|
|
@@ -5,32 +5,95 @@ const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
|
|
|
5
5
|
/** Stage 1: Group manifest by pass number */
|
|
6
6
|
function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
/** * --- MODIFIED: Returns detailed missing dependencies for logging ---
|
|
9
|
+
* Stage 2: Check root data dependencies for a calc
|
|
10
|
+
*/
|
|
11
|
+
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
12
|
+
const missing = [];
|
|
13
|
+
if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) {
|
|
14
|
+
return { canRun: true, missing };
|
|
15
|
+
}
|
|
16
|
+
for (const dep of calcManifest.rootDataDependencies) {
|
|
17
|
+
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
|
|
18
|
+
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
19
|
+
else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
|
|
20
|
+
else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
|
|
21
|
+
}
|
|
22
|
+
return { canRun: missing.length === 0, missing };
|
|
12
23
|
}
|
|
13
24
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const { logger } = dependencies; // Destructure logger for local use
|
|
25
|
+
/** * --- MODIFIED: Uses earliestDates map to avoid unnecessary queries ---
|
|
26
|
+
* Stage 3: Check root data availability for a date
|
|
27
|
+
*/
|
|
28
|
+
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
29
|
+
const { logger } = dependencies;
|
|
20
30
|
logger.log('INFO', `[PassRunner] Checking root data for ${dateStr}...`);
|
|
31
|
+
|
|
32
|
+
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
33
|
+
let portfolioRefs = [], insightsData = null, socialData = null, historyRefs = [];
|
|
34
|
+
let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
|
|
35
|
+
|
|
21
36
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
const tasks = [];
|
|
38
|
+
|
|
39
|
+
if (dateToProcess >= earliestDates.portfolio) {
|
|
40
|
+
tasks.push(
|
|
41
|
+
getPortfolioPartRefs(config, dependencies, dateStr).then(res => {
|
|
42
|
+
portfolioRefs = res;
|
|
43
|
+
hasPortfolio = !!(res?.length);
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (dateToProcess >= earliestDates.insights) {
|
|
49
|
+
tasks.push(
|
|
50
|
+
loadDailyInsights(config, dependencies, dateStr).then(res => {
|
|
51
|
+
insightsData = res;
|
|
52
|
+
hasInsights = !!res;
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (dateToProcess >= earliestDates.social) {
|
|
58
|
+
tasks.push(
|
|
59
|
+
loadDailySocialPostInsights(config, dependencies, dateStr).then(res => {
|
|
60
|
+
socialData = res;
|
|
61
|
+
hasSocial = !!res;
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (dateToProcess >= earliestDates.history) {
|
|
67
|
+
tasks.push(
|
|
68
|
+
getHistoryPartRefs(config, dependencies, dateStr).then(res => {
|
|
69
|
+
historyRefs = res;
|
|
70
|
+
hasHistory = !!(res?.length);
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await Promise.all(tasks);
|
|
76
|
+
|
|
77
|
+
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) {
|
|
78
|
+
logger.log('WARN', `[PassRunner] No root data for ${dateStr}.`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
portfolioRefs,
|
|
84
|
+
insightsData,
|
|
85
|
+
socialData,
|
|
86
|
+
historyRefs,
|
|
87
|
+
status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message });
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
32
94
|
}
|
|
33
|
-
|
|
95
|
+
|
|
96
|
+
|
|
34
97
|
|
|
35
98
|
/** Stage 4: Fetch computed dependencies from Firestore */
|
|
36
99
|
async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
|
|
@@ -50,14 +113,47 @@ async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, conf
|
|
|
50
113
|
return fetched;
|
|
51
114
|
}
|
|
52
115
|
|
|
53
|
-
/**
|
|
116
|
+
/** * --- MODIFIED: Added detailed logging ---
|
|
117
|
+
* Stage 5: Filter calculations based on available root data and dependencies
|
|
118
|
+
*/
|
|
54
119
|
function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, fetchedDeps, passToRun, dateStr, logger) {
|
|
55
120
|
const skipped = new Set();
|
|
56
|
-
|
|
57
|
-
|
|
121
|
+
|
|
122
|
+
// Filter Standard Calcs
|
|
123
|
+
const standardCalcsToRun = standardCalcs.filter(c => {
|
|
124
|
+
const { canRun, missing } = checkRootDependencies(c, rootDataStatus);
|
|
125
|
+
if (canRun) return true;
|
|
126
|
+
|
|
127
|
+
logger.log('INFO', `[Pass ${passToRun}] Skipping ${c.name} for ${dateStr}. Missing root data: [${missing.join(', ')}]`);
|
|
128
|
+
skipped.add(c.name);
|
|
129
|
+
return false;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Filter Meta Calcs
|
|
133
|
+
const metaCalcsToRun = metaCalcs.filter(c => {
|
|
134
|
+
// 1. Check root data
|
|
135
|
+
const { canRun, missing: missingRoot } = checkRootDependencies(c, rootDataStatus);
|
|
136
|
+
if (!canRun) {
|
|
137
|
+
logger.log('INFO', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing root data: [${missingRoot.join(', ')}]`);
|
|
138
|
+
skipped.add(c.name);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. Check computed dependencies
|
|
143
|
+
const missingDeps = (c.dependencies || []).map(normalizeName).filter(d => !fetchedDeps[d]);
|
|
144
|
+
if (missingDeps.length > 0) {
|
|
145
|
+
logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${c.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
|
|
146
|
+
skipped.add(c.name);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true; // All checks passed
|
|
151
|
+
});
|
|
152
|
+
|
|
58
153
|
return { standardCalcsToRun, metaCalcsToRun };
|
|
59
154
|
}
|
|
60
155
|
|
|
156
|
+
|
|
61
157
|
/** Stage 6: Initialize calculator instances */
|
|
62
158
|
function initializeCalculators(calcs, logger) { const state = {}; for (const c of calcs) { const name=normalizeName(c.name), Cl=c.class; if(typeof Cl==='function') try { const inst=new Cl(); inst.manifest=c; state[name]=inst; } catch(e){logger.warn(`Init failed ${name}`,{errorMessage:e.message}); state[name]=null;} else {logger.warn(`Class missing ${name}`); state[name]=null;} } return state; }
|
|
63
159
|
|
|
@@ -79,20 +175,159 @@ async function streamAndProcess(dateStr, todayRefs, state, passName, config, dep
|
|
|
79
175
|
|
|
80
176
|
/** Stage 9: Run standard computations */
|
|
81
177
|
async function runStandardComputationPass(date, calcs, passName, config, deps, rootData) {
|
|
82
|
-
const dStr=date.toISOString().slice(0,10), logger=deps.logger;
|
|
178
|
+
const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
|
|
83
179
|
logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
|
|
84
|
-
const fullRoot=await loadHistoricalData(date, calcs, config, deps, rootData);
|
|
85
|
-
const state=initializeCalculators(calcs, logger);
|
|
180
|
+
const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
|
|
181
|
+
const state = initializeCalculators(calcs, logger);
|
|
86
182
|
await streamAndProcess(dStr, fullRoot.portfolioRefs, state, passName, config, deps, fullRoot);
|
|
87
|
-
|
|
88
|
-
|
|
183
|
+
|
|
184
|
+
// --- START: FULL COMMIT LOGIC ---
|
|
185
|
+
let success = 0;
|
|
186
|
+
const standardWrites = [];
|
|
187
|
+
const shardedWrites = {}; // Format: { [collectionName]: { [docId]: data } }
|
|
188
|
+
|
|
189
|
+
for (const name in state) {
|
|
190
|
+
const calc = state[name];
|
|
191
|
+
if (!calc || typeof calc.getResult !== 'function') continue;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const result = await Promise.resolve(calc.getResult());
|
|
195
|
+
if (result && Object.keys(result).length > 0) {
|
|
196
|
+
|
|
197
|
+
// Separate sharded data from standard data
|
|
198
|
+
const standardResult = {};
|
|
199
|
+
for (const key in result) {
|
|
200
|
+
if (key.startsWith('sharded_')) {
|
|
201
|
+
// This is sharded data, e.g., sharded_user_profitability
|
|
202
|
+
// The value is expected to be: { "collection_name": { "doc1": {...}, "doc2": {...} } }
|
|
203
|
+
const shardedData = result[key];
|
|
204
|
+
for (const collectionName in shardedData) {
|
|
205
|
+
if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
|
|
206
|
+
// Merge doc data (e.g., combining data for "user_profitability_shard_1")
|
|
207
|
+
Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// This is a standard, single-doc result
|
|
211
|
+
standardResult[key] = result[key];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Add standard result to the batch
|
|
216
|
+
if (Object.keys(standardResult).length > 0) {
|
|
217
|
+
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
218
|
+
.collection(config.resultsSubcollection).doc(calc.manifest.category)
|
|
219
|
+
.collection(config.computationsSubcollection).doc(name);
|
|
220
|
+
|
|
221
|
+
standardWrites.push({ ref: docRef, data: standardResult });
|
|
222
|
+
}
|
|
223
|
+
success++; // Mark as success even if only sharded data was produced
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
logger.log('ERROR', `getResult failed ${name} for ${dStr}`, { err: e.message, stack: e.stack });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Commit standard (non-sharded) writes in chunks
|
|
231
|
+
if (standardWrites.length > 0) {
|
|
232
|
+
await commitBatchInChunks(config, deps, standardWrites, `${passName} Standard ${dStr}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Commit all sharded writes
|
|
236
|
+
for (const collectionName in shardedWrites) {
|
|
237
|
+
const docs = shardedWrites[collectionName];
|
|
238
|
+
const shardedDocWrites = [];
|
|
239
|
+
for (const docId in docs) {
|
|
240
|
+
// This assumes docId is the full path for sharded docs, or just the doc ID
|
|
241
|
+
// Based on user_profitability_tracker, it's just the doc ID.
|
|
242
|
+
const docRef = deps.db.collection(collectionName).doc(docId);
|
|
243
|
+
shardedDocWrites.push({ ref: docRef, data: docs[docId] });
|
|
244
|
+
}
|
|
245
|
+
if (shardedDocWrites.length > 0) {
|
|
246
|
+
await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// --- END: FULL COMMIT LOGIC ---
|
|
250
|
+
|
|
251
|
+
logger.log(success === calcs.length ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`);
|
|
89
252
|
}
|
|
90
253
|
|
|
91
254
|
/** Stage 10: Run meta computations */
|
|
92
|
-
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
|
|
255
|
+
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
|
|
256
|
+
const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
|
|
93
257
|
logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
|
|
94
|
-
|
|
95
|
-
|
|
258
|
+
|
|
259
|
+
// --- START: FULL COMMIT LOGIC ---
|
|
260
|
+
let success = 0;
|
|
261
|
+
const standardWrites = [];
|
|
262
|
+
const shardedWrites = {}; // Format: { [collectionName]: { [docId]: data } }
|
|
263
|
+
|
|
264
|
+
for (const mCalc of calcs) {
|
|
265
|
+
const name = normalizeName(mCalc.name), Cl = mCalc.class;
|
|
266
|
+
if (typeof Cl !== 'function') {
|
|
267
|
+
logger.log('ERROR', `Invalid class ${name}`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const inst = new Cl();
|
|
271
|
+
try {
|
|
272
|
+
// Pass the full dependencies object to process()
|
|
273
|
+
const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData }, config, fetchedDeps));
|
|
274
|
+
|
|
275
|
+
if (result && Object.keys(result).length > 0) {
|
|
276
|
+
|
|
277
|
+
// Separate sharded data from standard data
|
|
278
|
+
const standardResult = {};
|
|
279
|
+
for (const key in result) {
|
|
280
|
+
if (key.startsWith('sharded_')) {
|
|
281
|
+
const shardedData = result[key];
|
|
282
|
+
for (const collectionName in shardedData) {
|
|
283
|
+
if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
|
|
284
|
+
Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
standardResult[key] = result[key];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Add standard result to the batch
|
|
292
|
+
if (Object.keys(standardResult).length > 0) {
|
|
293
|
+
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
294
|
+
.collection(config.resultsSubcollection).doc(mCalc.category)
|
|
295
|
+
.collection(config.computationsSubcollection).doc(name);
|
|
296
|
+
|
|
297
|
+
standardWrites.push({ ref: docRef, data: standardResult });
|
|
298
|
+
}
|
|
299
|
+
success++;
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
logger.log('ERROR', `Meta-calc failed ${name} for ${dStr}`, { err: e.message, stack: e.stack });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Commit standard (non-sharded) writes in chunks
|
|
307
|
+
if (standardWrites.length > 0) {
|
|
308
|
+
await commitBatchInChunks(config, deps, standardWrites, `${passName} Meta ${dStr}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Commit all sharded writes
|
|
312
|
+
for (const collectionName in shardedWrites) {
|
|
313
|
+
const docs = shardedWrites[collectionName];
|
|
314
|
+
const shardedDocWrites = [];
|
|
315
|
+
for (const docId in docs) {
|
|
316
|
+
// Special case for stateful meta-calcs that write to a specific path
|
|
317
|
+
const docRef = docId.includes('/')
|
|
318
|
+
? deps.db.doc(docId) // docId is a full path
|
|
319
|
+
: deps.db.collection(collectionName).doc(docId); // docId is just an ID
|
|
320
|
+
|
|
321
|
+
shardedDocWrites.push({ ref: docRef, data: docs[docId] });
|
|
322
|
+
}
|
|
323
|
+
if (shardedDocWrites.length > 0) {
|
|
324
|
+
await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// --- END: FULL COMMIT LOGIC ---
|
|
328
|
+
|
|
329
|
+
logger.log(success === calcs.length ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`);
|
|
96
330
|
}
|
|
97
331
|
|
|
332
|
+
|
|
98
333
|
module.exports = { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass };
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* @fileoverview Computation system sub-pipes and utils.
|
|
3
3
|
* REFACTORED: Now stateless and receive dependencies where needed.
|
|
4
4
|
* DYNAMIC: Categorization logic is removed, replaced by manifest.
|
|
5
|
+
* --- MODIFIED: getFirstDateFromSourceData is now getEarliestDataDates
|
|
6
|
+
* and queries all data sources to build an availability map. ---
|
|
5
7
|
*/
|
|
6
|
-
/** --- Computation System Sub-Pipes & Utils (Stateless) --- */
|
|
8
|
+
/** --- Computation System Sub-Pipes & Utils (Stateless, Dependency-Injection) --- */
|
|
7
9
|
|
|
8
10
|
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
9
11
|
|
|
@@ -46,13 +48,46 @@ function getExpectedDateStrings(startDate, endDate) {
|
|
|
46
48
|
return dateStrings;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* --- NEW HELPER ---
|
|
53
|
+
* Stage 4: Get the earliest date in a *flat* collection where doc IDs are dates.
|
|
54
|
+
*/
|
|
55
|
+
async function getFirstDateFromSimpleCollection(config, deps, collectionName) {
|
|
56
|
+
const { db, logger, calculationUtils } = deps;
|
|
57
|
+
const { withRetry } = calculationUtils;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if (!collectionName) {
|
|
61
|
+
logger.log('WARN', `[Core Utils] Collection name not provided for simple date query.`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const query = db.collection(collectionName)
|
|
65
|
+
.where(FieldPath.documentId(), '>=', '2000-01-01')
|
|
66
|
+
.orderBy(FieldPath.documentId(), 'asc')
|
|
67
|
+
.limit(1);
|
|
68
|
+
|
|
69
|
+
const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
|
|
70
|
+
|
|
71
|
+
if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) {
|
|
72
|
+
return new Date(snapshot.docs[0].id + 'T00:00:00Z');
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Stage 4: Get the earliest date in a sharded collection */
|
|
50
81
|
async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
51
82
|
const { db, logger, calculationUtils } = deps;
|
|
52
83
|
const { withRetry } = calculationUtils;
|
|
53
84
|
|
|
54
85
|
let earliestDate = null;
|
|
55
86
|
try {
|
|
87
|
+
if (!collectionName) {
|
|
88
|
+
logger.log('WARN', `[Core Utils] Collection name not provided for sharded date query.`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
56
91
|
const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
|
|
57
92
|
if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
|
|
58
93
|
|
|
@@ -75,26 +110,69 @@ async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
|
75
110
|
return earliestDate;
|
|
76
111
|
}
|
|
77
112
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
113
|
+
/** * --- MODIFIED FUNCTION ---
|
|
114
|
+
* Stage 5: Determine the earliest date from *all* source data.
|
|
115
|
+
*/
|
|
116
|
+
async function getEarliestDataDates(config, deps) {
|
|
80
117
|
const { logger } = deps;
|
|
81
|
-
logger.log('INFO', 'Querying for earliest date from source
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
logger.log('INFO', 'Querying for earliest date from ALL source data collections...');
|
|
119
|
+
|
|
120
|
+
// These return null on error or if empty
|
|
121
|
+
const [
|
|
122
|
+
investorDate,
|
|
123
|
+
speculatorDate,
|
|
124
|
+
investorHistoryDate,
|
|
125
|
+
speculatorHistoryDate,
|
|
126
|
+
insightsDate,
|
|
127
|
+
socialDate
|
|
128
|
+
] = await Promise.all([
|
|
129
|
+
getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection),
|
|
130
|
+
getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection),
|
|
131
|
+
getFirstDateFromCollection(config, deps, config.normalUserHistoryCollection),
|
|
132
|
+
getFirstDateFromCollection(config, deps, config.speculatorHistoryCollection),
|
|
133
|
+
getFirstDateFromSimpleCollection(config, deps, config.insightsCollectionName),
|
|
134
|
+
getFirstDateFromSimpleCollection(config, deps, config.socialInsightsCollectionName)
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
// Helper to find the minimum (earliest) of a set of dates
|
|
138
|
+
const getMinDate = (...dates) => {
|
|
139
|
+
const validDates = dates.filter(Boolean); // Filter out nulls
|
|
140
|
+
if (validDates.length === 0) return null;
|
|
141
|
+
return new Date(Math.min(...validDates));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
|
|
145
|
+
const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
|
|
146
|
+
const earliestInsightsDate = getMinDate(insightsDate); // Already a single date
|
|
147
|
+
const earliestSocialDate = getMinDate(socialDate); // Already a single date
|
|
148
|
+
|
|
149
|
+
const absoluteEarliest = getMinDate(
|
|
150
|
+
earliestPortfolioDate,
|
|
151
|
+
earliestHistoryDate,
|
|
152
|
+
earliestInsightsDate,
|
|
153
|
+
earliestSocialDate
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Fallback date
|
|
157
|
+
const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
|
|
158
|
+
|
|
159
|
+
const result = {
|
|
160
|
+
portfolio: earliestPortfolioDate || new Date('2999-12-31'), // Use a 'far future' date if null
|
|
161
|
+
history: earliestHistoryDate || new Date('2999-12-31'),
|
|
162
|
+
insights: earliestInsightsDate || new Date('2999-12-31'),
|
|
163
|
+
social: earliestSocialDate || new Date('2999-12-31'),
|
|
164
|
+
absoluteEarliest: absoluteEarliest || fallbackDate // Use fallback for the main loop
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
logger.log('INFO', 'Earliest data availability map built:', {
|
|
168
|
+
portfolio: result.portfolio.toISOString().slice(0, 10),
|
|
169
|
+
history: result.history.toISOString().slice(0, 10),
|
|
170
|
+
insights: result.insights.toISOString().slice(0, 10),
|
|
171
|
+
social: result.social.toISOString().slice(0, 10),
|
|
172
|
+
absoluteEarliest: result.absoluteEarliest.toISOString().slice(0, 10)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return result;
|
|
98
176
|
}
|
|
99
177
|
|
|
100
178
|
module.exports = {
|
|
@@ -103,5 +181,6 @@ module.exports = {
|
|
|
103
181
|
normalizeName,
|
|
104
182
|
commitBatchInChunks,
|
|
105
183
|
getExpectedDateStrings,
|
|
106
|
-
getFirstDateFromSourceData,
|
|
107
|
-
|
|
184
|
+
// getFirstDateFromSourceData, // This is replaced
|
|
185
|
+
getEarliestDataDates, // <-- EXPORT NEW FUNCTION
|
|
186
|
+
};
|