@memberjunction/core 6.2.0-edge.5 → 6.2.0-edge.6

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 (121) hide show
  1. package/dist/generic/BaseEntitySaveQueue.d.ts +15 -0
  2. package/dist/generic/BaseEntitySaveQueue.d.ts.map +1 -1
  3. package/dist/generic/BaseEntitySaveQueue.js +21 -0
  4. package/dist/generic/BaseEntitySaveQueue.js.map +1 -1
  5. package/dist/generic/alternateBaseViews.d.ts +119 -0
  6. package/dist/generic/alternateBaseViews.d.ts.map +1 -0
  7. package/dist/generic/alternateBaseViews.js +223 -0
  8. package/dist/generic/alternateBaseViews.js.map +1 -0
  9. package/dist/generic/baseEngine.d.ts +53 -9
  10. package/dist/generic/baseEngine.d.ts.map +1 -1
  11. package/dist/generic/baseEngine.js +194 -35
  12. package/dist/generic/baseEngine.js.map +1 -1
  13. package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
  14. package/dist/generic/baseEngineRegistry.js +2 -1
  15. package/dist/generic/baseEngineRegistry.js.map +1 -1
  16. package/dist/generic/baseEntity.d.ts +153 -17
  17. package/dist/generic/baseEntity.d.ts.map +1 -1
  18. package/dist/generic/baseEntity.js +345 -46
  19. package/dist/generic/baseEntity.js.map +1 -1
  20. package/dist/generic/contextTokens.d.ts +136 -0
  21. package/dist/generic/contextTokens.d.ts.map +1 -0
  22. package/dist/generic/contextTokens.js +319 -0
  23. package/dist/generic/contextTokens.js.map +1 -0
  24. package/dist/generic/databaseProviderBase.d.ts +66 -5
  25. package/dist/generic/databaseProviderBase.d.ts.map +1 -1
  26. package/dist/generic/databaseProviderBase.js +159 -12
  27. package/dist/generic/databaseProviderBase.js.map +1 -1
  28. package/dist/generic/embeddedRecord.d.ts +6 -0
  29. package/dist/generic/embeddedRecord.d.ts.map +1 -1
  30. package/dist/generic/embeddedRecord.js +15 -1
  31. package/dist/generic/embeddedRecord.js.map +1 -1
  32. package/dist/generic/entityCompanion.d.ts +7 -0
  33. package/dist/generic/entityCompanion.d.ts.map +1 -1
  34. package/dist/generic/entityCompanion.js +8 -0
  35. package/dist/generic/entityCompanion.js.map +1 -1
  36. package/dist/generic/entityInfo.d.ts +228 -9
  37. package/dist/generic/entityInfo.d.ts.map +1 -1
  38. package/dist/generic/entityInfo.js +574 -65
  39. package/dist/generic/entityInfo.js.map +1 -1
  40. package/dist/generic/entityValueFieldSecurity.d.ts +139 -0
  41. package/dist/generic/entityValueFieldSecurity.d.ts.map +1 -0
  42. package/dist/generic/entityValueFieldSecurity.js +250 -0
  43. package/dist/generic/entityValueFieldSecurity.js.map +1 -0
  44. package/dist/generic/fieldSecuritySQLGate.d.ts +350 -0
  45. package/dist/generic/fieldSecuritySQLGate.d.ts.map +1 -0
  46. package/dist/generic/fieldSecuritySQLGate.js +898 -0
  47. package/dist/generic/fieldSecuritySQLGate.js.map +1 -0
  48. package/dist/generic/indexRestrictedFields.d.ts +47 -0
  49. package/dist/generic/indexRestrictedFields.d.ts.map +1 -0
  50. package/dist/generic/indexRestrictedFields.js +129 -0
  51. package/dist/generic/indexRestrictedFields.js.map +1 -0
  52. package/dist/generic/interfaces.d.ts +14 -1
  53. package/dist/generic/interfaces.d.ts.map +1 -1
  54. package/dist/generic/interfaces.js.map +1 -1
  55. package/dist/generic/localCacheManager.d.ts +80 -11
  56. package/dist/generic/localCacheManager.d.ts.map +1 -1
  57. package/dist/generic/localCacheManager.js +209 -38
  58. package/dist/generic/localCacheManager.js.map +1 -1
  59. package/dist/generic/logging.d.ts.map +1 -1
  60. package/dist/generic/logging.js +7 -1
  61. package/dist/generic/logging.js.map +1 -1
  62. package/dist/generic/metadata.d.ts +3 -2
  63. package/dist/generic/metadata.d.ts.map +1 -1
  64. package/dist/generic/metadata.js +5 -2
  65. package/dist/generic/metadata.js.map +1 -1
  66. package/dist/generic/platformSQL.d.ts +54 -0
  67. package/dist/generic/platformSQL.d.ts.map +1 -1
  68. package/dist/generic/platformSQL.js +55 -0
  69. package/dist/generic/platformSQL.js.map +1 -1
  70. package/dist/generic/providerBase.d.ts +254 -56
  71. package/dist/generic/providerBase.d.ts.map +1 -1
  72. package/dist/generic/providerBase.js +698 -195
  73. package/dist/generic/providerBase.js.map +1 -1
  74. package/dist/generic/queryInfo.d.ts +13 -0
  75. package/dist/generic/queryInfo.d.ts.map +1 -1
  76. package/dist/generic/queryInfo.js +13 -0
  77. package/dist/generic/queryInfo.js.map +1 -1
  78. package/dist/generic/queryInfoInterfaces.d.ts +8 -0
  79. package/dist/generic/queryInfoInterfaces.d.ts.map +1 -1
  80. package/dist/generic/queryTemplateContext.d.ts +49 -0
  81. package/dist/generic/queryTemplateContext.d.ts.map +1 -0
  82. package/dist/generic/queryTemplateContext.js +113 -0
  83. package/dist/generic/queryTemplateContext.js.map +1 -0
  84. package/dist/generic/recordChangeFieldSecurity.d.ts.map +1 -1
  85. package/dist/generic/recordChangeFieldSecurity.js +13 -2
  86. package/dist/generic/recordChangeFieldSecurity.js.map +1 -1
  87. package/dist/generic/relatedRecordCollection.d.ts +13 -0
  88. package/dist/generic/relatedRecordCollection.d.ts.map +1 -1
  89. package/dist/generic/relatedRecordCollection.js +44 -5
  90. package/dist/generic/relatedRecordCollection.js.map +1 -1
  91. package/dist/generic/runQuery.d.ts +42 -0
  92. package/dist/generic/runQuery.d.ts.map +1 -1
  93. package/dist/generic/runQuery.js +14 -0
  94. package/dist/generic/runQuery.js.map +1 -1
  95. package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
  96. package/dist/generic/runQuerySQLFilterImplementations.js +38 -2
  97. package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
  98. package/dist/generic/saveEntityGraphOperation.d.ts.map +1 -1
  99. package/dist/generic/saveEntityGraphOperation.js +3 -0
  100. package/dist/generic/saveEntityGraphOperation.js.map +1 -1
  101. package/dist/generic/securityInfo.d.ts +11 -0
  102. package/dist/generic/securityInfo.d.ts.map +1 -1
  103. package/dist/generic/securityInfo.js +15 -0
  104. package/dist/generic/securityInfo.js.map +1 -1
  105. package/dist/generic/telemetryManager.d.ts +9 -0
  106. package/dist/generic/telemetryManager.d.ts.map +1 -1
  107. package/dist/generic/telemetryManager.js +5 -1
  108. package/dist/generic/telemetryManager.js.map +1 -1
  109. package/dist/index.d.ts +7 -0
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +7 -0
  112. package/dist/index.js.map +1 -1
  113. package/dist/views/runView.d.ts +89 -0
  114. package/dist/views/runView.d.ts.map +1 -1
  115. package/dist/views/runView.js +36 -0
  116. package/dist/views/runView.js.map +1 -1
  117. package/dist/views/viewProperties.d.ts +55 -0
  118. package/dist/views/viewProperties.d.ts.map +1 -0
  119. package/dist/views/viewProperties.js +258 -0
  120. package/dist/views/viewProperties.js.map +1 -0
  121. package/package.json +3 -3
@@ -206,6 +206,12 @@ export class BaseEngine extends BaseSingleton {
206
206
  this._externalChangeReloads = new Map();
207
207
  this._entityEventDebounceTime = 1500; // Default debounce time in milliseconds (1.5 seconds)
208
208
  this._eventRefreshRetryTimers = new Map();
209
+ /**
210
+ * Maps normalized entity names (`entityName.trim().toLowerCase()`) to dataset item properties
211
+ * loaded via `DatasetResultHandling === 'single_property'`, allowing in-place auto-refresh of
212
+ * dataset-backed engines (such as `TemplateEngineBase`) on entity save/delete events.
213
+ */
214
+ this._datasetItemMappings = new Map();
209
215
  /** Configs waiting for the cache manager to initialize before their callbacks can register. */
210
216
  this._deferredCacheCallbackConfigs = null;
211
217
  }
@@ -676,11 +682,16 @@ export class BaseEngine extends BaseSingleton {
676
682
  const matchingConfigs = this._metadataConfigs.filter((config) => {
677
683
  return config.AutoRefresh && config.EntityName && config.EntityName.trim().toLowerCase() === eName;
678
684
  });
679
- if (matchingConfigs.length === 0) {
685
+ const matchingDatasetItems = (this._datasetItemMappings.get(eName) ?? []).filter(m => m.config.AutoRefresh !== false);
686
+ if (matchingConfigs.length === 0 && matchingDatasetItems.length === 0) {
680
687
  return true;
681
688
  }
682
- // Check if ALL matching configs can use immediate mutation
683
- const allCanUseImmediate = matchingConfigs.every(config => this.canUseImmediateMutation(config));
689
+ // Check if ALL matching entity configs can use immediate mutation. A saved object
690
+ // that lacks columns (its saver could not read them) would put a narrowed row into
691
+ // an engine every user reads, so that save takes the full-refresh path instead,
692
+ // which reloads the row as this engine's own user.
693
+ const allCanUseImmediate = matchingConfigs.every(config => this.canUseImmediateMutation(config))
694
+ && this.savedRowIsComplete(event);
684
695
  if (allCanUseImmediate) {
685
696
  const applyAll = async () => {
686
697
  // mutation requires await because the entity must be cloned (with its
@@ -688,6 +699,9 @@ export class BaseEngine extends BaseSingleton {
688
699
  for (const config of matchingConfigs) {
689
700
  await this.applyImmediateMutation(config, event);
690
701
  }
702
+ if (matchingDatasetItems.length > 0) {
703
+ await this.applyDatasetItemMutations(matchingDatasetItems, event);
704
+ }
691
705
  };
692
706
  // The entity raises save/delete as soon as its own write returns, while an
693
707
  // enclosing transaction may still be open. Mutating the cache now would keep a
@@ -714,6 +728,21 @@ export class BaseEngine extends BaseSingleton {
714
728
  return true;
715
729
  }
716
730
  else {
731
+ if (matchingDatasetItems.length > 0) {
732
+ const provider = event.baseEntity.ProviderToUse;
733
+ if (provider instanceof DatabaseProviderBase && provider.TransactionDepth > 0) {
734
+ const token = provider.CapturePostCommitToken();
735
+ const savedKey = event.type === 'save' ? event.baseEntity.PrimaryKey.ToString() : null;
736
+ provider.RunAfterCommit(async () => {
737
+ if (savedKey !== null && event.baseEntity.PrimaryKey.ToString() !== savedKey)
738
+ return;
739
+ await this.applyDatasetItemMutations(matchingDatasetItems, event);
740
+ }, `BaseEngine dataset cache mutation for ${eName}`, token);
741
+ }
742
+ else {
743
+ await this.applyDatasetItemMutations(matchingDatasetItems, event);
744
+ }
745
+ }
717
746
  // At least one config requires full refresh, use debouncing
718
747
  return this.DebounceIndividualBaseEntityEvent(event);
719
748
  }
@@ -967,7 +996,9 @@ export class BaseEngine extends BaseSingleton {
967
996
  const needingWork = classified.filter(c => c.disposition.action === 'refresh');
968
997
  if (needingWork.length > 0) {
969
998
  // Check if we can use immediate array mutation instead of running a view
970
- if (this.canUseImmediateMutation(config)) {
999
+ // A save carrying a row with missing columns can't replace an engine row
1000
+ // in place (see savedRowIsComplete), so the whole window refreshes instead.
1001
+ if (this.canUseImmediateMutation(config) && needingWork.every(c => this.savedRowIsComplete(c.event))) {
971
1002
  // Apply, in order, every event the array hasn't seen yet
972
1003
  for (const c of needingWork) {
973
1004
  await this.applyImmediateMutation(config, c.event);
@@ -1032,7 +1063,11 @@ export class BaseEngine extends BaseSingleton {
1032
1063
  classifyEventForConfig(config, event) {
1033
1064
  if (event.type === 'save') {
1034
1065
  if (event.saveSubType === 'update' || event.saveSubType === 'create') {
1035
- if (this.isEntityAlreadyInArray(config, event.baseEntity)) {
1066
+ // When AlternateBaseView is set on the config, the SQL view applies a canonical
1067
+ // WHERE/JOIN predicate — an in-memory update can cause the record to fall out of
1068
+ // the alternate view (e.g. Status changed from 'Active' to 'Inactive'), so we
1069
+ // must refresh from the database rather than only notifying in place.
1070
+ if (!config.AlternateBaseView && this.isEntityAlreadyInArray(config, event.baseEntity)) {
1036
1071
  return { action: 'notify', changeType: event.saveSubType === 'create' ? 'add' : 'update' };
1037
1072
  }
1038
1073
  }
@@ -1217,6 +1252,11 @@ export class BaseEngine extends BaseSingleton {
1217
1252
  if (config.Filter) {
1218
1253
  return false;
1219
1254
  }
1255
+ // An AlternateBaseView applies a canonical SQL WHERE/JOIN in the database view;
1256
+ // immediate in-memory mutations cannot evaluate that SQL predicate.
1257
+ if (config.AlternateBaseView) {
1258
+ return false;
1259
+ }
1220
1260
  // If there's an OrderBy, we can't safely do immediate mutations because:
1221
1261
  // - For creates: we'd need to insert at the correct position
1222
1262
  // - For updates: the entity might need to move to a different position
@@ -1255,6 +1295,21 @@ export class BaseEngine extends BaseSingleton {
1255
1295
  // it means the subclass has overridden it
1256
1296
  return instanceProto.AdditionalLoading !== baseProto.AdditionalLoading;
1257
1297
  }
1298
+ /**
1299
+ * Whether a save event's entity carries every column, so it can replace an engine row in
1300
+ * place. Deletes and non-save events always qualify.
1301
+ */
1302
+ savedRowIsComplete(event) {
1303
+ if (event.type !== 'save' || !event.baseEntity) {
1304
+ return true;
1305
+ }
1306
+ const entity = event.baseEntity;
1307
+ // Asked of the object directly, so no throwaway GetAll() row is built per event. An object
1308
+ // that is not a BaseEntity (a test double) is judged on its row, as before.
1309
+ return typeof entity.HoldsEveryField === 'function'
1310
+ ? entity.HoldsEveryField()
1311
+ : LocalCacheManager.RowHasEveryField(entity.EntityInfo, entity.GetAll());
1312
+ }
1258
1313
  /**
1259
1314
  * Applies an immediate array mutation based on the entity event type.
1260
1315
  * This is faster than running a full view refresh for simple add/update/delete operations.
@@ -1404,16 +1459,9 @@ export class BaseEngine extends BaseSingleton {
1404
1459
  if (cache.IsBatchingEntityEvents(entity?.ProviderToUse) || cache.WasTakenByEntityEventBatch(event)) {
1405
1460
  return;
1406
1461
  }
1407
- // Get the connection string from the provider for fingerprint generation
1408
- // The provider is needed because fingerprints include connection prefix
1409
- const provider = this.ProviderToUse;
1410
- let connectionString;
1411
- if (provider && 'InstanceConnectionString' in provider) {
1412
- connectionString = provider.InstanceConnectionString;
1413
- }
1414
- // Use the shared builder so the fingerprint matches what LoadSingleEntityConfig
1415
- // and RegisterCacheChangeCallbacks produce — prevents silent cache-slot mismatches
1416
- const fingerprint = LocalCacheManager.Instance.GenerateRunViewFingerprint(this.BuildRunViewParamsForConfig(config), connectionString);
1462
+ // The same builder as LoadSingleEntityConfig and RegisterCacheChangeCallbacks, so the
1463
+ // slot maintained here is the one this engine reads and listens on.
1464
+ const fingerprint = this.configFingerprint(config);
1417
1465
  // Build CompositeKey from the entity's primary key fields
1418
1466
  const key = entity.PrimaryKey;
1419
1467
  if (!key || key.KeyValuePairs.length === 0 || key.KeyValuePairs.some(kv => kv.Value == null)) {
@@ -1435,6 +1483,12 @@ export class BaseEngine extends BaseSingleton {
1435
1483
  }
1436
1484
  else {
1437
1485
  const entityData = entity.GetAll();
1486
+ // A row-filtered or field-secured slot, or a saved row missing columns, cannot take the
1487
+ // saver's values in place: clear it and let the next read refill it.
1488
+ if (!LocalCacheManager.Instance.CanUpsertRowIntoSlot(fingerprint, entity.EntityInfo, entityData)) {
1489
+ await LocalCacheManager.Instance.InvalidateRunViewResult(fingerprint);
1490
+ return;
1491
+ }
1438
1492
  await LocalCacheManager.Instance.UpsertSingleEntity(fingerprint, entityData, key, updatedAt);
1439
1493
  }
1440
1494
  }
@@ -1498,6 +1552,7 @@ export class BaseEngine extends BaseSingleton {
1498
1552
  // now, break up the configs into two chunks, datasets and views of entities so we can load all the views in a single network call via RunViews()
1499
1553
  const entityConfigs = this._metadataConfigs.filter(c => c.Type === 'entity');
1500
1554
  const datasetConfigs = this._metadataConfigs.filter(c => c.Type === 'dataset');
1555
+ this.WarnAboutFieldSecuredEntities(entityConfigs);
1501
1556
  // All-or-nothing permission check: if the user lacks read access on ANY
1502
1557
  // entity config, skip ALL entity configs for this engine. Returns the
1503
1558
  // original array unchanged when permissions pass, or an empty array when
@@ -1508,6 +1563,35 @@ export class BaseEngine extends BaseSingleton {
1508
1563
  // Register cross-server cache change callbacks for entity configs
1509
1564
  this.RegisterCacheChangeCallbacks(entityConfigs);
1510
1565
  }
1566
+ /** Engine-class and entity pairs already warned about by {@link WarnAboutFieldSecuredEntities}. */
1567
+ static { this._fieldSecurityWarned = new Set(); }
1568
+ /**
1569
+ * On a server, says once per engine class and entity when an entity this engine caches has
1570
+ * field-level security (its own, or an IS-A ancestor's). Changes nothing about the load.
1571
+ *
1572
+ * A server-side engine loads as the system user (see {@link ResolveContextUser}), so its
1573
+ * cache holds every column, including ones some users are denied. That is correct for a
1574
+ * process-wide cache, and it makes every path that hands engine data to a person responsible
1575
+ * for projecting it to that person first.
1576
+ */
1577
+ WarnAboutFieldSecuredEntities(entityConfigs) {
1578
+ const provider = this.ProviderToUse;
1579
+ if (provider?.ProviderType !== ProviderType.Database) {
1580
+ return;
1581
+ }
1582
+ for (const config of entityConfigs) {
1583
+ const entityInfo = config.EntityName ? provider.EntityByName(config.EntityName) : undefined;
1584
+ if (!entityInfo?.FieldLevelSecurityApplies) {
1585
+ continue;
1586
+ }
1587
+ const key = `${this.constructor.name}|${entityInfo.Name}`.toLowerCase();
1588
+ if (BaseEngine._fieldSecurityWarned.has(key)) {
1589
+ continue;
1590
+ }
1591
+ BaseEngine._fieldSecurityWarned.add(key);
1592
+ LogStatus(`${this.constructor.name}: '${entityInfo.Name}' has field-level security. Engine data is loaded as the system user and holds every column; project it for the acting user before it reaches a person.`);
1593
+ }
1594
+ }
1511
1595
  /**
1512
1596
  * All-or-nothing permission gate: checks `CanRead` on every entity config. If ANY
1513
1597
  * entity is denied, ALL configs are skipped — the engine is marked permission-constrained
@@ -1659,14 +1743,8 @@ export class BaseEngine extends BaseSingleton {
1659
1743
  * IgnoreMaxRows, IncludeBinaryFields) are always consistent — preventing cache key mismatches that break
1660
1744
  * cross-server invalidation via Redis pub/sub and local cache upsert/remove operations.
1661
1745
  *
1662
- * **Row-level security is not part of the fingerprints built from these params.** The engine
1663
- * builds them with `GenerateRunViewFingerprint(params, connection)`, without the RLS clause the
1664
- * provider adds for the reading user. On a server that is correct: engines load as the system
1665
- * user, whose RLS clause is empty, so the provider's fingerprint for the same read matches. An
1666
- * engine loaded by an RLS-scoped user (possible on a client) would compute an unscoped
1667
- * fingerprint for a slot the provider stored under a scoped one, so its cache callbacks and
1668
- * `syncLocalCacheForConfig` would target a different slot. Pass the RLS clause here if such an
1669
- * engine is ever needed.
1746
+ * The slot fingerprint for these params also carries the engine user's row-level-security
1747
+ * and field-security segments; see {@link configFingerprint}.
1670
1748
  */
1671
1749
  BuildRunViewParamsForConfig(config, bypassCache = false) {
1672
1750
  return {
@@ -1674,6 +1752,7 @@ export class BaseEngine extends BaseSingleton {
1674
1752
  ResultType: config.ResultType || this.EngineDefaultResultType,
1675
1753
  ExtraFilter: config.Filter,
1676
1754
  OrderBy: config.OrderBy,
1755
+ ...(config.AlternateBaseView ? { AlternateBaseView: config.AlternateBaseView } : {}),
1677
1756
  IgnoreMaxRows: true, // Engines always need ALL data — bypass entity-level UserViewMaxRows caps
1678
1757
  _fromEngine: true, // Mark as engine-initiated to avoid false positive telemetry warnings
1679
1758
  CacheLocal: config.CacheLocal,
@@ -1834,6 +1913,58 @@ export class BaseEngine extends BaseSingleton {
1834
1913
  }
1835
1914
  }
1836
1915
  }
1916
+ /**
1917
+ * Applies an immediate in-place mutation to dataset item arrays when a constituent entity is saved or deleted,
1918
+ * then invokes {@link AdditionalLoading} so derived associations (e.g. Template Content / Params) stay current.
1919
+ */
1920
+ async applyDatasetItemMutations(mappings, event) {
1921
+ const entity = event.baseEntity;
1922
+ let mutated = false;
1923
+ const notifiedProperties = new Set();
1924
+ for (const mapping of mappings) {
1925
+ const container = this[mapping.config.PropertyName];
1926
+ const currentData = container?.[mapping.itemCode];
1927
+ if (!Array.isArray(currentData)) {
1928
+ continue;
1929
+ }
1930
+ if (event.type === 'save') {
1931
+ const provider = this.ProviderToUse;
1932
+ if (!provider)
1933
+ continue;
1934
+ const cached = await provider.GetEntityObject(mapping.entityName, this._contextUser);
1935
+ if (!cached)
1936
+ continue;
1937
+ await cached.LoadFromData(entity.GetAll());
1938
+ const existsByRef = currentData.indexOf(entity) >= 0;
1939
+ if (!existsByRef) {
1940
+ const index = this.findEntityIndexByPrimaryKeys(currentData, entity);
1941
+ if (index >= 0) {
1942
+ currentData[index] = cached;
1943
+ }
1944
+ else {
1945
+ currentData.push(cached);
1946
+ }
1947
+ }
1948
+ mutated = true;
1949
+ notifiedProperties.add(mapping.config.PropertyName);
1950
+ }
1951
+ else if (event.type === 'delete') {
1952
+ const oldValues = event.payload?.OldValues;
1953
+ const index = this.indexOfEntityByRefOrKey(currentData, entity, oldValues);
1954
+ if (index >= 0) {
1955
+ currentData.splice(index, 1);
1956
+ mutated = true;
1957
+ notifiedProperties.add(mapping.config.PropertyName);
1958
+ }
1959
+ }
1960
+ }
1961
+ if (mutated) {
1962
+ await this.AdditionalLoading(this._contextUser);
1963
+ for (const propName of notifiedProperties) {
1964
+ this.emitPropertyChange(propName);
1965
+ }
1966
+ }
1967
+ }
1837
1968
  /**
1838
1969
  * Handles the process of loading a single config of type 'dataset'.
1839
1970
  * @param config
@@ -1853,7 +1984,7 @@ export class BaseEngine extends BaseSingleton {
1853
1984
  await p.CacheDataset(config.DatasetName, config.DatasetItemFilters, result);
1854
1985
  }
1855
1986
  else {
1856
- result = await p.GetAndCacheDatasetByName(config.DatasetName, config.DatasetItemFilters);
1987
+ result = await p.GetAndCacheDatasetByName(config.DatasetName, config.DatasetItemFilters, contextUser);
1857
1988
  }
1858
1989
  if (!result) {
1859
1990
  const errorMsg = `GetAndCacheDatasetByName("${config.DatasetName}") returned undefined/null — provider: ${p?.constructor?.name}`;
@@ -1880,18 +2011,31 @@ export class BaseEngine extends BaseSingleton {
1880
2011
  if (result.Success) {
1881
2012
  if (config.AddToObject !== false) {
1882
2013
  if (config.DatasetResultHandling === 'single_property') {
2014
+ // Clear previous dataset item mappings for this property before re-indexing
2015
+ for (const [key, list] of this._datasetItemMappings.entries()) {
2016
+ const filtered = list.filter(m => m.config.PropertyName !== config.PropertyName);
2017
+ if (filtered.length === 0) {
2018
+ this._datasetItemMappings.delete(key);
2019
+ }
2020
+ else if (filtered.length !== list.length) {
2021
+ this._datasetItemMappings.set(key, filtered);
2022
+ }
2023
+ }
1883
2024
  const singleObject = {};
1884
- for (const item of result.Results) {
1885
- //convert the results to entity objects before
1886
- //adding them to the singleObject
1887
- const entities = [];
1888
- for (const entityData of item.Results) {
2025
+ await Promise.all(result.Results.map(async (item) => {
2026
+ const entities = await Promise.all(item.Results.map(async (entityData) => {
1889
2027
  const entity = await p.GetEntityObject(item.EntityName, contextUser);
1890
2028
  await entity.LoadFromData(entityData);
1891
- entities.push(entity);
1892
- }
2029
+ return entity;
2030
+ }));
1893
2031
  singleObject[item.Code] = entities;
1894
- }
2032
+ if (item.EntityName && item.Code) {
2033
+ const normName = item.EntityName.trim().toLowerCase();
2034
+ const existing = this._datasetItemMappings.get(normName) ?? [];
2035
+ existing.push({ config, itemCode: item.Code, entityName: item.EntityName });
2036
+ this._datasetItemMappings.set(normName, existing);
2037
+ }
2038
+ }));
1895
2039
  this[config.PropertyName] = singleObject;
1896
2040
  }
1897
2041
  else {
@@ -2337,11 +2481,26 @@ export class BaseEngine extends BaseSingleton {
2337
2481
  // read-modify-write of this slot, and an unlocked replace would clobber it.
2338
2482
  await cache.ReplaceRunViewResultLocked(this.configFingerprint(config), params, plainRows, maxUpdatedAt, plainRows.length, this.ProviderToUse);
2339
2483
  }
2340
- /** The cache fingerprint of a config's slot, as {@link RegisterCacheChangeCallbacks} computes it. */
2484
+ /**
2485
+ * The cache fingerprint of a config's slot: the one {@link RegisterCacheChangeCallbacks} listens
2486
+ * on, {@link syncLocalCacheForConfig} maintains and {@link storeConfigRows} writes.
2487
+ *
2488
+ * Built by the provider for this engine's user, with that user's row-level-security and
2489
+ * field-security segments, so it names the same slot the provider's own RunView read used. An
2490
+ * engine running as a restricted user holds only what that user may see; without the segments
2491
+ * its rows would be written into, and its callbacks registered on, the slot unrestricted
2492
+ * readers share.
2493
+ */
2341
2494
  configFingerprint(config) {
2495
+ const params = this.BuildRunViewParamsForConfig(config);
2342
2496
  const provider = this.RunViewProviderToUse;
2343
- const connectionPrefix = provider instanceof ProviderBase ? provider.InstanceConnectionString : undefined;
2344
- return LocalCacheManager.Instance.GenerateRunViewFingerprint(this.BuildRunViewParamsForConfig(config), connectionPrefix);
2497
+ if (provider instanceof ProviderBase) {
2498
+ return provider.ComputeRunViewCacheFingerprint(params, this._contextUser);
2499
+ }
2500
+ const connection = provider && 'InstanceConnectionString' in provider
2501
+ ? provider.InstanceConnectionString
2502
+ : undefined;
2503
+ return LocalCacheManager.Instance.GenerateRunViewFingerprint(params, connection);
2345
2504
  }
2346
2505
  /**
2347
2506
  * Snapshot of what this engine holds: per property, the row count, newest `__mj_UpdatedAt`,