bulltrackers-module 1.0.733 → 1.0.734

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 (56) hide show
  1. package/functions/computation-system-v2/README.md +152 -0
  2. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
  3. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
  4. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
  5. package/functions/computation-system-v2/computations/TestComputation.js +46 -0
  6. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
  7. package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
  8. package/functions/computation-system-v2/framework/core/Computation.js +73 -0
  9. package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
  10. package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
  11. package/functions/computation-system-v2/framework/core/Rules.js +231 -0
  12. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
  13. package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
  14. package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
  15. package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
  16. package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
  17. package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
  18. package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
  19. package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
  20. package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
  21. package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
  22. package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
  23. package/functions/computation-system-v2/framework/index.js +45 -0
  24. package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
  25. package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
  26. package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
  27. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
  28. package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
  29. package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
  30. package/functions/computation-system-v2/framework/storage/index.js +9 -0
  31. package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
  32. package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
  33. package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
  34. package/functions/computation-system-v2/handlers/index.js +23 -0
  35. package/functions/computation-system-v2/handlers/onDemand.js +289 -0
  36. package/functions/computation-system-v2/handlers/scheduler.js +327 -0
  37. package/functions/computation-system-v2/index.js +163 -0
  38. package/functions/computation-system-v2/rules/index.js +49 -0
  39. package/functions/computation-system-v2/rules/instruments.js +465 -0
  40. package/functions/computation-system-v2/rules/metrics.js +304 -0
  41. package/functions/computation-system-v2/rules/portfolio.js +534 -0
  42. package/functions/computation-system-v2/rules/rankings.js +655 -0
  43. package/functions/computation-system-v2/rules/social.js +562 -0
  44. package/functions/computation-system-v2/rules/trades.js +545 -0
  45. package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
  46. package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
  47. package/functions/computation-system-v2/test/test-framework.js +500 -0
  48. package/functions/computation-system-v2/test/test-real-execution.js +166 -0
  49. package/functions/computation-system-v2/test/test-real-integration.js +194 -0
  50. package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
  51. package/functions/computation-system-v2/test/test-results.json +31 -0
  52. package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
  53. package/functions/computation-system-v2/test/test-scheduler.js +204 -0
  54. package/functions/computation-system-v2/test/test-storage.js +449 -0
  55. package/functions/orchestrator/index.js +18 -26
  56. package/package.json +3 -2
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @fileoverview Graph Algorithms Utility
3
+ * Provides generic implementations for graph operations:
4
+ * - Topological Sort (Kahn's Algorithm) with level/pass calculation
5
+ * - Cycle Detection (Tarjan's Algorithm)
6
+ */
7
+
8
+ class Graph {
9
+ /**
10
+ * Performs a topological sort on a directed acyclic graph (DAG).
11
+ * Computes 'pass' (level) numbers for parallel execution layers.
12
+ * @param {string[]} nodes - List of node identifiers
13
+ * @param {Map<string, string[]>} adjacency - Map of node -> [dependencies]
14
+ * @returns {Array<{id: string, pass: number}>} Sorted items with pass numbers
15
+ * @throws {Error} If a cycle is detected or dependencies are missing
16
+ */
17
+ static topologicalSort(nodes, adjacency) {
18
+ // 1. Validate: Ensure all dependencies actually exist in the nodes list.
19
+ // This prevents "ghost dependencies" from breaking the sort logic silently.
20
+ const nodeSet = new Set(nodes);
21
+ for (const [node, deps] of adjacency) {
22
+ for (const dep of deps) {
23
+ if (!nodeSet.has(dep)) {
24
+ throw new Error(`Integrity Error: Node '${node}' depends on '${dep}', but '${dep}' is not in the node list.`);
25
+ }
26
+ }
27
+ }
28
+
29
+ const inDegree = new Map();
30
+ const reverseAdj = new Map(); // dependent -> [dependents that depend on it]
31
+
32
+ // Initialize
33
+ for (const node of nodes) {
34
+ inDegree.set(node, 0);
35
+ if (!reverseAdj.has(node)) reverseAdj.set(node, []);
36
+ }
37
+
38
+ // Build graph properties
39
+ for (const [node, deps] of adjacency) {
40
+ // Adjacency is Node -> [Dependencies]
41
+ // So if A depends on B, the edge is B -> A
42
+ inDegree.set(node, deps.length);
43
+
44
+ for (const dep of deps) {
45
+ if (!reverseAdj.has(dep)) reverseAdj.set(dep, []);
46
+ reverseAdj.get(dep).push(node);
47
+ }
48
+ }
49
+
50
+ const queue = [];
51
+ const sorted = [];
52
+ const passes = new Map(); // node -> pass number
53
+
54
+ // 2. Enqueue 0-degree nodes (Pass 1)
55
+ for (const [node, degree] of inDegree) {
56
+ if (degree === 0) {
57
+ queue.push(node);
58
+ passes.set(node, 1);
59
+ }
60
+ }
61
+
62
+ // Sort initial queue for deterministic behavior
63
+ queue.sort();
64
+
65
+ // 3. Process Queue
66
+ while (queue.length > 0) {
67
+ const current = queue.shift();
68
+ const currentPass = passes.get(current);
69
+
70
+ sorted.push({
71
+ id: current,
72
+ pass: currentPass
73
+ });
74
+
75
+ // Find nodes that depend on current
76
+ const dependents = reverseAdj.get(current) || [];
77
+
78
+ for (const dependent of dependents) {
79
+ const newDegree = inDegree.get(dependent) - 1;
80
+ inDegree.set(dependent, newDegree);
81
+
82
+ // Update pass: dependent must run after current
83
+ const existingPass = passes.get(dependent) || 0;
84
+ if (currentPass + 1 > existingPass) {
85
+ passes.set(dependent, currentPass + 1);
86
+ }
87
+
88
+ if (newDegree === 0) {
89
+ queue.push(dependent);
90
+ }
91
+ }
92
+
93
+ // Re-sort queue to ensure deterministic processing order within the same level
94
+ queue.sort();
95
+ }
96
+
97
+ // 4. Verify Sort Integrity
98
+ if (sorted.length !== nodes.length) {
99
+ // Auto-diagnose: Try to find the cycle to give a helpful error
100
+ const cycle = Graph.detectCycle(nodes, adjacency);
101
+ if (cycle) {
102
+ throw new Error(`Graph cycle detected during sort: ${cycle}`);
103
+ }
104
+ throw new Error('Graph sort failed (unknown integrity error, possibly disconnected islands or ghost deps)');
105
+ }
106
+
107
+ return sorted;
108
+ }
109
+
110
+ /**
111
+ * Detects cycles in a graph using an iterative version of Tarjan's SCC algorithm.
112
+ * Iterative implementation is used to avoid stack overflow on very deep dependency chains.
113
+ * @param {string[]} nodes - List of node identifiers
114
+ * @param {Map<string, string[]>} adjacency - Map of node -> [dependencies]
115
+ * @returns {string|null} Description of cycle if found, null otherwise
116
+ */
117
+ static detectCycle(nodes, adjacency) {
118
+ let index = 0;
119
+ const indices = new Map();
120
+ const lowLinks = new Map();
121
+ const onStack = new Set();
122
+ const sccStack = [];
123
+ const nodeSet = new Set(nodes);
124
+
125
+ // Explicit stack for DFS state to avoid recursion limit
126
+ // State: { v: node, iter: iterator of neighbors, parent: node }
127
+ const workStack = [];
128
+
129
+ const getNeighbors = (node) => {
130
+ const deps = adjacency.get(node) || [];
131
+ return deps.filter(w => nodeSet.has(w))[Symbol.iterator]();
132
+ };
133
+
134
+ for (const node of nodes) {
135
+ if (indices.has(node)) continue;
136
+
137
+ // Initialize DFS for this component
138
+ workStack.push({ v: node, iter: getNeighbors(node), parent: null });
139
+
140
+ while (workStack.length > 0) {
141
+ const state = workStack[workStack.length - 1];
142
+ const { v, iter, parent } = state;
143
+
144
+ // First time visiting this node?
145
+ if (!indices.has(v)) {
146
+ indices.set(v, index);
147
+ lowLinks.set(v, index);
148
+ index++;
149
+ sccStack.push(v);
150
+ onStack.add(v);
151
+ }
152
+
153
+ const next = iter.next();
154
+
155
+ if (!next.done) {
156
+ const w = next.value;
157
+ if (!indices.has(w)) {
158
+ // Tree edge: Push child onto stack (simulate recursion)
159
+ workStack.push({ v: w, iter: getNeighbors(w), parent: v });
160
+ } else if (onStack.has(w)) {
161
+ // Back edge: Update lowLink immediately
162
+ lowLinks.set(v, Math.min(lowLinks.get(v), indices.get(w)));
163
+ }
164
+ } else {
165
+ // All neighbors processed: Pop from work stack (simulate return)
166
+ workStack.pop();
167
+
168
+ // Propagate lowLink to parent (if any)
169
+ if (parent) {
170
+ lowLinks.set(parent, Math.min(lowLinks.get(parent), lowLinks.get(v)));
171
+ }
172
+
173
+ // Check if v is a root of an SCC
174
+ if (lowLinks.get(v) === indices.get(v)) {
175
+ const scc = [];
176
+ let w;
177
+ do {
178
+ w = sccStack.pop();
179
+ onStack.delete(w);
180
+ scc.push(w);
181
+ } while (w !== v);
182
+
183
+ // If SCC has > 1 node, it's a cycle
184
+ if (scc.length > 1) {
185
+ return scc.join(' -> ') + ' -> ' + scc[0];
186
+ }
187
+
188
+ // Check for self-loop (single node cycle)
189
+ if (scc.length === 1) {
190
+ const self = scc[0];
191
+ const deps = adjacency.get(self) || [];
192
+ if (deps.includes(self)) {
193
+ return `${self} -> ${self}`;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ return null;
202
+ }
203
+ }
204
+
205
+ module.exports = { Graph };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @fileoverview Computation Dispatcher
3
+ * * RESPONSIBILITIES:
4
+ * 1. Receive HTTP request (from Cloud Tasks or User)
5
+ * 2. Delegate execution to the Orchestrator
6
+ * 3. Translate Orchestrator STATUS (blocked, completed) into HTTP CODES (503, 200)
7
+ * * Why is this file so short?
8
+ * All validation logic (Schedule, Dependencies, Data) has moved to
9
+ * framework/core/RunAnalyzer.js to ensure consistency between
10
+ * the Scheduler and the Worker.
11
+ */
12
+
13
+ const system = require('../index');
14
+
15
+ exports.dispatcherHandler = async (req, res) => {
16
+ const startTime = Date.now();
17
+
18
+ try {
19
+ const {
20
+ computationName,
21
+ targetDate,
22
+ source = 'scheduled', // 'scheduled' | 'on-demand'
23
+ entityIds, // Optional: specific entities
24
+ dryRun = false,
25
+ force = false // Optional: run even if hash matches
26
+ } = req.body || {};
27
+
28
+ // 1. Basic Validation
29
+ if (!computationName) {
30
+ return res.status(400).json({
31
+ status: 'error',
32
+ reason: 'MISSING_COMPUTATION',
33
+ message: 'computationName is required'
34
+ });
35
+ }
36
+
37
+ const date = targetDate || new Date().toISOString().split('T')[0];
38
+ console.log(`[Dispatcher] Received ${source} request: ${computationName} for ${date}`);
39
+
40
+ // 2. DELEGATE TO ORCHESTRATOR
41
+ // The Orchestrator calls RunAnalyzer internally to check:
42
+ // - Is it scheduled?
43
+ // - Are dependencies ready?
44
+ // - Is data available?
45
+ // - Has the code changed?
46
+ const result = await system.runComputation({
47
+ date,
48
+ computation: computationName,
49
+ entityIds,
50
+ dryRun
51
+ });
52
+
53
+ const duration = Date.now() - startTime;
54
+
55
+ // 3. HANDLE SUCCESS (Completed or Skipped/Up-to-date)
56
+ if (result.status === 'completed' || result.status === 'skipped') {
57
+ console.log(`[Dispatcher] ${computationName} ${result.status}: ${result.resultCount || 0} entities in ${duration}ms`);
58
+
59
+ return res.status(200).json({
60
+ status: result.status,
61
+ computation: result.name,
62
+ date,
63
+ entityCount: result.resultCount,
64
+ duration,
65
+ hash: result.hash,
66
+ reason: result.reason
67
+ });
68
+ }
69
+
70
+ // 4. HANDLE BLOCKS (The Critical Retry Logic)
71
+ // If Orchestrator says "blocked", we MUST return 503 for Cloud Tasks to retry.
72
+ if (result.status === 'blocked' || result.status === 'impossible') {
73
+ console.log(`[Dispatcher] ${computationName} blocked: ${result.reason}`);
74
+
75
+ // Scheduled tasks need 503 to trigger retry
76
+ // On-demand users need 200 to see the error message immediately
77
+ const httpStatus = source === 'scheduled' ? 503 : 200;
78
+
79
+ return res.status(httpStatus).json({
80
+ status: result.status,
81
+ reason: result.reason,
82
+ message: `Computation blocked: ${result.reason}`
83
+ });
84
+ }
85
+
86
+ // 5. Fallback for other statuses (e.g., 'not_scheduled')
87
+ return res.status(200).json(result);
88
+
89
+ } catch (error) {
90
+ const duration = Date.now() - startTime;
91
+ console.error(`[Dispatcher] Error after ${duration}ms:`, error);
92
+
93
+ // Handle "Not Found" specifically
94
+ if (error.message && error.message.includes('Computation not found')) {
95
+ return res.status(400).json({
96
+ status: 'error',
97
+ reason: 'UNKNOWN_COMPUTATION',
98
+ message: error.message
99
+ });
100
+ }
101
+
102
+ // Return 500 to trigger Cloud Tasks retry for crash/network errors
103
+ return res.status(500).json({
104
+ status: 'error',
105
+ reason: 'EXECUTION_FAILED',
106
+ message: error.message
107
+ });
108
+ }
109
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview Cloud Function Handlers
3
+ *
4
+ * Export handlers for deployment as Cloud Functions:
5
+ * - computeScheduler: Single scheduler triggered every minute
6
+ * - computeDispatcher: Receives tasks from Cloud Tasks queue
7
+ * - computeOnDemand: Receives requests from frontend
8
+ */
9
+
10
+ const { schedulerHandler } = require('./scheduler');
11
+ const { dispatcherHandler } = require('./dispatcher');
12
+ const { onDemandHandler } = require('./onDemand');
13
+
14
+ module.exports = {
15
+ // Unified scheduler - triggered every minute by Cloud Scheduler
16
+ computeScheduler: schedulerHandler,
17
+
18
+ // Main dispatcher - handles scheduled tasks from Cloud Tasks
19
+ computeDispatcher: dispatcherHandler,
20
+
21
+ // On-demand API - handles frontend requests
22
+ computeOnDemand: onDemandHandler
23
+ };
@@ -0,0 +1,289 @@
1
+ /**
2
+ * @fileoverview On-Demand Computation API
3
+ *
4
+ * Frontend-facing endpoint for user-triggered computation requests.
5
+ *
6
+ * Responsibilities:
7
+ * 1. Authenticate user (Firebase Auth / JWT)
8
+ * 2. Rate limit requests per user
9
+ * 3. Validate user can access requested entities
10
+ * 4. Forward to Dispatcher for validation and execution
11
+ *
12
+ * This is a lightweight gateway - all computation logic is in the Dispatcher.
13
+ */
14
+
15
+ const { dispatcherHandler, initialize } = require('./dispatcher');
16
+ const config = require('../config/bulltrackers.config');
17
+
18
+ // Simple in-memory rate limiter (for Cloud Functions)
19
+ // In production, consider using Redis or Firestore for distributed rate limiting
20
+ const rateLimitStore = new Map();
21
+
22
+ /**
23
+ * Main on-demand handler.
24
+ *
25
+ * @param {Object} req - HTTP request
26
+ * @param {Object} res - HTTP response
27
+ */
28
+ async function onDemandHandler(req, res) {
29
+ // Set CORS headers
30
+ res.set('Access-Control-Allow-Origin', '*');
31
+ res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
32
+ res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
33
+
34
+ // Handle preflight
35
+ if (req.method === 'OPTIONS') {
36
+ return res.status(204).send('');
37
+ }
38
+
39
+ try {
40
+ // 1. Authenticate user
41
+ const user = await authenticateRequest(req);
42
+ if (!user) {
43
+ return res.status(401).json({
44
+ status: 'error',
45
+ reason: 'UNAUTHORIZED',
46
+ message: 'Authentication required'
47
+ });
48
+ }
49
+
50
+ // 2. Rate limit
51
+ const rateLimitResult = checkRateLimit(user.id);
52
+ if (!rateLimitResult.allowed) {
53
+ return res.status(429).json({
54
+ status: 'error',
55
+ reason: 'RATE_LIMITED',
56
+ message: 'Too many requests. Please wait.',
57
+ retryAfter: rateLimitResult.retryAfter
58
+ });
59
+ }
60
+
61
+ const { computation, date, entityIds, force } = req.body || {};
62
+
63
+ // 3. Validate request
64
+ if (!computation) {
65
+ return res.status(400).json({
66
+ status: 'error',
67
+ reason: 'MISSING_COMPUTATION',
68
+ message: 'computation field is required'
69
+ });
70
+ }
71
+
72
+ // 4. Validate user can access requested entities
73
+ if (entityIds && entityIds.length > 0) {
74
+ const accessCheck = validateEntityAccess(user, entityIds);
75
+ if (!accessCheck.allowed) {
76
+ return res.status(403).json({
77
+ status: 'error',
78
+ reason: 'ACCESS_DENIED',
79
+ message: accessCheck.message
80
+ });
81
+ }
82
+ }
83
+
84
+ // 5. Check if computation is allowed for on-demand
85
+ const allowedComputations = config.onDemand?.allowedComputations;
86
+ if (allowedComputations && !allowedComputations.includes(computation)) {
87
+ return res.status(403).json({
88
+ status: 'error',
89
+ reason: 'COMPUTATION_NOT_ALLOWED',
90
+ message: `${computation} is not available for on-demand requests`
91
+ });
92
+ }
93
+
94
+ // 6. Forward to Dispatcher
95
+ console.log(`[OnDemand] User ${user.id} requesting ${computation} for entities: ${entityIds?.join(', ') || 'all'}`);
96
+
97
+ // Create a mock request/response to call dispatcher
98
+ const dispatcherRequest = {
99
+ body: {
100
+ computationName: computation,
101
+ targetDate: date || getTodayDate(),
102
+ source: 'on-demand',
103
+ entityIds: entityIds,
104
+ force: force || false,
105
+ requestedBy: user.id
106
+ }
107
+ };
108
+
109
+ // Capture dispatcher response
110
+ const dispatcherResponse = await callDispatcher(dispatcherRequest);
111
+
112
+ // Forward response to client
113
+ return res.status(dispatcherResponse.status).json(dispatcherResponse.body);
114
+
115
+ } catch (error) {
116
+ console.error('[OnDemand] Error:', error);
117
+ return res.status(500).json({
118
+ status: 'error',
119
+ reason: 'INTERNAL_ERROR',
120
+ message: 'An internal error occurred'
121
+ });
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Authenticate the request.
127
+ * Returns user object or null if not authenticated.
128
+ */
129
+ async function authenticateRequest(req) {
130
+ const authHeader = req.headers.authorization;
131
+
132
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
133
+ return null;
134
+ }
135
+
136
+ const token = authHeader.substring(7);
137
+
138
+ try {
139
+ // Option 1: Firebase Admin SDK (if available)
140
+ if (global.firebaseAdmin) {
141
+ const decodedToken = await global.firebaseAdmin.auth().verifyIdToken(token);
142
+ return {
143
+ id: decodedToken.uid,
144
+ email: decodedToken.email,
145
+ type: decodedToken.user_type || 'unknown'
146
+ };
147
+ }
148
+
149
+ // Option 2: Simple JWT decode (for development/testing)
150
+ // In production, always verify with Firebase Admin
151
+ const payload = decodeJwt(token);
152
+ if (payload && payload.sub) {
153
+ return {
154
+ id: payload.sub,
155
+ email: payload.email,
156
+ type: payload.user_type || 'unknown'
157
+ };
158
+ }
159
+
160
+ return null;
161
+ } catch (error) {
162
+ console.warn('[OnDemand] Auth error:', error.message);
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Simple JWT decode (without verification - for development only).
169
+ */
170
+ function decodeJwt(token) {
171
+ try {
172
+ const parts = token.split('.');
173
+ if (parts.length !== 3) return null;
174
+
175
+ const payload = Buffer.from(parts[1], 'base64').toString('utf8');
176
+ return JSON.parse(payload);
177
+ } catch (e) {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Check rate limit for a user.
184
+ */
185
+ function checkRateLimit(userId) {
186
+ const maxRequests = config.onDemand?.maxRequestsPerMinute || 5;
187
+ const windowMs = 60 * 1000; // 1 minute
188
+
189
+ const now = Date.now();
190
+ const userKey = `ratelimit:${userId}`;
191
+
192
+ // Get or create user's request history
193
+ let history = rateLimitStore.get(userKey);
194
+ if (!history) {
195
+ history = [];
196
+ rateLimitStore.set(userKey, history);
197
+ }
198
+
199
+ // Remove old entries
200
+ const cutoff = now - windowMs;
201
+ while (history.length > 0 && history[0] < cutoff) {
202
+ history.shift();
203
+ }
204
+
205
+ // Check limit
206
+ if (history.length >= maxRequests) {
207
+ const oldestRequest = history[0];
208
+ const retryAfter = Math.ceil((oldestRequest + windowMs - now) / 1000);
209
+ return { allowed: false, retryAfter };
210
+ }
211
+
212
+ // Record this request
213
+ history.push(now);
214
+
215
+ return { allowed: true };
216
+ }
217
+
218
+ /**
219
+ * Validate user can access the requested entities.
220
+ */
221
+ function validateEntityAccess(user, entityIds) {
222
+ // Default policy: users can only access their own data
223
+ // This can be extended based on user roles
224
+
225
+ for (const entityId of entityIds) {
226
+ // Allow if entityId matches user's ID
227
+ if (entityId === user.id) {
228
+ continue;
229
+ }
230
+
231
+ // Allow if user is admin (could add role checking)
232
+ // if (user.role === 'admin') continue;
233
+
234
+ // Deny access
235
+ return {
236
+ allowed: false,
237
+ message: `You cannot access data for entity: ${entityId}`
238
+ };
239
+ }
240
+
241
+ return { allowed: true };
242
+ }
243
+
244
+ /**
245
+ * Call the dispatcher and capture response.
246
+ */
247
+ async function callDispatcher(req) {
248
+ // Initialize dispatcher if needed
249
+ await initialize();
250
+
251
+ // Create a mock response object to capture the dispatcher's response
252
+ let responseStatus = 200;
253
+ let responseBody = null;
254
+
255
+ const mockRes = {
256
+ status: (code) => {
257
+ responseStatus = code;
258
+ return mockRes;
259
+ },
260
+ json: (body) => {
261
+ responseBody = body;
262
+ return mockRes;
263
+ }
264
+ };
265
+
266
+ await dispatcherHandler(req, mockRes);
267
+
268
+ return {
269
+ status: responseStatus,
270
+ body: responseBody
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Get today's date in YYYY-MM-DD format.
276
+ */
277
+ function getTodayDate() {
278
+ return new Date().toISOString().split('T')[0];
279
+ }
280
+
281
+ // Export for Cloud Functions
282
+ module.exports = {
283
+ onDemandHandler,
284
+
285
+ // For testing
286
+ _authenticateRequest: authenticateRequest,
287
+ _checkRateLimit: checkRateLimit,
288
+ _validateEntityAccess: validateEntityAccess
289
+ };