@memberjunction/core 2.128.0 → 2.130.0

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 (58) hide show
  1. package/dist/__tests__/mocks/TestMetadataProvider.d.ts +4 -2
  2. package/dist/__tests__/mocks/TestMetadataProvider.d.ts.map +1 -1
  3. package/dist/__tests__/mocks/TestMetadataProvider.js +9 -3
  4. package/dist/__tests__/mocks/TestMetadataProvider.js.map +1 -1
  5. package/dist/generic/RegisterForStartup.d.ts +228 -0
  6. package/dist/generic/RegisterForStartup.d.ts.map +1 -0
  7. package/dist/generic/RegisterForStartup.js +233 -0
  8. package/dist/generic/RegisterForStartup.js.map +1 -0
  9. package/dist/generic/baseEngine.d.ts +191 -8
  10. package/dist/generic/baseEngine.d.ts.map +1 -1
  11. package/dist/generic/baseEngine.js +360 -14
  12. package/dist/generic/baseEngine.js.map +1 -1
  13. package/dist/generic/baseEngineRegistry.d.ts +247 -0
  14. package/dist/generic/baseEngineRegistry.d.ts.map +1 -0
  15. package/dist/generic/baseEngineRegistry.js +470 -0
  16. package/dist/generic/baseEngineRegistry.js.map +1 -0
  17. package/dist/generic/entityInfo.d.ts +50 -0
  18. package/dist/generic/entityInfo.d.ts.map +1 -1
  19. package/dist/generic/entityInfo.js +56 -0
  20. package/dist/generic/entityInfo.js.map +1 -1
  21. package/dist/generic/graphqlTypeNames.d.ts +90 -0
  22. package/dist/generic/graphqlTypeNames.d.ts.map +1 -0
  23. package/dist/generic/graphqlTypeNames.js +119 -0
  24. package/dist/generic/graphqlTypeNames.js.map +1 -0
  25. package/dist/generic/interfaces.d.ts +234 -3
  26. package/dist/generic/interfaces.d.ts.map +1 -1
  27. package/dist/generic/interfaces.js.map +1 -1
  28. package/dist/generic/localCacheManager.d.ts +388 -0
  29. package/dist/generic/localCacheManager.d.ts.map +1 -0
  30. package/dist/generic/localCacheManager.js +856 -0
  31. package/dist/generic/localCacheManager.js.map +1 -0
  32. package/dist/generic/providerBase.d.ts +227 -13
  33. package/dist/generic/providerBase.d.ts.map +1 -1
  34. package/dist/generic/providerBase.js +751 -6
  35. package/dist/generic/providerBase.js.map +1 -1
  36. package/dist/generic/queryInfo.d.ts +18 -0
  37. package/dist/generic/queryInfo.d.ts.map +1 -1
  38. package/dist/generic/queryInfo.js +18 -0
  39. package/dist/generic/queryInfo.js.map +1 -1
  40. package/dist/generic/queryInfoInterfaces.d.ts +17 -0
  41. package/dist/generic/queryInfoInterfaces.d.ts.map +1 -1
  42. package/dist/generic/runQuery.d.ts +30 -0
  43. package/dist/generic/runQuery.d.ts.map +1 -1
  44. package/dist/generic/runQuery.js +13 -0
  45. package/dist/generic/runQuery.js.map +1 -1
  46. package/dist/generic/telemetryManager.d.ts +628 -0
  47. package/dist/generic/telemetryManager.d.ts.map +1 -0
  48. package/dist/generic/telemetryManager.js +1011 -0
  49. package/dist/generic/telemetryManager.js.map +1 -0
  50. package/dist/index.d.ts +5 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/views/runView.d.ts +25 -0
  55. package/dist/views/runView.d.ts.map +1 -1
  56. package/dist/views/runView.js +4 -5
  57. package/dist/views/runView.js.map +1 -1
  58. package/package.json +2 -2
@@ -5,9 +5,11 @@ exports.ProviderBase = exports.AllMetadataArrays = exports.MetadataFromSimpleObj
5
5
  const baseEntity_1 = require("./baseEntity");
6
6
  const entityInfo_1 = require("./entityInfo");
7
7
  const interfaces_1 = require("./interfaces");
8
+ const localCacheManager_1 = require("./localCacheManager");
8
9
  const applicationInfo_1 = require("../generic/applicationInfo");
9
10
  const securityInfo_1 = require("./securityInfo");
10
11
  const global_1 = require("@memberjunction/global");
12
+ const telemetryManager_1 = require("./telemetryManager");
11
13
  const logging_1 = require("./logging");
12
14
  const queryInfo_1 = require("./queryInfo");
13
15
  const libraryInfo_1 = require("./libraryInfo");
@@ -98,6 +100,127 @@ class ProviderBase {
98
100
  this._refresh = false;
99
101
  this._cachedVisibleExplorerNavigationItems = null;
100
102
  }
103
+ // ========================================================================
104
+ // PUBLIC API METHODS - Orchestrate Pre → Cache → Internal → Post flow
105
+ // ========================================================================
106
+ /**
107
+ * Runs a view based on the provided parameters.
108
+ * This method orchestrates the full execution flow: pre-processing, cache check,
109
+ * internal execution, post-processing, and cache storage.
110
+ * @param params - The view parameters
111
+ * @param contextUser - Optional user context for permissions (required server-side)
112
+ * @returns The view results
113
+ */
114
+ async RunView(params, contextUser) {
115
+ // Pre-processing: telemetry, validation, entity status check
116
+ const preResult = await this.PreRunView(params, contextUser);
117
+ // Check for cached result - end telemetry with cache hit info
118
+ if (preResult.cachedResult) {
119
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
120
+ cacheHit: true,
121
+ cacheStatus: preResult.cacheStatus,
122
+ resultCount: preResult.cachedResult.Results?.length ?? 0
123
+ });
124
+ return preResult.cachedResult;
125
+ }
126
+ // Execute the internal implementation
127
+ const result = await this.InternalRunView(params, contextUser);
128
+ // Post-processing: transformation, cache storage, telemetry end
129
+ await this.PostRunView(result, params, preResult, contextUser);
130
+ return result;
131
+ }
132
+ /**
133
+ * Runs multiple views based on the provided parameters.
134
+ * This method orchestrates the full execution flow for batch operations.
135
+ * @param params - Array of view parameters
136
+ * @param contextUser - Optional user context for permissions (required server-side)
137
+ * @returns Array of view results
138
+ */
139
+ async RunViews(params, contextUser) {
140
+ // Pre-processing for batch
141
+ const preResult = await this.PreRunViews(params, contextUser);
142
+ // Check for smart cache check mode
143
+ if (preResult.useSmartCacheCheck && preResult.smartCacheCheckParams) {
144
+ return this.executeSmartCacheCheck(params, preResult, contextUser);
145
+ }
146
+ // Check for cached results - if all are cached, end telemetry and return early
147
+ if (preResult.allCached && preResult.cachedResults) {
148
+ const totalResults = preResult.cachedResults.reduce((sum, r) => sum + (r.Results?.length ?? 0), 0);
149
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
150
+ cacheHit: true,
151
+ allCached: true,
152
+ batchSize: params.length,
153
+ totalResultCount: totalResults
154
+ });
155
+ return preResult.cachedResults;
156
+ }
157
+ // Execute the internal implementation for non-cached items
158
+ const results = await this.InternalRunViews(preResult.uncachedParams || params, contextUser);
159
+ // Merge cached and fresh results if needed
160
+ const finalResults = preResult.cachedResults
161
+ ? this.mergeCachedAndFreshResults(preResult, results)
162
+ : results;
163
+ // Post-processing for batch
164
+ await this.PostRunViews(finalResults, params, preResult, contextUser);
165
+ return finalResults;
166
+ }
167
+ /**
168
+ * Runs a query based on the provided parameters.
169
+ * This method orchestrates the full execution flow: pre-processing, cache check,
170
+ * internal execution, post-processing, and cache storage.
171
+ * @param params - The query parameters
172
+ * @param contextUser - Optional user context for permissions (required server-side)
173
+ * @returns The query results
174
+ */
175
+ async RunQuery(params, contextUser) {
176
+ // Pre-processing: telemetry, cache check
177
+ const preResult = await this.PreRunQuery(params, contextUser);
178
+ // Check for cached result - end telemetry with cache hit info
179
+ if (preResult.cachedResult) {
180
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
181
+ cacheHit: true,
182
+ cacheStatus: preResult.cacheStatus,
183
+ resultCount: preResult.cachedResult.Results?.length ?? 0
184
+ });
185
+ return preResult.cachedResult;
186
+ }
187
+ // Execute the internal implementation
188
+ const result = await this.InternalRunQuery(params, contextUser);
189
+ // Post-processing: cache storage, telemetry end
190
+ await this.PostRunQuery(result, params, preResult, contextUser);
191
+ return result;
192
+ }
193
+ /**
194
+ * Runs multiple queries based on the provided parameters.
195
+ * This method orchestrates the full execution flow for batch query operations.
196
+ * @param params - Array of query parameters
197
+ * @param contextUser - Optional user context for permissions (required server-side)
198
+ * @returns Array of query results
199
+ */
200
+ async RunQueries(params, contextUser) {
201
+ // Pre-processing for batch
202
+ const preResult = await this.PreRunQueries(params, contextUser);
203
+ // Check for cached results - if all are cached, end telemetry and return early
204
+ if (preResult.allCached && preResult.cachedResults) {
205
+ const totalResults = preResult.cachedResults.reduce((sum, r) => sum + (r.Results?.length ?? 0), 0);
206
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
207
+ cacheHit: true,
208
+ allCached: true,
209
+ batchSize: params.length,
210
+ totalResultCount: totalResults
211
+ });
212
+ return preResult.cachedResults;
213
+ }
214
+ // Execute the internal implementation for non-cached items
215
+ const results = await this.InternalRunQueries(preResult.uncachedParams || params, contextUser);
216
+ // Merge cached and fresh results if needed
217
+ const finalResults = preResult.cachedResults
218
+ ? this.mergeQueryCachedAndFreshResults(preResult, results)
219
+ : results;
220
+ // Post-processing for batch
221
+ await this.PostRunQueries(finalResults, params, preResult, contextUser);
222
+ return finalResults;
223
+ }
101
224
  /**
102
225
  * Used to check to see if the entity in question is active or not
103
226
  * If it is not active, it will throw an exception or log a warning depending on the status of the entity being
@@ -113,12 +236,609 @@ class ProviderBase {
113
236
  }
114
237
  entityInfo_1.EntityInfo.AssertEntityActiveStatus(entity, callerName);
115
238
  }
239
+ // Type aliases for cleaner code
240
+ get PreRunViewResult() { return this._preRunViewResultType; }
241
+ get PreRunViewsResult() { return this._preRunViewsResultType; }
242
+ get PreRunQueryResult() { return this._preRunQueryResultType; }
243
+ get PreRunQueriesResult() { return this._preRunQueriesResultType; }
244
+ // ========================================================================
245
+ // PRE-PROCESSING HOOKS
246
+ // ========================================================================
116
247
  /**
117
- * Base class pre-processor that all sub-classes should call before they start their RunView process
118
- * @param params
119
- * @param contextUser
248
+ * Pre-processing hook for RunView.
249
+ * Handles telemetry, validation, entity status check, and cache lookup.
250
+ * @param params - The view parameters
251
+ * @param contextUser - Optional user context
252
+ * @returns Pre-processing result with cache status and optional cached result
253
+ */
254
+ async PreRunView(params, contextUser) {
255
+ const preViewStart = performance.now();
256
+ // Start telemetry tracking
257
+ const telemetryStart = performance.now();
258
+ const telemetryEventId = telemetryManager_1.TelemetryManager.Instance.StartEvent('RunView', 'ProviderBase.RunView', {
259
+ EntityName: params.EntityName,
260
+ ViewID: params.ViewID,
261
+ ViewName: params.ViewName,
262
+ ExtraFilter: params.ExtraFilter,
263
+ OrderBy: params.OrderBy,
264
+ ResultType: params.ResultType,
265
+ MaxRows: params.MaxRows,
266
+ StartRow: params.StartRow,
267
+ CacheLocal: params.CacheLocal,
268
+ _fromEngine: params._fromEngine
269
+ }, contextUser?.ID);
270
+ const telemetryTime = performance.now() - telemetryStart;
271
+ // Entity status check
272
+ const entityCheckStart = performance.now();
273
+ await this.EntityStatusCheck(params, 'PreRunView');
274
+ const entityCheckTime = performance.now() - entityCheckStart;
275
+ // Handle entity_object result type - need all fields
276
+ const entityLookupStart = performance.now();
277
+ if (params.ResultType === 'entity_object') {
278
+ const entity = this.Entities.find(e => e.Name.trim().toLowerCase() === params.EntityName.trim().toLowerCase());
279
+ if (!entity)
280
+ throw new Error(`Entity ${params.EntityName} not found in metadata`);
281
+ params.Fields = entity.Fields.map(f => f.Name);
282
+ }
283
+ const entityLookupTime = performance.now() - entityLookupStart;
284
+ // Check local cache if enabled
285
+ const cacheCheckStart = performance.now();
286
+ let cacheStatus = 'disabled';
287
+ let cachedResult;
288
+ let fingerprint;
289
+ if (params.CacheLocal && localCacheManager_1.LocalCacheManager.Instance.IsInitialized) {
290
+ fingerprint = localCacheManager_1.LocalCacheManager.Instance.GenerateRunViewFingerprint(params, this.InstanceConnectionString);
291
+ const cached = await localCacheManager_1.LocalCacheManager.Instance.GetRunViewResult(fingerprint);
292
+ if (cached) {
293
+ // Reconstruct RunViewResult from cached data
294
+ cachedResult = {
295
+ Success: true,
296
+ Results: cached.results,
297
+ RowCount: cached.results.length,
298
+ TotalRowCount: cached.results.length,
299
+ ExecutionTime: 0, // Cached, no execution time
300
+ ErrorMessage: '',
301
+ UserViewRunID: ''
302
+ };
303
+ cacheStatus = 'hit';
304
+ }
305
+ else {
306
+ cacheStatus = 'miss';
307
+ }
308
+ }
309
+ const cacheCheckTime = performance.now() - cacheCheckStart;
310
+ const totalPreTime = performance.now() - preViewStart;
311
+ if (totalPreTime > 50) {
312
+ console.log(`[PERF-PRE] PreRunView ${params.EntityName}: ${totalPreTime.toFixed(1)}ms (telemetry=${telemetryTime.toFixed(1)}ms, entityCheck=${entityCheckTime.toFixed(1)}ms, entityLookup=${entityLookupTime.toFixed(1)}ms, cache=${cacheCheckTime.toFixed(1)}ms)`);
313
+ }
314
+ return {
315
+ telemetryEventId,
316
+ cacheStatus,
317
+ cachedResult,
318
+ fingerprint
319
+ };
320
+ }
321
+ /**
322
+ * Pre-processing hook for RunViews (batch).
323
+ * Handles telemetry, validation, and cache lookup for multiple views.
324
+ * @param params - Array of view parameters
325
+ * @param contextUser - Optional user context
326
+ * @returns Pre-processing result with cache status for each view
327
+ */
328
+ async PreRunViews(params, contextUser) {
329
+ // Start telemetry tracking for batch operation
330
+ const fromEngine = params.some(p => p._fromEngine);
331
+ const telemetryEventId = telemetryManager_1.TelemetryManager.Instance.StartEvent('RunView', 'ProviderBase.RunViews', {
332
+ BatchSize: params.length,
333
+ Entities: params.map(p => p.EntityName || p.ViewName || p.ViewID).filter(Boolean),
334
+ _fromEngine: fromEngine
335
+ }, contextUser?.ID);
336
+ // Check if any params have CacheLocal enabled - smart caching is always used when caching locally
337
+ const useSmartCacheCheck = params.some(p => p.CacheLocal);
338
+ // If local caching is enabled, use smart cache check flow
339
+ if (useSmartCacheCheck && localCacheManager_1.LocalCacheManager.Instance.IsInitialized) {
340
+ return this.prepareSmartCacheCheckParams(params, telemetryEventId, contextUser);
341
+ }
342
+ // Traditional caching flow
343
+ const cacheStatusMap = new Map();
344
+ const uncachedParams = [];
345
+ const cachedResults = [];
346
+ let allCached = true;
347
+ for (let i = 0; i < params.length; i++) {
348
+ const param = params[i];
349
+ // Entity status check
350
+ await this.EntityStatusCheck(param, 'PreRunViews');
351
+ // Handle entity_object result type - need all fields
352
+ if (param.ResultType === 'entity_object') {
353
+ const entity = this.Entities.find(e => e.Name.trim().toLowerCase() === param.EntityName.trim().toLowerCase());
354
+ if (!entity) {
355
+ throw new Error(`Entity ${param.EntityName} not found in metadata`);
356
+ }
357
+ param.Fields = entity.Fields.map(f => f.Name);
358
+ }
359
+ // Check local cache if enabled
360
+ if (param.CacheLocal && localCacheManager_1.LocalCacheManager.Instance.IsInitialized) {
361
+ const fingerprint = localCacheManager_1.LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
362
+ const cached = await localCacheManager_1.LocalCacheManager.Instance.GetRunViewResult(fingerprint);
363
+ if (cached) {
364
+ const cachedViewResult = {
365
+ Success: true,
366
+ Results: cached.results,
367
+ RowCount: cached.results.length,
368
+ TotalRowCount: cached.results.length,
369
+ ExecutionTime: 0,
370
+ ErrorMessage: '',
371
+ UserViewRunID: ''
372
+ };
373
+ // if needed this will transform each result into an entity object
374
+ await this.TransformSimpleObjectToEntityObject(param, cachedViewResult, contextUser);
375
+ cacheStatusMap.set(i, { status: 'hit', result: cachedViewResult });
376
+ cachedResults.push(cachedViewResult);
377
+ continue;
378
+ }
379
+ cacheStatusMap.set(i, { status: 'miss' });
380
+ }
381
+ else {
382
+ cacheStatusMap.set(i, { status: 'disabled' });
383
+ }
384
+ allCached = false;
385
+ uncachedParams.push(param);
386
+ cachedResults.push(null); // Placeholder for uncached
387
+ }
388
+ return {
389
+ telemetryEventId,
390
+ allCached,
391
+ cachedResults: allCached ? cachedResults.filter(r => r !== null) : undefined,
392
+ uncachedParams: allCached ? undefined : uncachedParams,
393
+ cacheStatusMap
394
+ };
395
+ }
396
+ /**
397
+ * Prepares smart cache check parameters for RunViews when CacheLocal is enabled.
398
+ * Instead of returning cached data immediately, this builds params to send to the server
399
+ * which will validate if the cache is current or return fresh data.
400
+ */
401
+ async prepareSmartCacheCheckParams(params, telemetryEventId, contextUser) {
402
+ const smartCacheCheckParams = [];
403
+ for (const param of params) {
404
+ // Entity status check
405
+ await this.EntityStatusCheck(param, 'PreRunViews');
406
+ // Handle entity_object result type - need all fields
407
+ if (param.ResultType === 'entity_object') {
408
+ const entity = this.Entities.find(e => e.Name.trim().toLowerCase() === param.EntityName?.trim().toLowerCase());
409
+ if (!entity) {
410
+ throw new Error(`Entity ${param.EntityName} not found in metadata`);
411
+ }
412
+ param.Fields = entity.Fields.map(f => f.Name);
413
+ }
414
+ // Build the cache check param with optional cache status
415
+ let cacheStatus;
416
+ if (param.CacheLocal && localCacheManager_1.LocalCacheManager.Instance.IsInitialized) {
417
+ const fingerprint = localCacheManager_1.LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
418
+ const cached = await localCacheManager_1.LocalCacheManager.Instance.GetRunViewResult(fingerprint);
419
+ if (cached) {
420
+ cacheStatus = {
421
+ maxUpdatedAt: cached.maxUpdatedAt,
422
+ rowCount: cached.rowCount
423
+ };
424
+ }
425
+ }
426
+ smartCacheCheckParams.push({
427
+ params: param,
428
+ cacheStatus
429
+ });
430
+ }
431
+ return {
432
+ telemetryEventId,
433
+ allCached: false, // Don't return cached directly - let server validate
434
+ useSmartCacheCheck: true,
435
+ smartCacheCheckParams,
436
+ cacheStatusMap: new Map()
437
+ };
438
+ }
439
+ /**
440
+ * Executes the smart cache check flow for RunViews.
441
+ * Calls RunViewsWithCacheCheck on the provider (if available) and processes the results,
442
+ * using cached data for 'current' items and fresh data for 'stale' items.
443
+ *
444
+ * Optimized to process all results in parallel using Promise.all for cache lookups,
445
+ * cache updates, and entity transformations.
446
+ */
447
+ async executeSmartCacheCheck(params, preResult, contextUser) {
448
+ // Cast to access RunViewsWithCacheCheck method
449
+ const provider = this;
450
+ // Execute the smart cache check
451
+ const response = await provider.RunViewsWithCacheCheck(preResult.smartCacheCheckParams, contextUser);
452
+ if (!response.success) {
453
+ // If the smart cache check failed, log and return empty results
454
+ (0, logging_1.LogError)(`SmartCacheCheck failed: ${response.errorMessage}`);
455
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
456
+ smartCacheCheck: true,
457
+ success: false,
458
+ errorMessage: response.errorMessage
459
+ });
460
+ return params.map(() => ({
461
+ Success: false,
462
+ Results: [],
463
+ RowCount: 0,
464
+ TotalRowCount: 0,
465
+ ExecutionTime: 0,
466
+ ErrorMessage: response.errorMessage || 'SmartCacheCheck failed',
467
+ UserViewRunID: ''
468
+ }));
469
+ }
470
+ // Process all results in parallel
471
+ const processingPromises = params.map((param, i) => this.processSingleSmartCacheResult(param, i, response.results, contextUser));
472
+ const processedResults = await Promise.all(processingPromises);
473
+ // Aggregate telemetry stats
474
+ let cacheHits = 0;
475
+ let cacheMisses = 0;
476
+ for (const result of processedResults) {
477
+ if (result.cacheHit)
478
+ cacheHits++;
479
+ if (result.cacheMiss)
480
+ cacheMisses++;
481
+ }
482
+ // End telemetry
483
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
484
+ smartCacheCheck: true,
485
+ success: true,
486
+ cacheHits,
487
+ cacheMisses,
488
+ batchSize: params.length
489
+ });
490
+ return processedResults.map(r => r.result);
491
+ }
492
+ /**
493
+ * Processes a single smart cache check result.
494
+ * Handles cache lookup for 'current' items and cache update for 'stale' items.
495
+ */
496
+ async processSingleSmartCacheResult(param, index, serverResults, contextUser) {
497
+ const checkResult = serverResults.find(r => r.viewIndex === index);
498
+ if (!checkResult) {
499
+ return {
500
+ result: {
501
+ Success: false,
502
+ Results: [],
503
+ RowCount: 0,
504
+ TotalRowCount: 0,
505
+ ExecutionTime: 0,
506
+ ErrorMessage: 'No result returned from server',
507
+ UserViewRunID: ''
508
+ },
509
+ cacheHit: false,
510
+ cacheMiss: false
511
+ };
512
+ }
513
+ if (checkResult.status === 'current') {
514
+ // Cache is current - use cached data
515
+ const fingerprint = localCacheManager_1.LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
516
+ const cached = await localCacheManager_1.LocalCacheManager.Instance.GetRunViewResult(fingerprint);
517
+ if (cached) {
518
+ const cachedResult = {
519
+ Success: true,
520
+ Results: cached.results,
521
+ RowCount: cached.rowCount,
522
+ TotalRowCount: cached.rowCount,
523
+ ExecutionTime: 0,
524
+ ErrorMessage: '',
525
+ UserViewRunID: ''
526
+ };
527
+ // Transform to entity objects if needed
528
+ await this.TransformSimpleObjectToEntityObject(param, cachedResult, contextUser);
529
+ return { result: cachedResult, cacheHit: true, cacheMiss: false };
530
+ }
531
+ else {
532
+ // Cache miss - shouldn't happen but handle gracefully
533
+ return {
534
+ result: {
535
+ Success: false,
536
+ Results: [],
537
+ RowCount: 0,
538
+ TotalRowCount: 0,
539
+ ExecutionTime: 0,
540
+ ErrorMessage: 'Cache marked current but no cached data found',
541
+ UserViewRunID: ''
542
+ },
543
+ cacheHit: false,
544
+ cacheMiss: false
545
+ };
546
+ }
547
+ }
548
+ else if (checkResult.status === 'stale') {
549
+ // Cache is stale - use fresh data and update cache
550
+ const freshResult = {
551
+ Success: true,
552
+ Results: checkResult.results || [],
553
+ RowCount: checkResult.rowCount || 0,
554
+ TotalRowCount: checkResult.rowCount || 0,
555
+ ExecutionTime: 0,
556
+ ErrorMessage: '',
557
+ UserViewRunID: ''
558
+ };
559
+ // Update the local cache with fresh data (don't await - fire and forget for performance)
560
+ if (param.CacheLocal && checkResult.maxUpdatedAt && localCacheManager_1.LocalCacheManager.Instance.IsInitialized) {
561
+ const fingerprint = localCacheManager_1.LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
562
+ // Note: We don't await here to avoid blocking the response
563
+ // Cache update happens in background
564
+ localCacheManager_1.LocalCacheManager.Instance.SetRunViewResult(fingerprint, param, checkResult.results || [], checkResult.maxUpdatedAt, checkResult.rowCount).catch(e => (0, logging_1.LogError)(`Failed to update cache: ${e}`));
565
+ }
566
+ // Transform to entity objects if needed
567
+ await this.TransformSimpleObjectToEntityObject(param, freshResult, contextUser);
568
+ return { result: freshResult, cacheHit: false, cacheMiss: true };
569
+ }
570
+ else {
571
+ // Error status
572
+ return {
573
+ result: {
574
+ Success: false,
575
+ Results: [],
576
+ RowCount: 0,
577
+ TotalRowCount: 0,
578
+ ExecutionTime: 0,
579
+ ErrorMessage: checkResult.errorMessage || 'Unknown error',
580
+ UserViewRunID: ''
581
+ },
582
+ cacheHit: false,
583
+ cacheMiss: false
584
+ };
585
+ }
586
+ }
587
+ /**
588
+ * Pre-processing hook for RunQuery.
589
+ * Handles telemetry and cache lookup.
590
+ * @param params - The query parameters
591
+ * @param contextUser - Optional user context
592
+ * @returns Pre-processing result with cache status and optional cached result
593
+ */
594
+ async PreRunQuery(params, contextUser) {
595
+ // Start telemetry tracking
596
+ const telemetryEventId = telemetryManager_1.TelemetryManager.Instance.StartEvent('RunQuery', 'ProviderBase.RunQuery', {
597
+ QueryID: params.QueryID,
598
+ QueryName: params.QueryName,
599
+ CategoryPath: params.CategoryPath,
600
+ CategoryID: params.CategoryID,
601
+ MaxRows: params.MaxRows,
602
+ StartRow: params.StartRow,
603
+ HasParameters: params.Parameters ? Object.keys(params.Parameters).length > 0 : false
604
+ }, contextUser?.ID);
605
+ // Query caching is handled internally by the provider's query cache mechanism
606
+ // We just return the telemetry info here - actual cache check happens in InternalRunQuery
607
+ return {
608
+ telemetryEventId,
609
+ cacheStatus: 'disabled', // Query caching is handled differently
610
+ cachedResult: undefined,
611
+ fingerprint: undefined
612
+ };
613
+ }
614
+ /**
615
+ * Pre-processing hook for RunQueries (batch).
616
+ * Handles telemetry for batch query operations.
617
+ * @param params - Array of query parameters
618
+ * @param contextUser - Optional user context
619
+ * @returns Pre-processing result
620
+ */
621
+ async PreRunQueries(params, contextUser) {
622
+ // Start telemetry tracking for batch operation
623
+ const telemetryEventId = telemetryManager_1.TelemetryManager.Instance.StartEvent('RunQuery', 'ProviderBase.RunQueries', {
624
+ BatchSize: params.length,
625
+ Queries: params.map(p => p.QueryName || p.QueryID).filter(Boolean)
626
+ }, contextUser?.ID);
627
+ // Query caching is handled internally by each query execution
628
+ return {
629
+ telemetryEventId,
630
+ allCached: false,
631
+ cachedResults: undefined,
632
+ uncachedParams: params,
633
+ cacheStatusMap: undefined
634
+ };
635
+ }
636
+ // ========================================================================
637
+ // POST-PROCESSING HOOKS
638
+ // ========================================================================
639
+ /**
640
+ * Post-processing hook for RunView.
641
+ * Handles result transformation, cache storage, and telemetry end.
642
+ * @param result - The view result
643
+ * @param params - The view parameters
644
+ * @param preResult - The pre-processing result
645
+ * @param contextUser - Optional user context
646
+ */
647
+ async PostRunView(result, params, preResult, contextUser) {
648
+ // Transform the result set into BaseEntity-derived objects, if needed
649
+ await this.TransformSimpleObjectToEntityObject(params, result, contextUser);
650
+ // Store in local cache if enabled and we have a successful result
651
+ if (params.CacheLocal && result.Success && preResult.fingerprint && localCacheManager_1.LocalCacheManager.Instance.IsInitialized) {
652
+ // Extract maxUpdatedAt from results if available
653
+ const maxUpdatedAt = this.extractMaxUpdatedAt(result.Results);
654
+ await localCacheManager_1.LocalCacheManager.Instance.SetRunViewResult(preResult.fingerprint, params, result.Results, maxUpdatedAt);
655
+ }
656
+ // End telemetry tracking with cache miss info
657
+ if (preResult.telemetryEventId) {
658
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
659
+ cacheHit: false,
660
+ cacheStatus: preResult.cacheStatus,
661
+ resultCount: result.Results?.length ?? 0,
662
+ success: result.Success
663
+ });
664
+ }
665
+ }
666
+ /**
667
+ * Post-processing hook for RunViews (batch).
668
+ * Handles result transformation, cache storage, and telemetry end.
669
+ * @param results - Array of view results
670
+ * @param params - Array of view parameters
671
+ * @param preResult - The pre-processing result
672
+ * @param contextUser - Optional user context
673
+ */
674
+ async PostRunViews(results, params, preResult, contextUser) {
675
+ // Transform results in parallel
676
+ const promises = [];
677
+ for (let i = 0; i < results.length; i++) {
678
+ promises.push(this.TransformSimpleObjectToEntityObject(params[i], results[i], contextUser));
679
+ // Store in local cache if enabled
680
+ if (params[i].CacheLocal && results[i].Success && localCacheManager_1.LocalCacheManager.Instance.IsInitialized) {
681
+ const fingerprint = localCacheManager_1.LocalCacheManager.Instance.GenerateRunViewFingerprint(params[i], this.InstanceConnectionString);
682
+ const maxUpdatedAt = this.extractMaxUpdatedAt(results[i].Results);
683
+ promises.push(localCacheManager_1.LocalCacheManager.Instance.SetRunViewResult(fingerprint, params[i], results[i].Results, maxUpdatedAt));
684
+ }
685
+ }
686
+ await Promise.all(promises);
687
+ // End telemetry tracking with batch info
688
+ if (preResult.telemetryEventId) {
689
+ const totalResults = results.reduce((sum, r) => sum + (r.Results?.length ?? 0), 0);
690
+ const cachedCount = preResult.cacheStatusMap
691
+ ? [...preResult.cacheStatusMap.values()].filter(s => s.status === 'hit').length
692
+ : 0;
693
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
694
+ cacheHit: false,
695
+ allCached: false,
696
+ batchSize: params.length,
697
+ cachedCount,
698
+ fetchedCount: params.length - cachedCount,
699
+ totalResultCount: totalResults
700
+ });
701
+ }
702
+ }
703
+ /**
704
+ * Post-processing hook for RunQuery.
705
+ * Handles cache storage and telemetry end.
706
+ * @param result - The query result
707
+ * @param params - The query parameters
708
+ * @param preResult - The pre-processing result
709
+ * @param contextUser - Optional user context
710
+ */
711
+ async PostRunQuery(result, params, preResult, contextUser) {
712
+ // Query caching is handled internally by the provider
713
+ // End telemetry tracking with cache miss info
714
+ if (preResult.telemetryEventId) {
715
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
716
+ cacheHit: false,
717
+ cacheStatus: preResult.cacheStatus,
718
+ resultCount: result.Results?.length ?? 0,
719
+ success: result.Success
720
+ });
721
+ }
722
+ }
723
+ /**
724
+ * Post-processing hook for RunQueries (batch).
725
+ * Handles telemetry end.
726
+ * @param results - Array of query results
727
+ * @param params - Array of query parameters
728
+ * @param preResult - The pre-processing result
729
+ * @param contextUser - Optional user context
730
+ */
731
+ async PostRunQueries(results, params, preResult, contextUser) {
732
+ // Query caching is handled internally by each query execution
733
+ // End telemetry tracking with batch info
734
+ if (preResult.telemetryEventId) {
735
+ const totalResults = results.reduce((sum, r) => sum + (r.Results?.length ?? 0), 0);
736
+ const cachedCount = preResult.cacheStatusMap
737
+ ? [...preResult.cacheStatusMap.values()].filter(s => s.status === 'hit').length
738
+ : 0;
739
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(preResult.telemetryEventId, {
740
+ cacheHit: false,
741
+ allCached: false,
742
+ batchSize: params.length,
743
+ cachedCount,
744
+ fetchedCount: params.length - cachedCount,
745
+ totalResultCount: totalResults
746
+ });
747
+ }
748
+ }
749
+ // ========================================================================
750
+ // CACHE HELPERS
751
+ // ========================================================================
752
+ /**
753
+ * Extracts the maximum __mj_UpdatedAt timestamp from a set of results.
754
+ * This is used for cache freshness checking.
755
+ * @param results - Array of result objects that may contain __mj_UpdatedAt
756
+ * @returns ISO string of the max timestamp, or current time if none found
757
+ */
758
+ extractMaxUpdatedAt(results) {
759
+ let maxDate = null;
760
+ for (const item of results) {
761
+ if (item && typeof item === 'object') {
762
+ const record = item;
763
+ // Check for __mj_UpdatedAt field (standard MJ timestamp field)
764
+ const updatedAt = record['__mj_UpdatedAt'] || record['UpdatedAt'];
765
+ if (updatedAt) {
766
+ const date = updatedAt instanceof Date ? updatedAt : new Date(updatedAt);
767
+ if (!isNaN(date.getTime()) && (!maxDate || date > maxDate)) {
768
+ maxDate = date;
769
+ }
770
+ }
771
+ }
772
+ }
773
+ return maxDate ? maxDate.toISOString() : new Date().toISOString();
774
+ }
775
+ /**
776
+ * Merges cached and fresh results for RunViews, maintaining original order.
777
+ * @param preResult - The pre-processing result with cache info
778
+ * @param freshResults - The fresh results from InternalRunViews
779
+ * @returns Combined results in original order
780
+ */
781
+ mergeCachedAndFreshResults(preResult, freshResults) {
782
+ if (!preResult.cacheStatusMap) {
783
+ return freshResults;
784
+ }
785
+ const merged = [];
786
+ let freshIndex = 0;
787
+ for (let i = 0; i < preResult.cacheStatusMap.size; i++) {
788
+ const cacheInfo = preResult.cacheStatusMap.get(i);
789
+ if (cacheInfo?.status === 'hit' && cacheInfo.result) {
790
+ merged.push(cacheInfo.result);
791
+ }
792
+ else {
793
+ merged.push(freshResults[freshIndex++]);
794
+ }
795
+ }
796
+ return merged;
797
+ }
798
+ /**
799
+ * Merges cached and fresh results for RunQueries, maintaining original order.
800
+ * @param preResult - The pre-processing result with cache info
801
+ * @param freshResults - The fresh results from InternalRunQueries
802
+ * @returns Combined results in original order
803
+ */
804
+ mergeQueryCachedAndFreshResults(preResult, freshResults) {
805
+ if (!preResult.cacheStatusMap) {
806
+ return freshResults;
807
+ }
808
+ const merged = [];
809
+ let freshIndex = 0;
810
+ for (let i = 0; i < preResult.cacheStatusMap.size; i++) {
811
+ const cacheInfo = preResult.cacheStatusMap.get(i);
812
+ if (cacheInfo?.status === 'hit' && cacheInfo.result) {
813
+ merged.push(cacheInfo.result);
814
+ }
815
+ else {
816
+ merged.push(freshResults[freshIndex++]);
817
+ }
818
+ }
819
+ return merged;
820
+ }
821
+ // ========================================================================
822
+ // LEGACY METHODS (kept for backward compatibility, will be removed)
823
+ // ========================================================================
824
+ /**
825
+ * @deprecated Use PreRunView instead. This method is kept for backward compatibility.
120
826
  */
121
827
  async PreProcessRunView(params, contextUser) {
828
+ // Start telemetry tracking
829
+ const eventId = telemetryManager_1.TelemetryManager.Instance.StartEvent('RunView', 'ProviderBase.RunView', {
830
+ EntityName: params.EntityName,
831
+ ViewID: params.ViewID,
832
+ ViewName: params.ViewName,
833
+ ExtraFilter: params.ExtraFilter,
834
+ OrderBy: params.OrderBy,
835
+ ResultType: params.ResultType,
836
+ MaxRows: params.MaxRows,
837
+ StartRow: params.StartRow,
838
+ _fromEngine: params._fromEngine
839
+ }, contextUser?.ID);
840
+ // Store on params object for retrieval in PostProcessRunView
841
+ params._telemetryEventId = eventId;
122
842
  await this.EntityStatusCheck(params, 'PreProcessRunView');
123
843
  // FIRST, if the resultType is entity_object, we need to run the view with ALL fields in the entity
124
844
  // so that we can get the data to populate the entity object with.
@@ -139,6 +859,12 @@ class ProviderBase {
139
859
  async PostProcessRunView(result, params, contextUser) {
140
860
  // Transform the result set into BaseEntity-derived objects, if needed
141
861
  await this.TransformSimpleObjectToEntityObject(params, result, contextUser);
862
+ // End telemetry tracking
863
+ const eventId = params._telemetryEventId;
864
+ if (eventId) {
865
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(eventId);
866
+ delete params._telemetryEventId;
867
+ }
142
868
  }
143
869
  /**
144
870
  * Base class implementation for handling pre-processing of RunViews() each sub-class should call this
@@ -148,6 +874,17 @@ class ProviderBase {
148
874
  * @returns
149
875
  */
150
876
  async PreProcessRunViews(params, contextUser) {
877
+ // Start telemetry tracking for batch operation
878
+ const fromEngine = params.some(p => p._fromEngine);
879
+ const eventId = telemetryManager_1.TelemetryManager.Instance.StartEvent('RunView', 'ProviderBase.RunViews', {
880
+ BatchSize: params.length,
881
+ Entities: params.map(p => p.EntityName || p.ViewName || p.ViewID).filter(Boolean),
882
+ _fromEngine: fromEngine
883
+ }, contextUser?.ID);
884
+ // Store on first param for retrieval in PostProcessRunViews (using a special key to avoid collision)
885
+ if (params.length > 0) {
886
+ params[0]._telemetryBatchEventId = eventId;
887
+ }
151
888
  if (params && params.length > 0) {
152
889
  for (const param of params) {
153
890
  this.EntityStatusCheck(param, 'PreProcessRunViews');
@@ -180,6 +917,12 @@ class ProviderBase {
180
917
  }
181
918
  // await the promises for all transformations
182
919
  await Promise.all(promises);
920
+ // End telemetry tracking for batch operation
921
+ const eventId = params[0]._telemetryBatchEventId;
922
+ if (eventId) {
923
+ telemetryManager_1.TelemetryManager.Instance.EndEvent(eventId);
924
+ delete params[0]._telemetryBatchEventId;
925
+ }
183
926
  }
184
927
  }
185
928
  /**
@@ -190,7 +933,7 @@ class ProviderBase {
190
933
  */
191
934
  async TransformSimpleObjectToEntityObject(param, result, contextUser) {
192
935
  // only if needed (e.g. ResultType==='entity_object'), transform the result set into BaseEntity-derived objects
193
- if (param.ResultType === 'entity_object' && result && result.Success) {
936
+ if (param.ResultType === 'entity_object' && result && result.Success && result.Results?.length > 0) {
194
937
  // we need to transform each of the items in the result set into a BaseEntity-derived object
195
938
  // Create entities and load data in parallel for better performance
196
939
  const entityPromises = result.Results.map(async (item) => {
@@ -376,12 +1119,15 @@ class ProviderBase {
376
1119
  */
377
1120
  PostProcessEntityMetadata(entities, fields, fieldValues, permissions, relationships, settings) {
378
1121
  const result = [];
1122
+ // Sort entities alphabetically by name to ensure deterministic ordering
1123
+ // This prevents non-deterministic output in CodeGen and other metadata consumers
1124
+ const sortedEntities = entities.sort((a, b) => a.Name.localeCompare(b.Name));
379
1125
  if (fieldValues && fieldValues.length > 0)
380
1126
  for (let f of fields) {
381
1127
  // populate the field values for each field, if we have them
382
1128
  f.EntityFieldValues = fieldValues.filter(fv => fv.EntityFieldID === f.ID);
383
1129
  }
384
- for (let e of entities) {
1130
+ for (let e of sortedEntities) {
385
1131
  e.EntityFields = fields.filter(f => f.EntityID === e.ID).sort((a, b) => a.Sequence - b.Sequence);
386
1132
  e.EntityPermissions = permissions.filter(p => p.EntityID === e.ID);
387
1133
  e.EntityRelationships = relationships.filter(r => r.EntityID === e.ID);
@@ -936,7 +1682,6 @@ class ProviderBase {
936
1682
  const temp = JSON.parse(await ls.GetItem(this.LocalStoragePrefix + _a.localStorageAllMetadataKey)); // we now have a simple object for all the metadata
937
1683
  if (temp) {
938
1684
  // we have local metadata
939
- (0, logging_1.LogStatus)('Metadata loaded from local storage');
940
1685
  const metadata = MetadataFromSimpleObject(temp, this); // create a new object to start this up
941
1686
  this.UpdateLocalMetadata(metadata);
942
1687
  }