@memberjunction/sqlserver-dataprovider 2.128.0 → 2.129.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/SQLServerDataProvider.d.ts +140 -11
- package/dist/SQLServerDataProvider.d.ts.map +1 -1
- package/dist/SQLServerDataProvider.js +670 -54
- package/dist/SQLServerDataProvider.js.map +1 -1
- package/dist/SQLServerTransactionGroup.d.ts.map +1 -1
- package/dist/SQLServerTransactionGroup.js +9 -8
- package/dist/SQLServerTransactionGroup.js.map +1 -1
- package/dist/UserCache.d.ts +2 -0
- package/dist/UserCache.d.ts.map +1 -1
- package/dist/UserCache.js +7 -0
- package/dist/UserCache.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/package.json +12 -11
|
@@ -51,6 +51,7 @@ const rxjs_1 = require("rxjs");
|
|
|
51
51
|
const SQLServerTransactionGroup_1 = require("./SQLServerTransactionGroup");
|
|
52
52
|
const SqlLogger_js_1 = require("./SqlLogger.js");
|
|
53
53
|
const actions_1 = require("@memberjunction/actions");
|
|
54
|
+
const encryption_1 = require("@memberjunction/encryption");
|
|
54
55
|
const uuid_1 = require("uuid");
|
|
55
56
|
/**
|
|
56
57
|
* Core SQL execution function - handles the actual database query execution
|
|
@@ -610,7 +611,8 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
610
611
|
/**************************************************************************/
|
|
611
612
|
// START ---- IRunQueryProvider
|
|
612
613
|
/**************************************************************************/
|
|
613
|
-
async
|
|
614
|
+
async InternalRunQuery(params, contextUser) {
|
|
615
|
+
// This is the internal implementation - pre/post processing is handled by ProviderBase.RunQuery()
|
|
614
616
|
try {
|
|
615
617
|
// Find and validate query
|
|
616
618
|
const query = await this.findAndValidateQuery(params, contextUser);
|
|
@@ -806,6 +808,231 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
806
808
|
this.queryCache.set(query.ID, parameters, results, cacheConfig);
|
|
807
809
|
(0, core_1.LogStatus)(`Cached results for query ${query.Name} (${query.ID})`);
|
|
808
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* Internal implementation of batch query execution.
|
|
813
|
+
* Runs multiple queries in parallel for efficiency.
|
|
814
|
+
* @param params - Array of query parameters
|
|
815
|
+
* @param contextUser - Optional user context for permissions
|
|
816
|
+
* @returns Array of query results
|
|
817
|
+
*/
|
|
818
|
+
async InternalRunQueries(params, contextUser) {
|
|
819
|
+
// This is the internal implementation - pre/post processing is handled by ProviderBase.RunQueries()
|
|
820
|
+
// Run all queries in parallel
|
|
821
|
+
const promises = params.map((p) => this.InternalRunQuery(p, contextUser));
|
|
822
|
+
return Promise.all(promises);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* RunQueriesWithCacheCheck - Smart cache validation for batch RunQueries.
|
|
826
|
+
* For each query request, if cacheStatus is provided, uses the Query's CacheValidationSQL
|
|
827
|
+
* to check if the cached data is still current by comparing MAX(__mj_UpdatedAt) and COUNT(*)
|
|
828
|
+
* with client's values. Returns 'current' if cache is valid (no data), or 'stale' with fresh data.
|
|
829
|
+
*
|
|
830
|
+
* Queries without CacheValidationSQL configured will return 'no_validation' status with full data.
|
|
831
|
+
*/
|
|
832
|
+
async RunQueriesWithCacheCheck(params, contextUser) {
|
|
833
|
+
try {
|
|
834
|
+
const user = contextUser || this.CurrentUser;
|
|
835
|
+
if (!user) {
|
|
836
|
+
return {
|
|
837
|
+
success: false,
|
|
838
|
+
results: [],
|
|
839
|
+
errorMessage: 'No user context available',
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
// Separate items that need cache check from those that don't
|
|
843
|
+
const itemsNeedingCacheCheck = [];
|
|
844
|
+
const itemsWithoutCacheCheck = [];
|
|
845
|
+
const itemsWithoutValidationSQL = [];
|
|
846
|
+
const errorResults = [];
|
|
847
|
+
// Pre-process all items to resolve query info and validate
|
|
848
|
+
for (let i = 0; i < params.length; i++) {
|
|
849
|
+
const item = params[i];
|
|
850
|
+
// Resolve query info
|
|
851
|
+
const queryInfo = this.resolveQueryInfo(item.params);
|
|
852
|
+
if (!queryInfo) {
|
|
853
|
+
errorResults.push({
|
|
854
|
+
queryIndex: i,
|
|
855
|
+
queryId: item.params.QueryID || '',
|
|
856
|
+
status: 'error',
|
|
857
|
+
errorMessage: `Query not found: ${item.params.QueryID || item.params.QueryName}`,
|
|
858
|
+
});
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
// Check permissions
|
|
862
|
+
if (!queryInfo.UserCanRun(user)) {
|
|
863
|
+
errorResults.push({
|
|
864
|
+
queryIndex: i,
|
|
865
|
+
queryId: queryInfo.ID,
|
|
866
|
+
status: 'error',
|
|
867
|
+
errorMessage: `User does not have permission to run query: ${queryInfo.Name}`,
|
|
868
|
+
});
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (!item.cacheStatus) {
|
|
872
|
+
// No cache status provided - will run full query
|
|
873
|
+
itemsWithoutCacheCheck.push({ index: i, item });
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
// Check if query has CacheValidationSQL
|
|
877
|
+
if (!queryInfo.CacheValidationSQL) {
|
|
878
|
+
// No validation SQL configured - will run full query and return 'no_validation'
|
|
879
|
+
itemsWithoutValidationSQL.push({ index: i, item, queryInfo });
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
itemsNeedingCacheCheck.push({ index: i, item, queryInfo });
|
|
883
|
+
}
|
|
884
|
+
// Execute batched cache status check for all items that need it
|
|
885
|
+
const cacheStatusResults = await this.getBatchedQueryCacheStatus(itemsNeedingCacheCheck, contextUser);
|
|
886
|
+
// Determine which items are current vs stale
|
|
887
|
+
const staleItems = [];
|
|
888
|
+
const currentResults = [];
|
|
889
|
+
for (const { index, item, queryInfo } of itemsNeedingCacheCheck) {
|
|
890
|
+
const serverStatus = cacheStatusResults.get(index);
|
|
891
|
+
if (!serverStatus || !serverStatus.success) {
|
|
892
|
+
errorResults.push({
|
|
893
|
+
queryIndex: index,
|
|
894
|
+
queryId: queryInfo.ID,
|
|
895
|
+
status: 'error',
|
|
896
|
+
errorMessage: serverStatus?.errorMessage || 'Failed to get cache status',
|
|
897
|
+
});
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const isCurrent = this.isCacheCurrent(item.cacheStatus, serverStatus);
|
|
901
|
+
if (isCurrent) {
|
|
902
|
+
currentResults.push({
|
|
903
|
+
queryIndex: index,
|
|
904
|
+
queryId: queryInfo.ID,
|
|
905
|
+
status: 'current',
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
staleItems.push({ index, params: item.params, queryInfo });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// Run full queries in parallel for:
|
|
913
|
+
// 1. Items without cache status (no fingerprint from client)
|
|
914
|
+
// 2. Items without CacheValidationSQL (always return data with 'no_validation' status)
|
|
915
|
+
// 3. Items with stale cache
|
|
916
|
+
const fullQueryPromises = [
|
|
917
|
+
...itemsWithoutCacheCheck.map(({ index, item }) => this.runFullQueryAndReturnForQuery(item.params, index, 'stale', contextUser)),
|
|
918
|
+
...itemsWithoutValidationSQL.map(({ index, item, queryInfo }) => this.runFullQueryAndReturnForQuery(item.params, index, 'no_validation', contextUser, queryInfo.ID)),
|
|
919
|
+
...staleItems.map(({ index, params: queryParams, queryInfo }) => this.runFullQueryAndReturnForQuery(queryParams, index, 'stale', contextUser, queryInfo.ID)),
|
|
920
|
+
];
|
|
921
|
+
const fullQueryResults = await Promise.all(fullQueryPromises);
|
|
922
|
+
// Combine all results and sort by queryIndex
|
|
923
|
+
const allResults = [...errorResults, ...currentResults, ...fullQueryResults];
|
|
924
|
+
allResults.sort((a, b) => a.queryIndex - b.queryIndex);
|
|
925
|
+
return {
|
|
926
|
+
success: true,
|
|
927
|
+
results: allResults,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
catch (e) {
|
|
931
|
+
(0, core_1.LogError)(e);
|
|
932
|
+
return {
|
|
933
|
+
success: false,
|
|
934
|
+
results: [],
|
|
935
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Resolves QueryInfo from RunQueryParams (by ID or Name+CategoryPath).
|
|
941
|
+
*/
|
|
942
|
+
resolveQueryInfo(params) {
|
|
943
|
+
if (params.QueryID) {
|
|
944
|
+
return this.Queries.find((q) => q.ID === params.QueryID);
|
|
945
|
+
}
|
|
946
|
+
if (params.QueryName) {
|
|
947
|
+
// Match by name and optional category path
|
|
948
|
+
const matchingQueries = this.Queries.filter((q) => q.Name.trim().toLowerCase() === params.QueryName?.trim().toLowerCase());
|
|
949
|
+
if (matchingQueries.length === 0)
|
|
950
|
+
return undefined;
|
|
951
|
+
if (matchingQueries.length === 1)
|
|
952
|
+
return matchingQueries[0];
|
|
953
|
+
// Multiple matches - use CategoryPath or CategoryID to disambiguate
|
|
954
|
+
if (params.CategoryPath) {
|
|
955
|
+
const byPath = matchingQueries.find((q) => q.CategoryPath.toLowerCase() === params.CategoryPath?.toLowerCase());
|
|
956
|
+
if (byPath)
|
|
957
|
+
return byPath;
|
|
958
|
+
}
|
|
959
|
+
if (params.CategoryID) {
|
|
960
|
+
const byId = matchingQueries.find((q) => q.CategoryID === params.CategoryID);
|
|
961
|
+
if (byId)
|
|
962
|
+
return byId;
|
|
963
|
+
}
|
|
964
|
+
// Return first match if no category disambiguation
|
|
965
|
+
return matchingQueries[0];
|
|
966
|
+
}
|
|
967
|
+
return undefined;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Executes a batched cache status check for multiple queries using their CacheValidationSQL.
|
|
971
|
+
*/
|
|
972
|
+
async getBatchedQueryCacheStatus(items, contextUser) {
|
|
973
|
+
const results = new Map();
|
|
974
|
+
if (items.length === 0) {
|
|
975
|
+
return results;
|
|
976
|
+
}
|
|
977
|
+
// Build array of SQL statements for batch execution
|
|
978
|
+
const sqlStatements = [];
|
|
979
|
+
for (const { queryInfo } of items) {
|
|
980
|
+
// CacheValidationSQL should return MaxUpdatedAt and RowCount
|
|
981
|
+
sqlStatements.push(queryInfo.CacheValidationSQL);
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
// Execute the batched SQL
|
|
985
|
+
const resultSets = await this.ExecuteSQLBatch(sqlStatements, undefined, undefined, contextUser);
|
|
986
|
+
// Process each result set and map to the corresponding item index
|
|
987
|
+
for (let i = 0; i < items.length; i++) {
|
|
988
|
+
const { index } = items[i];
|
|
989
|
+
const resultSet = resultSets[i];
|
|
990
|
+
if (resultSet && resultSet.length > 0) {
|
|
991
|
+
const row = resultSet[0];
|
|
992
|
+
results.set(index, {
|
|
993
|
+
success: true,
|
|
994
|
+
rowCount: row.RowCount,
|
|
995
|
+
maxUpdatedAt: row.MaxUpdatedAt ? new Date(row.MaxUpdatedAt).toISOString() : undefined,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
results.set(index, { success: true, rowCount: 0, maxUpdatedAt: undefined });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
catch (e) {
|
|
1004
|
+
// If batch fails, mark all items as failed
|
|
1005
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1006
|
+
for (const { index } of items) {
|
|
1007
|
+
results.set(index, { success: false, errorMessage });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return results;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Runs the full query and returns results with cache metadata.
|
|
1014
|
+
*/
|
|
1015
|
+
async runFullQueryAndReturnForQuery(params, queryIndex, status, contextUser, queryId) {
|
|
1016
|
+
const result = await this.InternalRunQuery(params, contextUser);
|
|
1017
|
+
if (!result.Success) {
|
|
1018
|
+
return {
|
|
1019
|
+
queryIndex,
|
|
1020
|
+
queryId: queryId || result.QueryID || '',
|
|
1021
|
+
status: 'error',
|
|
1022
|
+
errorMessage: result.ErrorMessage || 'Unknown error executing query',
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
// Extract maxUpdatedAt from results
|
|
1026
|
+
const maxUpdatedAt = this.extractMaxUpdatedAt(result.Results);
|
|
1027
|
+
return {
|
|
1028
|
+
queryIndex,
|
|
1029
|
+
queryId: result.QueryID,
|
|
1030
|
+
status,
|
|
1031
|
+
results: result.Results,
|
|
1032
|
+
maxUpdatedAt,
|
|
1033
|
+
rowCount: result.Results.length,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
809
1036
|
/**************************************************************************/
|
|
810
1037
|
// END ---- IRunQueryProvider
|
|
811
1038
|
/**************************************************************************/
|
|
@@ -872,10 +1099,8 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
872
1099
|
/**************************************************************************/
|
|
873
1100
|
// START ---- IRunViewProvider
|
|
874
1101
|
/**************************************************************************/
|
|
875
|
-
async
|
|
876
|
-
//
|
|
877
|
-
// is handled in ProviderBase when sub-classes like this one invoke the pre/post process properly
|
|
878
|
-
await this.PreProcessRunView(params, contextUser);
|
|
1102
|
+
async InternalRunView(params, contextUser) {
|
|
1103
|
+
// This is the internal implementation - pre/post processing is handled by ProviderBase.RunView()
|
|
879
1104
|
const startTime = new Date();
|
|
880
1105
|
try {
|
|
881
1106
|
if (params) {
|
|
@@ -1078,9 +1303,6 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1078
1303
|
Success: true,
|
|
1079
1304
|
ErrorMessage: null,
|
|
1080
1305
|
};
|
|
1081
|
-
// add call to post-processor that was previously in the @memberjunction/core RunView class but now
|
|
1082
|
-
// is handled in ProviderBase when sub-classes like this one invoke the post process properly
|
|
1083
|
-
await this.PostProcessRunView(result, params, contextUser);
|
|
1084
1306
|
return result;
|
|
1085
1307
|
}
|
|
1086
1308
|
else {
|
|
@@ -1101,16 +1323,269 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1101
1323
|
};
|
|
1102
1324
|
}
|
|
1103
1325
|
}
|
|
1104
|
-
async
|
|
1105
|
-
//
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const promises = params.map((p) => this.RunView(p, contextUser));
|
|
1326
|
+
async InternalRunViews(params, contextUser) {
|
|
1327
|
+
// This is the internal implementation - pre/post processing is handled by ProviderBase.RunViews()
|
|
1328
|
+
// Note: We call InternalRunView directly since we're already inside the internal flow
|
|
1329
|
+
const promises = params.map((p) => this.InternalRunView(p, contextUser));
|
|
1109
1330
|
const results = await Promise.all(promises);
|
|
1110
|
-
// post-process in base class
|
|
1111
|
-
await this.PostProcessRunViews(results, params, contextUser);
|
|
1112
1331
|
return results;
|
|
1113
1332
|
}
|
|
1333
|
+
/**
|
|
1334
|
+
* RunViewsWithCacheCheck - Smart cache validation for batch RunViews.
|
|
1335
|
+
* For each view request, if cacheStatus is provided, first checks if the cache is current
|
|
1336
|
+
* by comparing MAX(__mj_UpdatedAt) and COUNT(*) with client's values.
|
|
1337
|
+
* Returns 'current' if cache is valid (no data), or 'stale' with fresh data if cache is outdated.
|
|
1338
|
+
*
|
|
1339
|
+
* Optimized to batch all cache status checks into a single SQL call with multiple result sets.
|
|
1340
|
+
*/
|
|
1341
|
+
async RunViewsWithCacheCheck(params, contextUser) {
|
|
1342
|
+
try {
|
|
1343
|
+
const user = contextUser || this.CurrentUser;
|
|
1344
|
+
if (!user) {
|
|
1345
|
+
return {
|
|
1346
|
+
success: false,
|
|
1347
|
+
results: [],
|
|
1348
|
+
errorMessage: 'No user context available',
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
// Separate items that need cache check from those that don't
|
|
1352
|
+
const itemsNeedingCacheCheck = [];
|
|
1353
|
+
const itemsWithoutCacheCheck = [];
|
|
1354
|
+
const errorResults = [];
|
|
1355
|
+
// Pre-process all items to build WHERE clauses and validate
|
|
1356
|
+
for (let i = 0; i < params.length; i++) {
|
|
1357
|
+
const item = params[i];
|
|
1358
|
+
if (!item.cacheStatus) {
|
|
1359
|
+
// No cache status - will run full query
|
|
1360
|
+
itemsWithoutCacheCheck.push({ index: i, item });
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
// Get entity info
|
|
1364
|
+
const entityInfo = this.Entities.find((e) => e.Name.trim().toLowerCase() === item.params.EntityName?.trim().toLowerCase());
|
|
1365
|
+
if (!entityInfo) {
|
|
1366
|
+
errorResults.push({
|
|
1367
|
+
viewIndex: i,
|
|
1368
|
+
status: 'error',
|
|
1369
|
+
errorMessage: `Entity ${item.params.EntityName} not found in metadata`,
|
|
1370
|
+
});
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
// Check permissions
|
|
1375
|
+
this.CheckUserReadPermissions(entityInfo.Name, user);
|
|
1376
|
+
// Build WHERE clause
|
|
1377
|
+
const whereSQL = await this.buildWhereClauseForCacheCheck(item.params, entityInfo, user);
|
|
1378
|
+
itemsNeedingCacheCheck.push({ index: i, item, entityInfo, whereSQL });
|
|
1379
|
+
}
|
|
1380
|
+
catch (e) {
|
|
1381
|
+
errorResults.push({
|
|
1382
|
+
viewIndex: i,
|
|
1383
|
+
status: 'error',
|
|
1384
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// Execute batched cache status check for all items that need it
|
|
1389
|
+
const cacheStatusResults = await this.getBatchedServerCacheStatus(itemsNeedingCacheCheck, contextUser);
|
|
1390
|
+
// Determine which items are current vs stale
|
|
1391
|
+
const staleItems = [];
|
|
1392
|
+
const currentResults = [];
|
|
1393
|
+
for (const { index, item } of itemsNeedingCacheCheck) {
|
|
1394
|
+
const serverStatus = cacheStatusResults.get(index);
|
|
1395
|
+
if (!serverStatus || !serverStatus.success) {
|
|
1396
|
+
errorResults.push({
|
|
1397
|
+
viewIndex: index,
|
|
1398
|
+
status: 'error',
|
|
1399
|
+
errorMessage: serverStatus?.errorMessage || 'Failed to get cache status',
|
|
1400
|
+
});
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const isCurrent = this.isCacheCurrent(item.cacheStatus, serverStatus);
|
|
1404
|
+
if (isCurrent) {
|
|
1405
|
+
currentResults.push({
|
|
1406
|
+
viewIndex: index,
|
|
1407
|
+
status: 'current',
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
staleItems.push({ index, params: item.params });
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
// Run full queries in parallel for:
|
|
1415
|
+
// 1. Items without cache status (no fingerprint from client)
|
|
1416
|
+
// 2. Items with stale cache
|
|
1417
|
+
const fullQueryPromises = [
|
|
1418
|
+
...itemsWithoutCacheCheck.map(({ index, item }) => this.runFullQueryAndReturn(item.params, index, contextUser)),
|
|
1419
|
+
...staleItems.map(({ index, params: viewParams }) => this.runFullQueryAndReturn(viewParams, index, contextUser)),
|
|
1420
|
+
];
|
|
1421
|
+
const fullQueryResults = await Promise.all(fullQueryPromises);
|
|
1422
|
+
// Combine all results and sort by viewIndex
|
|
1423
|
+
const allResults = [...errorResults, ...currentResults, ...fullQueryResults];
|
|
1424
|
+
allResults.sort((a, b) => a.viewIndex - b.viewIndex);
|
|
1425
|
+
return {
|
|
1426
|
+
success: true,
|
|
1427
|
+
results: allResults,
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
catch (e) {
|
|
1431
|
+
(0, core_1.LogError)(e);
|
|
1432
|
+
return {
|
|
1433
|
+
success: false,
|
|
1434
|
+
results: [],
|
|
1435
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Executes a batched cache status check for multiple views in a single SQL call.
|
|
1441
|
+
* Uses multiple result sets to return status for each view efficiently.
|
|
1442
|
+
*/
|
|
1443
|
+
async getBatchedServerCacheStatus(items, contextUser) {
|
|
1444
|
+
const results = new Map();
|
|
1445
|
+
if (items.length === 0) {
|
|
1446
|
+
return results;
|
|
1447
|
+
}
|
|
1448
|
+
// Build array of SQL statements for batch execution
|
|
1449
|
+
const sqlStatements = [];
|
|
1450
|
+
for (const { entityInfo, whereSQL } of items) {
|
|
1451
|
+
const statusSQL = `SELECT COUNT(*) AS TotalRows, MAX(__mj_UpdatedAt) AS MaxUpdatedAt FROM [${entityInfo.SchemaName}].${entityInfo.BaseView}${whereSQL ? ' WHERE ' + whereSQL : ''}`;
|
|
1452
|
+
sqlStatements.push(statusSQL);
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
// Execute the batched SQL using existing ExecuteSQLBatch method
|
|
1456
|
+
const resultSets = await this.ExecuteSQLBatch(sqlStatements, undefined, undefined, contextUser);
|
|
1457
|
+
// Process each result set and map to the corresponding item index
|
|
1458
|
+
for (let i = 0; i < items.length; i++) {
|
|
1459
|
+
const { index } = items[i];
|
|
1460
|
+
const resultSet = resultSets[i];
|
|
1461
|
+
if (resultSet && resultSet.length > 0) {
|
|
1462
|
+
const row = resultSet[0];
|
|
1463
|
+
results.set(index, {
|
|
1464
|
+
success: true,
|
|
1465
|
+
rowCount: row.TotalRows,
|
|
1466
|
+
maxUpdatedAt: row.MaxUpdatedAt ? new Date(row.MaxUpdatedAt).toISOString() : undefined,
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
results.set(index, { success: true, rowCount: 0, maxUpdatedAt: undefined });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
catch (e) {
|
|
1475
|
+
// If batch fails, mark all items as failed
|
|
1476
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1477
|
+
for (const { index } of items) {
|
|
1478
|
+
results.set(index, { success: false, errorMessage });
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return results;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Builds the WHERE clause for cache status check, using same logic as InternalRunView.
|
|
1485
|
+
*/
|
|
1486
|
+
async buildWhereClauseForCacheCheck(params, entityInfo, user) {
|
|
1487
|
+
let whereSQL = '';
|
|
1488
|
+
let bHasWhere = false;
|
|
1489
|
+
// Extra filter
|
|
1490
|
+
if (params.ExtraFilter && params.ExtraFilter.length > 0) {
|
|
1491
|
+
if (!this.validateUserProvidedSQLClause(params.ExtraFilter)) {
|
|
1492
|
+
throw new Error(`Invalid Extra Filter: ${params.ExtraFilter}`);
|
|
1493
|
+
}
|
|
1494
|
+
whereSQL = `(${params.ExtraFilter})`;
|
|
1495
|
+
bHasWhere = true;
|
|
1496
|
+
}
|
|
1497
|
+
// User search string
|
|
1498
|
+
if (params.UserSearchString && params.UserSearchString.length > 0) {
|
|
1499
|
+
if (!this.validateUserProvidedSQLClause(params.UserSearchString)) {
|
|
1500
|
+
throw new Error(`Invalid User Search SQL clause: ${params.UserSearchString}`);
|
|
1501
|
+
}
|
|
1502
|
+
const sUserSearchSQL = this.createViewUserSearchSQL(entityInfo, params.UserSearchString);
|
|
1503
|
+
if (sUserSearchSQL.length > 0) {
|
|
1504
|
+
if (bHasWhere) {
|
|
1505
|
+
whereSQL += ` AND (${sUserSearchSQL})`;
|
|
1506
|
+
}
|
|
1507
|
+
else {
|
|
1508
|
+
whereSQL = `(${sUserSearchSQL})`;
|
|
1509
|
+
bHasWhere = true;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
// Row Level Security
|
|
1514
|
+
if (!entityInfo.UserExemptFromRowLevelSecurity(user, core_1.EntityPermissionType.Read)) {
|
|
1515
|
+
const rlsWhereClause = entityInfo.GetUserRowLevelSecurityWhereClause(user, core_1.EntityPermissionType.Read, '');
|
|
1516
|
+
if (rlsWhereClause && rlsWhereClause.length > 0) {
|
|
1517
|
+
if (bHasWhere) {
|
|
1518
|
+
whereSQL += ` AND (${rlsWhereClause})`;
|
|
1519
|
+
}
|
|
1520
|
+
else {
|
|
1521
|
+
whereSQL = `(${rlsWhereClause})`;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return whereSQL;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Compares client cache status with server status to determine if cache is current.
|
|
1529
|
+
*/
|
|
1530
|
+
isCacheCurrent(clientStatus, serverStatus) {
|
|
1531
|
+
// Row count must match
|
|
1532
|
+
if (clientStatus.rowCount !== serverStatus.rowCount) {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
// Compare maxUpdatedAt dates
|
|
1536
|
+
const clientDate = new Date(clientStatus.maxUpdatedAt);
|
|
1537
|
+
const serverDate = serverStatus.maxUpdatedAt ? new Date(serverStatus.maxUpdatedAt) : null;
|
|
1538
|
+
if (!serverDate) {
|
|
1539
|
+
// No records on server, so if client has any, it's stale
|
|
1540
|
+
return clientStatus.rowCount === 0;
|
|
1541
|
+
}
|
|
1542
|
+
// Dates must match (compare as ISO strings for precision)
|
|
1543
|
+
return clientDate.toISOString() === serverDate.toISOString();
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Runs the full query and returns results with cache metadata.
|
|
1547
|
+
*/
|
|
1548
|
+
async runFullQueryAndReturn(params, viewIndex, contextUser) {
|
|
1549
|
+
const result = await this.InternalRunView(params, contextUser);
|
|
1550
|
+
if (!result.Success) {
|
|
1551
|
+
return {
|
|
1552
|
+
viewIndex,
|
|
1553
|
+
status: 'error',
|
|
1554
|
+
errorMessage: result.ErrorMessage || 'Unknown error executing view',
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
// Extract maxUpdatedAt from results
|
|
1558
|
+
const maxUpdatedAt = this.extractMaxUpdatedAt(result.Results);
|
|
1559
|
+
return {
|
|
1560
|
+
viewIndex,
|
|
1561
|
+
status: 'stale',
|
|
1562
|
+
results: result.Results,
|
|
1563
|
+
maxUpdatedAt,
|
|
1564
|
+
rowCount: result.Results.length,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Extracts the maximum __mj_UpdatedAt value from a result set.
|
|
1569
|
+
* @param results - Array of result objects that may contain __mj_UpdatedAt
|
|
1570
|
+
* @returns ISO string of the max timestamp, or current time if none found
|
|
1571
|
+
*/
|
|
1572
|
+
extractMaxUpdatedAt(results) {
|
|
1573
|
+
if (!results || results.length === 0) {
|
|
1574
|
+
return new Date().toISOString();
|
|
1575
|
+
}
|
|
1576
|
+
let maxDate = null;
|
|
1577
|
+
for (const row of results) {
|
|
1578
|
+
const rowObj = row;
|
|
1579
|
+
const updatedAt = rowObj['__mj_UpdatedAt'];
|
|
1580
|
+
if (updatedAt) {
|
|
1581
|
+
const date = new Date(updatedAt);
|
|
1582
|
+
if (!isNaN(date.getTime()) && (!maxDate || date > maxDate)) {
|
|
1583
|
+
maxDate = date;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return maxDate ? maxDate.toISOString() : new Date().toISOString();
|
|
1588
|
+
}
|
|
1114
1589
|
validateUserProvidedSQLClause(clause) {
|
|
1115
1590
|
// First, remove all string literals from the clause to avoid false positives
|
|
1116
1591
|
// This regex matches both single and double quoted strings, handling escaped quotes
|
|
@@ -1726,20 +2201,38 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1726
2201
|
}
|
|
1727
2202
|
}
|
|
1728
2203
|
/**
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
2204
|
+
* Generates the SQL Statement that will Save a record to the database.
|
|
2205
|
+
*
|
|
2206
|
+
* This method is used by the Save() method of this class, but it is marked as public because
|
|
2207
|
+
* it is also used by the SQLServerTransactionGroup to regenerate Save SQL if any values were
|
|
2208
|
+
* changed by the transaction group due to transaction variables being set into the object.
|
|
2209
|
+
*
|
|
2210
|
+
* @param entity - The entity to generate save SQL for
|
|
2211
|
+
* @param bNewRecord - Whether this is a new record (create) or existing record (update)
|
|
2212
|
+
* @param spName - The stored procedure name to call
|
|
2213
|
+
* @param user - The user context for the operation
|
|
2214
|
+
* @returns The full SQL statement for the save operation
|
|
2215
|
+
*
|
|
2216
|
+
* @security This method handles field-level encryption transparently.
|
|
2217
|
+
* Fields marked with Encrypt=true will have their values encrypted
|
|
2218
|
+
* before being included in the SQL statement.
|
|
1731
2219
|
*/
|
|
1732
|
-
GetSaveSQL(entity, bNewRecord, spName, user) {
|
|
1733
|
-
const result = this.GetSaveSQLWithDetails(entity, bNewRecord, spName, user);
|
|
2220
|
+
async GetSaveSQL(entity, bNewRecord, spName, user) {
|
|
2221
|
+
const result = await this.GetSaveSQLWithDetails(entity, bNewRecord, spName, user);
|
|
1734
2222
|
return result.fullSQL;
|
|
1735
2223
|
}
|
|
1736
2224
|
/**
|
|
1737
2225
|
* This function generates both the full SQL (with record change metadata) and the simple stored procedure call
|
|
1738
2226
|
* @returns Object with fullSQL and simpleSQL properties
|
|
2227
|
+
*
|
|
2228
|
+
* @security This method handles field-level encryption transparently.
|
|
2229
|
+
* Fields marked with Encrypt=true will have their values encrypted
|
|
2230
|
+
* before being included in the SQL statement.
|
|
1739
2231
|
*/
|
|
1740
|
-
GetSaveSQLWithDetails(entity, bNewRecord, spName, user) {
|
|
2232
|
+
async GetSaveSQLWithDetails(entity, bNewRecord, spName, user) {
|
|
1741
2233
|
// Generate the stored procedure parameters - now returns an object with structured SQL
|
|
1742
|
-
|
|
2234
|
+
// This is async because it may need to encrypt field values
|
|
2235
|
+
const spParams = await this.generateSPParams(entity, !bNewRecord, user);
|
|
1743
2236
|
// Build the simple SQL - use the new DECLARE/SET/EXEC pattern
|
|
1744
2237
|
let sSimpleSQL;
|
|
1745
2238
|
const execSQL = `EXEC [${entity.EntityInfo.SchemaName}].${spName} ${spParams.execParams}`;
|
|
@@ -1978,7 +2471,9 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1978
2471
|
// but they are supported (for now)
|
|
1979
2472
|
await this.HandleEntityAIActions(entity, 'save', true, user);
|
|
1980
2473
|
}
|
|
1981
|
-
|
|
2474
|
+
// Generate the SQL for the save operation
|
|
2475
|
+
// This is async because it may need to encrypt field values
|
|
2476
|
+
const sqlDetails = await this.GetSaveSQLWithDetails(entity, bNewRecord, spName, user);
|
|
1982
2477
|
const sSQL = sqlDetails.fullSQL;
|
|
1983
2478
|
if (entity.TransactionGroup && !bReplay /*we never participate in a transaction if we're in replay mode*/) {
|
|
1984
2479
|
// we have a transaction group, need to play nice and be part of it
|
|
@@ -2033,7 +2528,8 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2033
2528
|
description: `Save ${entity.EntityInfo.Name}`,
|
|
2034
2529
|
simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined
|
|
2035
2530
|
}, user);
|
|
2036
|
-
|
|
2531
|
+
// Process rows with user context for decryption
|
|
2532
|
+
result = await this.ProcessEntityRows(rawResult, entity.EntityInfo, user);
|
|
2037
2533
|
}
|
|
2038
2534
|
catch (e) {
|
|
2039
2535
|
throw e; // rethrow
|
|
@@ -2110,7 +2606,23 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2110
2606
|
}
|
|
2111
2607
|
return sRet;
|
|
2112
2608
|
}
|
|
2113
|
-
|
|
2609
|
+
/**
|
|
2610
|
+
* Generates the stored procedure parameters for a save operation.
|
|
2611
|
+
*
|
|
2612
|
+
* This method handles:
|
|
2613
|
+
* - Value type conversions (datetimeoffset, uniqueidentifier, etc.)
|
|
2614
|
+
* - Field-level encryption for fields marked with Encrypt=true
|
|
2615
|
+
* - Primary key handling for create/update operations
|
|
2616
|
+
*
|
|
2617
|
+
* @param entity - The entity being saved
|
|
2618
|
+
* @param isUpdate - Whether this is an update (true) or create (false) operation
|
|
2619
|
+
* @param contextUser - The user context for encryption operations
|
|
2620
|
+
* @returns An object containing the SQL components for the stored procedure call
|
|
2621
|
+
*
|
|
2622
|
+
* @security Fields with Encrypt=true are encrypted before being sent to the database.
|
|
2623
|
+
* Encryption uses the key specified in EncryptionKeyID.
|
|
2624
|
+
*/
|
|
2625
|
+
async generateSPParams(entity, isUpdate, contextUser) {
|
|
2114
2626
|
// Generate a unique suffix for variable names to avoid collisions in batch scripts
|
|
2115
2627
|
const uniqueSuffix = '_' + (0, uuid_1.v4)().substring(0, 8).replace(/-/g, '');
|
|
2116
2628
|
const declarations = [];
|
|
@@ -2118,6 +2630,8 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2118
2630
|
const execParams = [];
|
|
2119
2631
|
let simpleParams = '';
|
|
2120
2632
|
let bFirst = true;
|
|
2633
|
+
// Get the encryption engine instance (lazy - only used if needed)
|
|
2634
|
+
let encryptionEngine = null;
|
|
2121
2635
|
for (let i = 0; i < entity.EntityInfo.Fields.length; i++) {
|
|
2122
2636
|
const f = entity.EntityInfo.Fields[i];
|
|
2123
2637
|
// For CREATE operations, include primary keys that are not auto-increment and have actual values
|
|
@@ -2148,6 +2662,36 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2148
2662
|
continue; // skip this field entirely by going to the next iteration of the loop
|
|
2149
2663
|
}
|
|
2150
2664
|
}
|
|
2665
|
+
// ========================================================================
|
|
2666
|
+
// FIELD-LEVEL ENCRYPTION
|
|
2667
|
+
// If the field is marked for encryption and has a non-null value,
|
|
2668
|
+
// encrypt it before storing in the database.
|
|
2669
|
+
// ========================================================================
|
|
2670
|
+
if (f.Encrypt && f.EncryptionKeyID && value !== null && value !== undefined) {
|
|
2671
|
+
// Lazy-load encryption engine only when needed
|
|
2672
|
+
if (!encryptionEngine) {
|
|
2673
|
+
encryptionEngine = encryption_1.EncryptionEngine.Instance;
|
|
2674
|
+
await encryptionEngine.Config(false, contextUser);
|
|
2675
|
+
}
|
|
2676
|
+
// Only encrypt if the value is not already encrypted
|
|
2677
|
+
// This handles cases where values may already be encrypted (e.g., re-save scenarios)
|
|
2678
|
+
const keyMarker = encryptionEngine.GetKeyByID(f.EncryptionKeyID)?.Marker;
|
|
2679
|
+
if (!encryptionEngine.IsEncrypted(value, keyMarker)) {
|
|
2680
|
+
try {
|
|
2681
|
+
// Convert value to string for encryption if it isn't already
|
|
2682
|
+
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
2683
|
+
value = await encryptionEngine.Encrypt(stringValue, f.EncryptionKeyID, contextUser);
|
|
2684
|
+
}
|
|
2685
|
+
catch (encryptError) {
|
|
2686
|
+
// Log the error but throw to prevent unencrypted storage
|
|
2687
|
+
// SECURITY: Never store unencrypted data in an encrypted field
|
|
2688
|
+
const message = encryptError instanceof Error ? encryptError.message : String(encryptError);
|
|
2689
|
+
throw new Error(`Failed to encrypt field "${f.Name}" on entity "${entity.EntityInfo.Name}": ${message}. ` +
|
|
2690
|
+
'The save operation has been aborted to prevent storing unencrypted sensitive data.');
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
// ========================================================================
|
|
2151
2695
|
// Generate variable name with unique suffix
|
|
2152
2696
|
const varName = `@${f.CodeName}${uniqueSuffix}`;
|
|
2153
2697
|
// Add declaration with proper SQL type using existing SQLFullType property
|
|
@@ -2547,7 +3091,8 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2547
3091
|
}).join(' AND ');
|
|
2548
3092
|
const sql = `SELECT * FROM [${entity.EntityInfo.SchemaName}].${entity.EntityInfo.BaseView} WHERE ${where}`;
|
|
2549
3093
|
const rawData = await this.ExecuteSQL(sql, undefined, undefined, user);
|
|
2550
|
-
|
|
3094
|
+
// Process rows with user context for decryption
|
|
3095
|
+
const d = await this.ProcessEntityRows(rawData, entity.EntityInfo, user);
|
|
2551
3096
|
if (d && d.length > 0) {
|
|
2552
3097
|
// got the record, now process the relationships if there are any
|
|
2553
3098
|
const ret = d[0];
|
|
@@ -3181,29 +3726,50 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3181
3726
|
return ret;
|
|
3182
3727
|
}
|
|
3183
3728
|
/**
|
|
3184
|
-
* Processes entity rows returned from SQL Server to handle
|
|
3729
|
+
* Processes entity rows returned from SQL Server to handle:
|
|
3730
|
+
* 1. Timezone conversions for datetime fields
|
|
3731
|
+
* 2. Field-level decryption for encrypted fields
|
|
3732
|
+
*
|
|
3185
3733
|
* This method specifically handles the conversion of datetime2 fields (which SQL Server returns without timezone info)
|
|
3186
3734
|
* to proper UTC dates, preventing JavaScript from incorrectly interpreting them as local time.
|
|
3187
3735
|
*
|
|
3736
|
+
* For encrypted fields, this method decrypts values at the data provider level.
|
|
3737
|
+
* API-level filtering (AllowDecryptInAPI/SendEncryptedValue) is handled by the GraphQL layer.
|
|
3738
|
+
*
|
|
3188
3739
|
* @param rows The raw result rows from SQL Server
|
|
3189
3740
|
* @param entityInfo The entity metadata to determine field types
|
|
3190
|
-
* @
|
|
3741
|
+
* @param contextUser Optional user context for decryption operations
|
|
3742
|
+
* @returns The processed rows with corrected datetime values and decrypted fields
|
|
3743
|
+
*
|
|
3744
|
+
* @security Encrypted fields are decrypted here for internal use.
|
|
3745
|
+
* The API layer handles response filtering based on AllowDecryptInAPI settings.
|
|
3191
3746
|
*/
|
|
3192
|
-
async ProcessEntityRows(rows, entityInfo) {
|
|
3747
|
+
async ProcessEntityRows(rows, entityInfo, contextUser) {
|
|
3193
3748
|
if (!rows || rows.length === 0) {
|
|
3194
3749
|
return rows;
|
|
3195
3750
|
}
|
|
3196
3751
|
// Find all datetime fields in the entity
|
|
3197
3752
|
const datetimeFields = entityInfo.Fields.filter((field) => field.TSType === core_1.EntityFieldTSType.Date);
|
|
3198
|
-
//
|
|
3199
|
-
|
|
3753
|
+
// Find all encrypted fields in the entity
|
|
3754
|
+
const encryptedFields = entityInfo.Fields.filter((field) => field.Encrypt && field.EncryptionKeyID);
|
|
3755
|
+
// If there are no fields requiring processing, return the rows as-is
|
|
3756
|
+
if (datetimeFields.length === 0 && encryptedFields.length === 0) {
|
|
3200
3757
|
return rows;
|
|
3201
3758
|
}
|
|
3202
3759
|
// Check if we need datetimeoffset adjustment (lazy loaded on first use)
|
|
3203
|
-
const needsAdjustment = await this.NeedsDatetimeOffsetAdjustment();
|
|
3204
|
-
//
|
|
3205
|
-
|
|
3760
|
+
const needsAdjustment = datetimeFields.length > 0 ? await this.NeedsDatetimeOffsetAdjustment() : false;
|
|
3761
|
+
// Get encryption engine instance (lazy - only if we have encrypted fields)
|
|
3762
|
+
let encryptionEngine = null;
|
|
3763
|
+
if (encryptedFields.length > 0) {
|
|
3764
|
+
encryptionEngine = encryption_1.EncryptionEngine.Instance;
|
|
3765
|
+
await encryptionEngine.Config(false, contextUser);
|
|
3766
|
+
}
|
|
3767
|
+
// Process each row - need to use Promise.all for async decryption
|
|
3768
|
+
const processedRows = await Promise.all(rows.map(async (row) => {
|
|
3206
3769
|
const processedRow = { ...row };
|
|
3770
|
+
// ========================================================================
|
|
3771
|
+
// DATETIME FIELD PROCESSING
|
|
3772
|
+
// ========================================================================
|
|
3207
3773
|
for (const field of datetimeFields) {
|
|
3208
3774
|
const fieldValue = processedRow[field.Name];
|
|
3209
3775
|
// Skip null/undefined values
|
|
@@ -3261,8 +3827,39 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3261
3827
|
}
|
|
3262
3828
|
// For other types (date, time), leave as-is
|
|
3263
3829
|
}
|
|
3830
|
+
// ========================================================================
|
|
3831
|
+
// ENCRYPTED FIELD PROCESSING (DECRYPTION)
|
|
3832
|
+
// Decrypt at the data provider level for internal use.
|
|
3833
|
+
// API-level filtering based on AllowDecryptInAPI is handled by GraphQL resolvers.
|
|
3834
|
+
// ========================================================================
|
|
3835
|
+
if (encryptionEngine && encryptedFields.length > 0) {
|
|
3836
|
+
for (const field of encryptedFields) {
|
|
3837
|
+
const fieldValue = processedRow[field.Name];
|
|
3838
|
+
// Skip null/undefined/empty values
|
|
3839
|
+
if (fieldValue === null || fieldValue === undefined || fieldValue === '') {
|
|
3840
|
+
continue;
|
|
3841
|
+
}
|
|
3842
|
+
// Only decrypt if the value is actually encrypted
|
|
3843
|
+
const keyMarker = field.EncryptionKeyID ? encryptionEngine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
3844
|
+
if (typeof fieldValue === 'string' && encryptionEngine.IsEncrypted(fieldValue, keyMarker)) {
|
|
3845
|
+
try {
|
|
3846
|
+
const decryptedValue = await encryptionEngine.Decrypt(fieldValue, contextUser);
|
|
3847
|
+
processedRow[field.Name] = decryptedValue;
|
|
3848
|
+
}
|
|
3849
|
+
catch (decryptError) {
|
|
3850
|
+
// Log error but don't fail the entire operation
|
|
3851
|
+
// Return the encrypted value so the caller knows something is wrong
|
|
3852
|
+
const message = decryptError instanceof Error ? decryptError.message : String(decryptError);
|
|
3853
|
+
(0, core_1.LogError)(`Failed to decrypt field "${field.Name}" on entity "${entityInfo.Name}": ${message}. ` +
|
|
3854
|
+
'The encrypted value will be returned unchanged.');
|
|
3855
|
+
// Keep the encrypted value in the row - let the caller decide what to do
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3264
3860
|
return processedRow;
|
|
3265
|
-
});
|
|
3861
|
+
}));
|
|
3862
|
+
return processedRows;
|
|
3266
3863
|
}
|
|
3267
3864
|
/**
|
|
3268
3865
|
* Static method for executing SQL with proper handling of connections and logging.
|
|
@@ -3922,29 +4519,48 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3922
4519
|
}
|
|
3923
4520
|
}
|
|
3924
4521
|
exports.SQLServerDataProvider = SQLServerDataProvider;
|
|
3925
|
-
|
|
4522
|
+
/**
|
|
4523
|
+
* In-memory storage provider for Node.js server-side usage.
|
|
4524
|
+
* Uses a Map of Maps structure for category isolation:
|
|
4525
|
+
* Map<category, Map<key, value>>
|
|
4526
|
+
*
|
|
4527
|
+
* This implementation is purely in-memory and doesn't persist to disk.
|
|
4528
|
+
* Data is retained for the lifetime of the server process.
|
|
4529
|
+
*/
|
|
3926
4530
|
class NodeLocalStorageProvider {
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
4531
|
+
static DEFAULT_CATEGORY = 'default';
|
|
4532
|
+
_storage = new Map();
|
|
4533
|
+
/**
|
|
4534
|
+
* Gets or creates a category map
|
|
4535
|
+
*/
|
|
4536
|
+
getCategoryMap(category) {
|
|
4537
|
+
const cat = category || NodeLocalStorageProvider.DEFAULT_CATEGORY;
|
|
4538
|
+
let categoryMap = this._storage.get(cat);
|
|
4539
|
+
if (!categoryMap) {
|
|
4540
|
+
categoryMap = new Map();
|
|
4541
|
+
this._storage.set(cat, categoryMap);
|
|
4542
|
+
}
|
|
4543
|
+
return categoryMap;
|
|
3935
4544
|
}
|
|
3936
|
-
async
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
resolve();
|
|
3940
|
-
});
|
|
4545
|
+
async GetItem(key, category) {
|
|
4546
|
+
const categoryMap = this.getCategoryMap(category || NodeLocalStorageProvider.DEFAULT_CATEGORY);
|
|
4547
|
+
return categoryMap.get(key) ?? null;
|
|
3941
4548
|
}
|
|
3942
|
-
async
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
4549
|
+
async SetItem(key, value, category) {
|
|
4550
|
+
const categoryMap = this.getCategoryMap(category || NodeLocalStorageProvider.DEFAULT_CATEGORY);
|
|
4551
|
+
categoryMap.set(key, value);
|
|
4552
|
+
}
|
|
4553
|
+
async Remove(key, category) {
|
|
4554
|
+
const categoryMap = this.getCategoryMap(category || NodeLocalStorageProvider.DEFAULT_CATEGORY);
|
|
4555
|
+
categoryMap.delete(key);
|
|
4556
|
+
}
|
|
4557
|
+
async ClearCategory(category) {
|
|
4558
|
+
const cat = category || NodeLocalStorageProvider.DEFAULT_CATEGORY;
|
|
4559
|
+
this._storage.delete(cat);
|
|
4560
|
+
}
|
|
4561
|
+
async GetCategoryKeys(category) {
|
|
4562
|
+
const categoryMap = this._storage.get(category || NodeLocalStorageProvider.DEFAULT_CATEGORY);
|
|
4563
|
+
return categoryMap ? Array.from(categoryMap.keys()) : [];
|
|
3948
4564
|
}
|
|
3949
4565
|
}
|
|
3950
4566
|
//# sourceMappingURL=SQLServerDataProvider.js.map
|