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.
- package/functions/alert-system/helpers/alert_helpers.js +69 -77
- package/functions/alert-system/index.js +19 -29
- package/functions/api-v2/helpers/notification_helpers.js +187 -0
- package/functions/computation-system/helpers/computation_worker.js +1 -1
- package/functions/task-engine/helpers/popular_investor_helpers.js +11 -7
- package/index.js +0 -5
- package/package.json +1 -2
- package/functions/old-generic-api/admin-api/index.js +0 -895
- package/functions/old-generic-api/helpers/api_helpers.js +0 -457
- package/functions/old-generic-api/index.js +0 -204
- package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
- package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
- package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
- package/functions/old-generic-api/user-api/helpers/collection_helpers.js +0 -193
- package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +0 -68
- package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
- package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
- package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
- package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +0 -503
- package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
- package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
- package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +0 -174
- package/functions/old-generic-api/user-api/helpers/data_helpers.js +0 -87
- package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
- package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
- package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
- package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
- package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
- package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
- package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
- package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
- package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
- package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
- package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
- package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
- package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
- package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
- 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') };
|