bulltrackers-module 1.0.127 → 1.0.129
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/helpers/computation_pass_runner.js +20 -773
- package/functions/computation-system/helpers/orchestration_helpers.js +88 -867
- package/functions/computation-system/utils/data_loader.js +84 -151
- package/functions/computation-system/utils/utils.js +55 -98
- package/functions/orchestrator/helpers/discovery_helpers.js +40 -188
- package/functions/orchestrator/helpers/update_helpers.js +21 -61
- package/functions/orchestrator/index.js +42 -121
- package/functions/task-engine/handler_creator.js +22 -143
- package/functions/task-engine/helpers/discover_helpers.js +20 -90
- package/functions/task-engine/helpers/update_helpers.js +90 -185
- package/functions/task-engine/helpers/verify_helpers.js +43 -159
- package/functions/task-engine/utils/firestore_batch_manager.js +97 -290
- package/functions/task-engine/utils/task_engine_utils.js +99 -0
- package/package.json +1 -1
- package/functions/task-engine/utils/api_calls.js +0 -0
- package/functions/task-engine/utils/firestore_ops.js +0 -0
|
@@ -12,231 +12,164 @@
|
|
|
12
12
|
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
13
13
|
* @returns {Promise<Firestore.DocumentReference[]>} An array of DocumentReferences.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/** --- Data Loader Sub-Pipes (Stateless, Dependency-Injection) --- */
|
|
16
|
+
|
|
17
|
+
/** Stage 1: Get portfolio part document references for a given date */
|
|
18
|
+
async function getPortfolioPartRefs(config, deps, dateString) {
|
|
19
|
+
const { db, logger, calculationUtils } = deps;
|
|
18
20
|
const { withRetry } = calculationUtils;
|
|
19
|
-
// <<< END FIX >>>
|
|
20
21
|
|
|
21
22
|
logger.log('INFO', `Getting portfolio part references for date: ${dateString}`);
|
|
22
23
|
const allPartRefs = [];
|
|
23
24
|
const collectionsToQuery = [config.normalUserPortfolioCollection, config.speculatorPortfolioCollection];
|
|
24
25
|
|
|
25
26
|
for (const collectionName of collectionsToQuery) {
|
|
26
|
-
const blockDocsQuery = db.collection(collectionName);
|
|
27
|
-
const blockDocRefs = await withRetry(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
logger.log('WARN', `No block documents found in collection: ${collectionName}`);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const blockDocRef of blockDocRefs) {
|
|
27
|
+
const blockDocsQuery = db.collection(collectionName);
|
|
28
|
+
const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
|
|
29
|
+
if (!blockDocRefs.length) { logger.log('WARN', `No block documents in ${collectionName}`); continue; }
|
|
30
|
+
|
|
31
|
+
// --- START MODIFICATION ---
|
|
32
|
+
// Run all "listDocuments" calls in parallel instead of a sequential loop
|
|
33
|
+
const partsPromises = blockDocRefs.map(blockDocRef => {
|
|
38
34
|
const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection).doc(dateString).collection(config.partsSubcollection);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
// Each call is individually retried
|
|
36
|
+
return withRetry(() => partsCollectionRef.listDocuments(), `listDocuments(${partsCollectionRef.path})`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Wait for all parallel queries to finish
|
|
40
|
+
const partDocArrays = await Promise.all(partsPromises);
|
|
41
|
+
|
|
42
|
+
// Flatten the arrays of arrays into the final list
|
|
43
|
+
partDocArrays.forEach(partDocs => {
|
|
43
44
|
allPartRefs.push(...partDocs);
|
|
44
|
-
}
|
|
45
|
+
});
|
|
46
|
+
// --- END MODIFICATION ---
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
logger.log('INFO', `Found ${allPartRefs.length} part
|
|
49
|
+
logger.log('INFO', `Found ${allPartRefs.length} portfolio part refs for ${dateString}`);
|
|
48
50
|
return allPartRefs;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
55
|
-
* @param {Firestore.DocumentReference[]} refs - An array of DocumentReferences to load.
|
|
56
|
-
* @returns {Promise<object>} A single map of { [userId]: portfolioData }.
|
|
57
|
-
*/
|
|
58
|
-
async function loadDataByRefs(config, dependencies, refs) {
|
|
59
|
-
// <<< FIX: Destructure all dependencies here, inside the function >>>
|
|
60
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
53
|
+
/** Stage 2: Load data from an array of document references */
|
|
54
|
+
async function loadDataByRefs(config, deps, refs) {
|
|
55
|
+
const { db, logger, calculationUtils } = deps;
|
|
61
56
|
const { withRetry } = calculationUtils;
|
|
62
|
-
// <<< END FIX >>>
|
|
63
57
|
|
|
64
|
-
if (!refs || refs.length
|
|
58
|
+
if (!refs || !refs.length) return {};
|
|
65
59
|
const mergedPortfolios = {};
|
|
66
60
|
const batchSize = config.partRefBatchSize || 50;
|
|
67
61
|
|
|
68
62
|
for (let i = 0; i < refs.length; i += batchSize) {
|
|
69
63
|
const batchRefs = refs.slice(i, i + batchSize);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
() => db.getAll(...batchRefs),
|
|
73
|
-
`getAll(batch ${Math.floor(i / batchSize)})`
|
|
74
|
-
);
|
|
64
|
+
const snapshots = await withRetry(() => db.getAll(...batchRefs), `getAll(batch ${Math.floor(i / batchSize)})`);
|
|
65
|
+
|
|
75
66
|
for (const doc of snapshots) {
|
|
76
|
-
if (doc.exists)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} else {
|
|
81
|
-
logger.log('WARN', `Document ${doc.id} exists but data is not an object. Data:`, data);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
67
|
+
if (!doc.exists) continue;
|
|
68
|
+
const data = doc.data();
|
|
69
|
+
if (data && typeof data === 'object') Object.assign(mergedPortfolios, data);
|
|
70
|
+
else logger.log('WARN', `Doc ${doc.id} exists but data is not an object`, data);
|
|
84
71
|
}
|
|
85
72
|
}
|
|
73
|
+
|
|
86
74
|
return mergedPortfolios;
|
|
87
75
|
}
|
|
88
76
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* @param {Firestore.DocumentReference[]} partRefs - Array of part document references.
|
|
94
|
-
* @returns {Promise<object>} A single map of { [userId]: portfolioData }.
|
|
95
|
-
*/
|
|
96
|
-
async function loadFullDayMap(config, dependencies, partRefs) {
|
|
97
|
-
// <<< FIX: Destructure only what's needed for this specific function >>>
|
|
98
|
-
const { logger } = dependencies;
|
|
99
|
-
// <<< END FIX >>>
|
|
77
|
+
/** Stage 3: Load a full day map by delegating to loadDataByRefs */
|
|
78
|
+
async function loadFullDayMap(config, deps, partRefs) {
|
|
79
|
+
const { logger } = deps;
|
|
80
|
+
if (!partRefs.length) return {};
|
|
100
81
|
|
|
101
|
-
if (partRefs.length === 0) return {};
|
|
102
82
|
logger.log('TRACE', `Loading full day map from ${partRefs.length} references...`);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const fullMap = await loadDataByRefs(config, dependencies, partRefs);
|
|
106
|
-
logger.log('TRACE', `Full day map loaded with ${Object.keys(fullMap).length} users.`);
|
|
83
|
+
const fullMap = await loadDataByRefs(config, deps, partRefs);
|
|
84
|
+
logger.log('TRACE', `Full day map loaded with ${Object.keys(fullMap).length} users`);
|
|
107
85
|
return fullMap;
|
|
108
86
|
}
|
|
109
87
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* @param {object} config - The computation system configuration object.
|
|
114
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
115
|
-
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
116
|
-
* @returns {Promise<object|null>} The insights data object or null if not found/error.
|
|
117
|
-
*/
|
|
118
|
-
async function loadDailyInsights(config, dependencies, dateString) {
|
|
119
|
-
// <<< FIX: Destructure all dependencies here, inside the function >>>
|
|
120
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
88
|
+
/** Stage 4: Load daily instrument insights */
|
|
89
|
+
async function loadDailyInsights(config, deps, dateString) {
|
|
90
|
+
const { db, logger, calculationUtils } = deps;
|
|
121
91
|
const { withRetry } = calculationUtils;
|
|
122
|
-
// <<< END FIX >>>
|
|
123
92
|
|
|
124
|
-
const insightsCollectionName = config.insightsCollectionName || 'daily_instrument_insights';
|
|
125
|
-
logger.log('INFO', `Loading daily insights for
|
|
93
|
+
const insightsCollectionName = config.insightsCollectionName || 'daily_instrument_insights';
|
|
94
|
+
logger.log('INFO', `Loading daily insights for ${dateString} from ${insightsCollectionName}`);
|
|
95
|
+
|
|
126
96
|
try {
|
|
127
97
|
const docRef = db.collection(insightsCollectionName).doc(dateString);
|
|
128
98
|
const docSnap = await withRetry(() => docRef.get(), `getInsights(${dateString})`);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
logger.log('WARN', `Daily insights document not found for ${dateString}`);
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
logger.log('TRACE', `Successfully loaded insights for ${dateString}.`);
|
|
99
|
+
if (!docSnap.exists) { logger.log('WARN', `Insights not found for ${dateString}`); return null; }
|
|
100
|
+
logger.log('TRACE', `Successfully loaded insights for ${dateString}`);
|
|
135
101
|
return docSnap.data();
|
|
136
102
|
} catch (error) {
|
|
137
103
|
logger.log('ERROR', `Failed to load daily insights for ${dateString}`, { errorMessage: error.message });
|
|
138
|
-
return null;
|
|
104
|
+
return null;
|
|
139
105
|
}
|
|
140
106
|
}
|
|
141
107
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
* Fetches all analyzed social post documents for a specific date.
|
|
146
|
-
* @param {object} config - The computation system configuration object.
|
|
147
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
148
|
-
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
149
|
-
* @returns {Promise<object|null>} An object map of { [postId]: postData } or null.
|
|
150
|
-
*/
|
|
151
|
-
async function loadDailySocialPostInsights(config, dependencies, dateString) {
|
|
152
|
-
// <<< FIX: Destructure all dependencies here, inside the function >>>
|
|
153
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
108
|
+
/** Stage 5: Load daily social post insights */
|
|
109
|
+
async function loadDailySocialPostInsights(config, deps, dateString) {
|
|
110
|
+
const { db, logger, calculationUtils } = deps;
|
|
154
111
|
const { withRetry } = calculationUtils;
|
|
155
|
-
// <<< END FIX >>>
|
|
156
112
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
113
|
+
const collectionName = config.socialInsightsCollectionName || 'daily_social_insights';
|
|
114
|
+
logger.log('INFO', `Loading social post insights for ${dateString} from ${collectionName}`);
|
|
115
|
+
|
|
161
116
|
try {
|
|
162
|
-
const postsCollectionRef = db.collection(
|
|
117
|
+
const postsCollectionRef = db.collection(collectionName).doc(dateString).collection('posts');
|
|
163
118
|
const querySnapshot = await withRetry(() => postsCollectionRef.get(), `getSocialPosts(${dateString})`);
|
|
119
|
+
if (querySnapshot.empty) { logger.log('WARN', `No social post insights for ${dateString}`); return null; }
|
|
164
120
|
|
|
165
|
-
if (querySnapshot.empty) {
|
|
166
|
-
logger.log('WARN', `No social post insights found for ${dateString}.`);
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
121
|
const postsMap = {};
|
|
171
|
-
querySnapshot.forEach(doc => {
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
logger.log('TRACE', `Successfully loaded ${Object.keys(postsMap).length} social post insights for ${dateString}.`);
|
|
122
|
+
querySnapshot.forEach(doc => { postsMap[doc.id] = doc.data(); });
|
|
123
|
+
logger.log('TRACE', `Loaded ${Object.keys(postsMap).length} social post insights`);
|
|
176
124
|
return postsMap;
|
|
177
|
-
|
|
178
125
|
} catch (error) {
|
|
179
126
|
logger.log('ERROR', `Failed to load social post insights for ${dateString}`, { errorMessage: error.message });
|
|
180
127
|
return null;
|
|
181
128
|
}
|
|
182
129
|
}
|
|
183
|
-
// --- END NEW ---
|
|
184
130
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* --- NEW ---
|
|
189
|
-
* Sub-pipe: pipe.computationSystem.dataLoader.getHistoryPartRefs
|
|
190
|
-
* @param {object} config - The computation system configuration object.
|
|
191
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
192
|
-
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
193
|
-
* @returns {Promise<Firestore.DocumentReference[]>} An array of DocumentReferences.
|
|
194
|
-
*/
|
|
195
|
-
async function getHistoryPartRefs(config, dependencies, dateString) {
|
|
196
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
131
|
+
/** Stage 6: Get history part references for a given date */
|
|
132
|
+
async function getHistoryPartRefs(config, deps, dateString) {
|
|
133
|
+
const { db, logger, calculationUtils } = deps;
|
|
197
134
|
const { withRetry } = calculationUtils;
|
|
198
135
|
|
|
199
|
-
logger.log('INFO', `Getting history part references for
|
|
136
|
+
logger.log('INFO', `Getting history part references for ${dateString}`);
|
|
200
137
|
const allPartRefs = [];
|
|
201
|
-
// --- MODIFIED: Use new history collection config keys ---
|
|
202
138
|
const collectionsToQuery = [config.normalUserHistoryCollection, config.speculatorHistoryCollection];
|
|
203
139
|
|
|
204
140
|
for (const collectionName of collectionsToQuery) {
|
|
205
|
-
if (!collectionName) {
|
|
206
|
-
logger.log('WARN', `History collection name is undefined. Skipping.`);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
141
|
+
if (!collectionName) { logger.log('WARN', `History collection undefined. Skipping.`); continue; }
|
|
209
142
|
const blockDocsQuery = db.collection(collectionName);
|
|
210
|
-
const blockDocRefs = await withRetry(
|
|
211
|
-
|
|
212
|
-
`listDocuments(${collectionName})`
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
if (blockDocRefs.length === 0) {
|
|
216
|
-
logger.log('WARN', `No block documents found in collection: ${collectionName}`);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
143
|
+
const blockDocRefs = await withRetry(() => blockDocsQuery.listDocuments(), `listDocuments(${collectionName})`);
|
|
144
|
+
if (!blockDocRefs.length) { logger.log('WARN', `No block documents in ${collectionName}`); continue; }
|
|
219
145
|
|
|
220
|
-
|
|
146
|
+
// --- START MODIFICATION ---
|
|
147
|
+
// Run all "listDocuments" calls in parallel instead of a sequential loop
|
|
148
|
+
const partsPromises = blockDocRefs.map(blockDocRef => {
|
|
221
149
|
const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection).doc(dateString).collection(config.partsSubcollection);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
150
|
+
// Each call is individually retried
|
|
151
|
+
return withRetry(() => partsCollectionRef.listDocuments(), `listDocuments(${partsCollectionRef.path})`);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Wait for all parallel queries to finish
|
|
155
|
+
const partDocArrays = await Promise.all(partsPromises);
|
|
156
|
+
|
|
157
|
+
// Flatten the arrays of arrays into the final list
|
|
158
|
+
partDocArrays.forEach(partDocs => {
|
|
226
159
|
allPartRefs.push(...partDocs);
|
|
227
|
-
}
|
|
160
|
+
});
|
|
161
|
+
// --- END MODIFICATION ---
|
|
228
162
|
}
|
|
229
163
|
|
|
230
|
-
logger.log('INFO', `Found ${allPartRefs.length} history part
|
|
164
|
+
logger.log('INFO', `Found ${allPartRefs.length} history part refs for ${dateString}`);
|
|
231
165
|
return allPartRefs;
|
|
232
166
|
}
|
|
233
167
|
|
|
234
|
-
|
|
235
168
|
module.exports = {
|
|
236
169
|
getPortfolioPartRefs,
|
|
237
170
|
loadDataByRefs,
|
|
238
171
|
loadFullDayMap,
|
|
239
172
|
loadDailyInsights,
|
|
240
|
-
loadDailySocialPostInsights,
|
|
241
|
-
getHistoryPartRefs,
|
|
173
|
+
loadDailySocialPostInsights,
|
|
174
|
+
getHistoryPartRefs,
|
|
242
175
|
};
|
|
@@ -3,56 +3,37 @@
|
|
|
3
3
|
* REFACTORED: Now stateless and receive dependencies where needed.
|
|
4
4
|
* DYNAMIC: Categorization logic is removed, replaced by manifest.
|
|
5
5
|
*/
|
|
6
|
+
/** --- Computation System Sub-Pipes & Utils (Stateless) --- */
|
|
6
7
|
|
|
7
8
|
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
* Normalizes a calculation name to kebab-case.
|
|
11
|
-
* @param {string} name
|
|
12
|
-
* @returns {string}
|
|
13
|
-
*/
|
|
10
|
+
/** Stage 1: Normalize a calculation name to kebab-case */
|
|
14
11
|
function normalizeName(name) {
|
|
15
12
|
return name.replace(/_/g, '-');
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
22
|
-
* @param {Array<object>} writes - Array of { ref: DocumentReference, data: object }.
|
|
23
|
-
* @param {string} operationName - Name for logging.
|
|
24
|
-
*/
|
|
25
|
-
async function commitBatchInChunks(config, dependencies, writes, operationName) {
|
|
26
|
-
// --- MODIFIED: Get withRetry from dependencies ---
|
|
27
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
15
|
+
/** Stage 2: Commit a batch of writes in chunks */
|
|
16
|
+
async function commitBatchInChunks(config, deps, writes, operationName) {
|
|
17
|
+
const { db, logger, calculationUtils } = deps;
|
|
28
18
|
const { withRetry } = calculationUtils;
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
|
|
31
20
|
const batchSizeLimit = config.batchSizeLimit || 450;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
logger.log('WARN', `[${operationName}] No writes to commit.`);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
21
|
+
if (!writes.length) { logger.log('WARN', `[${operationName}] No writes to commit.`); return; }
|
|
22
|
+
|
|
37
23
|
for (let i = 0; i < writes.length; i += batchSizeLimit) {
|
|
38
|
-
const batch = db.batch(); // Use db
|
|
39
24
|
const chunk = writes.slice(i, i + batchSizeLimit);
|
|
25
|
+
const batch = db.batch();
|
|
40
26
|
chunk.forEach(write => batch.set(write.ref, write.data, { merge: true }));
|
|
41
27
|
|
|
42
28
|
const chunkNum = Math.floor(i / batchSizeLimit) + 1;
|
|
43
29
|
const totalChunks = Math.ceil(writes.length / batchSizeLimit);
|
|
44
|
-
await withRetry(
|
|
45
|
-
|
|
46
|
-
`${operationName} (Chunk ${chunkNum}/${totalChunks})`
|
|
47
|
-
);
|
|
30
|
+
await withRetry(() => batch.commit(), `${operationName} (Chunk ${chunkNum}/${totalChunks})`);
|
|
31
|
+
|
|
48
32
|
logger.log('INFO', `[${operationName}] Committed chunk ${chunkNum}/${totalChunks} (${chunk.length} ops).`);
|
|
49
33
|
}
|
|
50
34
|
}
|
|
51
35
|
|
|
52
|
-
/**
|
|
53
|
-
* Sub-pipe: pipe.computationSystem.computationUtils.getExpectedDateStrings
|
|
54
|
-
* (Stateless)
|
|
55
|
-
*/
|
|
36
|
+
/** Stage 3: Generate an array of expected date strings between two dates */
|
|
56
37
|
function getExpectedDateStrings(startDate, endDate) {
|
|
57
38
|
const dateStrings = [];
|
|
58
39
|
if (startDate <= endDate) {
|
|
@@ -65,86 +46,62 @@ function getExpectedDateStrings(startDate, endDate) {
|
|
|
65
46
|
return dateStrings;
|
|
66
47
|
}
|
|
67
48
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
async function getFirstDateFromCollection(config, dependencies, collectionName) {
|
|
72
|
-
// --- MODIFIED: Get withRetry from dependencies ---
|
|
73
|
-
const { db, logger, calculationUtils } = dependencies;
|
|
49
|
+
/** Stage 4: Get the earliest date in a collection */
|
|
50
|
+
async function getFirstDateFromCollection(config, deps, collectionName) {
|
|
51
|
+
const { db, logger, calculationUtils } = deps;
|
|
74
52
|
const { withRetry } = calculationUtils;
|
|
75
|
-
|
|
53
|
+
|
|
76
54
|
let earliestDate = null;
|
|
77
55
|
try {
|
|
78
|
-
const blockDocRefs = await withRetry(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
for (const blockDocRef of blockDocRefs) {
|
|
89
|
-
const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection)
|
|
90
|
-
.where(FieldPath.documentId(), '>=', '2000-01-01')
|
|
91
|
-
.orderBy(FieldPath.documentId(), 'asc')
|
|
92
|
-
.limit(1);
|
|
93
|
-
|
|
94
|
-
const snapshotSnap = await withRetry(
|
|
95
|
-
() => snapshotQuery.get(),
|
|
96
|
-
`GetEarliestSnapshot(${blockDocRef.path})`
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
|
|
100
|
-
const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
|
|
101
|
-
if (!earliestDate || foundDate < earliestDate) {
|
|
102
|
-
earliestDate = foundDate;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
56
|
+
const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
|
|
57
|
+
if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
|
|
58
|
+
|
|
59
|
+
for (const blockDocRef of blockDocRefs) {
|
|
60
|
+
const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection)
|
|
61
|
+
.where(FieldPath.documentId(), '>=', '2000-01-01')
|
|
62
|
+
.orderBy(FieldPath.documentId(), 'asc')
|
|
63
|
+
.limit(1);
|
|
106
64
|
|
|
65
|
+
const snapshotSnap = await withRetry(() => snapshotQuery.get(), `GetEarliestSnapshot(${blockDocRef.path})`);
|
|
66
|
+
if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
|
|
67
|
+
const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
|
|
68
|
+
if (!earliestDate || foundDate < earliestDate) earliestDate = foundDate;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
107
71
|
} catch (e) {
|
|
108
72
|
logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
|
|
109
73
|
}
|
|
74
|
+
|
|
110
75
|
return earliestDate;
|
|
111
76
|
}
|
|
112
77
|
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (earliestDate) {
|
|
135
|
-
logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
|
|
136
|
-
return earliestDate;
|
|
137
|
-
} else {
|
|
138
|
-
const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
|
|
139
|
-
logger.log('WARN', `No source data found. Defaulting first date to: ${fallbackDate.toISOString().slice(0, 10)}`);
|
|
140
|
-
return fallbackDate;
|
|
141
|
-
}
|
|
78
|
+
/** Stage 5: Determine the earliest date from source data across both user types */
|
|
79
|
+
async function getFirstDateFromSourceData(config, deps) {
|
|
80
|
+
const { logger } = deps;
|
|
81
|
+
logger.log('INFO', 'Querying for earliest date from source portfolio data...');
|
|
82
|
+
|
|
83
|
+
const investorDate = await getFirstDateFromCollection(config, deps, config.normalUserPortfolioCollection);
|
|
84
|
+
const speculatorDate = await getFirstDateFromCollection(config, deps, config.speculatorPortfolioCollection);
|
|
85
|
+
|
|
86
|
+
let earliestDate;
|
|
87
|
+
if (investorDate && speculatorDate) earliestDate = investorDate < speculatorDate ? investorDate : speculatorDate;
|
|
88
|
+
else earliestDate = investorDate || speculatorDate;
|
|
89
|
+
|
|
90
|
+
if (earliestDate) {
|
|
91
|
+
logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
|
|
92
|
+
return earliestDate;
|
|
93
|
+
} else {
|
|
94
|
+
const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
|
|
95
|
+
logger.log('WARN', `No source data found. Defaulting first date to: ${fallbackDate.toISOString().slice(0, 10)}`);
|
|
96
|
+
return fallbackDate;
|
|
97
|
+
}
|
|
142
98
|
}
|
|
143
99
|
|
|
144
100
|
module.exports = {
|
|
145
|
-
FieldValue,
|
|
101
|
+
FieldValue,
|
|
102
|
+
FieldPath,
|
|
146
103
|
normalizeName,
|
|
147
104
|
commitBatchInChunks,
|
|
148
|
-
getExpectedDateStrings,
|
|
105
|
+
getExpectedDateStrings,
|
|
149
106
|
getFirstDateFromSourceData,
|
|
150
|
-
};
|
|
107
|
+
};
|