bulltrackers-module 1.0.11 → 1.0.13
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.
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core helper logic for the Generic API.
|
|
3
|
+
* Contains validation, data fetching, and request handling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { FieldPath } = require('@google-cloud/firestore');
|
|
7
|
+
const MAX_DATE_RANGE = 100; // Limit queries to a 100-day range
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates request parameters.
|
|
11
|
+
* @param {object} query - The request query object.
|
|
12
|
+
* @returns {string|null} An error message if validation fails, otherwise null.
|
|
13
|
+
*/
|
|
14
|
+
const validateRequest = (query) => {
|
|
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
|
+
if (end < start) return "'endDate' must be after 'startDate'.";
|
|
22
|
+
|
|
23
|
+
const diffTime = Math.abs(end - start);
|
|
24
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
25
|
+
if (diffDays > MAX_DATE_RANGE) return `Date range cannot exceed ${MAX_DATE_RANGE} days.`;
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Flattens the calculation manifest into a simple lookup map.
|
|
32
|
+
* @param {Object} unifiedCalculations - The imported calculations object.
|
|
33
|
+
* @returns {Object} A map of { [calcName]: { category, path } }.
|
|
34
|
+
*/
|
|
35
|
+
const buildCalculationMap = (unifiedCalculations) => {
|
|
36
|
+
const calcMap = {};
|
|
37
|
+
for (const category in unifiedCalculations) {
|
|
38
|
+
for (const calcName in unifiedCalculations[category]) {
|
|
39
|
+
calcMap[calcName] = {
|
|
40
|
+
category: category,
|
|
41
|
+
path: `unified_insights/{date}/results/${category}/computations/${calcName}`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return calcMap;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generates an array of YYYY-MM-DD date strings between two dates.
|
|
50
|
+
* @param {string} startDate - The start date (YYYY-MM-DD).
|
|
51
|
+
* @param {string} endDate - The end date (YYYY-MM-DD).
|
|
52
|
+
* @returns {string[]} An array of date strings.
|
|
53
|
+
*/
|
|
54
|
+
const getDateStringsInRange = (startDate, endDate) => {
|
|
55
|
+
const dates = [];
|
|
56
|
+
const current = new Date(startDate + 'T00:00:00Z');
|
|
57
|
+
const end = new Date(endDate + 'T00:00:00Z');
|
|
58
|
+
|
|
59
|
+
while (current <= end) {
|
|
60
|
+
dates.push(current.toISOString().slice(0, 10));
|
|
61
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
62
|
+
}
|
|
63
|
+
return dates;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fetches all requested computation data from Firestore for the given date range.
|
|
68
|
+
* @param {Firestore} firestore - A Firestore client instance.
|
|
69
|
+
* @param {Logger} logger - A logger instance.
|
|
70
|
+
* @param {string[]} calcKeys - Array of computation keys to fetch.
|
|
71
|
+
* @param {string[]} dateStrings - Array of dates to fetch for.
|
|
72
|
+
* @param {Object} calcMap - The pre-built calculation lookup map.
|
|
73
|
+
* @returns {Promise<Object>} A nested object of [date][computationKey] = data.
|
|
74
|
+
*/
|
|
75
|
+
const fetchUnifiedData = async (firestore, logger, calcKeys, dateStrings, calcMap) => {
|
|
76
|
+
const response = {};
|
|
77
|
+
try {
|
|
78
|
+
for (const date of dateStrings) {
|
|
79
|
+
response[date] = {};
|
|
80
|
+
const docRefs = [];
|
|
81
|
+
const keyPaths = [];
|
|
82
|
+
|
|
83
|
+
for (const key of calcKeys) {
|
|
84
|
+
const pathInfo = calcMap[key];
|
|
85
|
+
if (pathInfo) {
|
|
86
|
+
const docRef = firestore.collection('unified_insights').doc(date)
|
|
87
|
+
.collection('results').doc(pathInfo.category)
|
|
88
|
+
.collection('computations').doc(key);
|
|
89
|
+
|
|
90
|
+
docRefs.push(docRef);
|
|
91
|
+
keyPaths.push(key);
|
|
92
|
+
} else {
|
|
93
|
+
logger.log('WARN', `[${date}] No path found for computation key: ${key}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (docRefs.length === 0) continue;
|
|
98
|
+
|
|
99
|
+
const snapshots = await firestore.getAll(...docRefs);
|
|
100
|
+
snapshots.forEach((doc, i) => {
|
|
101
|
+
const key = keyPaths[i];
|
|
102
|
+
if (doc.exists) {
|
|
103
|
+
response[date][key] = doc.data();
|
|
104
|
+
} else {
|
|
105
|
+
response[date][key] = null; // Indicate missing data
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.log('ERROR', 'API: Error fetching data from Firestore.', { errorMessage: error.message });
|
|
111
|
+
throw new Error('Failed to retrieve computation data.');
|
|
112
|
+
}
|
|
113
|
+
return response;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates the main Express request handler for the API.
|
|
118
|
+
* @param {Firestore} firestore - A Firestore client instance.
|
|
119
|
+
* @param {Logger} logger - A logger instance.
|
|
120
|
+
* @param {Object} calcMap - The pre-built calculation lookup map.
|
|
121
|
+
* @returns {Function} An async Express request handler.
|
|
122
|
+
*/
|
|
123
|
+
const createApiHandler = (firestore, logger, calcMap) => {
|
|
124
|
+
return async (req, res) => {
|
|
125
|
+
const validationError = validateRequest(req.query);
|
|
126
|
+
if (validationError) {
|
|
127
|
+
logger.log('WARN', 'Bad Request', { error: validationError });
|
|
128
|
+
return res.status(400).send({ status: 'error', message: validationError });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const computationKeys = req.query.computations.split(',');
|
|
133
|
+
const dateStrings = getDateStringsInRange(req.query.startDate, req.query.endDate);
|
|
134
|
+
|
|
135
|
+
const data = await fetchUnifiedData(firestore, logger, computationKeys, dateStrings, calcMap);
|
|
136
|
+
|
|
137
|
+
res.status(200).send({
|
|
138
|
+
status: 'success',
|
|
139
|
+
metadata: {
|
|
140
|
+
computations: computationKeys,
|
|
141
|
+
startDate: req.query.startDate,
|
|
142
|
+
endDate: req.query.endDate,
|
|
143
|
+
},
|
|
144
|
+
data,
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.log('ERROR', 'API processing failed.', { errorMessage: error.message, stack: error.stack });
|
|
148
|
+
res.status(500).send({ status: 'error', message: 'An internal error occurred.' });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
buildCalculationMap,
|
|
155
|
+
createApiHandler,
|
|
156
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main entry point for the Generic API module.
|
|
3
|
+
* Exports a function to create the fully configured Express app.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const express = require('express');
|
|
7
|
+
const cors = require('cors');
|
|
8
|
+
const { buildCalculationMap, createApiHandler } = require('./helpers/api_helpers.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates and configures the Express app for the Generic API.
|
|
12
|
+
* @param {Firestore} firestore A Firestore client instance.
|
|
13
|
+
* @param {Logger} logger A logger instance.
|
|
14
|
+
* @param {Object} unifiedCalculations The calculations manifest.
|
|
15
|
+
* @returns {express.Application} The configured Express app.
|
|
16
|
+
*/
|
|
17
|
+
function createApiApp(firestore, logger, unifiedCalculations) {
|
|
18
|
+
const app = express();
|
|
19
|
+
|
|
20
|
+
// --- Pre-compute Calculation Map ---
|
|
21
|
+
const calcMap = buildCalculationMap(unifiedCalculations);
|
|
22
|
+
|
|
23
|
+
// --- Middleware ---
|
|
24
|
+
app.use(cors({ origin: true }));
|
|
25
|
+
app.use(express.json());
|
|
26
|
+
|
|
27
|
+
// --- Main API Endpoint ---
|
|
28
|
+
// The handler is created with its dependencies injected
|
|
29
|
+
app.get('/', createApiHandler(firestore, logger, calcMap));
|
|
30
|
+
|
|
31
|
+
// --- Health Check Endpoint ---
|
|
32
|
+
app.get('/health', (req, res) => {
|
|
33
|
+
res.status(200).send('OK');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return app;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
createApiApp,
|
|
41
|
+
};
|
|
@@ -1,11 +1,136 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Main orchestration logic for Discovery and Updates.
|
|
3
3
|
*/
|
|
4
|
+
const { logger } = require("sharedsetup")(__filename); // Assuming sharedsetup is available in module context
|
|
5
|
+
const coreUtils = require('../../core/utils');
|
|
6
|
+
const { firestore: firestoreUtils } = coreUtils;
|
|
7
|
+
const { discovery, updates } = require('./helpers'); // Import local helpers
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Runs the complete discovery orchestration process for a given user type.
|
|
11
|
+
* @param {string} userType - 'normal' or 'speculator'.
|
|
12
|
+
* @param {object} config - The specific discovery config slice for the user type.
|
|
13
|
+
* @param {object} globalConfig - Global config like proxiesCollectionName.
|
|
14
|
+
*/
|
|
15
|
+
async function runDiscovery(userType, config, globalConfig) {
|
|
16
|
+
logger.log('INFO', `[Module Orchestrator] Starting discovery for ${userType} users...`);
|
|
17
|
+
|
|
18
|
+
// Reset locks happens once per entry point, keep it there.
|
|
19
|
+
|
|
20
|
+
// 1. Check Need
|
|
21
|
+
const { needsDiscovery, blocksToFill } = await discovery.checkDiscoveryNeed(
|
|
22
|
+
userType,
|
|
23
|
+
config.targetUsersPerBlock,
|
|
24
|
+
config // Pass the whole config slice down
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!needsDiscovery) {
|
|
28
|
+
logger.log('INFO', `[Module Orchestrator] No discovery needed for ${userType}.`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Get Candidates
|
|
33
|
+
const candidates = await discovery.getDiscoveryCandidates(
|
|
34
|
+
userType,
|
|
35
|
+
blocksToFill,
|
|
36
|
+
config // Pass the whole config slice down
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// 3. Dispatch
|
|
40
|
+
await discovery.dispatchDiscovery(
|
|
41
|
+
userType,
|
|
42
|
+
candidates,
|
|
43
|
+
config // Pass the whole config slice down
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
logger.log('SUCCESS', `[Module Orchestrator] Dispatched discovery tasks for ${userType}.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Runs the complete update orchestration process for a given user type.
|
|
51
|
+
* @param {string} userType - 'normal' or 'speculator'.
|
|
52
|
+
* @param {object} config - The update configuration object.
|
|
53
|
+
* @param {object} globalConfig - Global config like dispatcherTopicName.
|
|
54
|
+
*/
|
|
55
|
+
async function runUpdates(userType, config, globalConfig) {
|
|
56
|
+
logger.log('INFO', `[Module Orchestrator] Collecting users for daily update (${userType})...`);
|
|
57
|
+
|
|
58
|
+
// Reset locks happens once per entry point, keep it there.
|
|
59
|
+
|
|
60
|
+
// 1. Define Thresholds (Remains the same logic as before)
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const startOfTodayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
63
|
+
const sevenDaysAgoUTC = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // Speculator grace period
|
|
64
|
+
|
|
65
|
+
const thresholds = {
|
|
66
|
+
dateThreshold: startOfTodayUTC,
|
|
67
|
+
gracePeriodThreshold: sevenDaysAgoUTC
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// 2. Get Targets
|
|
71
|
+
const targets = await updates.getUpdateTargets(
|
|
72
|
+
userType,
|
|
73
|
+
thresholds,
|
|
74
|
+
config // Pass the whole update config down
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// 3. Dispatch
|
|
78
|
+
await updates.dispatchUpdates(
|
|
79
|
+
targets,
|
|
80
|
+
userType,
|
|
81
|
+
config.dispatcherTopicName, // Use topic name from config
|
|
82
|
+
config.taskBatchSize,
|
|
83
|
+
config.pubsubBatchSize
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
logger.log('SUCCESS', `[Module Orchestrator] Dispatched update tasks for ${userType}.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates the higher-level discovery orchestrator function.
|
|
92
|
+
* This is the function the Cloud Function entry point will call.
|
|
93
|
+
* @param {object} config - The full orchestratorConfig object loaded from config.js.
|
|
94
|
+
* @returns {Function} The async function handler for the Cloud Function.
|
|
95
|
+
*/
|
|
96
|
+
function createDiscoveryOrchestrator(config) {
|
|
97
|
+
return async (req, res) => {
|
|
98
|
+
logger.log('INFO', '🚀 Discovery Orchestrator triggered via module...');
|
|
99
|
+
try {
|
|
100
|
+
await firestoreUtils.resetProxyLocks(config.proxiesCollectionName);
|
|
101
|
+
await runDiscovery('normal', config.discoveryConfig.normal, config);
|
|
102
|
+
await runDiscovery('speculator', config.discoveryConfig.speculator, config);
|
|
103
|
+
res.status(200).send('Discovery orchestration complete.');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.log('ERROR', 'FATAL Error in Discovery Orchestrator (Module)', { errorMessage: error.message, errorStack: error.stack });
|
|
106
|
+
res.status(500).send("An internal discovery error occurred.");
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates the higher-level update orchestrator function.
|
|
113
|
+
* This is the function the Cloud Function entry point will call.
|
|
114
|
+
* @param {object} config - The full orchestratorConfig object loaded from config.js.
|
|
115
|
+
* @returns {Function} The async function handler for the Cloud Function.
|
|
116
|
+
*/
|
|
117
|
+
function createUpdateOrchestrator(config) {
|
|
118
|
+
return async (req, res) => {
|
|
119
|
+
logger.log('INFO', '🚀 Update Orchestrator triggered via module...');
|
|
120
|
+
try {
|
|
121
|
+
await firestoreUtils.resetProxyLocks(config.proxiesCollectionName);
|
|
122
|
+
await runUpdates('normal', config.updateConfig, config);
|
|
123
|
+
await runUpdates('speculator', config.updateConfig, config);
|
|
124
|
+
res.status(200).send('Update orchestration complete.');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.log('ERROR', 'FATAL Error in Update Orchestrator (Module)', { errorMessage: error.message, errorStack: error.stack });
|
|
127
|
+
res.status(500).send("An internal update error occurred.");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
7
131
|
|
|
8
132
|
module.exports = {
|
|
9
|
-
|
|
10
|
-
|
|
133
|
+
createDiscoveryOrchestrator,
|
|
134
|
+
createUpdateOrchestrator,
|
|
135
|
+
helpers: { discovery, updates } // Keep exporting helpers if needed elsewhere
|
|
11
136
|
};
|
package/index.js
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
const core = require('./functions/core');
|
|
8
8
|
const Orchestrator = require('./functions/orchestrator');
|
|
9
9
|
const TaskEngine = require('./functions/task-engine');
|
|
10
|
-
const ComputationSystem = require('./functions/computation-system');
|
|
10
|
+
const ComputationSystem = require('./functions/computation-system');
|
|
11
|
+
const GenericAPI = require('./functions/generic-api'); // <-- ADD THIS
|
|
11
12
|
|
|
12
13
|
module.exports = {
|
|
13
14
|
core,
|
|
14
15
|
Orchestrator,
|
|
15
16
|
TaskEngine,
|
|
16
|
-
ComputationSystem,
|
|
17
|
+
ComputationSystem,
|
|
18
|
+
GenericAPI, // <-- AND ADD THIS
|
|
17
19
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bulltrackers-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Helper Functions for Bulltrackers.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"functions/orchestrator/",
|
|
9
9
|
"functions/task-engine/",
|
|
10
10
|
"functions/core/",
|
|
11
|
-
"functions/computation-system/"
|
|
11
|
+
"functions/computation-system/",
|
|
12
|
+
"functions/generic-api/"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
14
15
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -25,7 +26,9 @@
|
|
|
25
26
|
"sharedsetup": "latest",
|
|
26
27
|
"require-all": "^3.0.0",
|
|
27
28
|
"aiden-shared-calculations-unified": "1.0.0",
|
|
28
|
-
"@google-cloud/pubsub": "latest"
|
|
29
|
+
"@google-cloud/pubsub": "latest",
|
|
30
|
+
"express": "^4.19.2",
|
|
31
|
+
"cors": "^2.8.5"
|
|
29
32
|
},
|
|
30
33
|
"engines": {
|
|
31
34
|
"node": ">=20"
|