@memberjunction/core 5.21.0 → 5.23.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/baseEngine.js +1 -1
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +17 -0
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +17 -0
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/interfaces.d.ts +159 -23
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js +1 -2
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +22 -3
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +96 -54
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/metadata.d.ts +32 -1
- package/dist/generic/metadata.d.ts.map +1 -1
- package/dist/generic/metadata.js +37 -0
- package/dist/generic/metadata.js.map +1 -1
- package/dist/generic/providerBase.d.ts +22 -1
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +131 -36
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/scoring/ReciprocalRankFusion.d.ts +45 -0
- package/dist/generic/scoring/ReciprocalRankFusion.d.ts.map +1 -0
- package/dist/generic/scoring/ReciprocalRankFusion.js +63 -0
- package/dist/generic/scoring/ReciprocalRankFusion.js.map +1 -0
- package/dist/generic/telemetryManager.d.ts +29 -2
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +1 -0
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/util.d.ts +46 -0
- package/dist/generic/util.d.ts.map +1 -1
- package/dist/generic/util.js +60 -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 +2 -2
- package/readme.md +6 -0
|
@@ -15,6 +15,7 @@ import { Metadata } from "./metadata.js";
|
|
|
15
15
|
import { RunView } from "../views/runView.js";
|
|
16
16
|
import { IsPlatformSQL } from "./platformSQL.js";
|
|
17
17
|
import { GetDataHooks } from "./dataHooks.js";
|
|
18
|
+
import { TransformSimpleObjectToEntityObject } from "./util.js";
|
|
18
19
|
/**
|
|
19
20
|
* Creates a new instance of AllMetadata from a simple object.
|
|
20
21
|
* Handles deserialization and proper instantiation of all metadata classes.
|
|
@@ -529,10 +530,12 @@ export class ProviderBase {
|
|
|
529
530
|
boundaries.push({ start: allParams.length, count: entry.params.length });
|
|
530
531
|
allParams.push(...entry.params);
|
|
531
532
|
}
|
|
532
|
-
const entityNames = allParams.map(p => p.EntityName || p.ViewName || '?')
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
533
|
+
const entityNames = allParams.map(p => p.EntityName || p.ViewName || '?');
|
|
534
|
+
const eventId = TelemetryManager.Instance.StartEvent('Coalesce', 'ProviderBase.flushCoalesceQueue', {
|
|
535
|
+
CallerCount: queue.length,
|
|
536
|
+
TotalEntityCount: allParams.length,
|
|
537
|
+
Entities: entityNames,
|
|
538
|
+
CallerBoundaries: [...boundaries]
|
|
536
539
|
});
|
|
537
540
|
try {
|
|
538
541
|
// Execute the mega-batch as a single pipeline call
|
|
@@ -543,8 +546,10 @@ export class ProviderBase {
|
|
|
543
546
|
const callerResults = allResults.slice(start, start + count);
|
|
544
547
|
queue[i].resolve(callerResults);
|
|
545
548
|
}
|
|
549
|
+
TelemetryManager.Instance.EndEvent(eventId);
|
|
546
550
|
}
|
|
547
551
|
catch (err) {
|
|
552
|
+
TelemetryManager.Instance.EndEvent(eventId, { success: false, error: String(err) });
|
|
548
553
|
// If the mega-batch fails, reject all callers
|
|
549
554
|
for (const entry of queue) {
|
|
550
555
|
entry.reject(err);
|
|
@@ -555,6 +560,111 @@ export class ProviderBase {
|
|
|
555
560
|
* Runs RunViews without coalescing (used internally after coalescing merge).
|
|
556
561
|
* This goes through dedup + linger + the full pipeline.
|
|
557
562
|
*/
|
|
563
|
+
/**
|
|
564
|
+
* Performs a full-text search across all entities that have FullTextSearchEnabled=true.
|
|
565
|
+
* Uses the existing RunView + UserSearchString infrastructure which routes through
|
|
566
|
+
* the database-native FTS capabilities (SQL Server FREETEXT functions, PostgreSQL tsvector).
|
|
567
|
+
*
|
|
568
|
+
* This is the default implementation that works across all database providers. Each provider's
|
|
569
|
+
* createViewUserSearchSQL() method handles the platform-specific SQL generation.
|
|
570
|
+
*
|
|
571
|
+
* @see /packages/MJCore/docs/FULL_TEXT_SEARCH_GUIDE.md for comprehensive documentation
|
|
572
|
+
*/
|
|
573
|
+
async FullTextSearch(params, contextUser) {
|
|
574
|
+
const startTime = Date.now();
|
|
575
|
+
try {
|
|
576
|
+
const md = new Metadata();
|
|
577
|
+
const maxRows = params.MaxRowsPerEntity ?? 10;
|
|
578
|
+
// Get all FTS-enabled entities
|
|
579
|
+
const ftsEntities = this.resolveFTSEntities(md, params.EntityNames);
|
|
580
|
+
if (ftsEntities.length === 0) {
|
|
581
|
+
return {
|
|
582
|
+
Success: true,
|
|
583
|
+
Results: [],
|
|
584
|
+
TotalCount: 0,
|
|
585
|
+
EntitiesSearched: 0,
|
|
586
|
+
ElapsedMs: Date.now() - startTime
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
// Build RunView params for each FTS entity
|
|
590
|
+
const viewParams = ftsEntities.map(entity => ({
|
|
591
|
+
EntityName: entity.Name,
|
|
592
|
+
UserSearchString: params.SearchText,
|
|
593
|
+
MaxRows: maxRows,
|
|
594
|
+
ResultType: 'simple'
|
|
595
|
+
}));
|
|
596
|
+
// Execute all searches in parallel via RunViews
|
|
597
|
+
const viewResults = await this.RunViews(viewParams, contextUser);
|
|
598
|
+
// Convert results to FullTextSearchResultItems
|
|
599
|
+
const allResults = [];
|
|
600
|
+
for (let i = 0; i < viewResults.length; i++) {
|
|
601
|
+
const result = viewResults[i];
|
|
602
|
+
const entity = ftsEntities[i];
|
|
603
|
+
if (!result.Success)
|
|
604
|
+
continue;
|
|
605
|
+
const titleField = this.findBestField(entity, ['Name', 'Title', 'Subject', 'Label']);
|
|
606
|
+
const snippetField = this.findBestField(entity, ['Description', 'Summary', 'Body', 'Content', 'Text', 'Notes']);
|
|
607
|
+
for (let j = 0; j < result.Results.length; j++) {
|
|
608
|
+
const record = result.Results[j];
|
|
609
|
+
allResults.push({
|
|
610
|
+
EntityName: entity.Name,
|
|
611
|
+
RecordID: String(record[entity.FirstPrimaryKey?.Name ?? 'ID'] ?? ''),
|
|
612
|
+
Title: String(record[titleField] ?? 'Untitled'),
|
|
613
|
+
Snippet: String(record[snippetField] ?? '').substring(0, 200),
|
|
614
|
+
Score: 1.0 / (j + 1) // Rank-based scoring for RRF compatibility
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Sort by score descending
|
|
619
|
+
allResults.sort((a, b) => b.Score - a.Score);
|
|
620
|
+
return {
|
|
621
|
+
Success: true,
|
|
622
|
+
Results: allResults,
|
|
623
|
+
TotalCount: allResults.length,
|
|
624
|
+
EntitiesSearched: ftsEntities.length,
|
|
625
|
+
ElapsedMs: Date.now() - startTime
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
630
|
+
LogError(`FullTextSearch failed: ${msg}`);
|
|
631
|
+
return {
|
|
632
|
+
Success: false,
|
|
633
|
+
ErrorMessage: msg,
|
|
634
|
+
Results: [],
|
|
635
|
+
TotalCount: 0,
|
|
636
|
+
EntitiesSearched: 0,
|
|
637
|
+
ElapsedMs: Date.now() - startTime
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Resolve which entities to search. Filters to only FTS-enabled entities.
|
|
643
|
+
* If entityNames provided, intersects with the FTS-enabled set.
|
|
644
|
+
*/
|
|
645
|
+
resolveFTSEntities(md, entityNames) {
|
|
646
|
+
const allEntities = md.Entities.filter(e => e.FullTextSearchEnabled);
|
|
647
|
+
if (!entityNames || entityNames.length === 0) {
|
|
648
|
+
return allEntities;
|
|
649
|
+
}
|
|
650
|
+
// Intersect requested names with FTS-enabled entities only
|
|
651
|
+
const nameSet = new Set(entityNames.map(n => n.toLowerCase()));
|
|
652
|
+
return allEntities.filter(e => nameSet.has(e.Name.toLowerCase()));
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Find the best display field from an entity by checking preferred field names.
|
|
656
|
+
* Returns the first match or falls back to the first text field.
|
|
657
|
+
*/
|
|
658
|
+
findBestField(entity, preferredNames) {
|
|
659
|
+
for (const name of preferredNames) {
|
|
660
|
+
const field = entity.Fields.find(f => f.Name === name);
|
|
661
|
+
if (field)
|
|
662
|
+
return field.Name;
|
|
663
|
+
}
|
|
664
|
+
// Fallback to first text field
|
|
665
|
+
const textField = entity.Fields.find(f => f.Type.toLowerCase().includes('varchar') || f.Type.toLowerCase().includes('text'));
|
|
666
|
+
return textField?.Name ?? entity.FirstPrimaryKey?.Name ?? 'ID';
|
|
667
|
+
}
|
|
558
668
|
async RunViewsUncoalesced(params, contextUser) {
|
|
559
669
|
const key = this.GenerateDedupKey(params, contextUser);
|
|
560
670
|
const existing = this._inflightViews.get(key);
|
|
@@ -828,7 +938,7 @@ export class ProviderBase {
|
|
|
828
938
|
Success: true,
|
|
829
939
|
Results: cached.results,
|
|
830
940
|
RowCount: cached.results.length,
|
|
831
|
-
TotalRowCount: cached.results.length,
|
|
941
|
+
TotalRowCount: cached.totalRowCount ?? cached.results.length,
|
|
832
942
|
ExecutionTime: 0, // Cached, no execution time
|
|
833
943
|
ErrorMessage: '',
|
|
834
944
|
UserViewRunID: '',
|
|
@@ -956,7 +1066,7 @@ export class ProviderBase {
|
|
|
956
1066
|
Success: true,
|
|
957
1067
|
Results: cached.results,
|
|
958
1068
|
RowCount: cached.results.length,
|
|
959
|
-
TotalRowCount: cached.results.length,
|
|
1069
|
+
TotalRowCount: cached.totalRowCount ?? cached.results.length,
|
|
960
1070
|
ExecutionTime: 0,
|
|
961
1071
|
ErrorMessage: '',
|
|
962
1072
|
UserViewRunID: '',
|
|
@@ -1119,7 +1229,7 @@ export class ProviderBase {
|
|
|
1119
1229
|
Success: true,
|
|
1120
1230
|
Results: cached.results,
|
|
1121
1231
|
RowCount: cached.rowCount,
|
|
1122
|
-
TotalRowCount: cached.rowCount,
|
|
1232
|
+
TotalRowCount: cached.totalRowCount ?? cached.rowCount,
|
|
1123
1233
|
ExecutionTime: 0,
|
|
1124
1234
|
ErrorMessage: '',
|
|
1125
1235
|
UserViewRunID: '',
|
|
@@ -1161,7 +1271,7 @@ export class ProviderBase {
|
|
|
1161
1271
|
Success: true,
|
|
1162
1272
|
Results: merged.results,
|
|
1163
1273
|
RowCount: merged.rowCount,
|
|
1164
|
-
TotalRowCount: merged.rowCount,
|
|
1274
|
+
TotalRowCount: merged.totalRowCount ?? merged.rowCount,
|
|
1165
1275
|
ExecutionTime: 0,
|
|
1166
1276
|
ErrorMessage: '',
|
|
1167
1277
|
UserViewRunID: '',
|
|
@@ -1182,11 +1292,12 @@ export class ProviderBase {
|
|
|
1182
1292
|
}
|
|
1183
1293
|
else if (checkResult.status === 'stale') {
|
|
1184
1294
|
// Cache is stale - use fresh data and update cache (entity doesn't support differential)
|
|
1295
|
+
const staleResults = checkResult.results || [];
|
|
1185
1296
|
const freshResult = {
|
|
1186
1297
|
Success: true,
|
|
1187
|
-
Results:
|
|
1188
|
-
RowCount:
|
|
1189
|
-
TotalRowCount: checkResult.rowCount ||
|
|
1298
|
+
Results: staleResults,
|
|
1299
|
+
RowCount: staleResults.length,
|
|
1300
|
+
TotalRowCount: checkResult.rowCount || staleResults.length,
|
|
1190
1301
|
ExecutionTime: 0,
|
|
1191
1302
|
ErrorMessage: '',
|
|
1192
1303
|
UserViewRunID: '',
|
|
@@ -1197,8 +1308,8 @@ export class ProviderBase {
|
|
|
1197
1308
|
const fingerprint = LocalCacheManager.Instance.GenerateRunViewFingerprint(param, this.InstanceConnectionString);
|
|
1198
1309
|
// Note: We don't await here to avoid blocking the response
|
|
1199
1310
|
// Cache update happens in background
|
|
1200
|
-
LocalCacheManager.Instance.SetRunViewResult(fingerprint, param, checkResult.results || [], checkResult.maxUpdatedAt, checkResult.aggregateResults // Include aggregate results in cache
|
|
1201
|
-
).catch(e => LogError(`Failed to update cache: ${e}`));
|
|
1311
|
+
LocalCacheManager.Instance.SetRunViewResult(fingerprint, param, checkResult.results || [], checkResult.maxUpdatedAt, checkResult.aggregateResults, // Include aggregate results in cache
|
|
1312
|
+
checkResult.rowCount).catch(e => LogError(`Failed to update cache: ${e}`));
|
|
1202
1313
|
}
|
|
1203
1314
|
// Transform to entity objects if needed
|
|
1204
1315
|
await this.TransformSimpleObjectToEntityObject(param, freshResult, contextUser);
|
|
@@ -1287,9 +1398,9 @@ export class ProviderBase {
|
|
|
1287
1398
|
// with circular subscriber references that break JSON.stringify.
|
|
1288
1399
|
// On cache read, TransformSimpleObjectToEntityObject is called to restore
|
|
1289
1400
|
// entity objects when ResultType === 'entity_object'.
|
|
1290
|
-
if (params.CacheLocal && result.Success && preResult.fingerprint && LocalCacheManager.Instance.IsInitialized) {
|
|
1401
|
+
if ((params.CacheLocal || this.TrustLocalCacheCompletely) && result.Success && preResult.fingerprint && LocalCacheManager.Instance.IsInitialized) {
|
|
1291
1402
|
const maxUpdatedAt = this.extractMaxUpdatedAt(result.Results);
|
|
1292
|
-
await LocalCacheManager.Instance.SetRunViewResult(preResult.fingerprint, params, result.Results, maxUpdatedAt, result.AggregateResults);
|
|
1403
|
+
await LocalCacheManager.Instance.SetRunViewResult(preResult.fingerprint, params, result.Results, maxUpdatedAt, result.AggregateResults, result.TotalRowCount);
|
|
1293
1404
|
}
|
|
1294
1405
|
else if (this.shouldAutoCache(params, result)) {
|
|
1295
1406
|
// Server-side auto-cache: small, unfiltered, unsorted results are
|
|
@@ -1297,7 +1408,7 @@ export class ProviderBase {
|
|
|
1297
1408
|
// safe for in-place upsert on entity changes (no filter to evaluate).
|
|
1298
1409
|
const fingerprint = preResult.fingerprint || LocalCacheManager.Instance.GenerateRunViewFingerprint(params, this.InstanceConnectionString);
|
|
1299
1410
|
const maxUpdatedAt = this.extractMaxUpdatedAt(result.Results);
|
|
1300
|
-
await LocalCacheManager.Instance.SetRunViewResult(fingerprint, params, result.Results, maxUpdatedAt, result.AggregateResults);
|
|
1411
|
+
await LocalCacheManager.Instance.SetRunViewResult(fingerprint, params, result.Results, maxUpdatedAt, result.AggregateResults, result.TotalRowCount);
|
|
1301
1412
|
LogStatusEx({ message: ` 📦 [Auto-Cache] RunView "${params.EntityName || params.ViewName || 'unknown'}" — ${result.Results.length} rows auto-cached (small + unfiltered)`, verboseOnly: true });
|
|
1302
1413
|
}
|
|
1303
1414
|
// Transform the result set into BaseEntity-derived objects, if needed
|
|
@@ -1339,13 +1450,13 @@ export class ProviderBase {
|
|
|
1339
1450
|
continue;
|
|
1340
1451
|
}
|
|
1341
1452
|
const fingerprint = LocalCacheManager.Instance.GenerateRunViewFingerprint(params[i], this.InstanceConnectionString);
|
|
1342
|
-
if (params[i].CacheLocal && results[i].Success && LocalCacheManager.Instance.IsInitialized) {
|
|
1453
|
+
if ((params[i].CacheLocal || this.TrustLocalCacheCompletely) && results[i].Success && LocalCacheManager.Instance.IsInitialized) {
|
|
1343
1454
|
const maxUpdatedAt = this.extractMaxUpdatedAt(results[i].Results);
|
|
1344
|
-
cachePromises.push(LocalCacheManager.Instance.SetRunViewResult(fingerprint, params[i], results[i].Results, maxUpdatedAt, results[i].AggregateResults));
|
|
1455
|
+
cachePromises.push(LocalCacheManager.Instance.SetRunViewResult(fingerprint, params[i], results[i].Results, maxUpdatedAt, results[i].AggregateResults, results[i].TotalRowCount));
|
|
1345
1456
|
}
|
|
1346
1457
|
else if (this.shouldAutoCache(params[i], results[i])) {
|
|
1347
1458
|
const maxUpdatedAt = this.extractMaxUpdatedAt(results[i].Results);
|
|
1348
|
-
cachePromises.push(LocalCacheManager.Instance.SetRunViewResult(fingerprint, params[i], results[i].Results, maxUpdatedAt, results[i].AggregateResults));
|
|
1459
|
+
cachePromises.push(LocalCacheManager.Instance.SetRunViewResult(fingerprint, params[i], results[i].Results, maxUpdatedAt, results[i].AggregateResults, results[i].TotalRowCount));
|
|
1349
1460
|
LogStatusEx({ message: ` 📦 [Auto-Cache] RunViews "${params[i].EntityName || params[i].ViewName || 'unknown'}" — ${results[i].Results.length} rows auto-cached (small + unfiltered)`, verboseOnly: true });
|
|
1350
1461
|
}
|
|
1351
1462
|
// Register OnDataChanged callback if provided
|
|
@@ -1677,24 +1788,8 @@ export class ProviderBase {
|
|
|
1677
1788
|
* @param contextUser - The user context for permissions
|
|
1678
1789
|
*/
|
|
1679
1790
|
async TransformSimpleObjectToEntityObject(param, result, contextUser) {
|
|
1680
|
-
// only if needed (e.g. ResultType==='entity_object'), transform the result set into BaseEntity-derived objects
|
|
1681
1791
|
if (param.ResultType === 'entity_object' && result && result.Success && result.Results?.length > 0) {
|
|
1682
|
-
|
|
1683
|
-
// Create entities and load data in parallel for better performance
|
|
1684
|
-
const entityPromises = result.Results.map(async (item) => {
|
|
1685
|
-
if (item instanceof BaseEntity || (typeof item.Save === 'function')) {
|
|
1686
|
-
// the second check is a "duck-typing" check in case we have different runtime
|
|
1687
|
-
// loading sources where the instanceof will fail
|
|
1688
|
-
return item;
|
|
1689
|
-
}
|
|
1690
|
-
else {
|
|
1691
|
-
// not a base entity sub-class already so convert
|
|
1692
|
-
const entity = await this.GetEntityObject(param.EntityName, contextUser);
|
|
1693
|
-
await entity.LoadFromData(item);
|
|
1694
|
-
return entity;
|
|
1695
|
-
}
|
|
1696
|
-
});
|
|
1697
|
-
result.Results = await Promise.all(entityPromises);
|
|
1792
|
+
result.Results = await TransformSimpleObjectToEntityObject(this, param.EntityName, result.Results, contextUser);
|
|
1698
1793
|
}
|
|
1699
1794
|
}
|
|
1700
1795
|
/**
|