bulltrackers-module 1.0.231 → 1.0.233

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.
@@ -1,53 +1,144 @@
1
1
  /**
2
2
  * @fileoverview API sub-pipes.
3
- * REFACTORED: Now stateless and receive dependencies.
4
- * NEW: getDynamicSchema now reads static schema.
5
- * NEW: createManifestHandler filters out STALE schemas (>7 days old).
3
+ * REFACTORED: Fixed Category Resolution to match ManifestBuilder logic.
4
+ * Implements Status-Based Availability Caching and Smart Date Resolution.
6
5
  */
7
6
 
8
7
  const { FieldPath } = require('@google-cloud/firestore');
9
8
 
9
+ // --- AVAILABILITY CACHE ---
10
+ class AvailabilityCache {
11
+ constructor(db, logger, ttlMs = 5 * 60 * 1000) { // 5 Minute TTL
12
+ this.db = db;
13
+ this.logger = logger;
14
+ this.ttlMs = ttlMs;
15
+ this.cache = null;
16
+ this.lastFetched = 0;
17
+ this.statusCollection = 'computation_status';
18
+ }
19
+
20
+ async getMap() {
21
+ const now = Date.now();
22
+ if (this.cache && (now - this.lastFetched < this.ttlMs)) {
23
+ return this.cache;
24
+ }
25
+
26
+ this.logger.log('INFO', '[AvailabilityCache] Refreshing availability map from Firestore...');
27
+
28
+ // Fetch recent status to build the map (Limit 400 days)
29
+ const snapshot = await this.db.collection(this.statusCollection)
30
+ .orderBy(FieldPath.documentId(), 'desc')
31
+ .limit(400)
32
+ .get();
33
+
34
+ const newMap = {};
35
+
36
+ snapshot.forEach(doc => {
37
+ const dateStr = doc.id;
38
+ const statusData = doc.data();
39
+
40
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return;
41
+
42
+ for (const [calcName, status] of Object.entries(statusData)) {
43
+ if (status && status !== 'IMPOSSIBLE') {
44
+ if (!newMap[calcName]) newMap[calcName] = [];
45
+ newMap[calcName].push(dateStr);
46
+ }
47
+ }
48
+ });
49
+
50
+ this.cache = newMap;
51
+ this.lastFetched = now;
52
+ this.logger.log('INFO', `[AvailabilityCache] Refreshed. Tracked ${Object.keys(newMap).length} computations.`);
53
+ return this.cache;
54
+ }
55
+ }
10
56
 
11
57
  /**
12
- * Sub-pipe: pipe.api.helpers.validateRequest
58
+ * Helper: Resolve which dates to fetch based on mode and availability.
13
59
  */
14
- const validateRequest = (query, config) => {
15
- if (!query.computations) return "Missing 'computations' parameter.";
16
- if (!query.startDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.startDate)) return "Missing or invalid 'startDate'.";
17
- if (!query.endDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.endDate)) return "Missing or invalid 'endDate'.";
18
-
19
- const start = new Date(query.startDate);
20
- const end = new Date(query.endDate);
21
-
22
- if (end < start) return "'endDate' must be after 'startDate'.";
60
+ async function resolveTargetDates(availabilityCache, computationKeys, mode, limit) {
61
+ const map = await availabilityCache.getMap();
23
62
 
24
- const maxDateRange = config.maxDateRange || 100;
25
- const diffTime = Math.abs(end - start);
26
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
63
+ const relevantDatesSet = new Set();
64
+ computationKeys.forEach(key => {
65
+ const dates = map[key] || [];
66
+ dates.forEach(d => relevantDatesSet.add(d));
67
+ });
68
+
69
+ const sortedDates = Array.from(relevantDatesSet).sort((a, b) => b.localeCompare(a));
70
+
71
+ if (sortedDates.length === 0) return [];
72
+
73
+ if (mode === 'latest') {
74
+ return [sortedDates[0]];
75
+ }
27
76
 
28
- if (diffDays > maxDateRange) return `Date range cannot exceed ${maxDateRange} days.`;
77
+ if (mode === 'series') {
78
+ return sortedDates.slice(0, limit);
79
+ }
80
+
81
+ return [];
82
+ }
83
+
84
+ /**
85
+ * Sub-pipe: pipe.api.helpers.validateRequest
86
+ */
87
+ const validateRequest = (query) => {
88
+ if (!query.computations) return "Missing 'computations' parameter.";
29
89
 
90
+ const allowedModes = ['latest', 'series'];
91
+ if (query.mode && !allowedModes.includes(query.mode)) {
92
+ return "Invalid 'mode'. Must be 'latest' or 'series'.";
93
+ }
94
+
95
+ if (query.mode === 'series') {
96
+ const limit = parseInt(query.limit);
97
+ if (query.limit && (isNaN(limit) || limit < 1 || limit > 365)) {
98
+ return "Invalid 'limit'. Must be between 1 and 365.";
99
+ }
100
+ }
101
+
30
102
  return null;
31
103
  };
32
104
 
33
105
  /**
34
106
  * Sub-pipe: pipe.api.helpers.buildCalculationMap
35
- * --- CRITICAL UPDATE ---
36
- * This function now stores the class itself in the map,
37
- * which is required by the /manifest/generate endpoint.
107
+ * FIX APPLIED: Checks class metadata for category override (mirroring ManifestBuilder).
38
108
  */
39
109
  const buildCalculationMap = (unifiedCalculations) => {
40
110
  const calcMap = {};
111
+
112
+ // Helper to resolve category exactly like ManifestBuilder.js does
113
+ const resolveCategory = (cls, folderName) => {
114
+ if (typeof cls.getMetadata === 'function') {
115
+ const meta = cls.getMetadata();
116
+ // If folder is 'core', allow metadata to override
117
+ if (folderName === 'core' && meta.category) {
118
+ return meta.category;
119
+ }
120
+ }
121
+ return folderName;
122
+ };
123
+
41
124
  for (const category in unifiedCalculations) {
42
125
  for (const subKey in unifiedCalculations[category]) {
43
126
  const item = unifiedCalculations[category][subKey];
127
+
44
128
  if (subKey === 'historical' && typeof item === 'object') {
45
129
  for (const calcName in item) {
46
- calcMap[calcName] = { category: category, class: item[calcName] };
130
+ const cls = item[calcName];
131
+ calcMap[calcName] = {
132
+ category: resolveCategory(cls, category),
133
+ class: cls
134
+ };
47
135
  }
48
136
  } else if (typeof item === 'function') {
49
137
  const calcName = subKey;
50
- calcMap[calcName] = { category: category, class: item };
138
+ calcMap[calcName] = {
139
+ category: resolveCategory(item, category),
140
+ class: item
141
+ };
51
142
  }
52
143
  }
53
144
  }
@@ -55,21 +146,7 @@ const buildCalculationMap = (unifiedCalculations) => {
55
146
  };
56
147
 
57
148
  /**
58
- * Internal helper for date strings.
59
- */
60
- const getDateStringsInRange = (startDate, endDate) => {
61
- const dates = [];
62
- const current = new Date(startDate + 'T00:00:00Z');
63
- const end = new Date(endDate + 'T00:00:00Z');
64
- while (current <= end) {
65
- dates.push(current.toISOString().slice(0, 10));
66
- current.setUTCDate(current.getUTCDate() + 1);
67
- }
68
- return dates;
69
- };
70
-
71
- /**
72
- * Sub-pipe: pipe.api.helpers.fetchData
149
+ * Sub-pipe: pipe.api.helpers.fetchUnifiedData
73
150
  */
74
151
  const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, calcMap) => {
75
152
  const { db, logger } = dependencies;
@@ -78,11 +155,13 @@ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, cal
78
155
  const resultsSub = config.resultsSubcollection || 'results';
79
156
  const compsSub = config.computationsSubcollection || 'computations';
80
157
 
158
+ if (dateStrings.length === 0) return {};
159
+
81
160
  try {
161
+ const readPromises = [];
162
+
82
163
  for (const date of dateStrings) {
83
- response[date] = {};
84
- const docRefs = [];
85
- const keyPaths = [];
164
+ response[date] = {};
86
165
 
87
166
  for (const key of calcKeys) {
88
167
  const pathInfo = calcMap[key];
@@ -90,18 +169,27 @@ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, cal
90
169
  const docRef = db.collection(insightsCollection).doc(date)
91
170
  .collection(resultsSub).doc(pathInfo.category)
92
171
  .collection(compsSub).doc(key);
93
- docRefs.push(docRef);
94
- keyPaths.push(key);
172
+
173
+ readPromises.push({ date, key, ref: docRef });
95
174
  } else {
96
- logger.log('WARN', `[${date}] No path info found for computation key: ${key}`);
175
+ // If no path info, we can't fetch.
176
+ response[date][key] = null;
97
177
  }
98
178
  }
179
+ }
180
+
181
+ if (readPromises.length === 0) return response;
182
+
183
+ // Batch reads
184
+ const CHUNK_SIZE = 100;
185
+ for (let i = 0; i < readPromises.length; i += CHUNK_SIZE) {
186
+ const chunk = readPromises.slice(i, i + CHUNK_SIZE);
187
+ const refs = chunk.map(item => item.ref);
99
188
 
100
- if (docRefs.length === 0) continue;
189
+ const snapshots = await db.getAll(...refs);
101
190
 
102
- const snapshots = await db.getAll(...docRefs);
103
- snapshots.forEach((doc, i) => {
104
- const key = keyPaths[i];
191
+ snapshots.forEach((doc, idx) => {
192
+ const { date, key } = chunk[idx];
105
193
  if (doc.exists) {
106
194
  response[date][key] = doc.data();
107
195
  } else {
@@ -109,6 +197,7 @@ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, cal
109
197
  }
110
198
  });
111
199
  }
200
+
112
201
  } catch (error) {
113
202
  logger.log('ERROR', 'API: Error fetching data from Firestore.', { errorMessage: error.message });
114
203
  throw new Error('Failed to retrieve computation data.');
@@ -120,24 +209,53 @@ const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, cal
120
209
  * Factory for the main API handler.
121
210
  */
122
211
  const createApiHandler = (config, dependencies, calcMap) => {
123
- const { logger } = dependencies;
212
+ const { logger, db } = dependencies;
213
+
214
+ // Singleton Cache
215
+ const availabilityCache = new AvailabilityCache(db, logger);
216
+
124
217
  return async (req, res) => {
125
- const validationError = validateRequest(req.query, config);
218
+ const validationError = validateRequest(req.query);
126
219
  if (validationError) {
127
220
  logger.log('WARN', 'API Bad Request', { error: validationError, query: req.query });
128
221
  return res.status(400).send({ status: 'error', message: validationError });
129
222
  }
223
+
130
224
  try {
131
225
  const computationKeys = req.query.computations.split(',');
132
- const dateStrings = getDateStringsInRange(req.query.startDate, req.query.endDate);
226
+ const mode = req.query.mode || 'latest';
227
+ const limit = parseInt(req.query.limit) || 30;
228
+
229
+ // 1. Resolve Dates
230
+ const dateStrings = await resolveTargetDates(availabilityCache, computationKeys, mode, limit);
231
+
232
+ if (dateStrings.length === 0) {
233
+ return res.status(200).send({
234
+ status: 'success',
235
+ metadata: {
236
+ computations: computationKeys,
237
+ mode,
238
+ count: 0,
239
+ dates: []
240
+ },
241
+ data: {}
242
+ });
243
+ }
244
+
245
+ // 2. Fetch Data
133
246
  const data = await fetchUnifiedData(config, dependencies, computationKeys, dateStrings, calcMap);
247
+
134
248
  res.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
135
249
  res.status(200).send({
136
250
  status: 'success',
137
251
  metadata: {
138
252
  computations: computationKeys,
139
- startDate: req.query.startDate,
140
- endDate: req.query.endDate,
253
+ mode,
254
+ limit: mode === 'series' ? limit : 1,
255
+ dateRange: {
256
+ start: dateStrings[dateStrings.length - 1],
257
+ end: dateStrings[0]
258
+ }
141
259
  },
142
260
  data,
143
261
  });
@@ -148,9 +266,6 @@ const createApiHandler = (config, dependencies, calcMap) => {
148
266
  };
149
267
  };
150
268
 
151
- /**
152
- * Internal helper for snippet generation.
153
- */
154
269
  function createStructureSnippet(data, maxKeys = 20) {
155
270
  if (data === null || typeof data !== 'object') {
156
271
  if (typeof data === 'number') return 0;
@@ -184,9 +299,6 @@ function createStructureSnippet(data, maxKeys = 20) {
184
299
  return newObj;
185
300
  }
186
301
 
187
- /**
188
- * Sub-pipe: pipe.api.helpers.getComputationStructure
189
- */
190
302
  async function getComputationStructure(computationName, calcMap, config, dependencies) {
191
303
  const { db, logger } = dependencies;
192
304
  try {
@@ -218,10 +330,6 @@ async function getComputationStructure(computationName, calcMap, config, depende
218
330
  }
219
331
  }
220
332
 
221
-
222
- /**
223
- * --- UPDATED: DYNAMIC SCHEMA GENERATION HARNESS ---
224
- */
225
333
  async function getDynamicSchema(CalcClass, calcName) {
226
334
  if (CalcClass && typeof CalcClass.getSchema === 'function') {
227
335
  try {
@@ -235,47 +343,31 @@ async function getDynamicSchema(CalcClass, calcName) {
235
343
  }
236
344
  }
237
345
 
238
-
239
- /**
240
- * --- NEW: MANIFEST API HANDLER (With Filtering) ---
241
- */
242
346
  const createManifestHandler = (config, dependencies, calcMap) => {
243
347
  const { db, logger } = dependencies;
244
348
  const schemaCollection = config.schemaCollection || 'computation_schemas';
245
349
 
246
350
  return async (req, res) => {
247
351
  try {
248
- logger.log('INFO', '[API /manifest] Fetching all computation schemas...');
249
352
  const snapshot = await db.collection(schemaCollection).get();
250
-
251
- if (snapshot.empty) {
252
- logger.log('WARN', '[API /manifest] No schemas found in collection.');
253
- return res.status(404).send({ status: 'error', message: 'No computation schemas have been generated yet.' });
254
- }
353
+ if (snapshot.empty) { return res.status(404).send({ status: 'error', message: 'No computation schemas have been generated yet.' }); }
255
354
 
256
- // --- FILTERING LOGIC ---
257
355
  const manifest = {};
258
356
  const now = Date.now();
259
- const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
357
+ const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
260
358
 
261
359
  let activeCount = 0;
262
360
  let staleCount = 0;
263
361
 
264
362
  snapshot.forEach(doc => {
265
363
  const data = doc.data();
266
-
267
- // Safe Timestamp conversion
268
364
  let lastUpdatedMs = 0;
269
365
  if (data.lastUpdated && typeof data.lastUpdated.toMillis === 'function') {
270
366
  lastUpdatedMs = data.lastUpdated.toMillis();
271
367
  } else if (data.lastUpdated instanceof Date) {
272
368
  lastUpdatedMs = data.lastUpdated.getTime();
273
- } else {
274
- // Fallback for very old records without a timestamp
275
- lastUpdatedMs = 0;
276
369
  }
277
370
 
278
- // Exclude stale records
279
371
  if ((now - lastUpdatedMs) < MAX_AGE_MS) {
280
372
  manifest[doc.id] = {
281
373
  category: data.category,
@@ -284,16 +376,9 @@ const createManifestHandler = (config, dependencies, calcMap) => {
284
376
  lastUpdated: data.lastUpdated
285
377
  };
286
378
  activeCount++;
287
- } else {
288
- staleCount++;
289
- }
379
+ } else { staleCount++; }
290
380
  });
291
381
 
292
- // Log filtering results
293
- if (staleCount > 0) {
294
- logger.log('INFO', `[API /manifest] Filtered out ${staleCount} stale schemas (older than 7 days). Returning ${activeCount} active.`);
295
- }
296
-
297
382
  res.status(200).send({
298
383
  status: 'success',
299
384
  summary: {
@@ -301,21 +386,18 @@ const createManifestHandler = (config, dependencies, calcMap) => {
301
386
  totalComputations: snapshot.size,
302
387
  schemasAvailable: activeCount,
303
388
  schemasFiltered: staleCount,
304
- lastUpdated: Math.max(...Object.values(manifest).map(m =>
305
- (m.lastUpdated && m.lastUpdated.toMillis) ? m.lastUpdated.toMillis() : 0
306
- ), 0)
389
+ lastUpdated: Date.now()
307
390
  },
308
391
  manifest: manifest
309
392
  });
310
393
 
311
394
  } catch (error) {
312
- logger.log('ERROR', 'API /manifest handler failed.', { errorMessage: error.message, stack: error.stack });
395
+ logger.log('ERROR', 'API /manifest handler failed.', { errorMessage: error.message });
313
396
  res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
314
397
  }
315
398
  };
316
399
  };
317
400
 
318
-
319
401
  module.exports = {
320
402
  validateRequest,
321
403
  buildCalculationMap,
@@ -1,142 +1,190 @@
1
1
  /**
2
2
  * @fileoverview Main entry point for the Generic API module.
3
- * Exports the 'createApiApp' main pipe function.
4
- * REFACTORED: /manifest endpoint now reads static schemas from Firestore.
5
- * REFACTORED: /manifest/generate endpoint now reads static schema from class.
6
- *
7
- * --- MODIFIED: Added in-memory cache wrapper for the main API handler ---
3
+ * Export the 'createApiApp' main pipe function.
4
+ * REFACTORED: API V3 - Status-Aware Data Fetching.
8
5
  */
9
6
 
10
7
  const express = require('express');
11
8
  const cors = require('cors');
12
- const { FieldPath } = require('@google-cloud/firestore');
13
- const { buildCalculationMap, createApiHandler, getComputationStructure,createManifestHandler, getDynamicSchema } = require('./helpers/api_helpers.js');
9
+ const { buildCalculationMap, createApiHandler, getComputationStructure, createManifestHandler, getDynamicSchema } = require('./helpers/api_helpers.js');
14
10
 
15
11
  /**
16
- * --- NEW: In-Memory Cache Handler ---
17
- * A wrapper function that adds a time-to-live (TTL) in-memory cache
18
- * to any Express request handler.
19
- * @param {function} handler - The original (req, res) handler to wrap.
20
- * @param {object} dependencies - { logger }
21
- * @returns {function} The new (req, res) handler with caching logic.
12
+ * In-Memory Cache Handler
13
+ * Wrapper that adds TTL cache to GET requests.
22
14
  */
23
15
  const createCacheHandler = (handler, { logger }) => {
24
- // 1. Cache
25
16
  const CACHE = {};
26
- const CACHE_TTL_MS = 10 * 60 * 1000;
27
- // 2. Return the new handler
28
- return async (req, res) => { const cacheKey = req.url; const now = Date.now();
29
- // 3. --- Cache HIT ---
30
- if (CACHE[cacheKey] && (now - CACHE[cacheKey].timestamp) < CACHE_TTL_MS) {
31
- logger.log('INFO', `[API] Cache HIT for ${cacheKey}`);
32
- return res.status(CACHE[cacheKey].status).send(CACHE[cacheKey].data); }
33
- // 4. --- Cache MISS ---
34
- logger.log('INFO', `[API] Cache MISS for ${cacheKey}`);
35
- const originalSend = res.send;
36
- const originalStatus = res.status;
37
- let capturedData = null;
38
- let capturedStatus = 200;
39
- res.status = (statusCode) => { capturedStatus = statusCode; return originalStatus.call(res, statusCode); };
40
- res.send = (data) => { capturedData = data; return originalSend.call(res, data); };
41
- // 5. Call the original handler (which will now use our patched functions)
42
- await handler(req, res);
43
- // 6. If the response was successful, cache it
44
- if (capturedStatus === 200 && capturedData) {
45
- logger.log('INFO', `[API] Caching new entry for ${cacheKey}`);
46
- CACHE[cacheKey] = { data: capturedData, status: capturedStatus, timestamp: now }; } };
17
+ const CACHE_TTL_MS = 10 * 60 * 1000; // 10 Minutes
18
+
19
+ return async (req, res) => {
20
+ // Cache Key now includes mode and limit
21
+ const cacheKey = req.url;
22
+ const now = Date.now();
23
+
24
+ if (CACHE[cacheKey] && (now - CACHE[cacheKey].timestamp) < CACHE_TTL_MS) {
25
+ logger.log('INFO', `[API] Cache HIT for ${cacheKey}`);
26
+ return res.status(CACHE[cacheKey].status).send(CACHE[cacheKey].data);
27
+ }
28
+
29
+ logger.log('INFO', `[API] Cache MISS for ${cacheKey}`);
30
+
31
+ const originalSend = res.send;
32
+ const originalStatus = res.status;
33
+ let capturedData = null;
34
+ let capturedStatus = 200;
35
+
36
+ res.status = (statusCode) => {
37
+ capturedStatus = statusCode;
38
+ return originalStatus.call(res, statusCode);
39
+ };
40
+
41
+ res.send = (data) => {
42
+ capturedData = data;
43
+ return originalSend.call(res, data);
44
+ };
45
+
46
+ await handler(req, res);
47
+
48
+ if (capturedStatus === 200 && capturedData) {
49
+ logger.log('INFO', `[API] Caching new entry for ${cacheKey}`);
50
+ CACHE[cacheKey] = {
51
+ data: capturedData,
52
+ status: capturedStatus,
53
+ timestamp: now
54
+ };
55
+ }
56
+ };
47
57
  };
48
58
 
49
59
 
50
60
  /**
51
61
  * Main pipe: pipe.api.createApiApp
52
- * Creates and configures the Express app for the Generic API.
53
- * @param {object} config - The Generic API V2 configuration object.
54
- * @param {object} dependencies - Shared dependencies { db, logger }.
55
- * @param {Object} unifiedCalculations - The calculations manifest from 'aiden-shared-calculations-unified'.
56
- * @returns {express.Application} The configured Express app.
57
62
  */
58
63
  function createApiApp(config, dependencies, unifiedCalculations) {
59
64
  const app = express();
60
65
  const { logger, db } = dependencies;
61
66
 
62
- // --- Pre-compute Calculation Map (now includes classes) ---
67
+ // Build Calc Map once
63
68
  const calcMap = buildCalculationMap(unifiedCalculations);
64
69
 
65
- // --- Middleware ---
70
+ // Middleware
66
71
  app.use(cors({ origin: true }));
67
72
  app.use(express.json());
68
73
 
69
- // --- Main API Endpoint ---
74
+ // --- Main API V3 Endpoint ---
75
+ // createApiHandler now initializes the AvailabilityCache internally
70
76
  const originalApiHandler = createApiHandler(config, dependencies, calcMap);
71
77
  const cachedApiHandler = createCacheHandler(originalApiHandler, dependencies);
78
+
79
+ // This handler now supports ?mode=latest and ?mode=series&limit=X
72
80
  app.get('/', cachedApiHandler);
73
81
 
74
- // --- Health Check Endpoint ---
82
+ // Health Check
75
83
  app.get('/health', (req, res) => { res.status(200).send('OK'); });
76
84
 
77
- // --- Debug Endpoint to list all computation keys ---
85
+ // Debug: List keys
78
86
  app.get('/list-computations', (req, res) => {
79
- try { const computationKeys = Object.keys(calcMap);
80
- res.status(200).send({ status: 'success', count: computationKeys.length, computations: computationKeys.sort(), });
81
- } catch (error) { logger.log('ERROR', 'API /list-computations failed.', { errorMessage: error.message });
82
- res.status(500).send({ status: 'error', message: 'An internal error occurred.' }); } });
87
+ try {
88
+ const computationKeys = Object.keys(calcMap);
89
+ res.status(200).send({
90
+ status: 'success',
91
+ count: computationKeys.length,
92
+ computations: computationKeys.sort(),
93
+ });
94
+ } catch (error) {
95
+ logger.log('ERROR', 'API /list-computations failed.', { errorMessage: error.message });
96
+ res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
97
+ }
98
+ });
83
99
 
84
- // --- Debug Endpoint to get *stored* structure from Firestore ---
100
+ // Structure Inspection
85
101
  app.get('/structure/:computationName', async (req, res) => {
86
102
  const { computationName } = req.params;
87
103
  const result = await getComputationStructure(computationName, calcMap, config, dependencies);
88
- if (result.status === 'error') { const statusCode = result.message.includes('not found') ? 404 : 500;
89
- return res.status(statusCode).send(result); }
90
- res.status(200).send(result); });
104
+ if (result.status === 'error') {
105
+ const statusCode = result.message.includes('not found') ? 404 : 500;
106
+ return res.status(statusCode).send(result);
107
+ }
108
+ res.status(200).send(result);
109
+ });
110
+
111
+ // Manifests (Schema Generation)
91
112
  app.get('/manifest', createManifestHandler(config, dependencies, calcMap));
113
+
114
+ // Manual Schema Gen Trigger
92
115
  app.post('/manifest/generate/:computationName', async (req, res) => {
93
116
  const { computationName } = req.params;
94
117
  logger.log('INFO', `Manual static schema generation requested for: ${computationName}`);
95
118
 
96
119
  try {
97
- // 1. Find the calculation class from the calcMap
98
120
  const calcInfo = calcMap[computationName];
99
- if (!calcInfo || !calcInfo.class) { return res.status(404).send({ status: 'error', message: `Computation '${computationName}' not found or has no class in calculation map.` }); }
121
+ if (!calcInfo || !calcInfo.class) {
122
+ return res.status(404).send({ status: 'error', message: `Computation '${computationName}' not found.` });
123
+ }
100
124
  const targetCalcClass = calcInfo.class;
101
125
  const targetCategory = calcInfo.category;
102
126
 
103
- // 2. Use the getDynamicSchema helper (which now just reads the static method)
104
127
  const schemaStructure = await getDynamicSchema(targetCalcClass, computationName);
105
- if (schemaStructure.ERROR) { return res.status(400).send({ status: 'error', message: `Failed to get static schema: ${schemaStructure.ERROR}` }); }
128
+ if (schemaStructure.ERROR) {
129
+ return res.status(400).send({ status: 'error', message: `Failed to get static schema: ${schemaStructure.ERROR}` });
130
+ }
106
131
 
107
- // 3. Import the new batchStoreSchemas utility
108
132
  const { batchStoreSchemas } = require('../computation-system/utils/schema_capture.js');
109
133
 
110
- // 4. Get metadata (as much as we can from the class)
111
- const metadata = { isHistorical: !!(targetCalcClass.toString().includes('yesterdayPortfolio')), dependencies: (typeof targetCalcClass.getDependencies === 'function') ? targetCalcClass.getDependencies() : [], rootDataDependencies: [], pass: 'unknown', type: (targetCategory === 'meta' || targetCategory === 'socialPosts') ? targetCategory : 'standard', note: "Manually generated via API" };
112
-
113
- // 5. Store the schema in Firestore
114
- await batchStoreSchemas( dependencies, config, [{ name: computationName, category: targetCategory, schema: schemaStructure, metadata: metadata }] );
115
-
116
- // 6. Respond with the schema
117
- res.status(200).send({ status: 'success', message: `Static schema read and stored for ${computationName}`, computation: computationName, category: targetCategory, schema: schemaStructure });
134
+ const metadata = {
135
+ isHistorical: !!(targetCalcClass.toString().includes('yesterdayPortfolio')),
136
+ dependencies: (typeof targetCalcClass.getDependencies === 'function') ? targetCalcClass.getDependencies() : [],
137
+ rootDataDependencies: [],
138
+ type: (targetCategory === 'meta' || targetCategory === 'socialPosts') ? targetCategory : 'standard',
139
+ note: "Manually generated via API"
140
+ };
141
+
142
+ await batchStoreSchemas(dependencies, config, [{
143
+ name: computationName,
144
+ category: targetCategory,
145
+ schema: schemaStructure,
146
+ metadata: metadata
147
+ }]);
148
+
149
+ res.status(200).send({
150
+ status: 'success',
151
+ message: `Static schema read and stored for ${computationName}`,
152
+ computation: computationName,
153
+ category: targetCategory,
154
+ schema: schemaStructure
155
+ });
118
156
 
119
157
  } catch (error) {
120
- logger.log('ERROR', `Failed to generate schema for ${computationName}`, { errorMessage: error.message, stack: error.stack });
121
- res.status(5.00).send({ status: 'error', message: `Failed to generate/store schema: ${error.message}` });
158
+ logger.log('ERROR', `Failed to generate schema for ${computationName}`, { errorMessage: error.message });
159
+ res.status(500).send({ status: 'error', message: `Failed: ${error.message}` });
122
160
  }
123
161
  });
124
162
 
125
- /**
126
- * This endpoint is fine as-is. It reads from the Firestore
127
- * collection that the /manifest and /manifest/generate routes populate.
128
- */
163
+ // Single Manifest Get
129
164
  app.get('/manifest/:computationName', async (req, res) => {
130
165
  const { computationName } = req.params;
131
166
  try {
132
167
  const schemaCollection = config.schemaCollection || 'computation_schemas';
133
168
  const schemaDoc = await db.collection(schemaCollection).doc(computationName).get();
134
- if (!schemaDoc.exists) { return res.status(4404).send({ status: 'error', message: `Schema not found for computation: ${computationName}`, hint: 'Try running the computation system or use POST /manifest/generate/:computationName' }); }
169
+ if (!schemaDoc.exists) {
170
+ return res.status(404).send({ status: 'error', message: `Schema not found for ${computationName}` });
171
+ }
135
172
  const data = schemaDoc.data();
136
- res.status(200).send({ status: 'success', computation: computationName, category: data.category, structure: data.schema, metadata: data.metadata || {}, lastUpdated: data.lastUpdated });
137
- } catch (error) { logger.log('ERROR', `Failed to fetch schema for ${computationName}`, { errorMessage: error.message }); res.status(500).send({ status: 'error', message: 'An internal error occurred.' }); }});
173
+ res.status(200).send({
174
+ status: 'success',
175
+ computation: computationName,
176
+ category: data.category,
177
+ structure: data.schema,
178
+ metadata: data.metadata || {},
179
+ lastUpdated: data.lastUpdated
180
+ });
181
+ } catch (error) {
182
+ logger.log('ERROR', `Failed to fetch schema for ${computationName}`, { errorMessage: error.message });
183
+ res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
184
+ }
185
+ });
186
+
138
187
  return app;
139
188
  }
140
189
 
141
-
142
190
  module.exports = { createApiApp, helpers: require('./helpers/api_helpers.js') };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.231",
3
+ "version": "1.0.233",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -1,146 +0,0 @@
1
- /**
2
- * FIXED: computation_controller.js
3
- * V5.1: Exports LEGACY_MAPPING for Manifest Builder
4
- */
5
-
6
- // Load all layers dynamically from the index
7
- const mathLayer = require('../layers/index');
8
- const { loadDailyInsights, loadDailySocialPostInsights, getRelevantShardRefs, getPriceShardRefs } = require('../utils/data_loader');
9
-
10
- // Legacy Keys Mapping (Ensures backward compatibility with existing Calculations)
11
- // Maps the new modular class names to the property names expected by existing code (e.g. math.extract)
12
- const LEGACY_MAPPING = {
13
- DataExtractor: 'extract',
14
- HistoryExtractor: 'history',
15
- MathPrimitives: 'compute',
16
- Aggregators: 'aggregate',
17
- Validators: 'validate',
18
- SignalPrimitives: 'signals',
19
- SCHEMAS: 'schemas',
20
- DistributionAnalytics: 'distribution',
21
- TimeSeries: 'TimeSeries',
22
- priceExtractor: 'priceExtractor',
23
- InsightsExtractor: 'insights',
24
- UserClassifier: 'classifier',
25
- Psychometrics: 'psychometrics',
26
- CognitiveBiases: 'bias',
27
- SkillAttribution: 'skill',
28
- ExecutionAnalytics: 'execution',
29
- AdaptiveAnalytics: 'adaptive'
30
- };
31
-
32
- class DataLoader {
33
- constructor(config, dependencies) { this.config = config; this.deps = dependencies; this.cache = { mappings: null, insights: new Map(), social: new Map(), prices: null }; }
34
- get mappings() { return this.cache.mappings; }
35
- async loadMappings() { if (this.cache.mappings) return this.cache.mappings; const { calculationUtils } = this.deps; this.cache.mappings = await calculationUtils.loadInstrumentMappings(); return this.cache.mappings; }
36
- async loadInsights(dateStr) { if (this.cache.insights.has(dateStr)) return this.cache.insights.get(dateStr); const insights = await loadDailyInsights(this.config, this.deps, dateStr); this.cache.insights.set(dateStr, insights); return insights; }
37
- async loadSocial(dateStr) { if (this.cache.social.has(dateStr)) return this.cache.social.get(dateStr); const social = await loadDailySocialPostInsights(this.config, this.deps, dateStr); this.cache.social.set(dateStr, social); return social; }
38
- async getPriceShardReferences() { return getPriceShardRefs(this.config, this.deps); }
39
- async getSpecificPriceShardReferences (targetInstrumentIds) { return getRelevantShardRefs(this.config, this.deps, targetInstrumentIds); }
40
- async loadPriceShard(docRef) { try { const snap = await docRef.get(); if (!snap.exists) return {}; return snap.data(); } catch (e) { console.error(`Error loading shard ${docRef.path}:`, e); return {}; } }
41
- }
42
-
43
- class ContextBuilder {
44
- static buildMathContext() {
45
- const mathContext = {};
46
- for (const [key, value] of Object.entries(mathLayer)) { mathContext[key] = value; const legacyKey = LEGACY_MAPPING[key]; if (legacyKey) { mathContext[legacyKey] = value; } }
47
- return mathContext;
48
- }
49
- static buildPerUserContext(options) {
50
- const { todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory, userId, userType, dateStr, metadata, mappings, insights, socialData, computedDependencies, previousComputedDependencies, config, deps } = options;
51
- return {
52
- user: { id: userId, type: userType, portfolio: { today: todayPortfolio, yesterday: yesterdayPortfolio }, history: { today: todayHistory, yesterday: yesterdayHistory } },
53
- date: { today: dateStr },
54
- insights: { today: insights?.today, yesterday: insights?.yesterday },
55
- social: { today: socialData?.today, yesterday: socialData?.yesterday },
56
- mappings: mappings || {},
57
- math: ContextBuilder.buildMathContext(),
58
- computed: computedDependencies || {},
59
- previousComputed: previousComputedDependencies || {},
60
- meta: metadata, config, deps
61
- };
62
- }
63
-
64
- static buildMetaContext(options) {
65
- const { dateStr, metadata, mappings, insights, socialData, prices, computedDependencies, previousComputedDependencies, config, deps } = options;
66
- return {
67
- date: { today: dateStr },
68
- insights: { today: insights?.today, yesterday: insights?.yesterday },
69
- social: { today: socialData?.today, yesterday: socialData?.yesterday },
70
- prices: prices || {},
71
- mappings: mappings || {},
72
- math: ContextBuilder.buildMathContext(),
73
- computed: computedDependencies || {},
74
- previousComputed: previousComputedDependencies || {},
75
- meta: metadata, config, deps
76
- };
77
- }
78
- }
79
-
80
- class ComputationExecutor {
81
- constructor(config, dependencies, dataLoader) {
82
- this.config = config;
83
- this.deps = dependencies;
84
- this.loader = dataLoader;
85
- }
86
-
87
- async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
88
- const { logger } = this.deps;
89
- const targetUserType = metadata.userType;
90
- const mappings = await this.loader.loadMappings();
91
- const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
92
-
93
- // Access SCHEMAS dynamically from the loaded layer
94
- const SCHEMAS = mathLayer.SCHEMAS;
95
-
96
- for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
97
- const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
98
- const todayHistory = historyData ? historyData[userId] : null;
99
- const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
100
- if (targetUserType !== 'all') {
101
- const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
102
- if (mappedTarget !== actualUserType) continue;
103
- }
104
- const context = ContextBuilder.buildPerUserContext({ todayPortfolio, yesterdayPortfolio, todayHistory, userId, userType: actualUserType, dateStr, metadata, mappings, insights, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
105
- try { await calcInstance.process(context); } catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
106
- }
107
- }
108
-
109
- async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps) {
110
- const mappings = await this.loader.loadMappings();
111
- const { logger } = this.deps;
112
- const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
113
- const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
114
-
115
- if (metadata.rootDataDependencies?.includes('price')) {
116
- logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
117
- const shardRefs = await this.loader.getPriceShardReferences();
118
- if (shardRefs.length === 0) { logger.log('WARN', '[Executor] No price shards found.'); return {}; }
119
- let processedCount = 0;
120
- for (const ref of shardRefs) {
121
- const shardData = await this.loader.loadPriceShard(ref);
122
- const partialContext = ContextBuilder.buildMetaContext({ dateStr, metadata, mappings, insights, socialData: social, prices: { history: shardData }, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
123
- await calcInstance.process(partialContext);
124
- partialContext.prices = null;
125
- processedCount++;
126
- if (processedCount % 10 === 0) { if (global.gc) { global.gc(); } }
127
- }
128
- logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
129
- return calcInstance.getResult ? await calcInstance.getResult() : {};
130
- } else {
131
- const context = ContextBuilder.buildMetaContext({ dateStr, metadata, mappings, insights, socialData: social, prices: {}, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
132
- return await calcInstance.process(context);
133
- }
134
- }
135
- }
136
-
137
- class ComputationController {
138
- constructor(config, dependencies) {
139
- this.config = config;
140
- this.deps = dependencies;
141
- this.loader = new DataLoader(config, dependencies);
142
- this.executor = new ComputationExecutor(config, dependencies, this.loader);
143
- }
144
- }
145
-
146
- module.exports = { ComputationController, LEGACY_MAPPING };