bulltrackers-module 1.0.768 → 1.0.769

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 (51) hide show
  1. package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
  2. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +557 -337
  3. package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
  4. package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
  5. package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
  6. package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
  7. package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
  8. package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
  9. package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
  10. package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
  11. package/functions/computation-system-v2/config/bulltrackers.config.js +30 -128
  12. package/functions/computation-system-v2/core-api.js +17 -9
  13. package/functions/computation-system-v2/data_schema_reference.MD +108 -0
  14. package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
  15. package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
  16. package/functions/computation-system-v2/devtools/index.js +36 -0
  17. package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
  18. package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
  19. package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
  20. package/functions/computation-system-v2/devtools/shared/index.js +16 -0
  21. package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
  22. package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
  23. package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
  24. package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
  25. package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
  26. package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
  27. package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
  28. package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
  29. package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
  30. package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
  31. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
  32. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
  33. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
  34. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
  35. package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
  36. package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
  37. package/functions/computation-system-v2/framework/data/DataFetcher.js +250 -184
  38. package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
  39. package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
  40. package/functions/computation-system-v2/framework/execution/Orchestrator.js +215 -129
  41. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
  42. package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
  43. package/functions/computation-system-v2/framework/storage/StorageManager.js +105 -67
  44. package/functions/computation-system-v2/framework/testing/ComputationTester.js +12 -6
  45. package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
  46. package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
  47. package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
  48. package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
  49. package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
  50. package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
  51. package/package.json +1 -1
@@ -0,0 +1,362 @@
1
+ /**
2
+ * @fileoverview Declarative Computation Builder
3
+ *
4
+ * Generates computation code from YAML specifications.
5
+ *
6
+ * Usage:
7
+ * node builder.js <spec.yaml> [--output <dir>]
8
+ * node builder.js --interactive
9
+ *
10
+ * YAML Schema:
11
+ * name: ComputationName
12
+ * description: What it does
13
+ * type: per-entity | global
14
+ * category: signed_in_user | popular_investor | global
15
+ * isHistorical: true | false
16
+ * requires:
17
+ * table_name:
18
+ * lookback: 30
19
+ * mandatory: true
20
+ * fields: [field1, field2]
21
+ * dependencies: [OtherComputation]
22
+ * storage:
23
+ * bigquery: true
24
+ * firestore:
25
+ * enabled: true
26
+ * path: 'collection/{entityId}'
27
+ * logic:
28
+ * extract:
29
+ * - variable: positions
30
+ * from: portfolio_snapshots
31
+ * using: rules.portfolio.extractPositions
32
+ * compute:
33
+ * - variable: result
34
+ * expression: positions.length
35
+ */
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+ const readline = require('readline');
40
+
41
+ // Simple YAML parser (no external deps)
42
+ function parseYAML(content) {
43
+ const lines = content.split('\n');
44
+ const result = {};
45
+ const stack = [{ indent: -2, obj: result }];
46
+ let currentKey = null;
47
+ let listContext = null;
48
+
49
+ for (const rawLine of lines) {
50
+ const line = rawLine.replace(/\r$/, '');
51
+ if (line.trim() === '' || line.trim().startsWith('#')) continue;
52
+
53
+ const indent = line.search(/\S/);
54
+ const trimmed = line.trim();
55
+
56
+ // Pop stack to find parent
57
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
58
+ stack.pop();
59
+ }
60
+ const parent = stack[stack.length - 1].obj;
61
+
62
+ // Handle list item
63
+ if (trimmed.startsWith('- ')) {
64
+ const value = trimmed.slice(2).trim();
65
+ if (!Array.isArray(parent[currentKey])) {
66
+ parent[currentKey] = [];
67
+ }
68
+
69
+ if (value.includes(':')) {
70
+ // Object in list
71
+ const obj = {};
72
+ const [k, v] = value.split(':').map(s => s.trim());
73
+ obj[k] = parseValue(v);
74
+ parent[currentKey].push(obj);
75
+ stack.push({ indent, obj });
76
+ } else {
77
+ parent[currentKey].push(parseValue(value));
78
+ }
79
+ continue;
80
+ }
81
+
82
+ // Handle key: value
83
+ const colonIdx = trimmed.indexOf(':');
84
+ if (colonIdx > 0) {
85
+ const key = trimmed.slice(0, colonIdx).trim();
86
+ const value = trimmed.slice(colonIdx + 1).trim();
87
+
88
+ if (value === '') {
89
+ // Nested object
90
+ parent[key] = {};
91
+ currentKey = key;
92
+ stack.push({ indent, obj: parent[key] });
93
+ } else if (value.startsWith('[') && value.endsWith(']')) {
94
+ // Inline array
95
+ parent[key] = value.slice(1, -1).split(',').map(s => parseValue(s.trim()));
96
+ currentKey = key;
97
+ } else {
98
+ parent[key] = parseValue(value);
99
+ currentKey = key;
100
+ }
101
+ }
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ function parseValue(str) {
108
+ if (str === 'true') return true;
109
+ if (str === 'false') return false;
110
+ if (str === 'null') return null;
111
+ if (/^\d+$/.test(str)) return parseInt(str);
112
+ if (/^\d+\.\d+$/.test(str)) return parseFloat(str);
113
+ return str.replace(/^['"]|['"]$/g, '');
114
+ }
115
+
116
+ // Template engine
117
+ function generateCode(spec) {
118
+ const { name, description, type, category, isHistorical, requires, dependencies, storage, logic } = spec;
119
+
120
+ // Build requires block
121
+ const requiresCode = Object.entries(requires || {}).map(([table, config]) => {
122
+ const fields = config.fields ? `fields: [${config.fields.map(f => `'${f}'`).join(', ')}]` : '';
123
+ return ` '${table}': {
124
+ lookback: ${config.lookback || 0},
125
+ mandatory: ${config.mandatory || false}${fields ? `,\n ${fields}` : ''}
126
+ }`;
127
+ }).join(',\n');
128
+
129
+ // Build dependencies
130
+ const depsCode = dependencies && dependencies.length > 0
131
+ ? `\n dependencies: [${dependencies.map(d => `'${d}'`).join(', ')}],`
132
+ : '';
133
+
134
+ // Build storage config
135
+ let storageCode = '';
136
+ if (storage) {
137
+ const firestoreConfig = storage.firestore?.enabled
138
+ ? `\n firestore: {
139
+ enabled: true,
140
+ path: '${storage.firestore.path || 'collection/{entityId}'}',
141
+ merge: ${storage.firestore.merge || true}
142
+ }`
143
+ : '\n firestore: { enabled: false }';
144
+
145
+ storageCode = `
146
+ storage: {
147
+ bigquery: ${storage.bigquery !== false},${firestoreConfig}
148
+ }`;
149
+ }
150
+
151
+ // Build process method
152
+ const processCode = buildProcessCode(logic, requires, type);
153
+
154
+ return `const { Computation } = require('../framework');
155
+
156
+ /**
157
+ * ${name}
158
+ * ${description || 'Auto-generated computation'}
159
+ *
160
+ * Generated: ${new Date().toISOString()}
161
+ */
162
+ class ${name} extends Computation {
163
+ static getConfig() {
164
+ return {
165
+ name: '${name}',
166
+ description: '${description || ''}',
167
+ type: '${type || 'per-entity'}',
168
+ category: '${category || 'signed_in_user'}',
169
+ isHistorical: ${isHistorical || false},${depsCode}
170
+ requires: {
171
+ ${requiresCode}
172
+ },${storageCode}
173
+ };
174
+ }
175
+
176
+ ${processCode}
177
+ }
178
+
179
+ module.exports = ${name};
180
+ `;
181
+ }
182
+
183
+ function buildProcessCode(logic, requires, type) {
184
+ const lines = [];
185
+ const tables = Object.keys(requires || {});
186
+
187
+ if (type === 'global') {
188
+ lines.push(' async process(data, { targetDate, rules }) {');
189
+ } else {
190
+ lines.push(' async process(data, { entityId, targetDate, rules, getDependency }) {');
191
+ }
192
+
193
+ // Extract data from tables
194
+ for (const table of tables) {
195
+ if (type === 'global') {
196
+ lines.push(` const ${toCamelCase(table)} = data.${table} || {};`);
197
+ } else {
198
+ lines.push(` const ${toCamelCase(table)} = data.${table}[entityId] || [];`);
199
+ }
200
+ }
201
+
202
+ lines.push('');
203
+
204
+ // Generate extraction logic
205
+ if (logic?.extract) {
206
+ for (const ext of logic.extract) {
207
+ if (ext.using) {
208
+ lines.push(` const ${ext.variable} = ${ext.using}(${toCamelCase(ext.from)});`);
209
+ } else {
210
+ lines.push(` const ${ext.variable} = ${toCamelCase(ext.from)};`);
211
+ }
212
+ }
213
+ lines.push('');
214
+ }
215
+
216
+ // Generate computation logic
217
+ if (logic?.compute) {
218
+ for (const comp of logic.compute) {
219
+ lines.push(` const ${comp.variable} = ${comp.expression};`);
220
+ }
221
+ lines.push('');
222
+ }
223
+
224
+ // Build return statement
225
+ if (logic?.compute) {
226
+ const resultVars = logic.compute.map(c => c.variable);
227
+ lines.push(' return {');
228
+ for (const v of resultVars) {
229
+ lines.push(` ${v},`);
230
+ }
231
+ lines.push(' };');
232
+ } else {
233
+ lines.push(' // TODO: Implement computation logic');
234
+ lines.push(' return {};');
235
+ }
236
+
237
+ lines.push(' }');
238
+
239
+ return lines.join('\n');
240
+ }
241
+
242
+ function toCamelCase(str) {
243
+ return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
244
+ }
245
+
246
+ // Interactive CLI
247
+ async function runInteractive() {
248
+ const rl = readline.createInterface({
249
+ input: process.stdin,
250
+ output: process.stdout
251
+ });
252
+
253
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
254
+
255
+ console.log('\n🔧 Computation Builder - Interactive Mode\n');
256
+
257
+ const spec = {
258
+ requires: {},
259
+ storage: { bigquery: true, firestore: { enabled: false } }
260
+ };
261
+
262
+ spec.name = await ask('Computation name (PascalCase): ');
263
+ spec.description = await ask('Description: ');
264
+
265
+ const typeChoice = await ask('Type (1=per-entity, 2=global): ');
266
+ spec.type = typeChoice === '2' ? 'global' : 'per-entity';
267
+
268
+ const catChoice = await ask('Category (1=signed_in_user, 2=popular_investor, 3=global): ');
269
+ spec.category = { '1': 'signed_in_user', '2': 'popular_investor', '3': 'global' }[catChoice] || 'signed_in_user';
270
+
271
+ const historical = await ask('Is historical? (y/N): ');
272
+ spec.isHistorical = historical.toLowerCase() === 'y';
273
+
274
+ // Add tables
275
+ console.log('\nAdd required tables (empty name to finish):');
276
+ while (true) {
277
+ const table = await ask(' Table name: ');
278
+ if (!table) break;
279
+
280
+ const lookback = await ask(' Lookback days (0): ');
281
+ const mandatory = await ask(' Mandatory? (Y/n): ');
282
+
283
+ spec.requires[table] = {
284
+ lookback: parseInt(lookback) || 0,
285
+ mandatory: mandatory.toLowerCase() !== 'n'
286
+ };
287
+ }
288
+
289
+ // Dependencies
290
+ const deps = await ask('\nDependencies (comma-separated, or empty): ');
291
+ if (deps) {
292
+ spec.dependencies = deps.split(',').map(d => d.trim());
293
+ }
294
+
295
+ // Firestore
296
+ const useFirestore = await ask('Enable Firestore? (y/N): ');
297
+ if (useFirestore.toLowerCase() === 'y') {
298
+ spec.storage.firestore.enabled = true;
299
+ spec.storage.firestore.path = await ask(' Firestore path: ');
300
+ }
301
+
302
+ rl.close();
303
+
304
+ const code = generateCode(spec);
305
+ const filename = `${spec.name}.js`;
306
+
307
+ console.log(`\n📝 Generated ${filename}\n`);
308
+ console.log(code);
309
+
310
+ const outputPath = path.join(process.cwd(), filename);
311
+ fs.writeFileSync(outputPath, code);
312
+ console.log(`\n✅ Saved to ${outputPath}`);
313
+ }
314
+
315
+ // CLI Entry
316
+ async function main() {
317
+ const args = process.argv.slice(2);
318
+
319
+ if (args.includes('--interactive') || args.includes('-i')) {
320
+ await runInteractive();
321
+ return;
322
+ }
323
+
324
+ if (args.length === 0) {
325
+ console.log(`
326
+ Computation Builder
327
+
328
+ Usage:
329
+ node builder.js <spec.yaml> Generate from YAML spec
330
+ node builder.js --interactive Interactive wizard
331
+
332
+ Options:
333
+ --output <dir> Output directory (default: current)
334
+ `);
335
+ return;
336
+ }
337
+
338
+ const specFile = args[0];
339
+ if (!fs.existsSync(specFile)) {
340
+ console.error(`File not found: ${specFile}`);
341
+ process.exit(1);
342
+ }
343
+
344
+ const content = fs.readFileSync(specFile, 'utf8');
345
+ const spec = parseYAML(content);
346
+ const code = generateCode(spec);
347
+
348
+ const outputIdx = args.indexOf('--output');
349
+ const outputDir = outputIdx >= 0 && args[outputIdx + 1]
350
+ ? args[outputIdx + 1]
351
+ : process.cwd();
352
+
353
+ const filename = `${spec.name}.js`;
354
+ const outputPath = path.join(outputDir, filename);
355
+
356
+ fs.writeFileSync(outputPath, code);
357
+ console.log(`✅ Generated ${outputPath}`);
358
+ }
359
+
360
+ main().catch(console.error);
361
+
362
+ module.exports = { parseYAML, generateCode };
@@ -0,0 +1,26 @@
1
+ # Simple Example - No logic block (parser handles well)
2
+ name: UserPortfolioMetrics
3
+ description: Calculate portfolio metrics for signed-in users
4
+
5
+ type: per-entity
6
+ category: signed_in_user
7
+ isHistorical: true
8
+
9
+ requires:
10
+ portfolio_snapshots:
11
+ lookback: 30
12
+ mandatory: true
13
+ fields: [user_id, date, portfolio_data, cash, total_value]
14
+ trade_history_snapshots:
15
+ lookback: 30
16
+ mandatory: false
17
+ fields: [user_id, date, trade_history]
18
+
19
+ dependencies: [SignedInUserList]
20
+
21
+ storage:
22
+ bigquery: true
23
+ firestore:
24
+ enabled: true
25
+ path: users/{entityId}/metrics
26
+ merge: true
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @fileoverview DevTools Main Index
3
+ */
4
+
5
+ // Shared foundation
6
+ const shared = require('./shared');
7
+ const { SystemIntrospector, MockDataFactory, SchemaTemplates } = shared;
8
+
9
+ // Simulation engine
10
+ const simulation = require('./simulation');
11
+ const { SimulationEngine, SimulationServer, MockDataFetcher, MockStorageManager, DAGAnalyzer } = simulation;
12
+
13
+ // Builder
14
+ const { parseYAML, generateCode } = require('./builder/builder');
15
+
16
+ module.exports = {
17
+ // Foundation
18
+ SystemIntrospector,
19
+ MockDataFactory,
20
+ SchemaTemplates,
21
+
22
+ // Simulation
23
+ SimulationEngine,
24
+ SimulationServer,
25
+ MockDataFetcher,
26
+ MockStorageManager,
27
+ DAGAnalyzer,
28
+
29
+ // Builder
30
+ parseYAML,
31
+ generateCode,
32
+
33
+ // Re-export sub-modules
34
+ shared,
35
+ simulation
36
+ };
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @fileoverview Mock Data Factory
3
+ *
4
+ * Generates realistic mock data for the simulation engine.
5
+ * Uses SchemaTemplates to create data that matches actual BigQuery structures.
6
+ *
7
+ * Features:
8
+ * - Schema-aware row generation
9
+ * - Entity-keyed data for per-entity computations
10
+ * - Date range support for lookback simulation
11
+ * - Reproducible output with seeding
12
+ */
13
+
14
+ const { SchemaTemplates, helpers } = require('./SchemaTemplates');
15
+ const { randomInt, randomFloat, randomChoice, randomPastDate, randomUUID, randomHex } = helpers;
16
+
17
+ class MockDataFactory {
18
+ /**
19
+ * @param {Object} options
20
+ * @param {number} [options.seed] - Random seed for reproducibility
21
+ * @param {Object} [options.introspector] - SystemIntrospector for metadata
22
+ */
23
+ constructor(options = {}) {
24
+ this.introspector = options.introspector || null;
25
+ this.seed = options.seed || null;
26
+
27
+ // If seeded, we'd implement a seeded RNG here
28
+ // For now, using Math.random()
29
+ }
30
+
31
+ /**
32
+ * Generate mock data for a table.
33
+ *
34
+ * @param {string} tableName - Table to generate data for
35
+ * @param {Object} options
36
+ * @param {number} [options.entityCount=10] - Number of entities
37
+ * @param {number} [options.daysBack=7] - Days of history to generate
38
+ * @param {string[]} [options.entityIds] - Specific entity IDs to use
39
+ * @param {Date|string} [options.asOfDate] - Reference date (defaults to today)
40
+ * @returns {Object} { rows: [], byEntity: Map<entityId, rows[]> }
41
+ */
42
+ generate(tableName, options = {}) {
43
+ const template = SchemaTemplates[tableName];
44
+ if (!template) {
45
+ throw new Error(`No schema template for table: ${tableName}`);
46
+ }
47
+
48
+ const entityCount = options.entityCount || 10;
49
+ const daysBack = options.daysBack || 7;
50
+ const asOfDate = options.asOfDate ? new Date(options.asOfDate) : new Date();
51
+
52
+ // Generate entity IDs
53
+ const entityIds = options.entityIds || this._generateEntityIds(tableName, entityCount);
54
+
55
+ // Get table metadata
56
+ const meta = this.introspector?.getTableMetadata(tableName) || this._inferMetadata(tableName, template);
57
+
58
+ const rows = [];
59
+ const byEntity = new Map();
60
+
61
+ // Generate rows
62
+ for (const entityId of entityIds) {
63
+ const entityRows = [];
64
+
65
+ if (meta.dateField) {
66
+ // Date-partitioned table: generate one row per day
67
+ for (let d = 0; d <= daysBack; d++) {
68
+ const date = new Date(asOfDate);
69
+ date.setDate(date.getDate() - d);
70
+
71
+ const row = this._generateRow(tableName, template, entityId, date, meta);
72
+ rows.push(row);
73
+ entityRows.push(row);
74
+ }
75
+ } else {
76
+ // Non-partitioned table: single row per entity
77
+ const row = this._generateRow(tableName, template, entityId, null, meta);
78
+ rows.push(row);
79
+ entityRows.push(row);
80
+ }
81
+
82
+ byEntity.set(entityId, entityRows);
83
+ }
84
+
85
+ return { rows, byEntity };
86
+ }
87
+
88
+ /**
89
+ * Generate data mimicking the DataFetcher response format.
90
+ * This is what computations actually receive.
91
+ *
92
+ * @param {Object} requires - The computation's requires config
93
+ * @param {Object} options
94
+ * @param {number} [options.entityCount=10]
95
+ * @param {string[]} [options.entityIds]
96
+ * @param {Date|string} [options.asOfDate]
97
+ * @returns {Object} Data object keyed by table name
98
+ */
99
+ generateForComputation(requires, options = {}) {
100
+ const data = {};
101
+ const entityCount = options.entityCount || 10;
102
+ const entityIds = options.entityIds || this._generateEntityIds('default', entityCount);
103
+
104
+ for (const [tableName, tableConfig] of Object.entries(requires)) {
105
+ const lookback = tableConfig.lookback || 0;
106
+
107
+ try {
108
+ const { byEntity } = this.generate(tableName, {
109
+ entityCount,
110
+ entityIds,
111
+ daysBack: lookback,
112
+ asOfDate: options.asOfDate
113
+ });
114
+
115
+ // DataFetcher returns data keyed by entityId (for per-entity fetches)
116
+ // or as flat array (for global fetches)
117
+ data[tableName] = Object.fromEntries(byEntity);
118
+ } catch (e) {
119
+ // Table not in templates, return empty
120
+ data[tableName] = {};
121
+ }
122
+ }
123
+
124
+ return data;
125
+ }
126
+
127
+ /**
128
+ * Generate computation results for dependency simulation.
129
+ *
130
+ * @param {string} computationName
131
+ * @param {Object} results - Map of entityId -> result
132
+ * @param {Date|string} date
133
+ * @returns {Object[]} Rows in computation_results format
134
+ */
135
+ generateComputationResults(computationName, results, date) {
136
+ const rows = [];
137
+ const dateStr = (date instanceof Date ? date : new Date(date)).toISOString().split('T')[0];
138
+
139
+ for (const [entityId, result] of Object.entries(results)) {
140
+ rows.push({
141
+ date: dateStr,
142
+ computation_name: computationName,
143
+ category: 'mock',
144
+ entity_id: entityId,
145
+ result: result,
146
+ hash: randomHex(16),
147
+ created_at: new Date().toISOString()
148
+ });
149
+ }
150
+
151
+ return rows;
152
+ }
153
+
154
+ // =========================================================================
155
+ // PRIVATE METHODS
156
+ // =========================================================================
157
+
158
+ _generateRow(tableName, template, entityId, date, meta) {
159
+ const row = {};
160
+
161
+ for (const col of template.columns) {
162
+ const colName = col.name;
163
+
164
+ // Handle special fields
165
+ if (colName === meta.entityField) {
166
+ row[colName] = entityId;
167
+ } else if (colName === meta.dateField && date) {
168
+ row[colName] = date.toISOString().split('T')[0];
169
+ } else if (colName === 'fetched_at' || colName === 'last_updated') {
170
+ row[colName] = new Date().toISOString();
171
+ } else if (col.type === 'JSON') {
172
+ // Use JSON template
173
+ row[colName] = this._generateJsonField(tableName, colName, entityId, template);
174
+ } else if (template.generators && template.generators[colName]) {
175
+ // Use custom generator
176
+ row[colName] = template.generators[colName](entityId);
177
+ } else {
178
+ // Default by type
179
+ row[colName] = this._generateByType(col.type);
180
+ }
181
+ }
182
+
183
+ return row;
184
+ }
185
+
186
+ _generateJsonField(tableName, colName, entityId, template) {
187
+ const jsonTemplate = template.jsonTemplates?.[colName];
188
+ if (!jsonTemplate) return {};
189
+
190
+ const result = {};
191
+ for (const [key, generator] of Object.entries(jsonTemplate)) {
192
+ if (typeof generator === 'function') {
193
+ result[key] = generator(entityId);
194
+ } else {
195
+ result[key] = generator;
196
+ }
197
+ }
198
+
199
+ return result;
200
+ }
201
+
202
+ _generateByType(type) {
203
+ switch (type) {
204
+ case 'STRING': return randomHex(8);
205
+ case 'INTEGER': return randomInt(1, 1000);
206
+ case 'FLOAT': return randomFloat(0, 100);
207
+ case 'BOOLEAN': return randomChoice([true, false]);
208
+ case 'DATE': return new Date().toISOString().split('T')[0];
209
+ case 'TIMESTAMP': return new Date().toISOString();
210
+ case 'JSON': return {};
211
+ default: return null;
212
+ }
213
+ }
214
+
215
+ _generateEntityIds(tableName, count) {
216
+ // Generate realistic entity IDs based on table type
217
+ const ids = [];
218
+ for (let i = 0; i < count; i++) {
219
+ ids.push(String(randomInt(10000000, 50000000)));
220
+ }
221
+ return ids;
222
+ }
223
+
224
+ _inferMetadata(tableName, template) {
225
+ // Infer metadata from column names
226
+ const columns = template.columns.map(c => c.name);
227
+
228
+ return {
229
+ dateField: columns.includes('date') ? 'date' : null,
230
+ entityField: columns.find(c => ['user_id', 'pi_id', 'cid', 'entity_id', 'instrument_id'].includes(c)) || null
231
+ };
232
+ }
233
+ }
234
+
235
+ module.exports = { MockDataFactory };