@memberjunction/core 6.1.4 → 6.1.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/InMemoryLocalStorageProvider.d.ts +5 -0
- package/dist/generic/InMemoryLocalStorageProvider.d.ts.map +1 -1
- package/dist/generic/InMemoryLocalStorageProvider.js +5 -0
- package/dist/generic/InMemoryLocalStorageProvider.js.map +1 -1
- package/dist/generic/baseEngine.d.ts +21 -0
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +60 -5
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +2 -1
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +3 -4
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +57 -8
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +107 -42
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityRecordNameCache.d.ts +48 -0
- package/dist/generic/entityRecordNameCache.d.ts.map +1 -0
- package/dist/generic/entityRecordNameCache.js +92 -0
- package/dist/generic/entityRecordNameCache.js.map +1 -0
- package/dist/generic/interfaces.d.ts +59 -4
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +105 -11
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +221 -40
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/providerBase.d.ts +40 -22
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +93 -103
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +14 -2
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +18 -5
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/runQuery.d.ts +18 -1
- package/dist/generic/runQuery.d.ts.map +1 -1
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +7 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +65 -140
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts +11 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +40 -0
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/util.d.ts +22 -0
- package/dist/generic/util.d.ts.map +1 -1
- package/dist/generic/util.js +33 -0
- package/dist/generic/util.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -6,7 +6,7 @@ import { ComputeRRF } from "./scoring/ReciprocalRankFusion.js";
|
|
|
6
6
|
import { LocalCacheManager } from "./localCacheManager.js";
|
|
7
7
|
import { ApplicationInfo } from "../generic/applicationInfo.js";
|
|
8
8
|
import { AuditLogTypeInfo, AuthorizationInfo, AuthorizationRoleInfo, RoleInfo, RowLevelSecurityFilterInfo, UserInfo } from "./securityInfo.js";
|
|
9
|
-
import { MJGlobal, MJEventType, NormalizeUUID, UUIDsEqual,
|
|
9
|
+
import { MJGlobal, MJEventType, NormalizeUUID, UUIDsEqual, EscapeSQLString, ordinalCompare } from "@memberjunction/global";
|
|
10
10
|
import { FindReferencedIdentifiers } from "@memberjunction/sql-dialect";
|
|
11
11
|
import { TelemetryManager } from "./telemetryManager.js";
|
|
12
12
|
import { LogDebug, LogError, LogStatus, LogStatusEx } from "./logging.js";
|
|
@@ -186,10 +186,6 @@ export class ProviderBase {
|
|
|
186
186
|
this._localMetadata = new AllMetadata();
|
|
187
187
|
this._entityMapByName = new Map();
|
|
188
188
|
this._entityMapByID = new Map();
|
|
189
|
-
// Bounded LRU (unlike its siblings above, this cache holds one entry per distinct
|
|
190
|
-
// *record* touched via Load()/Save()/LoadFromData() — not per entity definition — so
|
|
191
|
-
// it can't be reset on metadata refresh; it needs its own eviction policy.
|
|
192
|
-
this._entityRecordNameCache = new MJLruCache({ maxSize: 10000, ttlMs: 60 * 60 * 1000 });
|
|
193
189
|
this._refresh = false;
|
|
194
190
|
/** Single-flight guard for the full metadata reload in {@link Config} — see the comment there. */
|
|
195
191
|
this._metadataReloadInFlight = null;
|
|
@@ -231,6 +227,8 @@ export class ProviderBase {
|
|
|
231
227
|
this._metadataMemberRefreshTimer = null;
|
|
232
228
|
this._clientFingerprintMemo = new WeakMap();
|
|
233
229
|
this._cachedVisibleExplorerNavigationItems = null;
|
|
230
|
+
/** Guard for {@link logMetadataSnapshotDisabledOnce}. */
|
|
231
|
+
this._metadataSnapshotSkipLogged = false;
|
|
234
232
|
}
|
|
235
233
|
/**
|
|
236
234
|
* Whether this provider can execute a multi-record unit of work atomically, in-process.
|
|
@@ -589,124 +587,49 @@ export class ProviderBase {
|
|
|
589
587
|
return false;
|
|
590
588
|
}
|
|
591
589
|
/**
|
|
592
|
-
*
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
*
|
|
599
|
-
* Use this for synchronous contexts (like template rendering) where you can't await GetEntityRecordName().
|
|
590
|
+
* Returns a record's name for synchronous display code, or `undefined` when it is not at hand.
|
|
591
|
+
*
|
|
592
|
+
* This provider keeps no record names, so without `loadIfNeeded` the answer is always
|
|
593
|
+
* `undefined`. A provider that serves a single user (`GraphQLDataProvider`) overrides the
|
|
594
|
+
* record-name methods with a cache; one shared by several users must not, since a name can be
|
|
595
|
+
* withheld from some of them.
|
|
596
|
+
*
|
|
600
597
|
* @param entityName - The name of the entity
|
|
601
598
|
* @param compositeKey - The primary key value(s) for the record
|
|
602
|
-
* @param loadIfNeeded -
|
|
603
|
-
* @returns The
|
|
599
|
+
* @param loadIfNeeded - Look the name up when it is not cached
|
|
600
|
+
* @returns The display name, or undefined if not cached and not looked up
|
|
604
601
|
*/
|
|
605
602
|
async GetCachedRecordName(entityName, compositeKey, loadIfNeeded) {
|
|
606
|
-
|
|
607
|
-
if (!cachedEntry && loadIfNeeded) {
|
|
608
|
-
cachedEntry = await this.GetEntityRecordName(entityName, compositeKey);
|
|
609
|
-
}
|
|
610
|
-
return cachedEntry;
|
|
603
|
+
return loadIfNeeded ? this.GetEntityRecordName(entityName, compositeKey) : undefined;
|
|
611
604
|
}
|
|
612
605
|
/**
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
* @param entityName - The name of the entity
|
|
616
|
-
* @param compositeKey - The primary key value(s) for the record
|
|
617
|
-
* @param recordName - The display name to cache
|
|
606
|
+
* Offers a record's name for later synchronous retrieval. `BaseEntity` calls it after every
|
|
607
|
+
* Load(), LoadFromData() and Save(). Ignored here; see {@link GetCachedRecordName}.
|
|
618
608
|
*/
|
|
619
609
|
SetCachedRecordName(entityName, compositeKey, recordName) {
|
|
620
|
-
this
|
|
610
|
+
// Intentionally empty: this provider keeps no record names.
|
|
621
611
|
}
|
|
622
612
|
/**
|
|
623
|
-
* Gets the display name for a single entity record
|
|
613
|
+
* Gets the display name for a single entity record.
|
|
624
614
|
* Uses the entity's IsNameField or falls back to 'Name' field if available.
|
|
625
615
|
* @param entityName - The name of the entity
|
|
626
616
|
* @param compositeKey - The primary key value(s) for the record
|
|
627
|
-
* @param contextUser -
|
|
628
|
-
* @param forceRefresh -
|
|
629
|
-
* @returns The display name of the record or
|
|
617
|
+
* @param contextUser - The acting user; field- and row-level security are applied for them
|
|
618
|
+
* @param forceRefresh - Bypass any cache. This provider has none, so it always looks up.
|
|
619
|
+
* @returns The display name of the record, or an empty string if not found or not readable
|
|
630
620
|
*/
|
|
631
621
|
async GetEntityRecordName(entityName, compositeKey, contextUser, forceRefresh = false) {
|
|
632
|
-
|
|
633
|
-
// Check cache unless forceRefresh
|
|
634
|
-
if (!forceRefresh) {
|
|
635
|
-
const cached = this._entityRecordNameCache.Get(cacheKey);
|
|
636
|
-
if (cached !== undefined) {
|
|
637
|
-
return cached;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
// Fetch from database via provider-specific implementation
|
|
641
|
-
const name = await this.InternalGetEntityRecordName(entityName, compositeKey, contextUser);
|
|
642
|
-
if (name) {
|
|
643
|
-
this._entityRecordNameCache.Set(cacheKey, name);
|
|
644
|
-
}
|
|
645
|
-
return name;
|
|
622
|
+
return this.InternalGetEntityRecordName(entityName, compositeKey, contextUser);
|
|
646
623
|
}
|
|
647
624
|
/**
|
|
648
|
-
* Gets display names for multiple entity records in a single operation
|
|
649
|
-
* More efficient than multiple GetEntityRecordName calls.
|
|
625
|
+
* Gets display names for multiple entity records in a single operation.
|
|
650
626
|
* @param info - Array of entity/key pairs to lookup
|
|
651
|
-
* @param contextUser -
|
|
652
|
-
* @param forceRefresh -
|
|
627
|
+
* @param contextUser - The acting user; field- and row-level security are applied for them
|
|
628
|
+
* @param forceRefresh - Bypass any cache. This provider has none, so it always looks up.
|
|
653
629
|
* @returns Array of results with names and status for each requested record
|
|
654
630
|
*/
|
|
655
631
|
async GetEntityRecordNames(info, contextUser, forceRefresh = false) {
|
|
656
|
-
|
|
657
|
-
// Check cache for each item, collect uncached items
|
|
658
|
-
const results = [];
|
|
659
|
-
const uncachedInfo = [];
|
|
660
|
-
const uncachedIndexes = [];
|
|
661
|
-
for (let i = 0; i < info.length; i++) {
|
|
662
|
-
const item = info[i];
|
|
663
|
-
const cacheKey = this.getCacheKey(item.EntityName, item.CompositeKey);
|
|
664
|
-
const cached = this._entityRecordNameCache.Get(cacheKey);
|
|
665
|
-
if (cached !== undefined) {
|
|
666
|
-
// Cache hit
|
|
667
|
-
results[i] = {
|
|
668
|
-
EntityName: item.EntityName,
|
|
669
|
-
CompositeKey: item.CompositeKey,
|
|
670
|
-
Status: 'cached',
|
|
671
|
-
Success: true,
|
|
672
|
-
RecordName: cached
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
else {
|
|
676
|
-
// Cache miss - need to fetch
|
|
677
|
-
uncachedInfo.push(item);
|
|
678
|
-
uncachedIndexes.push(i);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
// Fetch uncached items from database
|
|
682
|
-
if (uncachedInfo.length > 0) {
|
|
683
|
-
const uncachedResults = await this.InternalGetEntityRecordNames(uncachedInfo, contextUser);
|
|
684
|
-
// Merge results and update cache
|
|
685
|
-
for (let i = 0; i < uncachedResults.length; i++) {
|
|
686
|
-
const result = uncachedResults[i];
|
|
687
|
-
const originalIndex = uncachedIndexes[i];
|
|
688
|
-
results[originalIndex] = result;
|
|
689
|
-
// Cache successful results
|
|
690
|
-
if (result.Success && result.RecordName) {
|
|
691
|
-
const cacheKey = this.getCacheKey(result.EntityName, result.CompositeKey);
|
|
692
|
-
this._entityRecordNameCache.Set(cacheKey, result.RecordName);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
return results;
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
// Force refresh - bypass cache entirely
|
|
700
|
-
const results = await this.InternalGetEntityRecordNames(info, contextUser);
|
|
701
|
-
// Update cache with fresh results
|
|
702
|
-
for (const result of results) {
|
|
703
|
-
if (result.Success && result.RecordName) {
|
|
704
|
-
const cacheKey = this.getCacheKey(result.EntityName, result.CompositeKey);
|
|
705
|
-
this._entityRecordNameCache.Set(cacheKey, result.RecordName);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
return results;
|
|
709
|
-
}
|
|
632
|
+
return this.InternalGetEntityRecordNames(info, contextUser);
|
|
710
633
|
}
|
|
711
634
|
// ========================================================================
|
|
712
635
|
// PUBLIC API METHODS - Orchestrate Pre → Cache → Internal → Post flow
|
|
@@ -1777,7 +1700,9 @@ export class ProviderBase {
|
|
|
1777
1700
|
resolvable: true,
|
|
1778
1701
|
// No user (client-side / trusted single-user context) ⇒ nothing to authorize against;
|
|
1779
1702
|
// the gate only consults `authorized` when a contextUser is present.
|
|
1780
|
-
|
|
1703
|
+
// This provider's permissions, not the global provider's: a client connection that is not
|
|
1704
|
+
// the global one may have no global provider at all, or one holding another server's.
|
|
1705
|
+
authorized: !user || qInfo.UserCanRun(user, this),
|
|
1781
1706
|
categoryPath: qInfo.CategoryPath,
|
|
1782
1707
|
queryName: qInfo.Name
|
|
1783
1708
|
};
|
|
@@ -4966,6 +4891,12 @@ export class ProviderBase {
|
|
|
4966
4891
|
const ls = this.LocalStorageProvider;
|
|
4967
4892
|
if (!ls)
|
|
4968
4893
|
return;
|
|
4894
|
+
// Symmetrical with the save: an in-process store can only hand back a copy of the
|
|
4895
|
+
// metadata this heap already holds, at the cost of rebuilding every metadata object.
|
|
4896
|
+
if (!this.MetadataSnapshotPersistenceEnabled) {
|
|
4897
|
+
this.logMetadataSnapshotDisabledOnce();
|
|
4898
|
+
return;
|
|
4899
|
+
}
|
|
4969
4900
|
const overallStart = Date.now();
|
|
4970
4901
|
// The metadata snapshot uses three keys (timestamps, format, AllMetadata).
|
|
4971
4902
|
// Pull them in a single batched read — saves two IDB transaction round-trips
|
|
@@ -5056,6 +4987,18 @@ export class ProviderBase {
|
|
|
5056
4987
|
* Used for compressed metadata storage/retrieval.
|
|
5057
4988
|
*/
|
|
5058
4989
|
static base64ToArrayBuffer(base64) {
|
|
4990
|
+
// Node decodes natively; the atob path below allocates an intermediate string and fills the
|
|
4991
|
+
// array a byte at a time.
|
|
4992
|
+
if (typeof Buffer !== 'undefined') {
|
|
4993
|
+
const buf = Buffer.from(base64, 'base64');
|
|
4994
|
+
// Copy into an exact-size buffer: `buf.buffer` is a view into a shared pool, usually
|
|
4995
|
+
// larger than the payload, so returning it directly would carry unrelated bytes and a
|
|
4996
|
+
// wrong byteLength. It is also typed ArrayBufferLike and will not assign to the
|
|
4997
|
+
// declared return type.
|
|
4998
|
+
const exact = new ArrayBuffer(buf.byteLength);
|
|
4999
|
+
new Uint8Array(exact).set(buf);
|
|
5000
|
+
return exact;
|
|
5001
|
+
}
|
|
5059
5002
|
const binaryString = atob(base64);
|
|
5060
5003
|
const bytes = new Uint8Array(binaryString.length);
|
|
5061
5004
|
for (let i = 0; i < binaryString.length; i++) {
|
|
@@ -5068,6 +5011,11 @@ export class ProviderBase {
|
|
|
5068
5011
|
* Used for compressed metadata storage/retrieval.
|
|
5069
5012
|
*/
|
|
5070
5013
|
static arrayBufferToBase64(buffer) {
|
|
5014
|
+
// Node encodes natively. The loop below concatenates one character per byte, building a rope
|
|
5015
|
+
// the size of the payload that then has to be flattened.
|
|
5016
|
+
if (typeof Buffer !== 'undefined') {
|
|
5017
|
+
return Buffer.from(buffer).toString('base64');
|
|
5018
|
+
}
|
|
5071
5019
|
const bytes = new Uint8Array(buffer);
|
|
5072
5020
|
let binary = '';
|
|
5073
5021
|
for (let i = 0; i < bytes.byteLength; i++) {
|
|
@@ -5083,6 +5031,42 @@ export class ProviderBase {
|
|
|
5083
5031
|
get LocalStoragePrefix() {
|
|
5084
5032
|
return "";
|
|
5085
5033
|
}
|
|
5034
|
+
/**
|
|
5035
|
+
* Whether anything could read the metadata snapshot back, and therefore whether saving it is
|
|
5036
|
+
* worth the cost.
|
|
5037
|
+
*
|
|
5038
|
+
* The snapshot lets a cold process start from a cached copy of the metadata instead of querying
|
|
5039
|
+
* for it, which only works if the store outlives the writer. Where it does not, both halves of
|
|
5040
|
+
* the round trip are pure cost: the save serializes, gzips and base64-encodes the entire
|
|
5041
|
+
* metadata graph, and the load parses it and rebuilds every `EntityInfo` and `EntityFieldInfo`
|
|
5042
|
+
* from a copy of objects the heap already holds. On a large tenant that is expensive enough to
|
|
5043
|
+
* exhaust the heap.
|
|
5044
|
+
*
|
|
5045
|
+
* A provider that does not declare {@link ILocalStorageProvider.SupportsCrossProcessPersistence}
|
|
5046
|
+
* is treated as persistent, which fails in the safer direction.
|
|
5047
|
+
*/
|
|
5048
|
+
get MetadataSnapshotPersistenceEnabled() {
|
|
5049
|
+
const ls = this.LocalStorageProvider;
|
|
5050
|
+
if (!ls) {
|
|
5051
|
+
return false;
|
|
5052
|
+
}
|
|
5053
|
+
return ls.SupportsCrossProcessPersistence !== false;
|
|
5054
|
+
}
|
|
5055
|
+
/**
|
|
5056
|
+
* Explains the skip once per process. The refresh path runs every 30 seconds on a busy server,
|
|
5057
|
+
* so logging per call would bury the one line that matters.
|
|
5058
|
+
*/
|
|
5059
|
+
logMetadataSnapshotDisabledOnce() {
|
|
5060
|
+
if (this._metadataSnapshotSkipLogged) {
|
|
5061
|
+
return;
|
|
5062
|
+
}
|
|
5063
|
+
this._metadataSnapshotSkipLogged = true;
|
|
5064
|
+
const name = this.LocalStorageProvider?.constructor?.name ?? 'the local storage provider';
|
|
5065
|
+
LogStatusEx({
|
|
5066
|
+
message: `[Metadata Cache] Snapshot persistence disabled: ${name} is in-process only, so a saved snapshot could never be read back. Skipping the metadata snapshot save and load.`,
|
|
5067
|
+
verboseOnly: false
|
|
5068
|
+
});
|
|
5069
|
+
}
|
|
5086
5070
|
/**
|
|
5087
5071
|
* Saves current metadata to local storage for caching.
|
|
5088
5072
|
* Serializes both timestamps and full metadata collections.
|
|
@@ -5092,6 +5076,12 @@ export class ProviderBase {
|
|
|
5092
5076
|
const ls = this.LocalStorageProvider;
|
|
5093
5077
|
if (!ls)
|
|
5094
5078
|
return;
|
|
5079
|
+
// Nothing could ever read this snapshot back, so the entire serialize/compress/encode
|
|
5080
|
+
// pass buys nothing. See MetadataSnapshotPersistenceEnabled.
|
|
5081
|
+
if (!this.MetadataSnapshotPersistenceEnabled) {
|
|
5082
|
+
this.logMetadataSnapshotDisabledOnce();
|
|
5083
|
+
return;
|
|
5084
|
+
}
|
|
5095
5085
|
const start = Date.now();
|
|
5096
5086
|
// Serialize the AllMetadata object FIRST. If this throws (or the payload write below
|
|
5097
5087
|
// fails), nothing else has been written yet, so the previously stored timestamps still
|