bulltrackers-module 1.0.646 → 1.0.647

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,28 @@ 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
+ // Check if document exists but is effectively empty (only metadata)
278
+ if (isDataEmpty(data)) {
279
+ if (config.logger) {
280
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ Document exists but is empty at: ${path} (only contains metadata fields)`);
281
+ } else {
282
+ console.error(`[DependencyFetcher] ❌ Document exists but is empty at: ${path} (only contains metadata fields)`);
283
+ }
284
+ return null; // Return null so it gets caught as missing
285
+ }
286
+
175
287
  // 1. Handle Compression
176
288
  if (data._compressed && data.payload) {
177
289
  try {
@@ -183,7 +295,12 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
183
295
  data = { ...data, ...realData };
184
296
  delete data.payload;
185
297
  } catch (e) {
186
- console.warn(`[DependencyFetcher] Decompression failed for ${name}: ${e.message}`);
298
+ const errorMsg = `Decompression failed for ${name}: ${e.message}`;
299
+ if (config.logger) {
300
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
301
+ } else {
302
+ console.error(`[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
303
+ }
187
304
  return null;
188
305
  }
189
306
  }
@@ -200,21 +317,52 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
200
317
  const shardCol = docRef.collection('_shards');
201
318
  const shardSnaps = await shardCol.get();
202
319
 
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}`);
320
+ if (shardSnaps.empty) {
321
+ // No shards found - this is a problem
322
+ if (config.logger) {
323
+ config.logger.log('ERROR', `[DependencyFetcher] Document marked as sharded but no shards found at: ${shardPath}`);
324
+ } else {
325
+ console.error(`[DependencyFetcher] Document marked as sharded but no shards found at: ${shardPath}`);
326
+ }
327
+ return null; // Return null so it gets caught as missing
328
+ }
329
+
330
+ // Merge shard contents
331
+ let hasData = false;
332
+ shardSnaps.forEach(shard => {
333
+ const shardData = shard.data();
334
+ const shardId = shard.id;
335
+ if (config.logger) {
336
+ config.logger.log('TRACE', `[DependencyFetcher] 📂 Loading Shard '${shardId}' for '${name}' from: ${shardPath}/${shardId}`);
337
+ }
338
+ // Merge shard contents, ignoring internal metadata if it clashes
339
+ Object.entries(shardData).forEach(([k, v]) => {
340
+ if (!k.startsWith('_')) {
341
+ data[k] = v;
342
+ hasData = true;
209
343
  }
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;
214
- }
215
- });
216
344
  });
345
+ });
346
+
347
+ // If shards contained no actual data, treat as empty
348
+ if (!hasData) {
349
+ if (config.logger) {
350
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
351
+ } else {
352
+ console.error(`[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
353
+ }
354
+ return null;
355
+ }
356
+ }
357
+
358
+ // Final validation: ensure we have usable data after all processing
359
+ if (isDataEmpty(data)) {
360
+ if (config.logger) {
361
+ config.logger.log('ERROR', `[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
362
+ } else {
363
+ console.error(`[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
217
364
  }
365
+ return null;
218
366
  }
219
367
 
220
368
  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.647",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [