@memberjunction/core 5.37.0 → 5.39.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.
- package/dist/generic/RegisterForStartup.d.ts +7 -0
- package/dist/generic/RegisterForStartup.d.ts.map +1 -1
- package/dist/generic/RegisterForStartup.js +26 -17
- package/dist/generic/RegisterForStartup.js.map +1 -1
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +34 -4
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineRegistry.d.ts +90 -0
- package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
- package/dist/generic/baseEngineRegistry.js +100 -0
- package/dist/generic/baseEngineRegistry.js.map +1 -1
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +33 -1
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +36 -1
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +96 -4
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/interfaces.d.ts +141 -0
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js +9 -0
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +3 -7
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/metadata.d.ts +48 -1
- package/dist/generic/metadata.d.ts.map +1 -1
- package/dist/generic/metadata.js +53 -0
- package/dist/generic/metadata.js.map +1 -1
- package/dist/generic/providerBase.d.ts +142 -38
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +309 -51
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +11 -0
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +11 -0
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/queryInfoInterfaces.d.ts +14 -0
- package/dist/generic/queryInfoInterfaces.d.ts.map +1 -1
- package/dist/generic/scoring/ReciprocalRankFusion.d.ts +11 -4
- package/dist/generic/scoring/ReciprocalRankFusion.d.ts.map +1 -1
- package/dist/generic/scoring/ReciprocalRankFusion.js +18 -6
- package/dist/generic/scoring/ReciprocalRankFusion.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +164 -5
- package/dist/generic/QueryCacheManager.d.ts +0 -115
- package/dist/generic/QueryCacheManager.d.ts.map +0 -1
- package/dist/generic/QueryCacheManager.js +0 -329
- package/dist/generic/QueryCacheManager.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseEntity } from "./baseEntity.js";
|
|
2
|
-
import { EntityDocumentTypeInfo, EntityInfo } from "./entityInfo.js";
|
|
2
|
+
import { EntityDocumentTypeInfo, EntityFieldTSType, EntityInfo } from "./entityInfo.js";
|
|
3
3
|
import { AllMetadata } from "./interfaces.js";
|
|
4
|
+
import { ComputeRRF } from "./scoring/ReciprocalRankFusion.js";
|
|
4
5
|
import { LocalCacheManager } from "./localCacheManager.js";
|
|
5
6
|
import { ApplicationInfo } from "../generic/applicationInfo.js";
|
|
6
7
|
import { AuditLogTypeInfo, AuthorizationInfo, AuthorizationRoleInfo, RoleInfo, RowLevelSecurityFilterInfo, UserInfo } from "./securityInfo.js";
|
|
@@ -768,6 +769,255 @@ export class ProviderBase {
|
|
|
768
769
|
});
|
|
769
770
|
return parts.join('||');
|
|
770
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Ranked search over **one** entity's records. See {@link IMetadataProvider.SearchEntity}
|
|
774
|
+
* for the contract and how this differs from {@link EntityByName} /
|
|
775
|
+
* {@link FullTextSearch}.
|
|
776
|
+
*
|
|
777
|
+
* Implementation overview (concrete on `ProviderBase`, used as-is by every
|
|
778
|
+
* server-side provider; `GraphQLDataProvider` overrides to proxy via GQL):
|
|
779
|
+
* 1. Resolve the EntityDocument (by `params.options.entityDocumentId`
|
|
780
|
+
* override or by looking up the active Search-category doc for the entity).
|
|
781
|
+
* 2. In parallel: run the lexical pass (RunView with LIKE filters on the
|
|
782
|
+
* name field + any `IncludeInUserSearchAPI` fields) and the semantic
|
|
783
|
+
* pass (`searchEntitiesSemanticPass`, the protected template method
|
|
784
|
+
* each concrete server provider implements).
|
|
785
|
+
* 3. Fuse via canonical `ComputeRRF()` with optional per-list weights.
|
|
786
|
+
* 4. Permission-filter via a second RunView constrained to the matched
|
|
787
|
+
* record IDs — that pipeline already enforces row-level read perms
|
|
788
|
+
* on this entity, so any rows the user can't read drop out.
|
|
789
|
+
* 5. Slice to topK, apply minScore cutoff, return.
|
|
790
|
+
*/
|
|
791
|
+
async SearchEntity(params) {
|
|
792
|
+
const { entityName, searchText } = params;
|
|
793
|
+
const options = params.options ?? {};
|
|
794
|
+
const entity = this.EntityByName(entityName);
|
|
795
|
+
if (!entity) {
|
|
796
|
+
LogError(`SearchEntity: unknown entity "${entityName}"`);
|
|
797
|
+
return [];
|
|
798
|
+
}
|
|
799
|
+
if (!searchText || !searchText.trim()) {
|
|
800
|
+
return [];
|
|
801
|
+
}
|
|
802
|
+
const mode = options.mode ?? 'hybrid';
|
|
803
|
+
const topK = options.topK ?? 10;
|
|
804
|
+
const minScore = options.minScore ?? 0;
|
|
805
|
+
const overFetch = topK * 2;
|
|
806
|
+
const contextUser = options.contextUser;
|
|
807
|
+
// Resolve EntityDocument when semantic ranking is in play
|
|
808
|
+
let entityDocumentId = null;
|
|
809
|
+
let embeddingAIModelId = null;
|
|
810
|
+
if (mode === 'semantic' || mode === 'hybrid') {
|
|
811
|
+
const resolved = await this.resolveSearchEntityDocument(entity.ID, options.entityDocumentId, contextUser);
|
|
812
|
+
if (resolved) {
|
|
813
|
+
entityDocumentId = resolved.id;
|
|
814
|
+
embeddingAIModelId = resolved.aiModelId;
|
|
815
|
+
}
|
|
816
|
+
if (!entityDocumentId && mode === 'semantic') {
|
|
817
|
+
LogError(`SearchEntity: no active 'Search' EntityDocument for entity "${entityName}"; cannot run semantic-only mode`);
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// Dispatch lexical and semantic in parallel. Wrapping the lexical pass
|
|
822
|
+
// in Promise.resolve adds no real cost today and keeps the shape ready
|
|
823
|
+
// if the lexical implementation ever goes async (FTS, external index).
|
|
824
|
+
const [lexicalRanked, semanticRanked] = await Promise.all([
|
|
825
|
+
mode === 'semantic'
|
|
826
|
+
? Promise.resolve([])
|
|
827
|
+
: this.searchEntitiesLexicalPass(entity, searchText, overFetch, contextUser),
|
|
828
|
+
mode === 'lexical' || !entityDocumentId
|
|
829
|
+
? Promise.resolve([])
|
|
830
|
+
: this.searchEntitiesSemanticPass(entityDocumentId, searchText, overFetch, embeddingAIModelId, contextUser),
|
|
831
|
+
]);
|
|
832
|
+
// Blend
|
|
833
|
+
const blended = this.searchEntitiesBlend(mode, lexicalRanked, semanticRanked, options);
|
|
834
|
+
// Build component-score lookup for the result-shape step
|
|
835
|
+
const lexicalScoreById = new Map();
|
|
836
|
+
for (const c of lexicalRanked)
|
|
837
|
+
lexicalScoreById.set(c.ID, c.Score);
|
|
838
|
+
const semanticScoreById = new Map();
|
|
839
|
+
const semanticDocByRecord = new Map();
|
|
840
|
+
for (const c of semanticRanked) {
|
|
841
|
+
semanticScoreById.set(c.ID, c.Score);
|
|
842
|
+
const erdId = c.Metadata?.['entityRecordDocumentId'];
|
|
843
|
+
if (erdId)
|
|
844
|
+
semanticDocByRecord.set(c.ID, erdId);
|
|
845
|
+
}
|
|
846
|
+
// Permission-filter post hoc via RunView constrained to the matched
|
|
847
|
+
// record IDs — the existing pipeline already enforces row-level read
|
|
848
|
+
// permissions on this entity, so unauthorized rows simply drop out.
|
|
849
|
+
const ids = blended.map(c => c.ID);
|
|
850
|
+
const allowedIds = ids.length > 0
|
|
851
|
+
? await this.searchEntitiesFilterByPermission(entity, ids, contextUser)
|
|
852
|
+
: new Set();
|
|
853
|
+
const results = [];
|
|
854
|
+
for (const cand of blended) {
|
|
855
|
+
if (!allowedIds.has(cand.ID))
|
|
856
|
+
continue;
|
|
857
|
+
if (cand.Score < minScore)
|
|
858
|
+
continue;
|
|
859
|
+
const lex = lexicalScoreById.get(cand.ID);
|
|
860
|
+
const sem = semanticScoreById.get(cand.ID);
|
|
861
|
+
const matchType = (lex != null && sem != null)
|
|
862
|
+
? 'hybrid'
|
|
863
|
+
: (lex != null ? 'lexical' : 'semantic');
|
|
864
|
+
results.push({
|
|
865
|
+
entityRecordDocumentId: semanticDocByRecord.get(cand.ID) ?? null,
|
|
866
|
+
recordId: cand.ID,
|
|
867
|
+
score: cand.Score,
|
|
868
|
+
matchType,
|
|
869
|
+
components: { lexical: lex, semantic: sem },
|
|
870
|
+
});
|
|
871
|
+
if (results.length >= topK)
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
return results;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Batch form of {@link SearchEntity}. Fans the input list out to N
|
|
878
|
+
* independent `SearchEntity` calls via `Promise.all`; result arrays come
|
|
879
|
+
* back aligned by input order (`result[i]` holds the matches for `params[i]`).
|
|
880
|
+
*
|
|
881
|
+
* On the server side, the per-entity passes are independent — running them
|
|
882
|
+
* concurrently is a real wall-clock win when the caller wants results from
|
|
883
|
+
* multiple entities. On the client side, `GraphQLDataProvider` overrides
|
|
884
|
+
* this method to pack the whole batch into a single GraphQL round-trip
|
|
885
|
+
* instead of issuing N parallel HTTP requests.
|
|
886
|
+
*
|
|
887
|
+
* See {@link IMetadataProvider.SearchEntities} for the contract.
|
|
888
|
+
*/
|
|
889
|
+
async SearchEntities(params) {
|
|
890
|
+
if (!params || params.length === 0)
|
|
891
|
+
return [];
|
|
892
|
+
return Promise.all(params.map(p => this.SearchEntity(p)));
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Look up the EntityDocument that backs entity search for `entityID`.
|
|
896
|
+
* Caller supplies an explicit ID override; otherwise we filter for an
|
|
897
|
+
* Active EntityDocument joined to the 'Search' type via the denormalized
|
|
898
|
+
* `Type` column on `vwEntityDocuments` (avoids a SQL subquery and keeps
|
|
899
|
+
* this metadata-layer code provider-agnostic).
|
|
900
|
+
*
|
|
901
|
+
* Returns just the EntityDocument PK and its AIModelID; concrete semantic-pass
|
|
902
|
+
* implementations look up the model from `AIEngine` to recover the driver
|
|
903
|
+
* class and API name (the view does not project them). Threading AIModelID
|
|
904
|
+
* through ensures the query embedding is generated with the *same* model
|
|
905
|
+
* used to build the index — anything else produces garbage cosine scores.
|
|
906
|
+
*/
|
|
907
|
+
async resolveSearchEntityDocument(entityID, explicitId, contextUser) {
|
|
908
|
+
const filter = explicitId
|
|
909
|
+
? `ID='${explicitId.replace(/'/g, "''")}'`
|
|
910
|
+
: `EntityID='${entityID.replace(/'/g, "''")}' AND Status='Active' AND Type='Search'`;
|
|
911
|
+
const r = await this.RunView({
|
|
912
|
+
EntityName: 'MJ: Entity Documents',
|
|
913
|
+
ExtraFilter: filter,
|
|
914
|
+
ResultType: 'simple',
|
|
915
|
+
MaxRows: 1,
|
|
916
|
+
}, contextUser);
|
|
917
|
+
if (!r.Success || (r.Results?.length ?? 0) === 0)
|
|
918
|
+
return null;
|
|
919
|
+
const row = r.Results[0];
|
|
920
|
+
return {
|
|
921
|
+
id: row.ID,
|
|
922
|
+
aiModelId: row.AIModelID ?? null,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Substring/prefix LIKE search against the entity's name field and any
|
|
927
|
+
* string-typed fields flagged `IncludeInUserSearchAPI`. Returns lexical
|
|
928
|
+
* scores in [0,1] blended by best match per row.
|
|
929
|
+
*
|
|
930
|
+
* **Wildcard handling.** SQL Server's `LIKE` treats `%`, `_`, and `[`
|
|
931
|
+
* specially; a user searching for `50%_off` would otherwise match far more
|
|
932
|
+
* than intended. We escape those three characters and declare an explicit
|
|
933
|
+
* `ESCAPE '\\'` so user-supplied text is matched literally.
|
|
934
|
+
*
|
|
935
|
+
* **Field filtering.** Only string-typed fields are searched. Bit/numeric/
|
|
936
|
+
* date fields can be flagged `IncludeInUserSearchAPI` via metadata edit;
|
|
937
|
+
* applying `LIKE` to those would error or implicit-convert in subtle ways.
|
|
938
|
+
*/
|
|
939
|
+
async searchEntitiesLexicalPass(entity, searchText, overFetch, contextUser) {
|
|
940
|
+
const trimmed = searchText.trim();
|
|
941
|
+
if (!trimmed)
|
|
942
|
+
return [];
|
|
943
|
+
// Escape quotes for SQL string literal, then escape LIKE wildcards
|
|
944
|
+
// (%, _, [) with the explicit ESCAPE character we declare below.
|
|
945
|
+
const sanitized = trimmed
|
|
946
|
+
.replace(/'/g, "''")
|
|
947
|
+
.replace(/\\/g, '\\\\')
|
|
948
|
+
.replace(/%/g, '\\%')
|
|
949
|
+
.replace(/_/g, '\\_')
|
|
950
|
+
.replace(/\[/g, '\\[');
|
|
951
|
+
const searchableFields = entity.Fields
|
|
952
|
+
.filter(f => (f.IncludeInUserSearchAPI || f.IsNameField) && f.TSType === EntityFieldTSType.String);
|
|
953
|
+
if (searchableFields.length === 0)
|
|
954
|
+
return [];
|
|
955
|
+
const likeClauses = searchableFields
|
|
956
|
+
.map(f => `${f.Name} LIKE '%${sanitized}%' ESCAPE '\\'`)
|
|
957
|
+
.join(' OR ');
|
|
958
|
+
const r = await this.RunView({
|
|
959
|
+
EntityName: entity.Name,
|
|
960
|
+
ExtraFilter: likeClauses,
|
|
961
|
+
ResultType: 'simple',
|
|
962
|
+
MaxRows: overFetch,
|
|
963
|
+
}, contextUser);
|
|
964
|
+
if (!r.Success)
|
|
965
|
+
return [];
|
|
966
|
+
const lower = trimmed.toLowerCase();
|
|
967
|
+
const nameField = entity.NameField?.Name ?? entity.Fields.find(f => f.IsNameField)?.Name ?? null;
|
|
968
|
+
const out = [];
|
|
969
|
+
for (const row of (r.Results ?? [])) {
|
|
970
|
+
const id = String(row['ID'] ?? '');
|
|
971
|
+
if (!id)
|
|
972
|
+
continue;
|
|
973
|
+
const nameVal = nameField ? String(row[nameField] ?? '').toLowerCase() : '';
|
|
974
|
+
let score = 0.5; // any match (in some searchable field)
|
|
975
|
+
if (nameVal) {
|
|
976
|
+
if (nameVal === lower)
|
|
977
|
+
score = 1.0;
|
|
978
|
+
else if (nameVal.startsWith(lower))
|
|
979
|
+
score = 0.85;
|
|
980
|
+
else if (nameVal.includes(lower))
|
|
981
|
+
score = 0.7;
|
|
982
|
+
}
|
|
983
|
+
out.push({ ID: id, Score: score });
|
|
984
|
+
}
|
|
985
|
+
// ComputeRRF reads order, not magnitude — sort by score so rank 1 = best lexical match
|
|
986
|
+
out.sort((a, b) => b.Score - a.Score);
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
/** Blend lexical + semantic via canonical weighted ComputeRRF. */
|
|
990
|
+
searchEntitiesBlend(mode, lexicalRanked, semanticRanked, options) {
|
|
991
|
+
if (mode === 'lexical')
|
|
992
|
+
return lexicalRanked;
|
|
993
|
+
if (mode === 'semantic')
|
|
994
|
+
return semanticRanked;
|
|
995
|
+
const weights = [
|
|
996
|
+
options.weights?.lexical ?? 1.0,
|
|
997
|
+
options.weights?.semantic ?? 1.0,
|
|
998
|
+
];
|
|
999
|
+
return ComputeRRF([lexicalRanked, semanticRanked], options.rrfK ?? 60, weights);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Run a permission-aware RunView restricted to the matched IDs. Whatever
|
|
1003
|
+
* comes back is what the user is allowed to see. Used as a post-filter
|
|
1004
|
+
* over the fused result set.
|
|
1005
|
+
*/
|
|
1006
|
+
async searchEntitiesFilterByPermission(entity, ids, contextUser) {
|
|
1007
|
+
if (ids.length === 0)
|
|
1008
|
+
return new Set();
|
|
1009
|
+
const escaped = ids.map(id => `'${id.replace(/'/g, "''")}'`).join(',');
|
|
1010
|
+
const r = await this.RunView({
|
|
1011
|
+
EntityName: entity.Name,
|
|
1012
|
+
ExtraFilter: `ID IN (${escaped})`,
|
|
1013
|
+
Fields: ['ID'],
|
|
1014
|
+
ResultType: 'simple',
|
|
1015
|
+
MaxRows: ids.length,
|
|
1016
|
+
}, contextUser);
|
|
1017
|
+
if (!r.Success)
|
|
1018
|
+
return new Set();
|
|
1019
|
+
return new Set((r.Results ?? []).map(row => row.ID));
|
|
1020
|
+
}
|
|
771
1021
|
/**
|
|
772
1022
|
* Returns true if any param in the batch has SaveViewResults set,
|
|
773
1023
|
* which means the call has a side effect (creating UserViewRun records)
|
|
@@ -1273,9 +1523,16 @@ export class ProviderBase {
|
|
|
1273
1523
|
const preResolvedCache = currentFingerprints.length > 0
|
|
1274
1524
|
? await LocalCacheManager.Instance.GetRunViewResults(currentFingerprints)
|
|
1275
1525
|
: new Map();
|
|
1526
|
+
// Pre-build index Map for server results lookup — avoids O(N^2) .find() scans below.
|
|
1527
|
+
const serverResultsByViewIndex = new Map();
|
|
1528
|
+
if (response.results) {
|
|
1529
|
+
for (const r of response.results) {
|
|
1530
|
+
serverResultsByViewIndex.set(r.viewIndex, r);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1276
1533
|
// Process all results in parallel — 'current' entries now read from the
|
|
1277
1534
|
// pre-resolved map instead of issuing their own GetRunViewResult calls.
|
|
1278
|
-
const processingPromises = params.map((param, i) => this.processSingleSmartCacheResult(param, i,
|
|
1535
|
+
const processingPromises = params.map((param, i) => this.processSingleSmartCacheResult(param, i, serverResultsByViewIndex.get(i), preResolvedCache, contextUser));
|
|
1279
1536
|
const processedResults = await Promise.all(processingPromises);
|
|
1280
1537
|
// Aggregate telemetry stats
|
|
1281
1538
|
let cacheHits = 0;
|
|
@@ -1305,8 +1562,7 @@ export class ProviderBase {
|
|
|
1305
1562
|
* 'current' items hit this map instead of issuing per-param IDB reads,
|
|
1306
1563
|
* amortizing IndexedDB transaction overhead across the whole batch.
|
|
1307
1564
|
*/
|
|
1308
|
-
async processSingleSmartCacheResult(param, index,
|
|
1309
|
-
const checkResult = serverResults.find(r => r.viewIndex === index);
|
|
1565
|
+
async processSingleSmartCacheResult(param, index, checkResult, preResolvedCache, contextUser) {
|
|
1310
1566
|
if (!checkResult) {
|
|
1311
1567
|
return {
|
|
1312
1568
|
result: {
|
|
@@ -2204,30 +2460,59 @@ export class ProviderBase {
|
|
|
2204
2460
|
* @param settings - Array of entity settings metadata
|
|
2205
2461
|
* @returns Processed array of EntityInfo instances with all relationships established
|
|
2206
2462
|
*/
|
|
2463
|
+
/**
|
|
2464
|
+
* Groups items into a Map keyed by a NormalizeUUID-transformed value.
|
|
2465
|
+
* Single-pass O(N) helper used to build pre-indexed lookup maps for
|
|
2466
|
+
* metadata post-processing, replacing repeated O(N*M) filter scans.
|
|
2467
|
+
*/
|
|
2468
|
+
groupByNormalizedUUID(items, keyFn) {
|
|
2469
|
+
const map = new Map();
|
|
2470
|
+
for (const item of items) {
|
|
2471
|
+
const key = NormalizeUUID(keyFn(item));
|
|
2472
|
+
let list = map.get(key);
|
|
2473
|
+
if (!list) {
|
|
2474
|
+
list = [];
|
|
2475
|
+
map.set(key, list);
|
|
2476
|
+
}
|
|
2477
|
+
list.push(item);
|
|
2478
|
+
}
|
|
2479
|
+
return map;
|
|
2480
|
+
}
|
|
2207
2481
|
PostProcessEntityMetadata(entities, fields, fieldValues, permissions, relationships, settings, organicKeys, organicKeyRelatedEntities) {
|
|
2208
2482
|
const result = [];
|
|
2209
2483
|
// Sort entities alphabetically by name to ensure deterministic ordering
|
|
2210
2484
|
// This prevents non-deterministic output in CodeGen and other metadata consumers
|
|
2211
2485
|
const sortedEntities = entities.sort((a, b) => a.Name.localeCompare(b.Name));
|
|
2212
|
-
if (fieldValues && fieldValues.length > 0)
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
f.EntityFieldValues =
|
|
2486
|
+
if (fieldValues && fieldValues.length > 0) {
|
|
2487
|
+
const fieldValuesByFieldId = this.groupByNormalizedUUID(fieldValues, fv => fv.EntityFieldID);
|
|
2488
|
+
for (const f of fields) {
|
|
2489
|
+
f.EntityFieldValues = fieldValuesByFieldId.get(NormalizeUUID(f.ID)) || [];
|
|
2216
2490
|
}
|
|
2491
|
+
}
|
|
2217
2492
|
// Link organic key related entities to their parent organic keys
|
|
2218
2493
|
if (organicKeys && organicKeyRelatedEntities && organicKeyRelatedEntities.length > 0) {
|
|
2494
|
+
const okreByOrganicKeyId = this.groupByNormalizedUUID(organicKeyRelatedEntities, okre => okre.EntityOrganicKeyID);
|
|
2219
2495
|
for (const ok of organicKeys) {
|
|
2220
|
-
ok.EntityOrganicKeyRelatedEntities =
|
|
2496
|
+
ok.EntityOrganicKeyRelatedEntities = okreByOrganicKeyId.get(NormalizeUUID(ok.ID)) || [];
|
|
2221
2497
|
}
|
|
2222
2498
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2499
|
+
const fieldsByEntityId = this.groupByNormalizedUUID(fields, f => f.EntityID);
|
|
2500
|
+
const permissionsByEntityId = this.groupByNormalizedUUID(permissions, p => p.EntityID);
|
|
2501
|
+
const relationshipsByEntityId = this.groupByNormalizedUUID(relationships, r => r.EntityID);
|
|
2502
|
+
const settingsByEntityId = this.groupByNormalizedUUID(settings, s => s.EntityID);
|
|
2503
|
+
const activeOrganicKeysByEntityId = organicKeys
|
|
2504
|
+
? this.groupByNormalizedUUID(organicKeys.filter(ok => ok.Status === 'Active'), ok => ok.EntityID)
|
|
2505
|
+
: new Map();
|
|
2506
|
+
for (const e of sortedEntities) {
|
|
2507
|
+
const entityIdKey = NormalizeUUID(e.ID);
|
|
2508
|
+
const entityFields = fieldsByEntityId.get(entityIdKey) || [];
|
|
2509
|
+
e.EntityFields = entityFields.sort((a, b) => a.Sequence - b.Sequence);
|
|
2510
|
+
e.EntityPermissions = permissionsByEntityId.get(entityIdKey) || [];
|
|
2511
|
+
e.EntityRelationships = relationshipsByEntityId.get(entityIdKey) || [];
|
|
2512
|
+
e.EntitySettings = settingsByEntityId.get(entityIdKey) || [];
|
|
2228
2513
|
// Link active organic keys to the entity
|
|
2229
2514
|
if (organicKeys) {
|
|
2230
|
-
e.EntityOrganicKeys =
|
|
2515
|
+
e.EntityOrganicKeys = activeOrganicKeysByEntityId.get(entityIdKey) || [];
|
|
2231
2516
|
}
|
|
2232
2517
|
result.push(new EntityInfo(e));
|
|
2233
2518
|
}
|
|
@@ -2330,66 +2615,39 @@ export class ProviderBase {
|
|
|
2330
2615
|
get AuthorizationRoles() {
|
|
2331
2616
|
return this._localMetadata.AllAuthorizationRoles;
|
|
2332
2617
|
}
|
|
2333
|
-
/**
|
|
2334
|
-
* Gets all saved queries in the system.
|
|
2335
|
-
* @returns Array of QueryInfo objects representing stored queries
|
|
2336
|
-
*/
|
|
2618
|
+
/** @deprecated Use `QueryEngine.Instance.Queries` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2337
2619
|
get Queries() {
|
|
2338
2620
|
return this._localMetadata.AllQueries;
|
|
2339
2621
|
}
|
|
2340
|
-
/**
|
|
2341
|
-
* Gets all query category definitions.
|
|
2342
|
-
* @returns Array of QueryCategoryInfo objects for query organization
|
|
2343
|
-
*/
|
|
2622
|
+
/** @deprecated Use `QueryEngine.Instance.Categories` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2344
2623
|
get QueryCategories() {
|
|
2345
2624
|
return this._localMetadata.AllQueryCategories;
|
|
2346
2625
|
}
|
|
2347
|
-
/**
|
|
2348
|
-
* Gets all query field definitions.
|
|
2349
|
-
* @returns Array of QueryFieldInfo objects defining query result columns
|
|
2350
|
-
*/
|
|
2626
|
+
/** @deprecated Use `QueryEngine.Instance.Fields` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2351
2627
|
get QueryFields() {
|
|
2352
2628
|
return this._localMetadata.AllQueryFields;
|
|
2353
2629
|
}
|
|
2354
|
-
/**
|
|
2355
|
-
* Gets all query permission assignments.
|
|
2356
|
-
* @returns Array of QueryPermissionInfo objects defining query access
|
|
2357
|
-
*/
|
|
2630
|
+
/** @deprecated Use `QueryEngine.Instance.Permissions` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2358
2631
|
get QueryPermissions() {
|
|
2359
2632
|
return this._localMetadata.AllQueryPermissions;
|
|
2360
2633
|
}
|
|
2361
|
-
/**
|
|
2362
|
-
* Gets all query entity associations.
|
|
2363
|
-
* @returns Array of QueryEntityInfo objects linking queries to entities
|
|
2364
|
-
*/
|
|
2634
|
+
/** @deprecated Use `QueryEngine.Instance.QueryEntities` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2365
2635
|
get QueryEntities() {
|
|
2366
2636
|
return this._localMetadata.AllQueryEntities;
|
|
2367
2637
|
}
|
|
2368
|
-
/**
|
|
2369
|
-
* Gets all query parameter definitions.
|
|
2370
|
-
* @returns Array of QueryParameterInfo objects for parameterized queries
|
|
2371
|
-
*/
|
|
2638
|
+
/** @deprecated Use `QueryEngine.Instance.Parameters` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2372
2639
|
get QueryParameters() {
|
|
2373
2640
|
return this._localMetadata.AllQueryParameters;
|
|
2374
2641
|
}
|
|
2375
|
-
/**
|
|
2376
|
-
* Gets all query dependency records tracking composition references between queries.
|
|
2377
|
-
* @returns Array of QueryDependencyInfo objects representing query-to-query dependencies
|
|
2378
|
-
*/
|
|
2642
|
+
/** @deprecated Use `QueryEngine.Instance.Dependencies` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2379
2643
|
get QueryDependencies() {
|
|
2380
2644
|
return this._localMetadata.AllQueryDependencies;
|
|
2381
2645
|
}
|
|
2382
|
-
/**
|
|
2383
|
-
* Gets all SQL dialect definitions.
|
|
2384
|
-
* @returns Array of SQLDialectInfo objects representing supported SQL dialects
|
|
2385
|
-
*/
|
|
2646
|
+
/** @deprecated Use `QueryEngine.Instance.SQLDialects` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2386
2647
|
get SQLDialects() {
|
|
2387
2648
|
return this._localMetadata.AllSQLDialects;
|
|
2388
2649
|
}
|
|
2389
|
-
/**
|
|
2390
|
-
* Gets all query SQL dialect variants.
|
|
2391
|
-
* @returns Array of QuerySQLInfo objects containing dialect-specific SQL for queries
|
|
2392
|
-
*/
|
|
2650
|
+
/** @deprecated Use `QueryEngine.Instance.QuerySQLs` from `@memberjunction/core-entities`. Will be removed in v6.x. */
|
|
2393
2651
|
get QuerySQLs() {
|
|
2394
2652
|
return this._localMetadata.AllQuerySQLs;
|
|
2395
2653
|
}
|