bulltrackers-module 1.0.297 → 1.0.298

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,12 +6,14 @@ const { FieldValue, FieldPath } = require('@google-cloud/firestore');
6
6
  const crypto = require('crypto');
7
7
 
8
8
  // [NEW] Single Source of Truth for Data Availability
9
+ // These are defaults/fallbacks. The dynamic function getEarliestDataDates below is the primary source.
9
10
  const DEFINITIVE_EARLIEST_DATES = {
10
- portfolio: new Date('2025-09-25T00:00:00Z'),
11
- history: new Date('2025-11-05T00:00:00Z'),
12
- social: new Date('2025-10-30T00:00:00Z'),
13
- insights: new Date('2025-08-26T00:00:00Z'),
14
- price: new Date('2025-08-01T00:00:00Z')
11
+ portfolio: new Date('2025-01-01T00:00:00Z'),
12
+ history: new Date('2025-08-01T00:00:00Z'),
13
+ social: new Date('2025-08-01T00:00:00Z'),
14
+ insights: new Date('2025-08-01T00:00:00Z'),
15
+ price: new Date('2025-08-01T00:00:00Z'),
16
+ absoluteEarliest: new Date('2023-08-01T00:00:00Z')
15
17
  };
16
18
 
17
19
  /** Stage 1: Normalize a calculation name to kebab-case */
@@ -162,95 +164,75 @@ function getExpectedDateStrings(startDate, endDate) {
162
164
  return dateStrings;
163
165
  }
164
166
 
165
- /** Stage 4: Get the earliest date in a *flat* collection where doc IDs are dates. */
166
- async function getFirstDateFromSimpleCollection(config, deps, collectionName) {
167
- const { db, logger } = deps;
168
- try {
169
- if (!collectionName) { logger.log('WARN', `[Core Utils] Collection name not provided for simple date query.`); return null; }
170
- const query = db.collection(collectionName).where(FieldPath.documentId(), '>=', '2000-01-01').orderBy(FieldPath.documentId(), 'asc').limit(1);
171
- const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
172
- if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) { return new Date(snapshot.docs[0].id + 'T00:00:00Z'); }
173
- } catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
174
- return null;
175
- }
176
-
177
- /** Stage 4: Get the earliest date in a sharded collection */
178
- async function getFirstDateFromCollection(config, deps, collectionName) {
167
+ /** * Stage 4: Get the earliest date from the Centralized Root Data Index.
168
+ * This REPLACES the expensive/error-prone raw collection scanning.
169
+ * It queries the 'system_root_data_index' to find the first date where data flags are TRUE.
170
+ */
171
+ async function getEarliestDataDates(config, deps) {
179
172
  const { db, logger } = deps;
180
- let earliestDate = null;
181
- try {
182
- if (!collectionName) { logger.log('WARN', `[Core Utils] Collection name not provided for sharded date query.`); return null; }
183
- const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
184
- if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
185
- for (const blockDocRef of blockDocRefs) {
186
- const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection).where(FieldPath.documentId(), '>=', '2000-01-01').orderBy(FieldPath.documentId(), 'asc').limit(1);
187
- const snapshotSnap = await withRetry(() => snapshotQuery.get(), `GetEarliestSnapshot(${blockDocRef.path})`);
188
- if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
189
- const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
190
- if (!earliestDate || foundDate < earliestDate) earliestDate = foundDate;
173
+ // Default to 'system_root_data_index' if not configured
174
+ const indexCollection = process.env.ROOT_DATA_AVAILABILITY_COLLECTION ||
175
+ config.rootDataAvailabilityCollection ||
176
+ 'system_root_data_index';
177
+
178
+ // Helper to find earliest date where a specific flag is true
179
+ // Efficient: Uses the document ID (date string) index.
180
+ const getEarliestForType = async (flagName) => {
181
+ try {
182
+ const snapshot = await db.collection(indexCollection)
183
+ .where(flagName, '==', true)
184
+ .orderBy(FieldPath.documentId(), 'asc')
185
+ .limit(1)
186
+ .get();
187
+
188
+ if (!snapshot.empty) {
189
+ const dateStr = snapshot.docs[0].id; // YYYY-MM-DD
190
+ // Safety check on date format
191
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
192
+ return new Date(dateStr + 'T00:00:00Z');
193
+ }
191
194
  }
195
+ } catch (e) {
196
+ logger.log('WARN', `[Utils] Failed to query index for ${flagName}: ${e.message}`);
192
197
  }
193
- } catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
194
- return earliestDate;
195
- }
198
+ return null;
199
+ };
196
200
 
197
- /** Stage 5: Determine the earliest date from *all* source data. */
198
- async function getEarliestDataDates(config, deps) {
199
- const { logger } = deps;
200
- const [ investorDate, speculatorDate, investorHistoryDate, speculatorHistoryDate, insightsDate, socialDate, priceDate ] = await Promise.all([
201
- getFirstDateFromCollection (config, deps, config.normalUserPortfolioCollection),
202
- getFirstDateFromCollection (config, deps, config.speculatorPortfolioCollection),
203
- getFirstDateFromCollection (config, deps, config.normalUserHistoryCollection),
204
- getFirstDateFromCollection (config, deps, config.speculatorHistoryCollection),
205
- getFirstDateFromSimpleCollection (config, deps, config.insightsCollectionName),
206
- getFirstDateFromSimpleCollection (config, deps, config.socialInsightsCollectionName),
207
- getFirstDateFromPriceCollection (config, deps)
201
+ // Parallel query for all data types
202
+ const [portfolioDate, historyDate, socialDate, insightsDate, priceDate] = await Promise.all([
203
+ getEarliestForType('hasPortfolio'),
204
+ getEarliestForType('hasHistory'),
205
+ getEarliestForType('hasSocial'),
206
+ getEarliestForType('hasInsights'),
207
+ getEarliestForType('hasPrices')
208
208
  ]);
209
209
 
210
- const getMinDate = (...dates) => {
211
- const validDates = dates.filter(Boolean);
212
- if (validDates.length === 0) return null;
213
- return new Date(Math.min(...validDates));
214
- };
215
-
216
- const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
217
- const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
218
- const earliestInsightsDate = getMinDate(insightsDate);
219
- const earliestSocialDate = getMinDate(socialDate);
220
- const earliestPriceDate = getMinDate(priceDate);
221
- const absoluteEarliest = getMinDate(earliestPortfolioDate, earliestHistoryDate, earliestInsightsDate, earliestSocialDate, earliestPriceDate);
210
+ // Calculate absolute earliest among found dates
211
+ const foundDates = [portfolioDate, historyDate, socialDate, insightsDate, priceDate].filter(d => d !== null);
212
+
213
+ let absoluteEarliest = null;
214
+ if (foundDates.length > 0) {
215
+ absoluteEarliest = new Date(Math.min(...foundDates));
216
+ } else {
217
+ // Fallback if index is empty
218
+ const configStart = config.earliestComputationDate || '2023-01-01';
219
+ absoluteEarliest = new Date(configStart + 'T00:00:00Z');
220
+ logger.log('WARN', `[Utils] No data found in Root Data Index (${indexCollection}). Defaulting to ${configStart}`);
221
+ }
222
222
 
223
- const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
223
+ // Update the static export for consumers who use it (best effort synchronization)
224
+ DEFINITIVE_EARLIEST_DATES.absoluteEarliest = absoluteEarliest;
224
225
 
225
226
  return {
226
- portfolio: earliestPortfolioDate || new Date('2999-12-31T00:00:00Z'),
227
- history: earliestHistoryDate || new Date('2999-12-31T00:00:00Z'),
228
- insights: earliestInsightsDate || new Date('2999-12-31T00:00:00Z'),
229
- social: earliestSocialDate || new Date('2999-12-31T00:00:00Z'),
230
- price: earliestPriceDate || new Date('2999-12-31T00:00:00Z'),
231
- absoluteEarliest: absoluteEarliest || fallbackDate
227
+ portfolio: portfolioDate || new Date('2999-12-31T00:00:00Z'),
228
+ history: historyDate || new Date('2999-12-31T00:00:00Z'),
229
+ insights: insightsDate || new Date('2999-12-31T00:00:00Z'),
230
+ social: socialDate || new Date('2999-12-31T00:00:00Z'),
231
+ price: priceDate || new Date('2999-12-31T00:00:00Z'),
232
+ absoluteEarliest: absoluteEarliest
232
233
  };
233
234
  }
234
235
 
235
- async function getFirstDateFromPriceCollection(config, deps) {
236
- const { db, logger } = deps;
237
- const collection = config.priceCollection || 'asset_prices';
238
- try {
239
- const snapshot = await withRetry(() => db.collection(collection).limit(10).get(), `GetPriceShards(${collection})`);
240
- let earliestDate = null;
241
- snapshot.forEach(doc => {
242
- const shardData = doc.data();
243
- for (const instrumentId in shardData) {
244
- const instrumentData = shardData[instrumentId];
245
- if (!instrumentData.prices) continue;
246
- const dates = Object.keys(instrumentData.prices).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort();
247
- if (dates.length > 0) { const firstDate = new Date(dates[0] + 'T00:00:00Z'); if (!earliestDate || firstDate < earliestDate) earliestDate = firstDate; }
248
- }
249
- });
250
- return earliestDate;
251
- } catch (e) { logger.log('ERROR', `Failed to get earliest price date from ${collection}`, { errorMessage: e.message }); return null; }
252
- }
253
-
254
236
  module.exports = {
255
237
  FieldValue,
256
238
  FieldPath,
@@ -259,7 +241,7 @@ module.exports = {
259
241
  getExpectedDateStrings,
260
242
  getEarliestDataDates,
261
243
  generateCodeHash,
262
- generateDataHash, // Exported
244
+ generateDataHash,
263
245
  withRetry,
264
246
  DEFINITIVE_EARLIEST_DATES
265
- };
247
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.297",
3
+ "version": "1.0.298",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [