@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.
- package/dist/generic/BaseEntitySaveQueue.d.ts +15 -0
- package/dist/generic/BaseEntitySaveQueue.d.ts.map +1 -1
- package/dist/generic/BaseEntitySaveQueue.js +21 -0
- package/dist/generic/BaseEntitySaveQueue.js.map +1 -1
- package/dist/generic/alternateBaseViews.d.ts +119 -0
- package/dist/generic/alternateBaseViews.d.ts.map +1 -0
- package/dist/generic/alternateBaseViews.js +223 -0
- package/dist/generic/alternateBaseViews.js.map +1 -0
- package/dist/generic/baseEngine.d.ts +53 -9
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +194 -35
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
- package/dist/generic/baseEngineRegistry.js +2 -1
- package/dist/generic/baseEngineRegistry.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +153 -17
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +345 -46
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/contextTokens.d.ts +136 -0
- package/dist/generic/contextTokens.d.ts.map +1 -0
- package/dist/generic/contextTokens.js +319 -0
- package/dist/generic/contextTokens.js.map +1 -0
- package/dist/generic/databaseProviderBase.d.ts +66 -5
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +159 -12
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/embeddedRecord.d.ts +6 -0
- package/dist/generic/embeddedRecord.d.ts.map +1 -1
- package/dist/generic/embeddedRecord.js +15 -1
- package/dist/generic/embeddedRecord.js.map +1 -1
- package/dist/generic/entityCompanion.d.ts +7 -0
- package/dist/generic/entityCompanion.d.ts.map +1 -1
- package/dist/generic/entityCompanion.js +8 -0
- package/dist/generic/entityCompanion.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +228 -9
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +574 -65
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/entityValueFieldSecurity.d.ts +139 -0
- package/dist/generic/entityValueFieldSecurity.d.ts.map +1 -0
- package/dist/generic/entityValueFieldSecurity.js +250 -0
- package/dist/generic/entityValueFieldSecurity.js.map +1 -0
- package/dist/generic/fieldSecuritySQLGate.d.ts +350 -0
- package/dist/generic/fieldSecuritySQLGate.d.ts.map +1 -0
- package/dist/generic/fieldSecuritySQLGate.js +898 -0
- package/dist/generic/fieldSecuritySQLGate.js.map +1 -0
- package/dist/generic/indexRestrictedFields.d.ts +47 -0
- package/dist/generic/indexRestrictedFields.d.ts.map +1 -0
- package/dist/generic/indexRestrictedFields.js +129 -0
- package/dist/generic/indexRestrictedFields.js.map +1 -0
- package/dist/generic/interfaces.d.ts +14 -1
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +80 -11
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +209 -38
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/logging.d.ts.map +1 -1
- package/dist/generic/logging.js +7 -1
- package/dist/generic/logging.js.map +1 -1
- package/dist/generic/metadata.d.ts +3 -2
- package/dist/generic/metadata.d.ts.map +1 -1
- package/dist/generic/metadata.js +5 -2
- package/dist/generic/metadata.js.map +1 -1
- package/dist/generic/platformSQL.d.ts +54 -0
- package/dist/generic/platformSQL.d.ts.map +1 -1
- package/dist/generic/platformSQL.js +55 -0
- package/dist/generic/platformSQL.js.map +1 -1
- package/dist/generic/providerBase.d.ts +254 -56
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +698 -195
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +13 -0
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +13 -0
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/queryInfoInterfaces.d.ts +8 -0
- package/dist/generic/queryInfoInterfaces.d.ts.map +1 -1
- package/dist/generic/queryTemplateContext.d.ts +49 -0
- package/dist/generic/queryTemplateContext.d.ts.map +1 -0
- package/dist/generic/queryTemplateContext.js +113 -0
- package/dist/generic/queryTemplateContext.js.map +1 -0
- package/dist/generic/recordChangeFieldSecurity.d.ts.map +1 -1
- package/dist/generic/recordChangeFieldSecurity.js +13 -2
- package/dist/generic/recordChangeFieldSecurity.js.map +1 -1
- package/dist/generic/relatedRecordCollection.d.ts +13 -0
- package/dist/generic/relatedRecordCollection.d.ts.map +1 -1
- package/dist/generic/relatedRecordCollection.js +44 -5
- package/dist/generic/relatedRecordCollection.js.map +1 -1
- package/dist/generic/runQuery.d.ts +42 -0
- package/dist/generic/runQuery.d.ts.map +1 -1
- package/dist/generic/runQuery.js +14 -0
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +38 -2
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/generic/saveEntityGraphOperation.d.ts.map +1 -1
- package/dist/generic/saveEntityGraphOperation.js +3 -0
- package/dist/generic/saveEntityGraphOperation.js.map +1 -1
- package/dist/generic/securityInfo.d.ts +11 -0
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +15 -0
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts +9 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +5 -1
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/views/runView.d.ts +89 -0
- package/dist/views/runView.d.ts.map +1 -1
- package/dist/views/runView.js +36 -0
- package/dist/views/runView.js.map +1 -1
- package/dist/views/viewProperties.d.ts +55 -0
- package/dist/views/viewProperties.d.ts.map +1 -0
- package/dist/views/viewProperties.js +258 -0
- package/dist/views/viewProperties.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1408
|
-
//
|
|
1409
|
-
const
|
|
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
|
-
*
|
|
1663
|
-
*
|
|
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
|
-
|
|
1885
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
2344
|
-
|
|
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`,
|