bulltrackers-module 1.0.768 → 1.0.770
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/computation-system-v2/UserPortfolioMetrics.js +50 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +557 -337
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
- package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
- package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
- package/functions/computation-system-v2/computations/RiskScoreIncrease.js +13 -13
- package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
- package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +30 -128
- package/functions/computation-system-v2/core-api.js +17 -9
- package/functions/computation-system-v2/data_schema_reference.MD +108 -0
- package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
- package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
- package/functions/computation-system-v2/devtools/index.js +36 -0
- package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
- package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
- package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
- package/functions/computation-system-v2/devtools/shared/index.js +16 -0
- package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
- package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
- package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
- package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
- package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
- package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
- package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
- package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
- package/functions/computation-system-v2/framework/data/DataFetcher.js +250 -184
- package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +215 -129
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
- package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
- package/functions/computation-system-v2/framework/storage/StorageManager.js +105 -67
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +12 -6
- package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
- package/functions/computation-system-v2/handlers/scheduler.js +172 -203
- package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
- package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
- package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
- package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
- package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
- package/package.json +1 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DAG Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes the computation DAG for visualization and debugging.
|
|
5
|
+
* Provides pass levels, dependency chains, and execution order info.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class DAGAnalyzer {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} introspector - SystemIntrospector instance
|
|
11
|
+
*/
|
|
12
|
+
constructor(introspector) {
|
|
13
|
+
this.introspector = introspector;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get full DAG structure for visualization.
|
|
18
|
+
* @returns {Object} { nodes: [], edges: [] }
|
|
19
|
+
*/
|
|
20
|
+
getVisualizationData() {
|
|
21
|
+
const graph = this.introspector.getDependencyGraph();
|
|
22
|
+
const levels = this.introspector.getPassLevels();
|
|
23
|
+
const configs = this.introspector.getAllComputationConfigs();
|
|
24
|
+
|
|
25
|
+
const nodes = [];
|
|
26
|
+
const edges = [];
|
|
27
|
+
|
|
28
|
+
for (const [name, deps] of graph) {
|
|
29
|
+
const config = configs.get(name);
|
|
30
|
+
nodes.push({
|
|
31
|
+
id: name,
|
|
32
|
+
label: name,
|
|
33
|
+
level: levels.get(name) || 1,
|
|
34
|
+
type: config?.type || 'unknown',
|
|
35
|
+
category: config?.category || 'unknown',
|
|
36
|
+
isHistorical: config?.isHistorical || false
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
for (const dep of deps) {
|
|
40
|
+
edges.push({
|
|
41
|
+
source: dep,
|
|
42
|
+
target: name,
|
|
43
|
+
id: `${dep}->${name}`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { nodes, edges };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get execution plan for a target computation.
|
|
53
|
+
* @param {string} targetComputation
|
|
54
|
+
* @returns {Object} Execution plan
|
|
55
|
+
*/
|
|
56
|
+
getExecutionPlan(targetComputation) {
|
|
57
|
+
const graph = this.introspector.getDependencyGraph();
|
|
58
|
+
const levels = this.introspector.getPassLevels();
|
|
59
|
+
|
|
60
|
+
const order = [];
|
|
61
|
+
const visited = new Set();
|
|
62
|
+
|
|
63
|
+
const visit = (name) => {
|
|
64
|
+
if (visited.has(name)) return;
|
|
65
|
+
visited.add(name);
|
|
66
|
+
|
|
67
|
+
const deps = graph.get(name) || [];
|
|
68
|
+
for (const dep of deps) {
|
|
69
|
+
visit(dep);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
order.push(name);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
visit(targetComputation);
|
|
76
|
+
|
|
77
|
+
// Group by pass level
|
|
78
|
+
const byPass = {};
|
|
79
|
+
for (const name of order) {
|
|
80
|
+
const level = levels.get(name) || 1;
|
|
81
|
+
if (!byPass[level]) byPass[level] = [];
|
|
82
|
+
byPass[level].push(name);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
target: targetComputation,
|
|
87
|
+
totalComputations: order.length,
|
|
88
|
+
executionOrder: order,
|
|
89
|
+
byPassLevel: byPass,
|
|
90
|
+
estimatedPasses: Object.keys(byPass).length
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Find all paths from source to target computation.
|
|
96
|
+
* @param {string} source
|
|
97
|
+
* @param {string} target
|
|
98
|
+
* @returns {string[][]} Array of paths
|
|
99
|
+
*/
|
|
100
|
+
findPaths(source, target) {
|
|
101
|
+
const graph = this.introspector.getDependencyGraph();
|
|
102
|
+
const paths = [];
|
|
103
|
+
|
|
104
|
+
const dfs = (current, path) => {
|
|
105
|
+
if (current === target) {
|
|
106
|
+
paths.push([...path, current]);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const deps = graph.get(current) || [];
|
|
111
|
+
for (const dep of deps) {
|
|
112
|
+
if (!path.includes(dep)) {
|
|
113
|
+
dfs(dep, [...path, current]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
dfs(source, []);
|
|
119
|
+
return paths;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get impact analysis - what would be affected if a computation changes.
|
|
124
|
+
* @param {string} computationName
|
|
125
|
+
* @returns {Object}
|
|
126
|
+
*/
|
|
127
|
+
getImpactAnalysis(computationName) {
|
|
128
|
+
const graph = this.introspector.getDependencyGraph();
|
|
129
|
+
const affected = new Set();
|
|
130
|
+
|
|
131
|
+
// Find all computations that depend (directly or indirectly) on this one
|
|
132
|
+
const findDependents = (name) => {
|
|
133
|
+
for (const [candidate, deps] of graph) {
|
|
134
|
+
if (deps.includes(name) && !affected.has(candidate)) {
|
|
135
|
+
affected.add(candidate);
|
|
136
|
+
findDependents(candidate);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
findDependents(computationName);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
computation: computationName,
|
|
145
|
+
directDependents: [...graph.entries()]
|
|
146
|
+
.filter(([_, deps]) => deps.includes(computationName))
|
|
147
|
+
.map(([name]) => name),
|
|
148
|
+
allAffected: Array.from(affected),
|
|
149
|
+
impactScore: affected.size
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Detect cycles in the DAG (should always be empty for valid DAG).
|
|
155
|
+
* @returns {string[][]} Any cycles found
|
|
156
|
+
*/
|
|
157
|
+
detectCycles() {
|
|
158
|
+
const graph = this.introspector.getDependencyGraph();
|
|
159
|
+
const cycles = [];
|
|
160
|
+
const visited = new Set();
|
|
161
|
+
const recursionStack = new Set();
|
|
162
|
+
|
|
163
|
+
const dfs = (node, path) => {
|
|
164
|
+
if (recursionStack.has(node)) {
|
|
165
|
+
const cycleStart = path.indexOf(node);
|
|
166
|
+
cycles.push(path.slice(cycleStart));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (visited.has(node)) return;
|
|
170
|
+
|
|
171
|
+
visited.add(node);
|
|
172
|
+
recursionStack.add(node);
|
|
173
|
+
|
|
174
|
+
const deps = graph.get(node) || [];
|
|
175
|
+
for (const dep of deps) {
|
|
176
|
+
dfs(dep, [...path, node]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
recursionStack.delete(node);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
for (const node of graph.keys()) {
|
|
183
|
+
dfs(node, []);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return cycles;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get summary statistics about the DAG.
|
|
191
|
+
* @returns {Object}
|
|
192
|
+
*/
|
|
193
|
+
getSummary() {
|
|
194
|
+
const graph = this.introspector.getDependencyGraph();
|
|
195
|
+
const levels = this.introspector.getPassLevels();
|
|
196
|
+
|
|
197
|
+
let totalEdges = 0;
|
|
198
|
+
let maxInDegree = 0;
|
|
199
|
+
let maxOutDegree = 0;
|
|
200
|
+
|
|
201
|
+
const outDegree = new Map();
|
|
202
|
+
const inDegree = new Map();
|
|
203
|
+
|
|
204
|
+
for (const [name, deps] of graph) {
|
|
205
|
+
outDegree.set(name, deps.length);
|
|
206
|
+
totalEdges += deps.length;
|
|
207
|
+
maxOutDegree = Math.max(maxOutDegree, deps.length);
|
|
208
|
+
|
|
209
|
+
for (const dep of deps) {
|
|
210
|
+
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const [_, count] of inDegree) {
|
|
215
|
+
maxInDegree = Math.max(maxInDegree, count);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Find entry points (no dependencies)
|
|
219
|
+
const entryPoints = [...graph.entries()]
|
|
220
|
+
.filter(([_, deps]) => deps.length === 0)
|
|
221
|
+
.map(([name]) => name);
|
|
222
|
+
|
|
223
|
+
// Find exit points (no dependents)
|
|
224
|
+
const allDeps = new Set();
|
|
225
|
+
for (const deps of graph.values()) {
|
|
226
|
+
for (const dep of deps) allDeps.add(dep);
|
|
227
|
+
}
|
|
228
|
+
const exitPoints = [...graph.keys()].filter(name => !allDeps.has(name) || true);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
totalNodes: graph.size,
|
|
232
|
+
totalEdges,
|
|
233
|
+
totalPasses: this.introspector.getTotalPasses(),
|
|
234
|
+
maxInDegree,
|
|
235
|
+
maxOutDegree,
|
|
236
|
+
entryPointCount: entryPoints.length,
|
|
237
|
+
entryPoints: entryPoints.slice(0, 5), // First 5
|
|
238
|
+
cycles: this.detectCycles()
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = { DAGAnalyzer };
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mock Data Fetcher
|
|
3
|
+
* * Drop-in replacement for DataFetcher that returns mock data.
|
|
4
|
+
* Designed to be injected into the real Orchestrator for 1:1 simulation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { MockDataFactory } = require('../shared/MockDataFactory');
|
|
8
|
+
|
|
9
|
+
class MockDataFetcher {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {Object} [options.mockDataFactory] - Custom MockDataFactory instance
|
|
13
|
+
* @param {Object} [options.introspector] - SystemIntrospector for metadata
|
|
14
|
+
* @param {Object} [options.preloadedData] - Pre-generated data { tableName: { entityId: rows[] } }
|
|
15
|
+
* @param {Object} [options.computationResults] - Results from earlier passes
|
|
16
|
+
* @param {number} [options.entityCount=10] - Default entity count
|
|
17
|
+
* @param {string[]} [options.entityIds] - Specific entity IDs to generate
|
|
18
|
+
*/
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.mockDataFactory = options.mockDataFactory || new MockDataFactory(options);
|
|
21
|
+
this.introspector = options.introspector || null;
|
|
22
|
+
this.preloadedData = options.preloadedData || {};
|
|
23
|
+
this.computationResults = options.computationResults || {};
|
|
24
|
+
this.entityCount = options.entityCount || 10;
|
|
25
|
+
this.entityIds = options.entityIds || null;
|
|
26
|
+
|
|
27
|
+
// Stats tracking (mimics real DataFetcher)
|
|
28
|
+
this.stats = {
|
|
29
|
+
queriesExecuted: 0,
|
|
30
|
+
rowsReturned: 0,
|
|
31
|
+
tablesAccessed: new Set(),
|
|
32
|
+
errors: []
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Query log for inspection
|
|
36
|
+
this.queryLog = [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Main entry point - matches DataFetcher.fetchForComputation signature.
|
|
41
|
+
* * @param {Object} requires - Computation requirements
|
|
42
|
+
* @param {Date|string} targetDate - Target date
|
|
43
|
+
* @param {string[]|null} entities - Specific entities to fetch
|
|
44
|
+
* @returns {Object} Data keyed by table name
|
|
45
|
+
*/
|
|
46
|
+
async fetchForComputation(requires, targetDate, entities = null) {
|
|
47
|
+
const results = {};
|
|
48
|
+
const errors = [];
|
|
49
|
+
|
|
50
|
+
const effectiveEntities = entities || this.entityIds || null;
|
|
51
|
+
|
|
52
|
+
for (const [key, spec] of Object.entries(requires)) {
|
|
53
|
+
try {
|
|
54
|
+
// Handle metric type (computation dependency)
|
|
55
|
+
if (spec.type === 'metric') {
|
|
56
|
+
results[key] = await this._fetchMetric(spec, targetDate, effectiveEntities);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Log the query
|
|
61
|
+
this.queryLog.push({
|
|
62
|
+
table: key,
|
|
63
|
+
targetDate,
|
|
64
|
+
lookback: spec.lookback || 0,
|
|
65
|
+
entities: effectiveEntities,
|
|
66
|
+
timestamp: new Date().toISOString()
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Fetch mock data
|
|
70
|
+
const data = await this.fetch({
|
|
71
|
+
table: key,
|
|
72
|
+
targetDate,
|
|
73
|
+
lookback: spec.lookback || 0,
|
|
74
|
+
mandatory: spec.mandatory || false,
|
|
75
|
+
fields: spec.fields || null,
|
|
76
|
+
entities: effectiveEntities
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
results[key] = data;
|
|
80
|
+
this.stats.tablesAccessed.add(key);
|
|
81
|
+
|
|
82
|
+
// Check mandatory
|
|
83
|
+
if (spec.mandatory && this._isEmpty(data)) {
|
|
84
|
+
errors.push({ table: key, reason: 'MANDATORY_MISSING' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (spec.mandatory) {
|
|
89
|
+
errors.push({ table: key, reason: e.message });
|
|
90
|
+
}
|
|
91
|
+
results[key] = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (errors.length > 0) {
|
|
96
|
+
const msg = errors.map(e => `${e.table}: ${e.reason}`).join(', ');
|
|
97
|
+
this.stats.errors.push(msg);
|
|
98
|
+
throw new Error(`[MockDataFetcher] Missing mandatory data: ${msg}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Low-level fetch - matches DataFetcher.fetch signature.
|
|
106
|
+
* * @param {Object} options
|
|
107
|
+
* @returns {Object} Data keyed by entity ID
|
|
108
|
+
*/
|
|
109
|
+
async fetch(options) {
|
|
110
|
+
// Destructure 'lookback' and 'targetDate' from options
|
|
111
|
+
const { table, targetDate, lookback = 0, entities = null, fields = null } = options;
|
|
112
|
+
|
|
113
|
+
this.stats.queriesExecuted++;
|
|
114
|
+
|
|
115
|
+
// Check for preloaded data first
|
|
116
|
+
if (this.preloadedData[table]) {
|
|
117
|
+
const preloaded = this.preloadedData[table];
|
|
118
|
+
if (entities) {
|
|
119
|
+
const filtered = {};
|
|
120
|
+
for (const id of entities) {
|
|
121
|
+
if (preloaded[id]) {
|
|
122
|
+
filtered[id] = preloaded[id];
|
|
123
|
+
this.stats.rowsReturned += preloaded[id].length;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return filtered;
|
|
127
|
+
}
|
|
128
|
+
this.stats.rowsReturned += Object.values(preloaded).reduce((sum, rows) => sum + rows.length, 0);
|
|
129
|
+
return preloaded;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Generate mock data
|
|
133
|
+
try {
|
|
134
|
+
const entityIds = entities || this.entityIds;
|
|
135
|
+
const { byEntity } = this.mockDataFactory.generate(table, {
|
|
136
|
+
entityCount: this.entityCount,
|
|
137
|
+
entityIds,
|
|
138
|
+
// FIX: Use 'lookback' and 'targetDate' variables, not 'daysBack'/'asOfDate'
|
|
139
|
+
daysBack: lookback || 0,
|
|
140
|
+
asOfDate: targetDate
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Convert Map to Object
|
|
144
|
+
const result = {};
|
|
145
|
+
for (const [id, rows] of byEntity) {
|
|
146
|
+
result[id] = rows;
|
|
147
|
+
this.stats.rowsReturned += rows.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// Log error to see why generation failed (optional but helpful)
|
|
153
|
+
console.error(`[MockDataFetcher] Generation failed for ${table}:`, e.message);
|
|
154
|
+
// Table not in templates - return empty data
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Fetch metric (computation result from earlier pass).
|
|
161
|
+
*/
|
|
162
|
+
async _fetchMetric(spec, targetDate, entities) {
|
|
163
|
+
const computationName = spec.computation;
|
|
164
|
+
if (!computationName) {
|
|
165
|
+
throw new Error('Metric spec missing "computation" field');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cached = this.computationResults[computationName];
|
|
169
|
+
if (!cached) {
|
|
170
|
+
// Generate empty metric result
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Filter by entities if specified
|
|
175
|
+
if (entities) {
|
|
176
|
+
const filtered = {};
|
|
177
|
+
for (const id of entities) {
|
|
178
|
+
if (cached[id]) {
|
|
179
|
+
filtered[id] = cached[id];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return filtered;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return cached;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Batched fetch - for streaming computations.
|
|
190
|
+
* Returns an async iterator that yields { data, entityIds } batches.
|
|
191
|
+
* NOTE: This is NOT async because Orchestrator calls it without await.
|
|
192
|
+
*/
|
|
193
|
+
fetchComputationBatched(requires, targetDate, batchSize = 1000) {
|
|
194
|
+
const self = this;
|
|
195
|
+
|
|
196
|
+
// Return an async iterable that yields batches in correct format
|
|
197
|
+
return {
|
|
198
|
+
[Symbol.asyncIterator]() {
|
|
199
|
+
let yielded = false;
|
|
200
|
+
return {
|
|
201
|
+
async next() {
|
|
202
|
+
if (yielded) {
|
|
203
|
+
return { done: true };
|
|
204
|
+
}
|
|
205
|
+
yielded = true;
|
|
206
|
+
|
|
207
|
+
// Fetch data inside the iterator
|
|
208
|
+
const allData = await self.fetchForComputation(requires, targetDate, self.entityIds);
|
|
209
|
+
|
|
210
|
+
// Get all entity IDs from the first table that has data
|
|
211
|
+
const firstTableWithData = Object.keys(allData).find(k => allData[k] && Object.keys(allData[k]).length > 0);
|
|
212
|
+
const entityIds = firstTableWithData ? Object.keys(allData[firstTableWithData]) : (self.entityIds || []);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
done: false,
|
|
216
|
+
value: { data: allData, entityIds }
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check data availability.
|
|
226
|
+
* Returns { canRun: boolean, missing: string[], available: { table: { available, rowCount } } }
|
|
227
|
+
*/
|
|
228
|
+
async checkAvailability(requires, targetDate) {
|
|
229
|
+
// In mock mode, all data is always available
|
|
230
|
+
const available = {};
|
|
231
|
+
for (const table of Object.keys(requires)) {
|
|
232
|
+
available[table] = { available: true, rowCount: this.entityCount };
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
canRun: true,
|
|
236
|
+
missing: [],
|
|
237
|
+
available
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if data exists for a table.
|
|
243
|
+
*/
|
|
244
|
+
async hasData(table, targetDate) {
|
|
245
|
+
return true; // Mock always has data
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get fetch statistics.
|
|
250
|
+
*/
|
|
251
|
+
getStats() {
|
|
252
|
+
return {
|
|
253
|
+
...this.stats,
|
|
254
|
+
tablesAccessed: Array.from(this.stats.tablesAccessed)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Reset statistics.
|
|
260
|
+
*/
|
|
261
|
+
resetStats() {
|
|
262
|
+
this.stats = {
|
|
263
|
+
queriesExecuted: 0,
|
|
264
|
+
rowsReturned: 0,
|
|
265
|
+
tablesAccessed: new Set(),
|
|
266
|
+
errors: []
|
|
267
|
+
};
|
|
268
|
+
this.queryLog = [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Clear cache (no-op for mock).
|
|
273
|
+
*/
|
|
274
|
+
clearCache() {
|
|
275
|
+
// No cache in mock
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Add computation results for dependency simulation.
|
|
280
|
+
* * @param {string} computationName
|
|
281
|
+
* @param {Object} results - { entityId: result }
|
|
282
|
+
*/
|
|
283
|
+
addComputationResults(computationName, results) {
|
|
284
|
+
this.computationResults[computationName] = results;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get query log for inspection.
|
|
289
|
+
*/
|
|
290
|
+
getQueryLog() {
|
|
291
|
+
return [...this.queryLog];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// =========================================================================
|
|
295
|
+
// HELPERS
|
|
296
|
+
// =========================================================================
|
|
297
|
+
|
|
298
|
+
_isEmpty(data) {
|
|
299
|
+
if (!data) return true;
|
|
300
|
+
if (Array.isArray(data)) return data.length === 0;
|
|
301
|
+
if (typeof data === 'object') return Object.keys(data).length === 0;
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = { MockDataFetcher };
|