bulltrackers-module 1.0.737 → 1.0.739

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,327 @@
1
+ /**
2
+ * @fileoverview Admin Test Endpoint for Computation System
3
+ *
4
+ * SECURITY: This endpoint is protected by GCP IAM (requireAuth: true).
5
+ * Only service accounts and users with cloudfunctions.invoker can access it.
6
+ *
7
+ * PURPOSE:
8
+ * - Test computations in production without waiting for schedule
9
+ * - Force re-runs of computations (bypass hash checks)
10
+ * - Test worker pool functionality
11
+ * - Run on specific entities for debugging
12
+ *
13
+ * USAGE:
14
+ * curl -X POST https://REGION-PROJECT.cloudfunctions.net/compute-admin-test \
15
+ * -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
16
+ * -H "Content-Type: application/json" \
17
+ * -d '{"action": "run", "computation": "UserPortfolioSummary", "date": "2026-01-25"}'
18
+ */
19
+
20
+ const system = require('../index');
21
+
22
+ /**
23
+ * Admin test handler.
24
+ */
25
+ async function adminTestHandler(req, res) {
26
+ const startTime = Date.now();
27
+
28
+ try {
29
+ const {
30
+ action = 'status',
31
+ computation,
32
+ date = new Date().toISOString().split('T')[0],
33
+ entityIds,
34
+ limit = 10,
35
+ force = true, // Default to force for testing
36
+ useWorkerPool, // Override: true/false/undefined (use config)
37
+ dryRun = false
38
+ } = req.body || {};
39
+
40
+ console.log(`[AdminTest] Action: ${action}, Computation: ${computation}, Date: ${date}`);
41
+
42
+ switch (action) {
43
+ // =========================================================
44
+ // STATUS: Show system status and available computations
45
+ // =========================================================
46
+ case 'status': {
47
+ const manifest = await system.getManifest();
48
+
49
+ return res.status(200).json({
50
+ status: 'ok',
51
+ action: 'status',
52
+ systemInfo: {
53
+ computationCount: manifest.length,
54
+ computations: manifest.map(c => ({
55
+ name: c.originalName || c.name,
56
+ type: c.type,
57
+ pass: c.pass,
58
+ schedule: c.schedule
59
+ })),
60
+ workerPool: {
61
+ enabled: process.env.WORKER_POOL_ENABLED === 'true',
62
+ localMode: process.env.WORKER_LOCAL_MODE === 'true'
63
+ }
64
+ },
65
+ timestamp: new Date().toISOString()
66
+ });
67
+ }
68
+
69
+ // =========================================================
70
+ // ANALYZE: Check what would run for a given date
71
+ // =========================================================
72
+ case 'analyze': {
73
+ const report = await system.analyze({ date });
74
+
75
+ return res.status(200).json({
76
+ status: 'ok',
77
+ action: 'analyze',
78
+ date,
79
+ report: {
80
+ runnable: report.runnable?.map(r => r.name || r) || [],
81
+ skipped: report.skipped?.map(r => ({ name: r.name, reason: r.reason })) || [],
82
+ blocked: report.blocked?.map(r => ({ name: r.name, reason: r.reason })) || [],
83
+ impossible: report.impossible?.map(r => ({ name: r.name, reason: r.reason })) || []
84
+ }
85
+ });
86
+ }
87
+
88
+ // =========================================================
89
+ // RUN: Execute a single computation
90
+ // =========================================================
91
+ case 'run': {
92
+ if (!computation) {
93
+ return res.status(400).json({
94
+ status: 'error',
95
+ error: 'Missing "computation" field. Use action: "status" to list available computations.'
96
+ });
97
+ }
98
+
99
+ // Log worker pool override if specified
100
+ if (useWorkerPool !== undefined) {
101
+ console.log(`[AdminTest] Worker pool override: ${useWorkerPool ? 'ENABLED' : 'DISABLED'}`);
102
+ }
103
+
104
+ console.log(`[AdminTest] Running ${computation} for ${date}...`);
105
+ console.log(`[AdminTest] Options: force=${force}, dryRun=${dryRun}, entityIds=${entityIds?.join(',') || 'all'}`);
106
+
107
+ const result = await system.runComputation({
108
+ date,
109
+ computation,
110
+ entityIds: entityIds || null,
111
+ dryRun,
112
+ force,
113
+ // Pass worker pool override explicitly (avoids env var caching issues)
114
+ useWorkerPool
115
+ });
116
+
117
+ const duration = Date.now() - startTime;
118
+
119
+ return res.status(200).json({
120
+ status: 'ok',
121
+ action: 'run',
122
+ computation,
123
+ date,
124
+ result: {
125
+ status: result.status,
126
+ duration: result.duration,
127
+ resultCount: result.resultCount,
128
+ reason: result.reason,
129
+ hash: result.hash
130
+ },
131
+ totalDuration: duration,
132
+ workerPoolUsed: useWorkerPool ?? (process.env.WORKER_POOL_ENABLED === 'true')
133
+ });
134
+ }
135
+
136
+ // =========================================================
137
+ // RUN_LIMITED: Run on a limited number of entities (safer)
138
+ // =========================================================
139
+ case 'run_limited': {
140
+ if (!computation) {
141
+ return res.status(400).json({
142
+ status: 'error',
143
+ error: 'Missing "computation" field'
144
+ });
145
+ }
146
+
147
+ // Get a sample of entities from BigQuery
148
+ const sampleEntities = await getSampleEntities(computation, date, limit);
149
+
150
+ if (!sampleEntities || sampleEntities.length === 0) {
151
+ return res.status(404).json({
152
+ status: 'error',
153
+ error: `No entities found for ${computation} on ${date}`
154
+ });
155
+ }
156
+
157
+ console.log(`[AdminTest] Running LIMITED test: ${sampleEntities.length} entities`);
158
+
159
+ const result = await system.runComputation({
160
+ date,
161
+ computation,
162
+ entityIds: sampleEntities,
163
+ dryRun,
164
+ force,
165
+ useWorkerPool // Pass worker pool override
166
+ });
167
+
168
+ const duration = Date.now() - startTime;
169
+
170
+ return res.status(200).json({
171
+ status: 'ok',
172
+ action: 'run_limited',
173
+ computation,
174
+ date,
175
+ entitiesTested: sampleEntities,
176
+ result: {
177
+ status: result.status,
178
+ duration: result.duration,
179
+ resultCount: result.resultCount
180
+ },
181
+ totalDuration: duration
182
+ });
183
+ }
184
+
185
+ // =========================================================
186
+ // TEST_WORKER: Direct test of worker function
187
+ // =========================================================
188
+ case 'test_worker': {
189
+ if (!computation || !entityIds || entityIds.length === 0) {
190
+ return res.status(400).json({
191
+ status: 'error',
192
+ error: 'Requires "computation" and "entityIds" array'
193
+ });
194
+ }
195
+
196
+ // Import worker's local execution function
197
+ const { executeLocal, loadComputation } = require('./worker');
198
+
199
+ // Verify computation exists
200
+ const CompClass = loadComputation(computation);
201
+ if (!CompClass) {
202
+ return res.status(400).json({
203
+ status: 'error',
204
+ error: `Unknown computation: ${computation}`
205
+ });
206
+ }
207
+
208
+ // Fetch real data for one entity
209
+ const config = require('../config/bulltrackers.config');
210
+ const { DataFetcher } = require('../framework/data/DataFetcher');
211
+ const { QueryBuilder } = require('../framework/data/QueryBuilder');
212
+ const { SchemaRegistry } = require('../framework/data/SchemaRegistry');
213
+
214
+ const schemaRegistry = new SchemaRegistry(config.bigquery, console);
215
+ const queryBuilder = new QueryBuilder(config.bigquery, schemaRegistry, console);
216
+ const dataFetcher = new DataFetcher(
217
+ { ...config.bigquery, tables: config.tables },
218
+ queryBuilder,
219
+ console
220
+ );
221
+
222
+ const compConfig = CompClass.getConfig();
223
+ const testEntityId = entityIds[0];
224
+
225
+ console.log(`[AdminTest] Fetching data for entity ${testEntityId}...`);
226
+ const data = await dataFetcher.fetchForComputation(compConfig.requires, date, [testEntityId]);
227
+
228
+ // Execute worker logic locally
229
+ console.log(`[AdminTest] Executing worker logic...`);
230
+ const workerResult = await executeLocal({
231
+ computationName: computation,
232
+ entityId: testEntityId,
233
+ date,
234
+ contextPackage: {
235
+ entityData: data,
236
+ references: {},
237
+ dependencies: {},
238
+ config: {}
239
+ }
240
+ });
241
+
242
+ const duration = Date.now() - startTime;
243
+
244
+ return res.status(200).json({
245
+ status: 'ok',
246
+ action: 'test_worker',
247
+ computation,
248
+ entityId: testEntityId,
249
+ date,
250
+ workerResult: workerResult.result,
251
+ duration
252
+ });
253
+ }
254
+
255
+ default:
256
+ return res.status(400).json({
257
+ status: 'error',
258
+ error: `Unknown action: ${action}`,
259
+ availableActions: ['status', 'analyze', 'run', 'run_limited', 'test_worker']
260
+ });
261
+ }
262
+
263
+ } catch (error) {
264
+ console.error('[AdminTest] Error:', error);
265
+ return res.status(500).json({
266
+ status: 'error',
267
+ error: error.message,
268
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
269
+ });
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Get a sample of entity IDs for testing
275
+ */
276
+ async function getSampleEntities(computation, date, limit) {
277
+ try {
278
+ const { BigQuery } = require('@google-cloud/bigquery');
279
+ const config = require('../config/bulltrackers.config');
280
+
281
+ const bigquery = new BigQuery({
282
+ projectId: config.bigquery.projectId
283
+ });
284
+
285
+ // Load computation to get its config
286
+ const { loadComputation } = require('./worker');
287
+ const CompClass = loadComputation(computation);
288
+
289
+ if (!CompClass) return null;
290
+
291
+ const compConfig = CompClass.getConfig();
292
+
293
+ // Find the driver table (first table with entityField)
294
+ let driverTable = null;
295
+ let entityField = null;
296
+
297
+ for (const [tableName, tableSpec] of Object.entries(compConfig.requires || {})) {
298
+ const tableConfig = config.tables[tableName];
299
+ if (tableConfig?.entityField) {
300
+ driverTable = tableName;
301
+ entityField = tableConfig.entityField;
302
+ break;
303
+ }
304
+ }
305
+
306
+ if (!driverTable) return null;
307
+
308
+ const query = `
309
+ SELECT DISTINCT ${entityField} as entity_id
310
+ FROM \`${config.bigquery.projectId}.${config.bigquery.dataset}.${driverTable}\`
311
+ WHERE date = @date
312
+ LIMIT @limit
313
+ `;
314
+
315
+ const [rows] = await bigquery.query({
316
+ query,
317
+ params: { date, limit }
318
+ });
319
+
320
+ return rows.map(r => r.entity_id);
321
+ } catch (e) {
322
+ console.error('[AdminTest] Failed to get sample entities:', e);
323
+ return null;
324
+ }
325
+ }
326
+
327
+ module.exports = { adminTestHandler };
@@ -12,6 +12,7 @@ const { schedulerHandler } = require('./scheduler');
12
12
  const { dispatcherHandler } = require('./dispatcher');
13
13
  const { onDemandHandler } = require('./onDemand');
14
14
  const { workerHandler, executeLocal } = require('./worker');
15
+ const { adminTestHandler } = require('./adminTest');
15
16
 
16
17
  module.exports = {
17
18
  // Unified scheduler - triggered every minute by Cloud Scheduler
@@ -27,6 +28,9 @@ module.exports = {
27
28
  // Invoked by RemoteTaskRunner from Orchestrator
28
29
  computationWorker: workerHandler,
29
30
 
31
+ // Admin test endpoint - for testing computations in production
32
+ computeAdminTest: adminTestHandler,
33
+
30
34
  // For local testing
31
35
  executeWorkerLocal: executeLocal
32
36
  };
@@ -83,6 +83,16 @@ async function execute(options) {
83
83
  /**
84
84
  * WORKER ENTRY POINT: Run a single computation.
85
85
  * (Used by Cloud Functions / Dispatcher)
86
+ *
87
+ * @param {Object} options
88
+ * @param {string} options.date - Target date (YYYY-MM-DD)
89
+ * @param {string} options.computation - Computation name
90
+ * @param {string[]} [options.entityIds] - Specific entities to run (null = all)
91
+ * @param {boolean} [options.dryRun] - If true, don't persist results
92
+ * @param {boolean} [options.force] - If true, bypass up-to-date checks
93
+ * @param {boolean} [options.useWorkerPool] - Override worker pool setting (undefined = use config)
94
+ * @param {Object} [options.config] - Override config
95
+ * @param {Object} [options.logger] - Custom logger
86
96
  */
87
97
  async function runComputation(options) {
88
98
  const {
@@ -90,6 +100,8 @@ async function runComputation(options) {
90
100
  computation,
91
101
  entityIds = null,
92
102
  dryRun = false,
103
+ force = false,
104
+ useWorkerPool, // Runtime override for worker pool
93
105
  config: customConfig = null,
94
106
  logger = null
95
107
  } = options;
@@ -112,7 +124,9 @@ async function runComputation(options) {
112
124
  // This handles dependencies, data fetching, middleware, etc.
113
125
  return orch.runSingle(entry, date, {
114
126
  entityIds,
115
- dryRun
127
+ dryRun,
128
+ force,
129
+ useWorkerPool // Pass override to Orchestrator
116
130
  });
117
131
  }
118
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.737",
3
+ "version": "1.0.739",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [