bulltrackers-module 1.0.629 → 1.0.631

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.
Files changed (42) hide show
  1. package/functions/alert-system/helpers/alert_helpers.js +69 -77
  2. package/functions/alert-system/index.js +19 -29
  3. package/functions/api-v2/helpers/notification_helpers.js +187 -0
  4. package/functions/computation-system/helpers/computation_worker.js +1 -1
  5. package/functions/task-engine/helpers/popular_investor_helpers.js +11 -7
  6. package/index.js +0 -5
  7. package/package.json +1 -2
  8. package/functions/old-generic-api/admin-api/index.js +0 -895
  9. package/functions/old-generic-api/helpers/api_helpers.js +0 -457
  10. package/functions/old-generic-api/index.js +0 -204
  11. package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
  12. package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
  13. package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
  14. package/functions/old-generic-api/user-api/helpers/collection_helpers.js +0 -193
  15. package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +0 -68
  16. package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
  17. package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
  18. package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
  19. package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +0 -503
  20. package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
  21. package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
  22. package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +0 -174
  23. package/functions/old-generic-api/user-api/helpers/data_helpers.js +0 -87
  24. package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
  25. package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
  26. package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
  27. package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
  28. package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
  29. package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
  30. package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
  31. package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
  32. package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
  33. package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
  34. package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
  35. package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
  36. package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
  37. package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
  38. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
  39. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
  40. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
  41. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
  42. package/functions/old-generic-api/user-api/index.js +0 -109
@@ -1,457 +0,0 @@
1
- /**
2
- * @fileoverview API sub-pipes.
3
- * REFACTORED: API V3 - Status-Aware Data Fetching.
4
- * UPDATED: Added GZIP Decompression support for fetching compressed results.
5
- */
6
-
7
- const { FieldPath } = require('@google-cloud/firestore');
8
- const zlib = require('zlib'); // [NEW] Required for decompression
9
-
10
- // --- HELPER: DECOMPRESSION ---
11
- /**
12
- * Checks if data is compressed and inflates it if necessary.
13
- * @param {object} data - The raw Firestore document data.
14
- * @returns {object} The original (decompressed) JSON object.
15
- */
16
- function tryDecompress(data) {
17
- if (data && data._compressed === true && data.payload) {
18
- try {
19
- // Firestore returns Buffers automatically for Blob types
20
- return JSON.parse(zlib.gunzipSync(data.payload).toString());
21
- } catch (e) {
22
- console.error('[API] Decompression failed:', e);
23
- // Return empty object or original data on failure to avoid crashing response
24
- return {};
25
- }
26
- }
27
- return data;
28
- }
29
-
30
- // --- AVAILABILITY CACHE ---
31
- class AvailabilityCache {
32
- constructor(db, logger, ttlMs = 5 * 60 * 1000) { // 5 Minute TTL
33
- this.db = db;
34
- this.logger = logger;
35
- this.ttlMs = ttlMs;
36
- this.cache = null;
37
- this.lastFetched = 0;
38
- this.statusCollection = 'computation_status';
39
- }
40
-
41
- async getMap() {
42
- const now = Date.now();
43
- if (this.cache && (now - this.lastFetched < this.ttlMs)) {
44
- return this.cache;
45
- }
46
-
47
- this.logger.log('INFO', '[AvailabilityCache] Refreshing availability map from Firestore...');
48
-
49
- // Fetch recent status to build the map (Limit 400 days)
50
- const snapshot = await this.db.collection(this.statusCollection)
51
- .orderBy(FieldPath.documentId(), 'desc')
52
- .limit(400)
53
- .get();
54
-
55
- const newMap = {};
56
-
57
- snapshot.forEach(doc => {
58
- const dateStr = doc.id;
59
- const statusData = doc.data();
60
-
61
- if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return;
62
-
63
- for (const [calcName, status] of Object.entries(statusData)) {
64
- if (status && status !== 'IMPOSSIBLE') {
65
- if (!newMap[calcName]) newMap[calcName] = [];
66
- newMap[calcName].push(dateStr);
67
- }
68
- }
69
- });
70
-
71
- this.cache = newMap;
72
- this.lastFetched = now;
73
- this.logger.log('INFO', `[AvailabilityCache] Refreshed. Tracked ${Object.keys(newMap).length} computations.`);
74
- return this.cache;
75
- }
76
- }
77
-
78
- /**
79
- * Helper: Resolve which dates to fetch based on mode and availability.
80
- * UPDATED: Added "Inferred Availability" for backfilled computations (user-history-reconstructor).
81
- */
82
- async function resolveTargetDates(availabilityCache, computationKeys, mode, limit) {
83
- const map = await availabilityCache.getMap();
84
- const relevantDatesSet = new Set();
85
-
86
- // [NEW] Special Handling for Backfilled Computations
87
- // These computations write to past dates but only update status for the current run date.
88
- // If we see a recent status, we infer that history exists backward from that point.
89
- const BACKFILL_COMPUTATIONS = ['user-history-reconstructor'];
90
- const hasBackfillCalc = computationKeys.some(k => BACKFILL_COMPUTATIONS.includes(k));
91
-
92
- computationKeys.forEach(key => {
93
- const dates = map[key] || [];
94
- dates.forEach(d => relevantDatesSet.add(d));
95
- });
96
-
97
- // If we are requesting a backfill-capable computation and have at least one valid date
98
- if (hasBackfillCalc && relevantDatesSet.size > 0 && mode === 'series') {
99
- const sortedExisting = Array.from(relevantDatesSet).sort((a, b) => b.localeCompare(a));
100
- const anchorDate = sortedExisting[0]; // Latest available date acts as the anchor
101
-
102
- // Generate 'limit' days backwards from the anchor
103
- const anchorTime = new Date(anchorDate).getTime();
104
- for (let i = 0; i < limit; i++) {
105
- const d = new Date(anchorTime - (i * 86400000));
106
- const dateStr = d.toISOString().slice(0, 10);
107
- relevantDatesSet.add(dateStr);
108
- }
109
- }
110
-
111
- // Standard sorting and slicing
112
- const sortedDates = Array.from(relevantDatesSet).sort((a, b) => b.localeCompare(a));
113
-
114
- if (sortedDates.length === 0) return [];
115
-
116
- if (mode === 'latest') {
117
- return [sortedDates[0]];
118
- }
119
-
120
- if (mode === 'series') {
121
- return sortedDates.slice(0, limit);
122
- }
123
-
124
- return [];
125
- }
126
-
127
- /**
128
- * Sub-pipe: pipe.api.helpers.validateRequest
129
- */
130
- const validateRequest = (query) => {
131
- if (!query.computations) return "Missing 'computations' parameter.";
132
-
133
- const allowedModes = ['latest', 'series'];
134
- if (query.mode && !allowedModes.includes(query.mode)) {
135
- return "Invalid 'mode'. Must be 'latest' or 'series'.";
136
- }
137
-
138
- if (query.mode === 'series') {
139
- const limit = parseInt(query.limit);
140
- if (query.limit && (isNaN(limit) || limit < 1 || limit > 365)) {
141
- return "Invalid 'limit'. Must be between 1 and 365.";
142
- }
143
- }
144
-
145
- return null;
146
- };
147
-
148
- /**
149
- * Sub-pipe: pipe.api.helpers.buildCalculationMap
150
- * FIX APPLIED: Checks class metadata for category override (mirroring ManifestBuilder).
151
- */
152
- const buildCalculationMap = (unifiedCalculations) => {
153
- const calcMap = {};
154
-
155
- // Helper to resolve category exactly like ManifestBuilder.js does
156
- const resolveCategory = (cls, folderName) => {
157
- if (typeof cls.getMetadata === 'function') {
158
- const meta = cls.getMetadata();
159
- // If folder is 'core', allow metadata to override
160
- if (folderName === 'core' && meta.category) {
161
- return meta.category;
162
- }
163
- }
164
- return folderName;
165
- };
166
-
167
- for (const category in unifiedCalculations) {
168
- for (const subKey in unifiedCalculations[category]) {
169
- const item = unifiedCalculations[category][subKey];
170
-
171
- if (subKey === 'historical' && typeof item === 'object') {
172
- for (const calcName in item) {
173
- const cls = item[calcName];
174
- calcMap[calcName] = {
175
- category: resolveCategory(cls, category),
176
- class: cls
177
- };
178
- }
179
- } else if (typeof item === 'function') {
180
- const calcName = subKey;
181
- calcMap[calcName] = {
182
- category: resolveCategory(item, category),
183
- class: item
184
- };
185
- }
186
- }
187
- }
188
- return calcMap;
189
- };
190
-
191
- /**
192
- * Sub-pipe: pipe.api.helpers.fetchUnifiedData
193
- * UPDATED: Uses tryDecompress to handle compressed payloads.
194
- */
195
- const fetchUnifiedData = async (config, dependencies, calcKeys, dateStrings, calcMap) => {
196
- const { db, logger } = dependencies;
197
- const response = {};
198
- const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
199
- const resultsSub = config.resultsSubcollection || 'results';
200
- const compsSub = config.computationsSubcollection || 'computations';
201
-
202
- if (dateStrings.length === 0) return {};
203
-
204
- try {
205
- const readPromises = [];
206
-
207
- for (const date of dateStrings) {
208
- response[date] = {};
209
-
210
- for (const key of calcKeys) {
211
- const pathInfo = calcMap[key];
212
- if (pathInfo) {
213
- const docRef = db.collection(insightsCollection).doc(date)
214
- .collection(resultsSub).doc(pathInfo.category)
215
- .collection(compsSub).doc(key);
216
-
217
- readPromises.push({ date, key, ref: docRef });
218
- } else {
219
- // If no path info, we can't fetch.
220
- response[date][key] = null;
221
- }
222
- }
223
- }
224
-
225
- if (readPromises.length === 0) return response;
226
-
227
- // Batch reads
228
- const CHUNK_SIZE = 100;
229
- for (let i = 0; i < readPromises.length; i += CHUNK_SIZE) {
230
- const chunk = readPromises.slice(i, i + CHUNK_SIZE);
231
- const refs = chunk.map(item => item.ref);
232
-
233
- const snapshots = await db.getAll(...refs);
234
-
235
- snapshots.forEach((doc, idx) => {
236
- const { date, key } = chunk[idx];
237
- if (doc.exists) {
238
- // [UPDATED] Decompress data if needed
239
- response[date][key] = tryDecompress(doc.data());
240
- } else {
241
- response[date][key] = null;
242
- }
243
- });
244
- }
245
-
246
- } catch (error) {
247
- logger.log('ERROR', 'API: Error fetching data from Firestore.', { errorMessage: error.message });
248
- throw new Error('Failed to retrieve computation data.');
249
- }
250
- return response;
251
- };
252
-
253
- /**
254
- * Factory for the main API handler.
255
- */
256
- const createApiHandler = (config, dependencies, calcMap) => {
257
- const { logger, db } = dependencies;
258
-
259
- // Singleton Cache
260
- const availabilityCache = new AvailabilityCache(db, logger);
261
-
262
- return async (req, res) => {
263
- const validationError = validateRequest(req.query);
264
- if (validationError) {
265
- logger.log('WARN', 'API Bad Request', { error: validationError, query: req.query });
266
- return res.status(400).send({ status: 'error', message: validationError });
267
- }
268
-
269
- try {
270
- const computationKeys = req.query.computations.split(',');
271
- const mode = req.query.mode || 'latest';
272
- const limit = parseInt(req.query.limit) || 30;
273
-
274
- // 1. Resolve Dates
275
- const dateStrings = await resolveTargetDates(availabilityCache, computationKeys, mode, limit);
276
-
277
- if (dateStrings.length === 0) {
278
- return res.status(200).send({
279
- status: 'success',
280
- metadata: {
281
- computations: computationKeys,
282
- mode,
283
- count: 0,
284
- dates: []
285
- },
286
- data: {}
287
- });
288
- }
289
-
290
- // 2. Fetch Data
291
- const data = await fetchUnifiedData(config, dependencies, computationKeys, dateStrings, calcMap);
292
-
293
- res.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
294
- res.status(200).send({
295
- status: 'success',
296
- metadata: {
297
- computations: computationKeys,
298
- mode,
299
- limit: mode === 'series' ? limit : 1,
300
- dateRange: {
301
- start: dateStrings[dateStrings.length - 1],
302
- end: dateStrings[0]
303
- }
304
- },
305
- data,
306
- });
307
- } catch (error) {
308
- logger.log('ERROR', 'API processing failed.', { errorMessage: error.message, stack: error.stack });
309
- res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
310
- }
311
- };
312
- };
313
-
314
- function createStructureSnippet(data, maxKeys = 20) {
315
- if (data === null || typeof data !== 'object') {
316
- if (typeof data === 'number') return 0;
317
- if (typeof data === 'string') return "string";
318
- if (typeof data === 'boolean') return true;
319
- return data;
320
- }
321
- if (Array.isArray(data)) {
322
- if (data.length === 0) return "<empty array>";
323
- return [ createStructureSnippet(data[0], maxKeys) ];
324
- }
325
- const newObj = {};
326
- const keys = Object.keys(data);
327
-
328
- if (keys.length > 0 && keys.every(k => k.match(/^[A-Z.]+$/) || k.includes('_') || k.match(/^[0-9]+$/))) {
329
- const exampleKey = keys[0];
330
- newObj[exampleKey] = createStructureSnippet(data[exampleKey], maxKeys);
331
- newObj["... (more items)"] = "...";
332
- return newObj;
333
- }
334
-
335
- if (keys.length > maxKeys) {
336
- const firstKey = keys[0] || "example_key";
337
- newObj[firstKey] = createStructureSnippet(data[firstKey], maxKeys);
338
- newObj[`... (${keys.length - 1} more keys)`] = "<object>";
339
- } else {
340
- for (const key of keys) {
341
- newObj[key] = createStructureSnippet(data[key], maxKeys);
342
- }
343
- }
344
- return newObj;
345
- }
346
-
347
- async function getComputationStructure(computationName, calcMap, config, dependencies) {
348
- const { db, logger } = dependencies;
349
- try {
350
- const pathInfo = calcMap[computationName];
351
- if (!pathInfo) { return { status: 'error', computation: computationName, message: `Computation not found in calculation map.` }; }
352
- const { category } = pathInfo;
353
- const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
354
- const resultsSub = config.resultsSubcollection || 'results';
355
- const compsSub = config.computationsSubcollection || 'computations';
356
- const computationQueryPath = `${category}.${computationName}`;
357
- const dateQuery = db.collection(insightsCollection)
358
- .where(computationQueryPath, '==', true)
359
- .orderBy(FieldPath.documentId(), 'desc')
360
- .limit(1);
361
- const dateSnapshot = await dateQuery.get();
362
- if (dateSnapshot.empty) { return { status: 'error', computation: computationName, message: `No computed data found. (Query path: ${computationQueryPath})` }; }
363
- const latestStoredDate = dateSnapshot.docs[0].id;
364
- const docRef = db.collection(insightsCollection).doc(latestStoredDate)
365
- .collection(resultsSub).doc(category)
366
- .collection(compsSub).doc(computationName);
367
- const doc = await docRef.get();
368
- if (!doc.exists) { return { status: 'error', computation: computationName, message: `Summary flag was present for ${latestStoredDate} but doc is missing.` }; }
369
-
370
- // [UPDATED] Decompress data for structure inspection
371
- const fullData = tryDecompress(doc.data());
372
- const structureSnippet = createStructureSnippet(fullData);
373
-
374
- return { status: 'success', computation: computationName, category: category, latestStoredDate: latestStoredDate, structureSnippet: structureSnippet, };
375
- } catch (error) {
376
- logger.log('ERROR', `API /structure/${computationName} helper failed.`, { errorMessage: error.message });
377
- return { status: 'error', computation: computationName, message: error.message };
378
- }
379
- }
380
-
381
- async function getDynamicSchema(CalcClass, calcName) {
382
- if (CalcClass && typeof CalcClass.getSchema === 'function') {
383
- try {
384
- return CalcClass.getSchema();
385
- } catch (e) {
386
- console.error(`Error running static getSchema() for ${calcName}: ${e.message}`);
387
- return { "ERROR": `Failed to get static schema: ${e.message}` };
388
- }
389
- } else {
390
- return { "ERROR": `Computation '${calcName}' does not have a static getSchema() method defined.` };
391
- }
392
- }
393
-
394
- const createManifestHandler = (config, dependencies, calcMap) => {
395
- const { db, logger } = dependencies;
396
- const schemaCollection = config.schemaCollection || 'computation_schemas';
397
-
398
- return async (req, res) => {
399
- try {
400
- const snapshot = await db.collection(schemaCollection).get();
401
- if (snapshot.empty) { return res.status(404).send({ status: 'error', message: 'No computation schemas have been generated yet.' }); }
402
-
403
- const manifest = {};
404
- const now = Date.now();
405
- const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
406
-
407
- let activeCount = 0;
408
- let staleCount = 0;
409
-
410
- snapshot.forEach(doc => {
411
- const data = doc.data();
412
- let lastUpdatedMs = 0;
413
- if (data.lastUpdated && typeof data.lastUpdated.toMillis === 'function') {
414
- lastUpdatedMs = data.lastUpdated.toMillis();
415
- } else if (data.lastUpdated instanceof Date) {
416
- lastUpdatedMs = data.lastUpdated.getTime();
417
- }
418
-
419
- if ((now - lastUpdatedMs) < MAX_AGE_MS) {
420
- manifest[doc.id] = {
421
- category: data.category,
422
- structure: data.schema,
423
- metadata: data.metadata,
424
- lastUpdated: data.lastUpdated
425
- };
426
- activeCount++;
427
- } else { staleCount++; }
428
- });
429
-
430
- res.status(200).send({
431
- status: 'success',
432
- summary: {
433
- source: 'firestore_computation_schemas',
434
- totalComputations: snapshot.size,
435
- schemasAvailable: activeCount,
436
- schemasFiltered: staleCount,
437
- lastUpdated: Date.now()
438
- },
439
- manifest: manifest
440
- });
441
-
442
- } catch (error) {
443
- logger.log('ERROR', 'API /manifest handler failed.', { errorMessage: error.message });
444
- res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
445
- }
446
- };
447
- };
448
-
449
- module.exports = {
450
- validateRequest,
451
- buildCalculationMap,
452
- fetchUnifiedData,
453
- createApiHandler,
454
- getComputationStructure,
455
- getDynamicSchema,
456
- createManifestHandler
457
- };
@@ -1,204 +0,0 @@
1
- /**
2
- * @fileoverview Main entry point for the Generic API module.
3
- * Export the 'createApiApp' main pipe function.
4
- * REFACTORED: API V3 - Status-Aware Data Fetching.
5
- * UPDATED: Mounts both Admin API and User API.
6
- */
7
-
8
- const express = require('express');
9
- const cors = require('cors');
10
- const { buildCalculationMap, createApiHandler, getComputationStructure, createManifestHandler, getDynamicSchema } = require('./helpers/api_helpers.js');
11
-
12
- // Import Sub-Routers
13
- const createAdminRouter = require('./admin-api/index');
14
- const createUserRouter = require('./user-api/index'); // [FIX] Import User Router
15
-
16
- /**
17
- * In-Memory Cache Handler
18
- * Wrapper that adds TTL cache to GET requests.
19
- */
20
- const createCacheHandler = (handler, { logger }) => {
21
- const CACHE = {};
22
- const CACHE_TTL_MS = 10 * 60 * 1000; // 10 Minutes
23
-
24
- return async (req, res) => {
25
- const cacheKey = req.url;
26
- const now = Date.now();
27
-
28
- if (CACHE[cacheKey] && (now - CACHE[cacheKey].timestamp) < CACHE_TTL_MS) {
29
- logger.log('INFO', `[API] Cache HIT for ${cacheKey}`);
30
- return res.status(CACHE[cacheKey].status).send(CACHE[cacheKey].data);
31
- }
32
-
33
- logger.log('INFO', `[API] Cache MISS for ${cacheKey}`);
34
-
35
- const originalSend = res.send;
36
- const originalStatus = res.status;
37
- let capturedData = null;
38
- let capturedStatus = 200;
39
-
40
- res.status = (statusCode) => {
41
- capturedStatus = statusCode;
42
- return originalStatus.call(res, statusCode);
43
- };
44
-
45
- res.send = (data) => {
46
- capturedData = data;
47
- return originalSend.call(res, data);
48
- };
49
-
50
- await handler(req, res);
51
-
52
- if (capturedStatus === 200 && capturedData) {
53
- logger.log('INFO', `[API] Caching new entry for ${cacheKey}`);
54
- CACHE[cacheKey] = {
55
- data: capturedData,
56
- status: capturedStatus,
57
- timestamp: now
58
- };
59
- }
60
- };
61
- };
62
-
63
- /**
64
- * Main pipe: pipe.api.createApiApp
65
- */
66
- function createApiApp(config, dependencies, unifiedCalculations) {
67
- const app = express();
68
- const { logger, db } = dependencies;
69
-
70
- // Build Calc Map once
71
- const calcMap = buildCalculationMap(unifiedCalculations);
72
-
73
- // Middleware
74
- app.use(cors({ origin: true }));
75
- app.use(express.json());
76
-
77
- // --- MOUNT SUB-ROUTERS ---
78
-
79
- // 1. Admin API
80
- // Signature: (config, dependencies, calculations)
81
- app.use('/admin', createAdminRouter(config, dependencies, unifiedCalculations));
82
-
83
- // 2. User API [FIX]
84
- // Signature: (dependencies, config) - Matching the definition from Phase 4
85
- if (createUserRouter) {
86
- app.use('/user', createUserRouter(dependencies, config));
87
- logger.log('INFO', '[API] Mounted /user routes');
88
- }
89
-
90
- // --- Main API V3 Endpoint ---
91
- const originalApiHandler = createApiHandler(config, dependencies, calcMap);
92
- const cachedApiHandler = createCacheHandler(originalApiHandler, dependencies);
93
-
94
- app.get('/', cachedApiHandler);
95
-
96
- // Health Check
97
- app.get('/health', (req, res) => { res.status(200).send('OK'); });
98
-
99
- // Debug: List keys
100
- app.get('/list-computations', (req, res) => {
101
- try {
102
- const computationKeys = Object.keys(calcMap);
103
- res.status(200).send({
104
- status: 'success',
105
- count: computationKeys.length,
106
- computations: computationKeys.sort(),
107
- });
108
- } catch (error) {
109
- logger.log('ERROR', 'API /list-computations failed.', { errorMessage: error.message });
110
- res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
111
- }
112
- });
113
-
114
- // Structure Inspection
115
- app.get('/structure/:computationName', async (req, res) => {
116
- const { computationName } = req.params;
117
- const result = await getComputationStructure(computationName, calcMap, config, dependencies);
118
- if (result.status === 'error') {
119
- const statusCode = result.message.includes('not found') ? 404 : 500;
120
- return res.status(statusCode).send(result);
121
- }
122
- res.status(200).send(result);
123
- });
124
-
125
- // Manifests (Schema Generation)
126
- app.get('/manifest', createManifestHandler(config, dependencies, calcMap));
127
-
128
- // Manual Schema Gen Trigger
129
- app.post('/manifest/generate/:computationName', async (req, res) => {
130
- const { computationName } = req.params;
131
- logger.log('INFO', `Manual static schema generation requested for: ${computationName}`);
132
-
133
- try {
134
- const calcInfo = calcMap[computationName];
135
- if (!calcInfo || !calcInfo.class) {
136
- return res.status(404).send({ status: 'error', message: `Computation '${computationName}' not found.` });
137
- }
138
- const targetCalcClass = calcInfo.class;
139
- const targetCategory = calcInfo.category;
140
-
141
- const schemaStructure = await getDynamicSchema(targetCalcClass, computationName);
142
- if (schemaStructure.ERROR) {
143
- return res.status(400).send({ status: 'error', message: `Failed to get static schema: ${schemaStructure.ERROR}` });
144
- }
145
-
146
- const { batchStoreSchemas } = require('../computation-system/utils/schema_capture.js');
147
-
148
- const metadata = {
149
- isHistorical: !!(targetCalcClass.toString().includes('yesterdayPortfolio')),
150
- dependencies: (typeof targetCalcClass.getDependencies === 'function') ? targetCalcClass.getDependencies() : [],
151
- rootDataDependencies: [],
152
- type: (targetCategory === 'meta' || targetCategory === 'socialPosts') ? targetCategory : 'standard',
153
- note: "Manually generated via API"
154
- };
155
-
156
- await batchStoreSchemas(dependencies, config, [{
157
- name: computationName,
158
- category: targetCategory,
159
- schema: schemaStructure,
160
- metadata: metadata
161
- }]);
162
-
163
- res.status(200).send({
164
- status: 'success',
165
- message: `Static schema read and stored for ${computationName}`,
166
- computation: computationName,
167
- category: targetCategory,
168
- schema: schemaStructure
169
- });
170
-
171
- } catch (error) {
172
- logger.log('ERROR', `Failed to generate schema for ${computationName}`, { errorMessage: error.message });
173
- res.status(500).send({ status: 'error', message: `Failed: ${error.message}` });
174
- }
175
- });
176
-
177
- // Single Manifest Get
178
- app.get('/manifest/:computationName', async (req, res) => {
179
- const { computationName } = req.params;
180
- try {
181
- const schemaCollection = config.schemaCollection || 'computation_schemas';
182
- const schemaDoc = await db.collection(schemaCollection).doc(computationName).get();
183
- if (!schemaDoc.exists) {
184
- return res.status(404).send({ status: 'error', message: `Schema not found for ${computationName}` });
185
- }
186
- const data = schemaDoc.data();
187
- res.status(200).send({
188
- status: 'success',
189
- computation: computationName,
190
- category: data.category,
191
- structure: data.schema,
192
- metadata: data.metadata || {},
193
- lastUpdated: data.lastUpdated
194
- });
195
- } catch (error) {
196
- logger.log('ERROR', `Failed to fetch schema for ${computationName}`, { errorMessage: error.message });
197
- res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
198
- }
199
- });
200
-
201
- return app;
202
- }
203
-
204
- module.exports = { createApiApp, helpers: require('./helpers/api_helpers.js') };