bulltrackers-module 1.0.646 → 1.0.648

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.
@@ -9,6 +9,34 @@
9
9
  const { normalizeName } = require('../utils/utils');
10
10
  const zlib = require('zlib');
11
11
 
12
+ /**
13
+ * Checks if data is effectively empty (no usable content).
14
+ * @param {any} data - The data to check
15
+ * @returns {boolean} True if data is empty/null/undefined or contains no meaningful content
16
+ */
17
+ function isDataEmpty(data) {
18
+ if (!data || data === null || data === undefined) return true;
19
+
20
+ // Check if it's an object with only metadata fields
21
+ if (typeof data === 'object' && !Array.isArray(data)) {
22
+ const keys = Object.keys(data);
23
+ // If only metadata/internal fields, consider it empty
24
+ const metadataFields = ['_completed', '_compressed', '_sharded', '_shardCount', '_isPageMode', '_pageCount', '_lastUpdated', '_expireAt'];
25
+ const hasOnlyMetadata = keys.length > 0 && keys.every(k => metadataFields.includes(k) || k.startsWith('_'));
26
+
27
+ if (hasOnlyMetadata) return true;
28
+
29
+ // If object has no keys (after filtering metadata), it's empty
30
+ const dataKeys = keys.filter(k => !k.startsWith('_'));
31
+ if (dataKeys.length === 0) return true;
32
+ }
33
+
34
+ // Check if it's an empty array
35
+ if (Array.isArray(data) && data.length === 0) return true;
36
+
37
+ return false;
38
+ }
39
+
12
40
  /**
13
41
  * BRIDGE FUNCTION: Matches WorkflowOrchestrator signature.
14
42
  * Adapts (dateStr, calcs, manifest, ...) -> (dateObj, calcs, ..., manifestLookup).
@@ -28,7 +56,9 @@ async function fetchExistingResults(dateStr, calcs, fullManifest, config, deps,
28
56
  const dateObj = new Date(dateStr + (dateStr.includes('T') ? '' : 'T00:00:00Z'));
29
57
 
30
58
  // 3. Delegate to fetchDependencies
31
- return fetchDependencies(dateObj, calcs, config, deps, manifestLookup);
59
+ // CRITICAL: For historical context (yesterday's data), allow missing dependencies
60
+ // Historical lookbacks are optional - gaps in historical data are permissible
61
+ return fetchDependencies(dateObj, calcs, config, deps, manifestLookup, isHistoricalContext);
32
62
  }
33
63
 
34
64
  /**
@@ -38,8 +68,9 @@ async function fetchExistingResults(dateStr, calcs, fullManifest, config, deps,
38
68
  * @param {Object} config - System config.
39
69
  * @param {Object} deps - System dependencies (db, logger).
40
70
  * @param {Object} manifestLookup - Map of { [calcName]: categoryString }.
71
+ * @param {boolean} allowMissing - If true, missing/empty dependencies are allowed (for historical/lookback scenarios).
41
72
  */
42
- async function fetchDependencies(date, calcs, config, deps, manifestLookup = {}) {
73
+ async function fetchDependencies(date, calcs, config, deps, manifestLookup = {}, allowMissing = false) {
43
74
  const { db, logger } = deps;
44
75
  const dStr = date.toISOString().slice(0, 10);
45
76
 
@@ -69,27 +100,83 @@ async function fetchDependencies(date, calcs, config, deps, manifestLookup = {})
69
100
  logger.log('INFO', `[DependencyFetcher] Fetching ${needed.size} dependencies for computation(s): ${calcNames} (date: ${dStr})`);
70
101
 
71
102
  const results = {};
103
+ const missingDeps = [];
104
+ const emptyDeps = [];
105
+
106
+ // Helper to build path string
107
+ const buildPath = (category, normName) => {
108
+ return `${config.resultsCollection || 'computation_results'}/${dStr}/${config.resultsSubcollection || 'results'}/${category}/${config.computationsSubcollection || 'computations'}/${normName}`;
109
+ };
110
+
72
111
  // CHANGED: Iterate over the entries to access both normalized and original names
73
112
  const promises = Array.from(needed.entries()).map(async ([normName, originalName]) => {
113
+ // Resolve Category from Lookup, default to 'analytics' if unknown
114
+ // Note: manifestLookup keys are expected to be normalized
115
+ const category = manifestLookup[normName] || 'analytics';
116
+ const path = buildPath(category, normName);
117
+
74
118
  try {
75
- // Resolve Category from Lookup, default to 'analytics' if unknown
76
- // Note: manifestLookup keys are expected to be normalized
77
- const category = manifestLookup[normName] || 'analytics';
78
-
79
119
  // Pass logger in config for fetchSingleResult
80
120
  const fetchConfig = { ...config, logger };
81
121
 
82
122
  // Fetch using the normalized name (system standard)
83
123
  const data = await fetchSingleResult(db, fetchConfig, dStr, normName, category);
84
124
 
85
- // CHANGED: Store result using the ORIGINAL name so context.computed['CaseSensitive'] works
86
- if (data) results[originalName] = data;
125
+ // CRITICAL: Validate that dependency exists and has data
126
+ if (!data) {
127
+ missingDeps.push({ name: originalName, normalizedName: normName, path });
128
+ // Log level depends on context - ERROR for current date, INFO for historical
129
+ if (allowMissing) {
130
+ logger.log('INFO', `[DependencyFetcher] ⚠️ Missing dependency '${originalName}' (${normName}) from: ${path} (Historical context - allowed)`);
131
+ } else {
132
+ logger.log('ERROR', `[DependencyFetcher] ❌ Missing required dependency '${originalName}' (${normName}) from: ${path}`);
133
+ }
134
+ } else if (isDataEmpty(data)) {
135
+ emptyDeps.push({ name: originalName, normalizedName: normName, path });
136
+ // Log level depends on context - ERROR for current date, INFO for historical
137
+ if (allowMissing) {
138
+ logger.log('INFO', `[DependencyFetcher] ⚠️ Empty dependency '${originalName}' (${normName}) from: ${path} (Historical context - allowed)`);
139
+ } else {
140
+ logger.log('ERROR', `[DependencyFetcher] ❌ Empty dependency '${originalName}' (${normName}) from: ${path} - Document exists but contains no usable data`);
141
+ }
142
+ } else {
143
+ // CHANGED: Store result using the ORIGINAL name so context.computed['CaseSensitive'] works
144
+ results[originalName] = data;
145
+ }
87
146
  } catch (e) {
88
- logger.log('WARN', `[DependencyFetcher] Failed to load dependency ${originalName}: ${e.message}`);
147
+ missingDeps.push({ name: originalName, normalizedName: normName, path, error: e.message });
148
+ // Log level depends on context - ERROR for current date, INFO for historical
149
+ if (allowMissing) {
150
+ logger.log('INFO', `[DependencyFetcher] ⚠️ Failed to load dependency '${originalName}' (${normName}) from: ${path} - Error: ${e.message} (Historical context - allowed)`);
151
+ } else {
152
+ logger.log('ERROR', `[DependencyFetcher] ❌ Failed to load dependency '${originalName}' (${normName}) from: ${path} - Error: ${e.message}`);
153
+ }
89
154
  }
90
155
  });
91
156
 
92
157
  await Promise.all(promises);
158
+
159
+ // CRITICAL: Fail if any required dependencies are missing or empty
160
+ // EXCEPTION: For historical/lookback scenarios, missing dependencies are permissible
161
+ if ((missingDeps.length > 0 || emptyDeps.length > 0) && !allowMissing) {
162
+ const missingList = missingDeps.map(d => `'${d.name}' (path: ${d.path}${d.error ? `, error: ${d.error}` : ''})`).join(', ');
163
+ const emptyList = emptyDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
164
+
165
+ const errorMsg = `[DependencyFetcher] ❌ CRITICAL: Cannot proceed - Required dependencies missing or empty for computation(s): ${calcNames}\n` +
166
+ `Missing dependencies (${missingDeps.length}): ${missingList}\n` +
167
+ (emptyDeps.length > 0 ? `Empty dependencies (${emptyDeps.length}): ${emptyList}\n` : '') +
168
+ `Date: ${dStr}\n` +
169
+ `This computation will FAIL and no results will be saved.`;
170
+
171
+ logger.log('ERROR', errorMsg);
172
+ throw new Error(errorMsg);
173
+ } else if (missingDeps.length > 0 || emptyDeps.length > 0) {
174
+ // Historical/lookback context - log but allow missing dependencies
175
+ const missingList = missingDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
176
+ const emptyList = emptyDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
177
+ logger.log('INFO', `[DependencyFetcher] ⚠️ Historical/Lookback context: Missing/empty dependencies allowed for ${calcNames} on ${dStr}. Missing: ${missingList}${emptyDeps.length > 0 ? `, Empty: ${emptyList}` : ''}`);
178
+ }
179
+
93
180
  return results;
94
181
  }
95
182
 
@@ -126,10 +213,17 @@ async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config,
126
213
  fetchOps.push(async () => {
127
214
  const fetchConfig = { ...config, logger };
128
215
  const val = await fetchSingleResult(db, fetchConfig, dateStr, rawName, category);
129
- if (val) {
216
+ // CRITICAL: For series/lookback, we allow missing dates (historical lookback may have gaps)
217
+ // This is expected behavior - not all historical dates will have data
218
+ // But we still validate that the data isn't empty if it exists
219
+ if (val && !isDataEmpty(val)) {
130
220
  if (!results[normName]) results[normName] = {};
131
221
  results[normName][dateStr] = val;
222
+ } else if (val && isDataEmpty(val)) {
223
+ // Log but don't fail - series can have gaps, empty data is treated as missing
224
+ logger.log('INFO', `[DependencyFetcher] ⚠️ Empty dependency '${rawName}' found at ${dateStr} in series (allowing gap - historical lookback)`);
132
225
  }
226
+ // If val is null, that's fine - missing dates in historical series are permissible
133
227
  });
134
228
  }
135
229
  }
@@ -168,10 +262,22 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
168
262
  .doc(name);
169
263
 
170
264
  const snap = await docRef.get();
171
- if (!snap.exists) return null;
265
+ if (!snap.exists) {
266
+ // Log the missing document path clearly
267
+ if (config.logger) {
268
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ Document does not exist at: ${path}`);
269
+ } else {
270
+ console.error(`[DependencyFetcher] ❌ Document does not exist at: ${path}`);
271
+ }
272
+ return null;
273
+ }
172
274
 
173
275
  let data = snap.data();
174
276
 
277
+ // CRITICAL: Don't check if empty yet - we need to load shards/compressed data first
278
+ // A sharded document will only have metadata in the pointer doc, but the actual data is in shards
279
+ // A compressed document will only have metadata + payload, but the actual data is in the payload
280
+
175
281
  // 1. Handle Compression
176
282
  if (data._compressed && data.payload) {
177
283
  try {
@@ -183,12 +289,17 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
183
289
  data = { ...data, ...realData };
184
290
  delete data.payload;
185
291
  } catch (e) {
186
- console.warn(`[DependencyFetcher] Decompression failed for ${name}: ${e.message}`);
292
+ const errorMsg = `Decompression failed for ${name}: ${e.message}`;
293
+ if (config.logger) {
294
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
295
+ } else {
296
+ console.error(`[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
297
+ }
187
298
  return null;
188
299
  }
189
300
  }
190
301
 
191
- // 2. Handle Sharding
302
+ // 2. Handle Sharding (MUST happen before empty check)
192
303
  if (data._sharded) {
193
304
  const shardPath = `${path}/_shards`;
194
305
  if (config.logger) {
@@ -200,21 +311,95 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
200
311
  const shardCol = docRef.collection('_shards');
201
312
  const shardSnaps = await shardCol.get();
202
313
 
203
- if (!shardSnaps.empty) {
204
- shardSnaps.forEach(shard => {
205
- const shardData = shard.data();
206
- const shardId = shard.id;
207
- if (config.logger) {
208
- config.logger.log('TRACE', `[DependencyFetcher] 📂 Loading Shard '${shardId}' for '${name}' from: ${shardPath}/${shardId}`);
209
- }
210
- // Merge shard contents, ignoring internal metadata if it clashes
211
- Object.entries(shardData).forEach(([k, v]) => {
212
- if (!k.startsWith('_')) {
213
- data[k] = v;
314
+ if (shardSnaps.empty) {
315
+ // No shards found - this is a problem
316
+ if (config.logger) {
317
+ config.logger.log('ERROR', `[DependencyFetcher] Document marked as sharded but no shards found at: ${shardPath}`);
318
+ } else {
319
+ console.error(`[DependencyFetcher] Document marked as sharded but no shards found at: ${shardPath}`);
320
+ }
321
+ return null; // Return null so it gets caught as missing
322
+ }
323
+
324
+ // Merge shard contents
325
+ let hasData = false;
326
+ shardSnaps.forEach(shard => {
327
+ let shardData = shard.data();
328
+ const shardId = shard.id;
329
+ if (config.logger) {
330
+ config.logger.log('TRACE', `[DependencyFetcher] 📂 Loading Shard '${shardId}' for '${name}' from: ${shardPath}/${shardId}`);
331
+ }
332
+
333
+ // CRITICAL: Shards themselves can be compressed (common in big data)
334
+ // Decompress the shard if needed before merging
335
+ if (shardData._compressed && shardData.payload) {
336
+ try {
337
+ const buffer = (shardData.payload instanceof Buffer) ? shardData.payload :
338
+ (shardData.payload._byteString ? Buffer.from(shardData.payload._byteString, 'base64') :
339
+ Buffer.from(shardData.payload));
340
+ const decompressed = zlib.gunzipSync(buffer);
341
+ const jsonStr = decompressed.toString('utf8');
342
+ const realData = JSON.parse(jsonStr);
343
+ // If it's double-encoded, parse again
344
+ const parsedData = (typeof realData === 'string') ? JSON.parse(realData) : realData;
345
+ shardData = { ...shardData, ...parsedData };
346
+ delete shardData.payload;
347
+ } catch (e) {
348
+ if (config.logger) {
349
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ Failed to decompress shard '${shardId}' for '${name}': ${e.message}`);
350
+ } else {
351
+ console.error(`[DependencyFetcher] ❌ Failed to decompress shard '${shardId}' for '${name}': ${e.message}`);
214
352
  }
215
- });
353
+ // Continue with uncompressed data if decompression fails
354
+ }
355
+ }
356
+
357
+ // Merge shard contents, ignoring internal metadata if it clashes
358
+ Object.entries(shardData).forEach(([k, v]) => {
359
+ if (!k.startsWith('_')) {
360
+ data[k] = v;
361
+ hasData = true;
362
+ }
216
363
  });
364
+ });
365
+
366
+ // If shards contained no actual data, treat as empty
367
+ if (!hasData) {
368
+ if (config.logger) {
369
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
370
+ } else {
371
+ console.error(`[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
372
+ }
373
+ return null;
374
+ }
375
+
376
+ // After loading shards, remove shard metadata from data object for cleaner output
377
+ // Keep only the actual data fields
378
+ const cleanedData = {};
379
+ const dataKeys = [];
380
+ Object.entries(data).forEach(([k, v]) => {
381
+ if (!k.startsWith('_')) {
382
+ cleanedData[k] = v;
383
+ dataKeys.push(k);
384
+ }
385
+ });
386
+ data = cleanedData;
387
+
388
+ // Log what we loaded for debugging
389
+ if (config.logger) {
390
+ config.logger.log('INFO', `[DependencyFetcher] ✅ Loaded ${shardSnaps.size} shard(s) for '${name}'. Data fields: ${dataKeys.length > 0 ? dataKeys.slice(0, 10).join(', ') + (dataKeys.length > 10 ? `... (+${dataKeys.length - 10} more)` : '') : 'none'}`);
391
+ }
392
+ }
393
+
394
+ // Final validation: ensure we have usable data after all processing (decompression + sharding)
395
+ // Only check if we haven't already determined it's empty
396
+ if (isDataEmpty(data)) {
397
+ if (config.logger) {
398
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
399
+ } else {
400
+ console.error(`[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
217
401
  }
402
+ return null;
218
403
  }
219
404
 
220
405
  return data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.646",
3
+ "version": "1.0.648",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [