@memberjunction/sqlserver-dataprovider 3.3.0 → 4.0.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/NodeFileSystemProvider.d.ts +16 -0
- package/dist/NodeFileSystemProvider.d.ts.map +1 -0
- package/dist/NodeFileSystemProvider.js +35 -0
- package/dist/NodeFileSystemProvider.js.map +1 -0
- package/dist/SQLServerDataProvider.d.ts +4 -2
- package/dist/SQLServerDataProvider.d.ts.map +1 -1
- package/dist/SQLServerDataProvider.js +161 -174
- package/dist/SQLServerDataProvider.js.map +1 -1
- package/dist/SQLServerTransactionGroup.js +16 -43
- package/dist/SQLServerTransactionGroup.js.map +1 -1
- package/dist/SqlLogger.d.ts +12 -4
- package/dist/SqlLogger.d.ts.map +1 -1
- package/dist/SqlLogger.js +29 -60
- package/dist/SqlLogger.js.map +1 -1
- package/dist/UserCache.d.ts +1 -1
- package/dist/UserCache.d.ts.map +1 -1
- package/dist/UserCache.js +9 -38
- package/dist/UserCache.js.map +1 -1
- package/dist/__tests__/setup.d.ts +5 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +17 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -17
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -17
- package/dist/index.js.map +1 -1
- package/dist/queryParameterProcessor.js +4 -32
- package/dist/queryParameterProcessor.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -6
- package/dist/types.js.map +1 -1
- package/package.json +23 -22
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* @fileoverview SQL Server Data Provider for MemberJunction
|
|
4
3
|
*
|
|
@@ -11,49 +10,25 @@
|
|
|
11
10
|
* @version 2.0
|
|
12
11
|
* @since 1.0
|
|
13
12
|
*/
|
|
14
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
-
if (k2 === undefined) k2 = k;
|
|
16
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
-
}
|
|
20
|
-
Object.defineProperty(o, k2, desc);
|
|
21
|
-
}) : (function(o, m, k, k2) {
|
|
22
|
-
if (k2 === undefined) k2 = k;
|
|
23
|
-
o[k2] = m[k];
|
|
24
|
-
}));
|
|
25
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
-
}) : function(o, v) {
|
|
28
|
-
o["default"] = v;
|
|
29
|
-
});
|
|
30
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
31
|
-
if (mod && mod.__esModule) return mod;
|
|
32
|
-
var result = {};
|
|
33
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
34
|
-
__setModuleDefault(result, mod);
|
|
35
|
-
return result;
|
|
36
|
-
};
|
|
37
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.SQLServerDataProvider = void 0;
|
|
39
13
|
/**************************************************************************************************************
|
|
40
14
|
* The SQLServerDataProvider provides a data provider for the entities framework that uses SQL Server directly
|
|
41
15
|
* In practice - this FILE will NOT exist in the entities library, we need to move to its own separate project
|
|
42
16
|
* so it is only included by the consumer of the entities library if they want to use it.
|
|
43
17
|
**************************************************************************************************************/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
18
|
+
import { ApplicationInfo, EntityFieldTSType, ProviderType, UserInfo, AuditLogTypeInfo, AuthorizationInfo, TransactionItem, EntityPermissionType, EntitySaveOptions, LogError, StripStopWords, LogStatus, CompositeKey, EntityDeleteOptions, BaseEntityResult, Metadata, DatabaseProviderBase, QueryCache, InMemoryLocalStorageProvider, } from '@memberjunction/core';
|
|
19
|
+
import { QueryParameterProcessor } from './queryParameterProcessor.js';
|
|
20
|
+
import { NodeFileSystemProvider } from './NodeFileSystemProvider.js';
|
|
21
|
+
import { ViewInfo, } from '@memberjunction/core-entities';
|
|
22
|
+
import { AIEngine } from '@memberjunction/aiengine';
|
|
23
|
+
import { QueueManager } from '@memberjunction/queue';
|
|
24
|
+
import sql from 'mssql';
|
|
25
|
+
import { BehaviorSubject, Subject, concatMap, from, tap, catchError, of } from 'rxjs';
|
|
26
|
+
import { SQLServerTransactionGroup } from './SQLServerTransactionGroup.js';
|
|
27
|
+
import { SqlLoggingSessionImpl } from './SqlLogger.js';
|
|
28
|
+
import { EntityActionEngineServer } from '@memberjunction/actions';
|
|
29
|
+
import { EncryptionEngine } from '@memberjunction/encryption';
|
|
30
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
31
|
+
import { MJGlobal, SQLExpressionValidator } from '@memberjunction/global';
|
|
57
32
|
/**
|
|
58
33
|
* Core SQL execution function - handles the actual database query execution
|
|
59
34
|
* This is outside the class to allow both static and instance methods to use it
|
|
@@ -155,24 +130,27 @@ async function executeSQLCore(query, parameters, context, options) {
|
|
|
155
130
|
* await provider.Config();
|
|
156
131
|
* ```
|
|
157
132
|
*/
|
|
158
|
-
class SQLServerDataProvider extends
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
133
|
+
export class SQLServerDataProvider extends DatabaseProviderBase {
|
|
134
|
+
constructor() {
|
|
135
|
+
super(...arguments);
|
|
136
|
+
this._transactionDepth = 0;
|
|
137
|
+
this._savepointCounter = 0;
|
|
138
|
+
this._savepointStack = [];
|
|
139
|
+
// Query cache instance
|
|
140
|
+
this.queryCache = new QueryCache();
|
|
141
|
+
this._bAllowRefresh = true;
|
|
142
|
+
this._needsDatetimeOffsetAdjustment = false;
|
|
143
|
+
this._datetimeOffsetTestComplete = false;
|
|
144
|
+
// Instance SQL execution queue for serializing transaction queries
|
|
145
|
+
// Non-transactional queries bypass this queue for maximum parallelism
|
|
146
|
+
this._sqlQueue$ = new Subject();
|
|
147
|
+
// Transaction state management
|
|
148
|
+
this._transactionState$ = new BehaviorSubject(false);
|
|
149
|
+
this._deferredTasks = [];
|
|
150
|
+
}
|
|
151
|
+
static { this._sqlLoggingSessionsKey = 'MJ_SQLServerDataProvider_SqlLoggingSessions'; }
|
|
174
152
|
get _sqlLoggingSessions() {
|
|
175
|
-
const g =
|
|
153
|
+
const g = MJGlobal.Instance.GetGlobalObjectStore();
|
|
176
154
|
if (g) {
|
|
177
155
|
if (!g[SQLServerDataProvider._sqlLoggingSessionsKey]) {
|
|
178
156
|
g[SQLServerDataProvider._sqlLoggingSessionsKey] = new Map();
|
|
@@ -183,14 +161,6 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
183
161
|
throw new Error('No global object store available for SQL logging session');
|
|
184
162
|
}
|
|
185
163
|
}
|
|
186
|
-
// Instance SQL execution queue for serializing transaction queries
|
|
187
|
-
// Non-transactional queries bypass this queue for maximum parallelism
|
|
188
|
-
_sqlQueue$ = new rxjs_1.Subject();
|
|
189
|
-
// Subscription for the queue processor
|
|
190
|
-
_queueSubscription;
|
|
191
|
-
// Transaction state management
|
|
192
|
-
_transactionState$ = new rxjs_1.BehaviorSubject(false);
|
|
193
|
-
_deferredTasks = [];
|
|
194
164
|
/**
|
|
195
165
|
* Observable that emits the current transaction state (true when active, false when not)
|
|
196
166
|
* External code can subscribe to this to know when transactions start and end
|
|
@@ -258,7 +228,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
258
228
|
return super.Config(configData, providerToUse); // now parent class can do it's config
|
|
259
229
|
}
|
|
260
230
|
catch (e) {
|
|
261
|
-
|
|
231
|
+
LogError(e);
|
|
262
232
|
throw e;
|
|
263
233
|
}
|
|
264
234
|
}
|
|
@@ -270,13 +240,13 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
270
240
|
// Each instance gets its own queue processor, but only do this ONCE if we get this method called more than once we don't need to reinit
|
|
271
241
|
// the sub, taht would cause duplicate rprocessing.
|
|
272
242
|
if (!this._queueSubscription) {
|
|
273
|
-
this._queueSubscription = this._sqlQueue$.pipe(
|
|
243
|
+
this._queueSubscription = this._sqlQueue$.pipe(concatMap(item => from(executeSQLCore(item.query, item.parameters, item.context, item.options)).pipe(
|
|
274
244
|
// Handle success
|
|
275
|
-
|
|
245
|
+
tap(result => item.resolve(result)),
|
|
276
246
|
// Handle errors
|
|
277
|
-
|
|
247
|
+
catchError(error => {
|
|
278
248
|
item.reject(error);
|
|
279
|
-
return
|
|
249
|
+
return of(null); // Continue processing queue even on errors
|
|
280
250
|
})))).subscribe();
|
|
281
251
|
}
|
|
282
252
|
}
|
|
@@ -348,9 +318,9 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
348
318
|
* ```
|
|
349
319
|
*/
|
|
350
320
|
async CreateSqlLogger(filePath, options) {
|
|
351
|
-
const sessionId = (
|
|
321
|
+
const sessionId = uuidv4();
|
|
352
322
|
const mjCoreSchema = this.ConfigData.MJCoreSchemaName;
|
|
353
|
-
const session = new
|
|
323
|
+
const session = new SqlLoggingSessionImpl(sessionId, filePath, {
|
|
354
324
|
defaultSchemaName: mjCoreSchema,
|
|
355
325
|
...options // if defaultSchemaName is not provided, it will use the MJCoreSchemaName, otherwise
|
|
356
326
|
// the caller's defaultSchemaName will be used
|
|
@@ -504,7 +474,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
504
474
|
*/
|
|
505
475
|
static async LogSQLStatement(query, parameters, description, isMutation = false, simpleSQLFallback, contextUser) {
|
|
506
476
|
// Get the current provider instance
|
|
507
|
-
const provider =
|
|
477
|
+
const provider = Metadata.Provider;
|
|
508
478
|
if (provider && provider._sqlLoggingSessions.size > 0) {
|
|
509
479
|
await provider._logSqlStatement(query, parameters, description, false, isMutation, simpleSQLFallback, contextUser);
|
|
510
480
|
}
|
|
@@ -668,7 +638,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
668
638
|
};
|
|
669
639
|
}
|
|
670
640
|
catch (e) {
|
|
671
|
-
|
|
641
|
+
LogError(e);
|
|
672
642
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
673
643
|
return {
|
|
674
644
|
Success: false,
|
|
@@ -721,7 +691,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
721
691
|
let finalSQL = query.SQL;
|
|
722
692
|
let appliedParameters = {};
|
|
723
693
|
if (query.UsesTemplate) {
|
|
724
|
-
const processingResult =
|
|
694
|
+
const processingResult = QueryParameterProcessor.processQueryTemplate(query, parameters);
|
|
725
695
|
if (!processingResult.success) {
|
|
726
696
|
throw new Error(processingResult.error);
|
|
727
697
|
}
|
|
@@ -730,7 +700,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
730
700
|
}
|
|
731
701
|
else if (parameters && Object.keys(parameters).length > 0) {
|
|
732
702
|
// Warn if parameters were provided but query doesn't use templates
|
|
733
|
-
|
|
703
|
+
LogStatus('Warning: Parameters provided but query does not use templates. Parameters will be ignored.');
|
|
734
704
|
}
|
|
735
705
|
return { finalSQL, appliedParameters };
|
|
736
706
|
}
|
|
@@ -746,7 +716,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
746
716
|
if (!cachedEntry) {
|
|
747
717
|
return null;
|
|
748
718
|
}
|
|
749
|
-
|
|
719
|
+
LogStatus(`Cache hit for query ${query.Name} (${query.ID})`);
|
|
750
720
|
// Apply pagination to cached results
|
|
751
721
|
const { paginatedResult, totalRowCount } = this.applyQueryPagination(cachedEntry.results, params);
|
|
752
722
|
const remainingTTL = (cachedEntry.timestamp + (cachedEntry.ttlMinutes * 60 * 1000)) - Date.now();
|
|
@@ -829,7 +799,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
829
799
|
}
|
|
830
800
|
// Cache the full result set (before pagination)
|
|
831
801
|
this.queryCache.set(query.ID, parameters, results, cacheConfig);
|
|
832
|
-
|
|
802
|
+
LogStatus(`Cached results for query ${query.Name} (${query.ID})`);
|
|
833
803
|
}
|
|
834
804
|
/**
|
|
835
805
|
* Internal implementation of batch query execution.
|
|
@@ -951,7 +921,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
951
921
|
};
|
|
952
922
|
}
|
|
953
923
|
catch (e) {
|
|
954
|
-
|
|
924
|
+
LogError(e);
|
|
955
925
|
return {
|
|
956
926
|
success: false,
|
|
957
927
|
results: [],
|
|
@@ -1092,7 +1062,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1092
1062
|
else
|
|
1093
1063
|
stack.push(variable); // add to the stack for circular reference detection
|
|
1094
1064
|
// variable values is the view ID of the view that we want to get its WHERE CLAUSE, so we need to get the view entity
|
|
1095
|
-
const innerViewEntity = await
|
|
1065
|
+
const innerViewEntity = await ViewInfo.GetViewEntity(variableValue, user);
|
|
1096
1066
|
if (innerViewEntity) {
|
|
1097
1067
|
// we have the inner view, so now call this function recursively to get the where clause for the inner view
|
|
1098
1068
|
const innerWhere = await this.RenderViewWhereClause(innerViewEntity, user, stack);
|
|
@@ -1115,7 +1085,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1115
1085
|
return sWhere;
|
|
1116
1086
|
}
|
|
1117
1087
|
catch (e) {
|
|
1118
|
-
|
|
1088
|
+
LogError(e);
|
|
1119
1089
|
throw e;
|
|
1120
1090
|
}
|
|
1121
1091
|
}
|
|
@@ -1126,7 +1096,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1126
1096
|
// This is the internal implementation - pre/post processing is handled by ProviderBase.RunView()
|
|
1127
1097
|
// Log aggregate input for debugging
|
|
1128
1098
|
if (params?.Aggregates?.length) {
|
|
1129
|
-
|
|
1099
|
+
LogStatus(`[SQLServerDataProvider] InternalRunView received aggregates: entityName=${params.EntityName}, viewID=${params.ViewID}, viewName=${params.ViewName}, aggregateCount=${params.Aggregates.length}, aggregates=${JSON.stringify(params.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
1130
1100
|
}
|
|
1131
1101
|
const startTime = new Date();
|
|
1132
1102
|
try {
|
|
@@ -1138,9 +1108,9 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1138
1108
|
if (params.ViewEntity)
|
|
1139
1109
|
viewEntity = params.ViewEntity;
|
|
1140
1110
|
else if (params.ViewID && params.ViewID.length > 0)
|
|
1141
|
-
viewEntity = await
|
|
1111
|
+
viewEntity = await ViewInfo.GetViewEntity(params.ViewID, contextUser);
|
|
1142
1112
|
else if (params.ViewName && params.ViewName.length > 0)
|
|
1143
|
-
viewEntity = await
|
|
1113
|
+
viewEntity = await ViewInfo.GetViewEntityByName(params.ViewName, contextUser);
|
|
1144
1114
|
if (!viewEntity) {
|
|
1145
1115
|
// if we don't have viewEntity, that means it is a dynamic view, so we need EntityName at a minimum
|
|
1146
1116
|
if (!params.EntityName || params.EntityName.length === 0)
|
|
@@ -1244,9 +1214,9 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1244
1214
|
}
|
|
1245
1215
|
}
|
|
1246
1216
|
// NEXT, apply Row Level Security (RLS)
|
|
1247
|
-
if (!entityInfo.UserExemptFromRowLevelSecurity(user,
|
|
1217
|
+
if (!entityInfo.UserExemptFromRowLevelSecurity(user, EntityPermissionType.Read)) {
|
|
1248
1218
|
// user is NOT exempt from RLS, so we need to apply it
|
|
1249
|
-
const rlsWhereClause = entityInfo.GetUserRowLevelSecurityWhereClause(user,
|
|
1219
|
+
const rlsWhereClause = entityInfo.GetUserRowLevelSecurityWhereClause(user, EntityPermissionType.Read, '');
|
|
1250
1220
|
if (rlsWhereClause && rlsWhereClause.length > 0) {
|
|
1251
1221
|
if (bHasWhere) {
|
|
1252
1222
|
whereSQL += ` AND (${rlsWhereClause})`;
|
|
@@ -1330,7 +1300,12 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1330
1300
|
resultMap[key] = results[index];
|
|
1331
1301
|
});
|
|
1332
1302
|
// Process data results
|
|
1333
|
-
|
|
1303
|
+
let retData = resultMap['data'] || [];
|
|
1304
|
+
// Process rows for datetime conversion and field-level decryption
|
|
1305
|
+
// This is critical for encrypted fields - without this, encrypted data stays encrypted in the UI
|
|
1306
|
+
if (retData.length > 0 && params.ResultType !== 'count_only') {
|
|
1307
|
+
retData = await this.ProcessEntityRows(retData, entityInfo, contextUser);
|
|
1308
|
+
}
|
|
1334
1309
|
// Process count results - also check if we need count based on result length
|
|
1335
1310
|
let rowCount = null;
|
|
1336
1311
|
if (willNeedCount && resultMap['count']) {
|
|
@@ -1422,7 +1397,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1422
1397
|
}
|
|
1423
1398
|
catch (e) {
|
|
1424
1399
|
const exceptionStopTime = new Date();
|
|
1425
|
-
|
|
1400
|
+
LogError(e);
|
|
1426
1401
|
return {
|
|
1427
1402
|
RowCount: 0,
|
|
1428
1403
|
TotalRowCount: 0,
|
|
@@ -1561,7 +1536,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1561
1536
|
};
|
|
1562
1537
|
}
|
|
1563
1538
|
catch (e) {
|
|
1564
|
-
|
|
1539
|
+
LogError(e);
|
|
1565
1540
|
return {
|
|
1566
1541
|
success: false,
|
|
1567
1542
|
results: [],
|
|
@@ -1644,8 +1619,8 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1644
1619
|
}
|
|
1645
1620
|
}
|
|
1646
1621
|
// Row Level Security
|
|
1647
|
-
if (!entityInfo.UserExemptFromRowLevelSecurity(user,
|
|
1648
|
-
const rlsWhereClause = entityInfo.GetUserRowLevelSecurityWhereClause(user,
|
|
1622
|
+
if (!entityInfo.UserExemptFromRowLevelSecurity(user, EntityPermissionType.Read)) {
|
|
1623
|
+
const rlsWhereClause = entityInfo.GetUserRowLevelSecurityWhereClause(user, EntityPermissionType.Read, '');
|
|
1649
1624
|
if (rlsWhereClause && rlsWhereClause.length > 0) {
|
|
1650
1625
|
if (bHasWhere) {
|
|
1651
1626
|
whereSQL += ` AND (${rlsWhereClause})`;
|
|
@@ -1740,7 +1715,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1740
1715
|
return results.map(r => r.RecordID);
|
|
1741
1716
|
}
|
|
1742
1717
|
catch (e) {
|
|
1743
|
-
|
|
1718
|
+
LogError(e);
|
|
1744
1719
|
return [];
|
|
1745
1720
|
}
|
|
1746
1721
|
}
|
|
@@ -1777,7 +1752,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1777
1752
|
return results;
|
|
1778
1753
|
}
|
|
1779
1754
|
catch (e) {
|
|
1780
|
-
|
|
1755
|
+
LogError(e);
|
|
1781
1756
|
return [];
|
|
1782
1757
|
}
|
|
1783
1758
|
}
|
|
@@ -1824,7 +1799,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1824
1799
|
// Validate: if impliedDeletes < 0, there are unexplained rows on the server
|
|
1825
1800
|
// This could happen with direct SQL inserts that bypassed MJ's tracking
|
|
1826
1801
|
if (impliedDeletes < 0) {
|
|
1827
|
-
|
|
1802
|
+
LogStatus(`Differential validation failed for ${entityInfo.Name}: impliedDeletes=${impliedDeletes} (negative). ` +
|
|
1828
1803
|
`clientRowCount=${clientRowCount}, newInserts=${newInserts}, serverRowCount=${serverRowCount}. ` +
|
|
1829
1804
|
`Falling back to full refresh.`);
|
|
1830
1805
|
return this.runFullQueryAndReturn(params, viewIndex, contextUser);
|
|
@@ -1832,7 +1807,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1832
1807
|
// Validate: if impliedDeletes > actualDeletes, there are "hidden" deletes
|
|
1833
1808
|
// not tracked in RecordChanges (e.g., direct SQL deletes)
|
|
1834
1809
|
if (impliedDeletes > actualDeletes) {
|
|
1835
|
-
|
|
1810
|
+
LogStatus(`Differential validation failed for ${entityInfo.Name}: hidden deletes detected. ` +
|
|
1836
1811
|
`impliedDeletes=${impliedDeletes}, actualDeletes=${actualDeletes}. ` +
|
|
1837
1812
|
`clientRowCount=${clientRowCount}, newInserts=${newInserts}, serverRowCount=${serverRowCount}. ` +
|
|
1838
1813
|
`Falling back to full refresh.`);
|
|
@@ -1855,7 +1830,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1855
1830
|
};
|
|
1856
1831
|
}
|
|
1857
1832
|
catch (e) {
|
|
1858
|
-
|
|
1833
|
+
LogError(e);
|
|
1859
1834
|
return {
|
|
1860
1835
|
viewIndex,
|
|
1861
1836
|
status: 'error',
|
|
@@ -1910,7 +1885,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
1910
1885
|
if (!aggregates || aggregates.length === 0) {
|
|
1911
1886
|
return { aggregateSQL: null, validationErrors: [] };
|
|
1912
1887
|
}
|
|
1913
|
-
const validator =
|
|
1888
|
+
const validator = SQLExpressionValidator.Instance;
|
|
1914
1889
|
const validationErrors = [];
|
|
1915
1890
|
const validExpressions = [];
|
|
1916
1891
|
const fieldNames = entityInfo.Fields.map(f => f.Name);
|
|
@@ -2054,7 +2029,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2054
2029
|
if (field)
|
|
2055
2030
|
fieldList.push(field);
|
|
2056
2031
|
else
|
|
2057
|
-
|
|
2032
|
+
LogError(`Field ${f} not found in entity ${entityInfo.Name}`);
|
|
2058
2033
|
});
|
|
2059
2034
|
}
|
|
2060
2035
|
else {
|
|
@@ -2070,7 +2045,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2070
2045
|
fieldList.push(c.EntityField);
|
|
2071
2046
|
}
|
|
2072
2047
|
else {
|
|
2073
|
-
|
|
2048
|
+
LogError(`View Field ${c.Name} doesn't match an Entity Field in entity ${entityInfo.Name}. This can happen if the view was saved with a field that no longer exists in the entity. It is best to update the view to remove this field.`);
|
|
2074
2049
|
}
|
|
2075
2050
|
}
|
|
2076
2051
|
});
|
|
@@ -2083,7 +2058,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2083
2058
|
}
|
|
2084
2059
|
}
|
|
2085
2060
|
catch (e) {
|
|
2086
|
-
|
|
2061
|
+
LogError(e);
|
|
2087
2062
|
}
|
|
2088
2063
|
finally {
|
|
2089
2064
|
return fieldList;
|
|
@@ -2134,7 +2109,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2134
2109
|
else {
|
|
2135
2110
|
// we have multiple words, so we need to convert the spaces to AND
|
|
2136
2111
|
// but first, let's strip the stopwords out of the string
|
|
2137
|
-
u =
|
|
2112
|
+
u = StripStopWords(userSearchString);
|
|
2138
2113
|
// next, include "AND" between all the words so that we have a full text search on all the words
|
|
2139
2114
|
u = u.replace(/ /g, ' AND ');
|
|
2140
2115
|
}
|
|
@@ -2195,7 +2170,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2195
2170
|
throw new Error(`Error saving audit log record`);
|
|
2196
2171
|
}
|
|
2197
2172
|
catch (err) {
|
|
2198
|
-
|
|
2173
|
+
LogError(err);
|
|
2199
2174
|
return null;
|
|
2200
2175
|
}
|
|
2201
2176
|
}
|
|
@@ -2219,7 +2194,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2219
2194
|
// START ---- IEntityDataProvider
|
|
2220
2195
|
/**************************************************************************/
|
|
2221
2196
|
get ProviderType() {
|
|
2222
|
-
return
|
|
2197
|
+
return ProviderType.Database;
|
|
2223
2198
|
}
|
|
2224
2199
|
async GetRecordFavoriteStatus(userId, entityName, CompositeKey, contextUser) {
|
|
2225
2200
|
const id = await this.GetRecordFavoriteID(userId, entityName, CompositeKey, contextUser);
|
|
@@ -2235,7 +2210,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2235
2210
|
return null;
|
|
2236
2211
|
}
|
|
2237
2212
|
catch (e) {
|
|
2238
|
-
|
|
2213
|
+
LogError(e);
|
|
2239
2214
|
throw e;
|
|
2240
2215
|
}
|
|
2241
2216
|
}
|
|
@@ -2268,7 +2243,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2268
2243
|
}
|
|
2269
2244
|
}
|
|
2270
2245
|
catch (e) {
|
|
2271
|
-
|
|
2246
|
+
LogError(e);
|
|
2272
2247
|
throw e;
|
|
2273
2248
|
}
|
|
2274
2249
|
}
|
|
@@ -2278,7 +2253,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2278
2253
|
return this.ExecuteSQL(sSQL, undefined, undefined, contextUser);
|
|
2279
2254
|
}
|
|
2280
2255
|
catch (e) {
|
|
2281
|
-
|
|
2256
|
+
LogError(e);
|
|
2282
2257
|
throw e;
|
|
2283
2258
|
}
|
|
2284
2259
|
}
|
|
@@ -2298,7 +2273,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2298
2273
|
// we do this in SQL by combining the pirmary key name and value for each row using the default separator defined by the CompositeKey class
|
|
2299
2274
|
// the output of this should be like the following 'Field1|Value1||Field2|Value2||Field3|Value3' where the || is the CompositeKey.DefaultFieldDelimiter and the | is the CompositeKey.DefaultValueDelimiter
|
|
2300
2275
|
const quotes = entity.FirstPrimaryKey.NeedsQuotes ? "'" : '';
|
|
2301
|
-
const primaryKeySelectString = `CONCAT(${entity.PrimaryKeys.map((pk) => `'${pk.Name}|', CAST(${pk.Name} AS NVARCHAR(MAX))`).join(`,'${
|
|
2276
|
+
const primaryKeySelectString = `CONCAT(${entity.PrimaryKeys.map((pk) => `'${pk.Name}|', CAST(${pk.Name} AS NVARCHAR(MAX))`).join(`,'${CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
2302
2277
|
// for this entity, check to see if it has any fields that are soft links, and for each of those, generate the SQL
|
|
2303
2278
|
entity.Fields.filter((f) => f.EntityIDFieldName && f.EntityIDFieldName.length > 0).forEach((f) => {
|
|
2304
2279
|
// each field in f must be processed
|
|
@@ -2327,7 +2302,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2327
2302
|
const entityInfo = this.Entities.find((e) => e.Name.trim().toLowerCase() === entityDependency.EntityName?.trim().toLowerCase());
|
|
2328
2303
|
const quotes = entityInfo.FirstPrimaryKey.NeedsQuotes ? "'" : '';
|
|
2329
2304
|
const relatedEntityInfo = this.Entities.find((e) => e.Name.trim().toLowerCase() === entityDependency.RelatedEntityName?.trim().toLowerCase());
|
|
2330
|
-
const primaryKeySelectString = `CONCAT(${entityInfo.PrimaryKeys.map((pk) => `'${pk.Name}|', CAST(${pk.Name} AS NVARCHAR(MAX))`).join(`,'${
|
|
2305
|
+
const primaryKeySelectString = `CONCAT(${entityInfo.PrimaryKeys.map((pk) => `'${pk.Name}|', CAST(${pk.Name} AS NVARCHAR(MAX))`).join(`,'${CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
2331
2306
|
if (sSQL.length > 0)
|
|
2332
2307
|
sSQL += ' UNION ALL ';
|
|
2333
2308
|
sSQL += `SELECT
|
|
@@ -2377,13 +2352,13 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2377
2352
|
// entityInfo.PrimaryKeys.forEach((pk) => {
|
|
2378
2353
|
// pkeyValues.push({FieldName: pk.Name, Value: r[pk.Name]}) // add all of the primary keys, which often is as simple as just "ID", but this is generic way to do it
|
|
2379
2354
|
// })
|
|
2380
|
-
const compositeKey = new
|
|
2355
|
+
const compositeKey = new CompositeKey();
|
|
2381
2356
|
// the row r will have a PrimaryKeyValue field that is a string that is a concatenation of the primary key field names and values
|
|
2382
2357
|
// we need to parse that out so that we can then pass it to the CompositeKey object
|
|
2383
2358
|
const pkeys = {};
|
|
2384
|
-
const keyValues = r.PrimaryKeyValue.split(
|
|
2359
|
+
const keyValues = r.PrimaryKeyValue.split(CompositeKey.DefaultFieldDelimiter);
|
|
2385
2360
|
keyValues.forEach((kv) => {
|
|
2386
|
-
const parts = kv.split(
|
|
2361
|
+
const parts = kv.split(CompositeKey.DefaultValueDelimiter);
|
|
2387
2362
|
pkeys[parts[0]] = parts[1];
|
|
2388
2363
|
});
|
|
2389
2364
|
compositeKey.LoadFromEntityInfoAndRecord(entityInfo, pkeys);
|
|
@@ -2399,7 +2374,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2399
2374
|
}
|
|
2400
2375
|
catch (e) {
|
|
2401
2376
|
// log and throw
|
|
2402
|
-
|
|
2377
|
+
LogError(e);
|
|
2403
2378
|
throw e;
|
|
2404
2379
|
}
|
|
2405
2380
|
}
|
|
@@ -2531,7 +2506,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2531
2506
|
return result;
|
|
2532
2507
|
}
|
|
2533
2508
|
catch (e) {
|
|
2534
|
-
|
|
2509
|
+
LogError(e);
|
|
2535
2510
|
await this.RollbackTransaction();
|
|
2536
2511
|
// attempt to persist the status to the DB, although that might fail
|
|
2537
2512
|
await this.CompleteMergeLogging(mergeRecordLog, result, contextUser);
|
|
@@ -2563,7 +2538,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2563
2538
|
throw new Error(`Error saving record merge log`);
|
|
2564
2539
|
}
|
|
2565
2540
|
catch (e) {
|
|
2566
|
-
|
|
2541
|
+
LogError(e);
|
|
2567
2542
|
throw e;
|
|
2568
2543
|
}
|
|
2569
2544
|
}
|
|
@@ -2595,7 +2570,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2595
2570
|
}
|
|
2596
2571
|
catch (e) {
|
|
2597
2572
|
// do nothing here because we often will get here since some conditions lead to no DB updates possible...
|
|
2598
|
-
|
|
2573
|
+
LogError(e);
|
|
2599
2574
|
// don't bubble up the error here as we're sometimes already in an exception block in caller
|
|
2600
2575
|
}
|
|
2601
2576
|
}
|
|
@@ -2648,7 +2623,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2648
2623
|
let oldData = null;
|
|
2649
2624
|
// use SQL Server CONCAT function to combine all of the primary key values and then combine them together
|
|
2650
2625
|
// using the default field delimiter and default value delimiter as defined in the CompositeKey class
|
|
2651
|
-
const concatPKIDString = `CONCAT(${entity.EntityInfo.PrimaryKeys.map((pk) => `'${pk.CodeName}','${
|
|
2626
|
+
const concatPKIDString = `CONCAT(${entity.EntityInfo.PrimaryKeys.map((pk) => `'${pk.CodeName}','${CompositeKey.DefaultValueDelimiter}',${pk.Name}`).join(`,'${CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
2652
2627
|
if (!bNewRecord)
|
|
2653
2628
|
oldData = entity.GetAll(true); // get all the OLD values, only do for existing records, for new records, not relevant
|
|
2654
2629
|
const logRecordChangeSQL = this.GetLogRecordChangeSQL(entity.GetAll(false), oldData, entity.EntityInfo.Name, '@ID', entity.EntityInfo, bNewRecord ? 'Create' : 'Update', user, false);
|
|
@@ -2706,7 +2681,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2706
2681
|
* @internal
|
|
2707
2682
|
*/
|
|
2708
2683
|
GetEntityAIActions(entityInfo, before) {
|
|
2709
|
-
return
|
|
2684
|
+
return AIEngine.Instance.EntityAIActions.filter((a) => a.EntityID === entityInfo.ID && a.TriggerEvent.toLowerCase().trim() === (before ? 'before save' : 'after save'));
|
|
2710
2685
|
}
|
|
2711
2686
|
/**
|
|
2712
2687
|
* Handles entity actions (non-AI) for save, delete, or validate operations
|
|
@@ -2721,14 +2696,14 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2721
2696
|
async HandleEntityActions(entity, baseType, before, user) {
|
|
2722
2697
|
// use the EntityActionEngine for this
|
|
2723
2698
|
try {
|
|
2724
|
-
const engine =
|
|
2699
|
+
const engine = EntityActionEngineServer.Instance;
|
|
2725
2700
|
await engine.Config(false, user);
|
|
2726
2701
|
const newRecord = entity.IsSaved ? false : true;
|
|
2727
2702
|
const baseTypeType = baseType === 'save' ? (newRecord ? 'Create' : 'Update') : 'Delete';
|
|
2728
2703
|
const invocationType = baseType === 'validate' ? 'Validate' : before ? 'Before' + baseTypeType : 'After' + baseTypeType;
|
|
2729
2704
|
const invocationTypeEntity = engine.InvocationTypes.find((i) => i.Name === invocationType);
|
|
2730
2705
|
if (!invocationTypeEntity) {
|
|
2731
|
-
|
|
2706
|
+
LogError(`Invocation Type ${invocationType} not found in metadata`);
|
|
2732
2707
|
return [];
|
|
2733
2708
|
// throw new Error(`Invocation Type ${invocationType} not found in metadata`);
|
|
2734
2709
|
}
|
|
@@ -2746,7 +2721,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2746
2721
|
return results;
|
|
2747
2722
|
}
|
|
2748
2723
|
catch (e) {
|
|
2749
|
-
|
|
2724
|
+
LogError(e);
|
|
2750
2725
|
return [];
|
|
2751
2726
|
}
|
|
2752
2727
|
}
|
|
@@ -2765,10 +2740,10 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2765
2740
|
if (baseType === 'delete')
|
|
2766
2741
|
return;
|
|
2767
2742
|
// Make sure AI Metadata is loaded here...
|
|
2768
|
-
await
|
|
2743
|
+
await AIEngine.Instance.Config(false, user);
|
|
2769
2744
|
const actions = this.GetEntityAIActions(entity.EntityInfo, before); // get the actions we need to do for this entity
|
|
2770
2745
|
if (actions && actions.length > 0) {
|
|
2771
|
-
const ai =
|
|
2746
|
+
const ai = AIEngine.Instance;
|
|
2772
2747
|
for (let i = 0; i < actions.length; i++) {
|
|
2773
2748
|
const a = actions[i];
|
|
2774
2749
|
if ((a.TriggerEvent === 'before save' && before) || (a.TriggerEvent === 'after save' && !before)) {
|
|
@@ -2791,11 +2766,11 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2791
2766
|
}
|
|
2792
2767
|
else {
|
|
2793
2768
|
// No transaction active, add the task immediately
|
|
2794
|
-
|
|
2769
|
+
QueueManager.AddTask('Entity AI Action', p, null, user);
|
|
2795
2770
|
}
|
|
2796
2771
|
}
|
|
2797
2772
|
catch (e) {
|
|
2798
|
-
|
|
2773
|
+
LogError(e.message);
|
|
2799
2774
|
}
|
|
2800
2775
|
}
|
|
2801
2776
|
}
|
|
@@ -2803,16 +2778,16 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2803
2778
|
}
|
|
2804
2779
|
}
|
|
2805
2780
|
catch (e) {
|
|
2806
|
-
|
|
2781
|
+
LogError(e);
|
|
2807
2782
|
}
|
|
2808
2783
|
}
|
|
2809
2784
|
async Save(entity, user, options) {
|
|
2810
|
-
const entityResult = new
|
|
2785
|
+
const entityResult = new BaseEntityResult();
|
|
2811
2786
|
try {
|
|
2812
2787
|
entity.RegisterTransactionPreprocessing();
|
|
2813
2788
|
const bNewRecord = !entity.IsSaved;
|
|
2814
2789
|
if (!options)
|
|
2815
|
-
options = new
|
|
2790
|
+
options = new EntitySaveOptions();
|
|
2816
2791
|
const bReplay = !!options.ReplayOnly;
|
|
2817
2792
|
if (!bReplay && !bNewRecord && !entity.EntityInfo.AllowUpdateAPI) {
|
|
2818
2793
|
// existing record and not allowed to update
|
|
@@ -2880,7 +2855,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2880
2855
|
// we are part of a transaction group, so just add our query to the list
|
|
2881
2856
|
// and when the transaction is committed, we will send all the queries at once
|
|
2882
2857
|
this._bAllowRefresh = false; // stop refreshes of metadata while we're doing work
|
|
2883
|
-
entity.TransactionGroup.AddTransaction(new
|
|
2858
|
+
entity.TransactionGroup.AddTransaction(new TransactionItem(entity, entityResult.Type === 'create' ? 'Create' : 'Update', sSQL, null, {
|
|
2884
2859
|
dataSource: this._pool,
|
|
2885
2860
|
simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined,
|
|
2886
2861
|
entityName: entity.EntityInfo.Name
|
|
@@ -2966,7 +2941,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
2966
2941
|
this._bAllowRefresh = true; // allow refreshes again if we get a failure here
|
|
2967
2942
|
entityResult.EndedAt = new Date();
|
|
2968
2943
|
entityResult.Message = e.message;
|
|
2969
|
-
|
|
2944
|
+
LogError(e);
|
|
2970
2945
|
throw e; // rethrow the error
|
|
2971
2946
|
}
|
|
2972
2947
|
}
|
|
@@ -3023,7 +2998,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3023
2998
|
*/
|
|
3024
2999
|
async generateSPParams(entity, isUpdate, contextUser) {
|
|
3025
3000
|
// Generate a unique suffix for variable names to avoid collisions in batch scripts
|
|
3026
|
-
const uniqueSuffix = '_' + (
|
|
3001
|
+
const uniqueSuffix = '_' + uuidv4().substring(0, 8).replace(/-/g, '');
|
|
3027
3002
|
const declarations = [];
|
|
3028
3003
|
const setStatements = [];
|
|
3029
3004
|
const execParams = [];
|
|
@@ -3069,7 +3044,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3069
3044
|
if (f.Encrypt && f.EncryptionKeyID && value !== null && value !== undefined) {
|
|
3070
3045
|
// Lazy-load encryption engine only when needed
|
|
3071
3046
|
if (!encryptionEngine) {
|
|
3072
|
-
encryptionEngine =
|
|
3047
|
+
encryptionEngine = EncryptionEngine.Instance;
|
|
3073
3048
|
await encryptionEngine.Config(false, contextUser);
|
|
3074
3049
|
}
|
|
3075
3050
|
// Only encrypt if the value is not already encrypted
|
|
@@ -3143,7 +3118,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3143
3118
|
generateSetStatementValue(f, value) {
|
|
3144
3119
|
let val = value;
|
|
3145
3120
|
switch (f.TSType) {
|
|
3146
|
-
case
|
|
3121
|
+
case EntityFieldTSType.Boolean:
|
|
3147
3122
|
// check to see if the value is a string and if it is equal to true, if so, set the value to 1
|
|
3148
3123
|
if (typeof value === 'string' && value.trim().toLowerCase() === 'true')
|
|
3149
3124
|
val = 1;
|
|
@@ -3152,7 +3127,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3152
3127
|
else
|
|
3153
3128
|
val = value ? 1 : 0;
|
|
3154
3129
|
return val.toString();
|
|
3155
|
-
case
|
|
3130
|
+
case EntityFieldTSType.String:
|
|
3156
3131
|
// Handle string escaping for SET statements
|
|
3157
3132
|
if (typeof val === 'string') {
|
|
3158
3133
|
val = val.replace(/'/g, "''");
|
|
@@ -3164,7 +3139,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3164
3139
|
val = val.replace(/'/g, "''");
|
|
3165
3140
|
}
|
|
3166
3141
|
return `${f.UnicodePrefix}'${val}'`;
|
|
3167
|
-
case
|
|
3142
|
+
case EntityFieldTSType.Date:
|
|
3168
3143
|
if (val !== null && val !== undefined) {
|
|
3169
3144
|
if (typeof val === 'number') {
|
|
3170
3145
|
// we have a timestamp - milliseconds since Unix Epoch
|
|
@@ -3177,7 +3152,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3177
3152
|
val = val.toISOString(); // convert the date to ISO format for storage in the DB
|
|
3178
3153
|
}
|
|
3179
3154
|
return `'${val}'`;
|
|
3180
|
-
case
|
|
3155
|
+
case EntityFieldTSType.Number:
|
|
3181
3156
|
return val.toString();
|
|
3182
3157
|
default:
|
|
3183
3158
|
// For other types, convert to string and quote if needed
|
|
@@ -3195,7 +3170,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3195
3170
|
let quotes = '';
|
|
3196
3171
|
let val = value;
|
|
3197
3172
|
switch (f.TSType) {
|
|
3198
|
-
case
|
|
3173
|
+
case EntityFieldTSType.Boolean:
|
|
3199
3174
|
// check to see if the value is a string and if it is equal to true, if so, set the value to 1
|
|
3200
3175
|
if (typeof value === 'string' && value.trim().toLowerCase() === 'true')
|
|
3201
3176
|
val = 1;
|
|
@@ -3204,10 +3179,10 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3204
3179
|
else
|
|
3205
3180
|
val = value ? 1 : 0;
|
|
3206
3181
|
break;
|
|
3207
|
-
case
|
|
3182
|
+
case EntityFieldTSType.String:
|
|
3208
3183
|
quotes = "'";
|
|
3209
3184
|
break;
|
|
3210
|
-
case
|
|
3185
|
+
case EntityFieldTSType.Date:
|
|
3211
3186
|
quotes = "'";
|
|
3212
3187
|
if (val !== null && val !== undefined) {
|
|
3213
3188
|
if (typeof val === 'number') {
|
|
@@ -3436,14 +3411,14 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3436
3411
|
bDiff = false; // this branch of logic ensures that undefined and null are treated the same
|
|
3437
3412
|
else {
|
|
3438
3413
|
switch (f.TSType) {
|
|
3439
|
-
case
|
|
3414
|
+
case EntityFieldTSType.String:
|
|
3440
3415
|
bDiff = oldData[key] !== newData[key];
|
|
3441
3416
|
break;
|
|
3442
|
-
case
|
|
3417
|
+
case EntityFieldTSType.Date:
|
|
3443
3418
|
bDiff = new Date(oldData[key]).getTime() !== new Date(newData[key]).getTime();
|
|
3444
3419
|
break;
|
|
3445
|
-
case
|
|
3446
|
-
case
|
|
3420
|
+
case EntityFieldTSType.Number:
|
|
3421
|
+
case EntityFieldTSType.Boolean:
|
|
3447
3422
|
bDiff = oldData[key] !== newData[key];
|
|
3448
3423
|
break;
|
|
3449
3424
|
}
|
|
@@ -3497,7 +3472,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3497
3472
|
const ret = d[0];
|
|
3498
3473
|
// we need to post process the retrieval to see if we have any char or nchar fields and we need to remove their trailing spaces
|
|
3499
3474
|
for (const field of entity.EntityInfo.Fields) {
|
|
3500
|
-
if (field.TSType ===
|
|
3475
|
+
if (field.TSType === EntityFieldTSType.String &&
|
|
3501
3476
|
field.Type.toLowerCase().includes('char') &&
|
|
3502
3477
|
!field.Type.toLowerCase().includes('varchar')) {
|
|
3503
3478
|
// trim trailing spaces for char and nchar fields
|
|
@@ -3536,7 +3511,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3536
3511
|
// Find the related entity info to process datetime fields correctly
|
|
3537
3512
|
const relEntityInfo = this.Entities.find((e) => e.Name.trim().toLowerCase() === relInfo.RelatedEntity.trim().toLowerCase());
|
|
3538
3513
|
if (relEntityInfo) {
|
|
3539
|
-
ret[rel] = await this.ProcessEntityRows(rawRelData, relEntityInfo);
|
|
3514
|
+
ret[rel] = await this.ProcessEntityRows(rawRelData, relEntityInfo, user);
|
|
3540
3515
|
}
|
|
3541
3516
|
else {
|
|
3542
3517
|
// Fallback if we can't find entity info
|
|
@@ -3629,11 +3604,11 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3629
3604
|
return { fullSQL: sSQL, simpleSQL: sSimpleSQL };
|
|
3630
3605
|
}
|
|
3631
3606
|
async Delete(entity, options, user) {
|
|
3632
|
-
const result = new
|
|
3607
|
+
const result = new BaseEntityResult();
|
|
3633
3608
|
try {
|
|
3634
3609
|
entity.RegisterTransactionPreprocessing();
|
|
3635
3610
|
if (!options)
|
|
3636
|
-
options = new
|
|
3611
|
+
options = new EntityDeleteOptions();
|
|
3637
3612
|
const bReplay = options.ReplayOnly;
|
|
3638
3613
|
if (!entity.IsSaved && !bReplay)
|
|
3639
3614
|
// existing record and not allowed to update
|
|
@@ -3661,7 +3636,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3661
3636
|
entity.RaiseReadyForTransaction();
|
|
3662
3637
|
// we are part of a transaction group, so just add our query to the list
|
|
3663
3638
|
// and when the transaction is committed, we will send all the queries at once
|
|
3664
|
-
entity.TransactionGroup.AddTransaction(new
|
|
3639
|
+
entity.TransactionGroup.AddTransaction(new TransactionItem(entity, 'Delete', sSQL, null, {
|
|
3665
3640
|
dataSource: this._pool,
|
|
3666
3641
|
simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined,
|
|
3667
3642
|
entityName: entity.EntityInfo.Name
|
|
@@ -3742,7 +3717,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3742
3717
|
}
|
|
3743
3718
|
}
|
|
3744
3719
|
catch (e) {
|
|
3745
|
-
|
|
3720
|
+
LogError(e);
|
|
3746
3721
|
result.Message = e.message;
|
|
3747
3722
|
result.Success = false;
|
|
3748
3723
|
result.EndedAt = new Date();
|
|
@@ -3814,7 +3789,15 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3814
3789
|
}
|
|
3815
3790
|
else {
|
|
3816
3791
|
// Process successful query result
|
|
3817
|
-
|
|
3792
|
+
let itemData = batchResults[queryIndex] || [];
|
|
3793
|
+
// Process rows for datetime conversion and field-level decryption
|
|
3794
|
+
// This is critical for datasets that contain encrypted fields
|
|
3795
|
+
if (itemData.length > 0) {
|
|
3796
|
+
const entityInfo = useThisProvider.Entities.find(e => e.Name.trim().toLowerCase() === item.Entity.trim().toLowerCase());
|
|
3797
|
+
if (entityInfo) {
|
|
3798
|
+
itemData = await useThisProvider.ProcessEntityRows(itemData, entityInfo, contextUser);
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3818
3801
|
const itemUpdatedAt = new Date(item.DatasetItemUpdatedAt);
|
|
3819
3802
|
const datasetUpdatedAt = new Date(item.DatasetUpdatedAt);
|
|
3820
3803
|
const datasetMaxUpdatedAt = new Date(Math.max(itemUpdatedAt.getTime(), datasetUpdatedAt.getTime()));
|
|
@@ -3951,7 +3934,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3951
3934
|
// the reason we continue below if we have NOT loaded Entities is that when the system first bootstraps, DATASET gets loaded
|
|
3952
3935
|
// FIRST before Entities are loaded to load the entity metadata so this would ALWAYS fail :)
|
|
3953
3936
|
// entity not found, return a failed result, shouldn't ever get here due to the foreign key constraint on the table
|
|
3954
|
-
|
|
3937
|
+
LogError(`Entity not found for dataset item ${item.Code} in dataset ${datasetName}`);
|
|
3955
3938
|
return null;
|
|
3956
3939
|
}
|
|
3957
3940
|
else {
|
|
@@ -3965,7 +3948,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
3965
3948
|
}
|
|
3966
3949
|
});
|
|
3967
3950
|
if (invalidColumns.length > 0) {
|
|
3968
|
-
|
|
3951
|
+
LogError(`Invalid columns specified for dataset item ${item.Code} in dataset ${datasetName}: ${invalidColumns.join(', ')}`);
|
|
3969
3952
|
return null;
|
|
3970
3953
|
}
|
|
3971
3954
|
}
|
|
@@ -4084,7 +4067,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4084
4067
|
const appEntities = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwApplicationEntities ORDER BY ApplicationName`, undefined, undefined, contextUser);
|
|
4085
4068
|
const ret = [];
|
|
4086
4069
|
for (let i = 0; i < apps.length; i++) {
|
|
4087
|
-
ret.push(new
|
|
4070
|
+
ret.push(new ApplicationInfo(this, {
|
|
4088
4071
|
...apps[i],
|
|
4089
4072
|
ApplicationEntities: appEntities.filter((ae) => ae.ApplicationName.trim().toLowerCase() === apps[i].Name.trim().toLowerCase()),
|
|
4090
4073
|
}));
|
|
@@ -4095,7 +4078,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4095
4078
|
const alts = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwAuditLogTypes`, null, undefined, contextUser);
|
|
4096
4079
|
const ret = [];
|
|
4097
4080
|
for (let i = 0; i < alts.length; i++) {
|
|
4098
|
-
const alt = new
|
|
4081
|
+
const alt = new AuditLogTypeInfo(alts[i]);
|
|
4099
4082
|
ret.push(alt);
|
|
4100
4083
|
}
|
|
4101
4084
|
return ret;
|
|
@@ -4105,7 +4088,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4105
4088
|
const userRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUserRoles ORDER BY UserID`, undefined, undefined, contextUser);
|
|
4106
4089
|
const ret = [];
|
|
4107
4090
|
for (let i = 0; i < users.length; i++) {
|
|
4108
|
-
ret.push(new
|
|
4091
|
+
ret.push(new UserInfo(this, {
|
|
4109
4092
|
...users[i],
|
|
4110
4093
|
UserRoles: userRoles.filter((ur) => ur.UserID === users[i].ID),
|
|
4111
4094
|
}));
|
|
@@ -4117,7 +4100,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4117
4100
|
const authRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwAuthorizationRoles ORDER BY AuthorizationName`, undefined, undefined, contextUser);
|
|
4118
4101
|
const ret = [];
|
|
4119
4102
|
for (let i = 0; i < auths.length; i++) {
|
|
4120
|
-
ret.push(new
|
|
4103
|
+
ret.push(new AuthorizationInfo(this, {
|
|
4121
4104
|
...auths[i],
|
|
4122
4105
|
AuthorizationRoles: authRoles.filter((ar) => ar.AuthorizationName.trim().toLowerCase() === auths[i].Name.trim().toLowerCase()),
|
|
4123
4106
|
}));
|
|
@@ -4148,7 +4131,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4148
4131
|
return rows;
|
|
4149
4132
|
}
|
|
4150
4133
|
// Find all datetime fields in the entity
|
|
4151
|
-
const datetimeFields = entityInfo.Fields.filter((field) => field.TSType ===
|
|
4134
|
+
const datetimeFields = entityInfo.Fields.filter((field) => field.TSType === EntityFieldTSType.Date);
|
|
4152
4135
|
// Find all encrypted fields in the entity
|
|
4153
4136
|
const encryptedFields = entityInfo.Fields.filter((field) => field.Encrypt && field.EncryptionKeyID);
|
|
4154
4137
|
// If there are no fields requiring processing, return the rows as-is
|
|
@@ -4160,7 +4143,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4160
4143
|
// Get encryption engine instance (lazy - only if we have encrypted fields)
|
|
4161
4144
|
let encryptionEngine = null;
|
|
4162
4145
|
if (encryptedFields.length > 0) {
|
|
4163
|
-
encryptionEngine =
|
|
4146
|
+
encryptionEngine = EncryptionEngine.Instance;
|
|
4164
4147
|
await encryptionEngine.Config(false, contextUser);
|
|
4165
4148
|
}
|
|
4166
4149
|
// Process each row - need to use Promise.all for async decryption
|
|
@@ -4249,7 +4232,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4249
4232
|
// Log error but don't fail the entire operation
|
|
4250
4233
|
// Return the encrypted value so the caller knows something is wrong
|
|
4251
4234
|
const message = decryptError instanceof Error ? decryptError.message : String(decryptError);
|
|
4252
|
-
|
|
4235
|
+
LogError(`Failed to decrypt field "${field.Name}" on entity "${entityInfo.Name}": ${message}. ` +
|
|
4253
4236
|
'The encrypted value will be returned unchanged.');
|
|
4254
4237
|
// Keep the encrypted value in the row - let the caller decide what to do
|
|
4255
4238
|
}
|
|
@@ -4286,7 +4269,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4286
4269
|
// This prevents EREQINPROG errors when multiple queries try to use the same transaction
|
|
4287
4270
|
return new Promise((resolve, reject) => {
|
|
4288
4271
|
this._sqlQueue$.next({
|
|
4289
|
-
id: (
|
|
4272
|
+
id: uuidv4(),
|
|
4290
4273
|
query,
|
|
4291
4274
|
parameters,
|
|
4292
4275
|
context,
|
|
@@ -4605,7 +4588,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4605
4588
|
}
|
|
4606
4589
|
}
|
|
4607
4590
|
catch (e) {
|
|
4608
|
-
|
|
4591
|
+
LogError(e);
|
|
4609
4592
|
throw e;
|
|
4610
4593
|
}
|
|
4611
4594
|
}
|
|
@@ -4713,7 +4696,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4713
4696
|
}
|
|
4714
4697
|
catch (e) {
|
|
4715
4698
|
this._transactionDepth--; // Restore depth on error
|
|
4716
|
-
|
|
4699
|
+
LogError(e);
|
|
4717
4700
|
throw e; // force caller to handle
|
|
4718
4701
|
}
|
|
4719
4702
|
}
|
|
@@ -4744,7 +4727,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4744
4727
|
}
|
|
4745
4728
|
}
|
|
4746
4729
|
catch (e) {
|
|
4747
|
-
|
|
4730
|
+
LogError(e);
|
|
4748
4731
|
throw e; // force caller to handle
|
|
4749
4732
|
}
|
|
4750
4733
|
}
|
|
@@ -4770,7 +4753,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4770
4753
|
const deferredCount = this._deferredTasks.length;
|
|
4771
4754
|
this._deferredTasks = [];
|
|
4772
4755
|
if (deferredCount > 0) {
|
|
4773
|
-
|
|
4756
|
+
LogStatus(`Cleared ${deferredCount} deferred tasks after transaction rollback`);
|
|
4774
4757
|
}
|
|
4775
4758
|
}
|
|
4776
4759
|
else {
|
|
@@ -4796,7 +4779,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4796
4779
|
this._savepointCounter = 0;
|
|
4797
4780
|
this._transactionState$.next(false);
|
|
4798
4781
|
}
|
|
4799
|
-
|
|
4782
|
+
LogError(e);
|
|
4800
4783
|
throw e; // force caller to handle
|
|
4801
4784
|
}
|
|
4802
4785
|
}
|
|
@@ -4808,7 +4791,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4808
4791
|
async RefreshIfNeeded() {
|
|
4809
4792
|
// Skip refresh if a transaction is active
|
|
4810
4793
|
if (this.isTransactionActive) {
|
|
4811
|
-
|
|
4794
|
+
LogStatus('Skipping metadata refresh - transaction is active');
|
|
4812
4795
|
return false;
|
|
4813
4796
|
}
|
|
4814
4797
|
// Call parent implementation if no transaction
|
|
@@ -4822,7 +4805,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4822
4805
|
async processDeferredTasks() {
|
|
4823
4806
|
if (this._deferredTasks.length === 0)
|
|
4824
4807
|
return;
|
|
4825
|
-
|
|
4808
|
+
LogStatus(`Processing ${this._deferredTasks.length} deferred tasks after transaction commit`);
|
|
4826
4809
|
// Copy and clear the deferred tasks array
|
|
4827
4810
|
const tasksToProcess = [...this._deferredTasks];
|
|
4828
4811
|
this._deferredTasks = [];
|
|
@@ -4831,22 +4814,27 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4831
4814
|
try {
|
|
4832
4815
|
if (task.type === 'Entity AI Action') {
|
|
4833
4816
|
// Process the AI action now that we're outside the transaction
|
|
4834
|
-
await
|
|
4817
|
+
await QueueManager.AddTask('Entity AI Action', task.data, task.options, task.user);
|
|
4835
4818
|
}
|
|
4836
4819
|
// Add other task types here as needed
|
|
4837
4820
|
}
|
|
4838
4821
|
catch (error) {
|
|
4839
|
-
|
|
4822
|
+
LogError(`Failed to process deferred ${task.type} task: ${error}`);
|
|
4840
4823
|
// Continue processing other tasks even if one fails
|
|
4841
4824
|
}
|
|
4842
4825
|
}
|
|
4843
|
-
|
|
4826
|
+
LogStatus(`Completed processing deferred tasks`);
|
|
4844
4827
|
}
|
|
4845
4828
|
get LocalStorageProvider() {
|
|
4846
4829
|
if (!this._localStorageProvider)
|
|
4847
|
-
this._localStorageProvider = new
|
|
4830
|
+
this._localStorageProvider = new InMemoryLocalStorageProvider();
|
|
4848
4831
|
return this._localStorageProvider;
|
|
4849
4832
|
}
|
|
4833
|
+
get FileSystemProvider() {
|
|
4834
|
+
if (!this._fileSystemProvider)
|
|
4835
|
+
this._fileSystemProvider = new NodeFileSystemProvider();
|
|
4836
|
+
return this._fileSystemProvider;
|
|
4837
|
+
}
|
|
4850
4838
|
async GetEntityRecordNames(info, contextUser) {
|
|
4851
4839
|
const promises = info.map(async (item) => {
|
|
4852
4840
|
const r = await this.GetEntityRecordName(item.EntityName, item.CompositeKey, contextUser);
|
|
@@ -4870,13 +4858,13 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4870
4858
|
return data[0][fields[0]]; // return first field
|
|
4871
4859
|
}
|
|
4872
4860
|
else {
|
|
4873
|
-
|
|
4861
|
+
LogError(`Entity ${entityName} record ${CompositeKey.ToString()} not found, returning null`);
|
|
4874
4862
|
return null;
|
|
4875
4863
|
}
|
|
4876
4864
|
}
|
|
4877
4865
|
}
|
|
4878
4866
|
catch (e) {
|
|
4879
|
-
|
|
4867
|
+
LogError(e);
|
|
4880
4868
|
return null;
|
|
4881
4869
|
}
|
|
4882
4870
|
}
|
|
@@ -4887,7 +4875,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4887
4875
|
else {
|
|
4888
4876
|
const f = e.NameField;
|
|
4889
4877
|
if (!f) {
|
|
4890
|
-
|
|
4878
|
+
LogError(`Entity ${entityName} does not have an IsNameField or a field with the column name of Name, returning null, use recordId`);
|
|
4891
4879
|
return null;
|
|
4892
4880
|
}
|
|
4893
4881
|
else {
|
|
@@ -4906,7 +4894,7 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4906
4894
|
}
|
|
4907
4895
|
}
|
|
4908
4896
|
async CreateTransactionGroup() {
|
|
4909
|
-
return new
|
|
4897
|
+
return new SQLServerTransactionGroup();
|
|
4910
4898
|
}
|
|
4911
4899
|
/**************************************************************************/
|
|
4912
4900
|
// END ---- IMetadataProvider
|
|
@@ -4915,5 +4903,4 @@ class SQLServerDataProvider extends core_1.DatabaseProviderBase {
|
|
|
4915
4903
|
return this;
|
|
4916
4904
|
}
|
|
4917
4905
|
}
|
|
4918
|
-
exports.SQLServerDataProvider = SQLServerDataProvider;
|
|
4919
4906
|
//# sourceMappingURL=SQLServerDataProvider.js.map
|