@memberjunction/sqlserver-dataprovider 2.50.0 → 2.51.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.
@@ -35,7 +35,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
35
35
  return result;
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.SQLServerDataProvider = exports.SQLServerProviderConfigData = void 0;
38
+ exports.SQLServerDataProvider = void 0;
39
39
  /**************************************************************************************************************
40
40
  * The SQLServerDataProvider provides a data provider for the entities framework that uses SQL Server directly
41
41
  * In practice - this FILE will NOT exist in the entities library, we need to move to its own separate project
@@ -46,251 +46,11 @@ const core_entities_1 = require("@memberjunction/core-entities");
46
46
  const aiengine_1 = require("@memberjunction/aiengine");
47
47
  const queue_1 = require("@memberjunction/queue");
48
48
  const sql = __importStar(require("mssql"));
49
+ const rxjs_1 = require("rxjs");
49
50
  const SQLServerTransactionGroup_1 = require("./SQLServerTransactionGroup");
50
- const UserCache_1 = require("./UserCache");
51
+ const SqlLogger_js_1 = require("./SqlLogger.js");
51
52
  const actions_1 = require("@memberjunction/actions");
52
53
  const uuid_1 = require("uuid");
53
- const sql_formatter_1 = require("sql-formatter");
54
- const fs = __importStar(require("fs"));
55
- const path = __importStar(require("path"));
56
- /**
57
- * Configuration data specific to SQL Server provider
58
- */
59
- class SQLServerProviderConfigData extends core_1.ProviderConfigDataBase {
60
- /**
61
- * Gets the SQL Server data source configuration
62
- */
63
- get DataSource() {
64
- return this.Data.DataSource;
65
- }
66
- /**
67
- * Gets the current user's email address
68
- */
69
- get CurrentUserEmail() {
70
- return this.Data.CurrentUserEmail;
71
- }
72
- /**
73
- * Gets the interval in seconds for checking metadata refresh
74
- */
75
- get CheckRefreshIntervalSeconds() {
76
- return this.Data.CheckRefreshIntervalSeconds;
77
- }
78
- constructor(dataSource, currentUserEmail, MJCoreSchemaName, checkRefreshIntervalSeconds = 0 /*default to disabling auto refresh */, includeSchemas, excludeSchemas) {
79
- super({
80
- DataSource: dataSource,
81
- CurrentUserEmail: currentUserEmail,
82
- CheckRefreshIntervalSeconds: checkRefreshIntervalSeconds,
83
- }, MJCoreSchemaName, includeSchemas, excludeSchemas);
84
- }
85
- }
86
- exports.SQLServerProviderConfigData = SQLServerProviderConfigData;
87
- /**
88
- * Internal implementation of SqlLoggingSession that handles SQL statement logging to files.
89
- * This class manages file I/O, SQL formatting, and filtering based on session options.
90
- *
91
- * @internal
92
- */
93
- class SqlLoggingSessionImpl {
94
- constructor(id, filePath, options = {}) {
95
- this._statementCount = 0;
96
- this._emittedStatementCount = 0; // Track actually emitted statements
97
- this._fileHandle = null;
98
- this._disposed = false;
99
- this.id = id;
100
- this.filePath = filePath;
101
- this.startTime = new Date();
102
- this.options = options;
103
- }
104
- /**
105
- * Gets the count of SQL statements actually written to the log file
106
- * @returns The number of emitted statements (after filtering)
107
- */
108
- get statementCount() {
109
- return this._emittedStatementCount; // Return actually emitted statements
110
- }
111
- /**
112
- * Initializes the logging session by creating the log file and writing the header
113
- * @throws Error if file creation fails
114
- */
115
- async initialize() {
116
- // Ensure directory exists
117
- const dir = path.dirname(this.filePath);
118
- await fs.promises.mkdir(dir, { recursive: true });
119
- // Open file for writing
120
- this._fileHandle = await fs.promises.open(this.filePath, 'w');
121
- // Write header comment
122
- const header = this._generateHeader();
123
- await this._fileHandle.writeFile(header);
124
- }
125
- /**
126
- * Logs a SQL statement to the file, applying filtering and formatting based on session options
127
- *
128
- * @param query - The SQL query to log
129
- * @param parameters - Optional parameters for the query
130
- * @param description - Optional description for this operation
131
- * @param isMutation - Whether this is a data mutation operation
132
- * @param simpleSQLFallback - Optional simple SQL to use if logRecordChangeMetadata=false
133
- */
134
- async logSqlStatement(query, parameters, description, isMutation = false, simpleSQLFallback) {
135
- if (this._disposed || !this._fileHandle) {
136
- return;
137
- }
138
- // Filter statements based on statementTypes option
139
- const statementTypes = this.options.statementTypes || 'both';
140
- if (statementTypes === 'mutations' && !isMutation) {
141
- return; // Skip logging non-mutation statements
142
- }
143
- if (statementTypes === 'queries' && isMutation) {
144
- return; // Skip logging mutation statements
145
- }
146
- let logEntry = '';
147
- // Add description comment if provided
148
- if (description) {
149
- logEntry += `-- ${description}\n`;
150
- }
151
- // Process the SQL statement
152
- let processedQuery = query;
153
- // Use simple SQL fallback if this session has logRecordChangeMetadata=false (default) and fallback is provided
154
- if (this.options.logRecordChangeMetadata !== true && simpleSQLFallback) {
155
- processedQuery = simpleSQLFallback;
156
- // Update description to indicate we're using the simplified version
157
- if (description && !description.includes('(core SP call only)')) {
158
- logEntry = logEntry.replace(`-- ${description}\n`, `-- ${description} (core SP call only)\n`);
159
- }
160
- }
161
- // Replace schema names with Flyway placeholders if migration format
162
- if (this.options.formatAsMigration) {
163
- processedQuery = processedQuery.replace(/\[(\w+)\]\./g, '[${flyway:defaultSchema}].');
164
- }
165
- // Apply pretty printing if enabled
166
- if (this.options.prettyPrint) {
167
- processedQuery = this._prettyPrintSql(processedQuery);
168
- }
169
- // Add the SQL statement
170
- logEntry += `${processedQuery};\n`;
171
- // Add parameter comment if parameters exist
172
- if (parameters) {
173
- if (Array.isArray(parameters)) {
174
- if (parameters.length > 0) {
175
- logEntry += `-- Parameters: ${parameters.map((p, i) => `@p${i}='${p}'`).join(', ')}\n`;
176
- }
177
- }
178
- else if (typeof parameters === 'object') {
179
- const paramStr = Object.entries(parameters)
180
- .map(([key, value]) => `@${key}='${value}'`)
181
- .join(', ');
182
- if (paramStr) {
183
- logEntry += `-- Parameters: ${paramStr}\n`;
184
- }
185
- }
186
- }
187
- // Add batch separator if specified
188
- if (this.options.batchSeparator) {
189
- logEntry += `\n${this.options.batchSeparator}\n`;
190
- }
191
- logEntry += '\n'; // Add blank line between statements
192
- await this._fileHandle.writeFile(logEntry);
193
- this._statementCount++;
194
- this._emittedStatementCount++; // Track actually emitted statements
195
- }
196
- /**
197
- * Disposes of the logging session, writes the footer, closes the file, and optionally deletes empty files
198
- */
199
- async dispose() {
200
- if (this._disposed) {
201
- return;
202
- }
203
- this._disposed = true;
204
- if (this._fileHandle) {
205
- // Write footer comment
206
- const footer = this._generateFooter();
207
- await this._fileHandle.writeFile(footer);
208
- await this._fileHandle.close();
209
- this._fileHandle = null;
210
- // Check if we should delete empty log files
211
- if (this._emittedStatementCount === 0 && !this.options.retainEmptyLogFiles) {
212
- try {
213
- await fs.promises.unlink(this.filePath);
214
- // Log that we deleted the empty file (optional)
215
- console.log(`Deleted empty SQL log file: ${this.filePath}`);
216
- }
217
- catch (error) {
218
- // Ignore errors during deletion (file might already be deleted, etc.)
219
- console.error(`Failed to delete empty SQL log file: ${this.filePath}`, error);
220
- }
221
- }
222
- }
223
- }
224
- _generateHeader() {
225
- let header = `-- SQL Logging Session\n`;
226
- header += `-- Session ID: ${this.id}\n`;
227
- header += `-- Started: ${this.startTime.toISOString()}\n`;
228
- if (this.options.description) {
229
- header += `-- Description: ${this.options.description}\n`;
230
- }
231
- if (this.options.formatAsMigration) {
232
- header += `-- Format: Migration-ready with Flyway schema placeholders\n`;
233
- }
234
- header += `-- Generated by MemberJunction SQLServerDataProvider\n`;
235
- header += `\n`;
236
- return header;
237
- }
238
- _generateFooter() {
239
- const endTime = new Date();
240
- const duration = endTime.getTime() - this.startTime.getTime();
241
- let footer = `\n-- End of SQL Logging Session\n`;
242
- footer += `-- Session ID: ${this.id}\n`;
243
- footer += `-- Completed: ${endTime.toISOString()}\n`;
244
- footer += `-- Duration: ${duration}ms\n`;
245
- footer += `-- Total Statements: ${this._emittedStatementCount}\n`;
246
- return footer;
247
- }
248
- /**
249
- * Format SQL using sql-formatter library with SQL Server dialect
250
- */
251
- _prettyPrintSql(sql) {
252
- if (!sql)
253
- return sql;
254
- try {
255
- let formatted = (0, sql_formatter_1.format)(sql, {
256
- language: 'tsql', // SQL Server Transact-SQL dialect
257
- tabWidth: 2,
258
- keywordCase: 'upper',
259
- functionCase: 'upper',
260
- dataTypeCase: 'upper',
261
- linesBetweenQueries: 1,
262
- });
263
- // Post-process to fix BEGIN/END formatting
264
- formatted = this._postProcessBeginEnd(formatted);
265
- return formatted;
266
- }
267
- catch (error) {
268
- // If formatting fails, return original SQL
269
- console.warn('SQL formatting failed, returning original:', error);
270
- return sql;
271
- }
272
- }
273
- /**
274
- * Post-process SQL to ensure BEGIN, END, and EXEC keywords are on their own lines
275
- */
276
- _postProcessBeginEnd(sql) {
277
- if (!sql)
278
- return sql;
279
- // Fix BEGIN keyword - ensure it's on its own line
280
- // Match: any non-whitespace followed by space(s) followed by BEGIN (word boundary)
281
- sql = sql.replace(/(\S)\s+(BEGIN\b)/g, '$1\n$2');
282
- // Fix BEGIN followed by other keywords - ensure what follows BEGIN is on a new line
283
- // Match: BEGIN followed by space(s) followed by non-whitespace
284
- sql = sql.replace(/(BEGIN\b)\s+(\S)/g, '$1\n$2');
285
- // Fix END keyword - ensure it's on its own line
286
- // Match: any non-whitespace followed by space(s) followed by END (word boundary)
287
- sql = sql.replace(/(\S)\s+(END\b)/g, '$1\n$2');
288
- // Fix EXEC keyword - ensure it's on its own line
289
- // Match: any non-whitespace followed by space(s) followed by EXEC (word boundary)
290
- sql = sql.replace(/(\S)\s+(EXEC\b)/g, '$1\n$2');
291
- return sql;
292
- }
293
- }
294
54
  /**
295
55
  * SQL Server implementation of the MemberJunction data provider interfaces.
296
56
  *
@@ -305,7 +65,7 @@ class SqlLoggingSessionImpl {
305
65
  *
306
66
  * @example
307
67
  * ```typescript
308
- * const config = new SQLServerProviderConfigData(dataSource, userEmail);
68
+ * const config = new SQLServerProviderConfigData(dataSource);
309
69
  * const provider = new SQLServerDataProvider(config);
310
70
  * await provider.Config();
311
71
  * ```
@@ -317,6 +77,26 @@ class SQLServerDataProvider extends core_1.ProviderBase {
317
77
  this._needsDatetimeOffsetAdjustment = false;
318
78
  this._datetimeOffsetTestComplete = false;
319
79
  this._sqlLoggingSessions = new Map();
80
+ // Transaction state management
81
+ this._transactionState$ = new rxjs_1.BehaviorSubject(false);
82
+ this._deferredTasks = [];
83
+ }
84
+ /**
85
+ * Observable that emits the current transaction state (true when active, false when not)
86
+ * External code can subscribe to this to know when transactions start and end
87
+ * @example
88
+ * provider.transactionState$.subscribe(isActive => {
89
+ * console.log('Transaction active:', isActive);
90
+ * });
91
+ */
92
+ get transactionState$() {
93
+ return this._transactionState$.asObservable();
94
+ }
95
+ /**
96
+ * Gets whether a transaction is currently active
97
+ */
98
+ get isTransactionActive() {
99
+ return this._transactionState$.value;
320
100
  }
321
101
  /**
322
102
  * Gets the current configuration data for this provider instance
@@ -333,7 +113,6 @@ class SQLServerDataProvider extends core_1.ProviderBase {
333
113
  async Config(configData) {
334
114
  try {
335
115
  this._pool = configData.DataSource; // Now expects a ConnectionPool instead of DataSource
336
- this._currentUserEmail = configData.CurrentUserEmail;
337
116
  return super.Config(configData); // now parent class can do it's config
338
117
  }
339
118
  catch (e) {
@@ -393,7 +172,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
393
172
  * @example
394
173
  * ```typescript
395
174
  * // Basic usage
396
- * const session = await provider.createSqlLogger('./logs/metadata-sync.sql');
175
+ * const session = await provider.CreateSqlLogger('./logs/metadata-sync.sql');
397
176
  * try {
398
177
  * // Perform operations that will be logged
399
178
  * await provider.ExecuteSQL('INSERT INTO ...');
@@ -402,15 +181,15 @@ class SQLServerDataProvider extends core_1.ProviderBase {
402
181
  * }
403
182
  *
404
183
  * // With migration formatting
405
- * const session = await provider.createSqlLogger('./migrations/changes.sql', {
184
+ * const session = await provider.CreateSqlLogger('./migrations/changes.sql', {
406
185
  * formatAsMigration: true,
407
186
  * description: 'MetadataSync push operation'
408
187
  * });
409
188
  * ```
410
189
  */
411
- async createSqlLogger(filePath, options) {
190
+ async CreateSqlLogger(filePath, options) {
412
191
  const sessionId = (0, uuid_1.v4)();
413
- const session = new SqlLoggingSessionImpl(sessionId, filePath, options);
192
+ const session = new SqlLogger_js_1.SqlLoggingSessionImpl(sessionId, filePath, options);
414
193
  // Initialize the session (create file, write header)
415
194
  await session.initialize();
416
195
  // Store in active sessions map
@@ -430,13 +209,16 @@ class SQLServerDataProvider extends core_1.ProviderBase {
430
209
  },
431
210
  };
432
211
  }
212
+ async GetCurrentUser() {
213
+ return this.CurrentUser;
214
+ }
433
215
  /**
434
216
  * Gets information about all active SQL logging sessions.
435
217
  * Useful for monitoring and debugging.
436
218
  *
437
219
  * @returns Array of session information objects
438
220
  */
439
- getActiveSqlLoggingSessions() {
221
+ GetActiveSqlLoggingSessions() {
440
222
  return Array.from(this._sqlLoggingSessions.values()).map((session) => ({
441
223
  id: session.id,
442
224
  filePath: session.filePath,
@@ -449,7 +231,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
449
231
  * Disposes all active SQL logging sessions.
450
232
  * Useful for cleanup on provider shutdown.
451
233
  */
452
- async disposeAllSqlLoggingSessions() {
234
+ async DisposeAllSqlLoggingSessions() {
453
235
  const disposePromises = Array.from(this._sqlLoggingSessions.values()).map((session) => session.dispose());
454
236
  await Promise.all(disposePromises);
455
237
  this._sqlLoggingSessions.clear();
@@ -465,13 +247,57 @@ class SQLServerDataProvider extends core_1.ProviderBase {
465
247
  * @param isMutation - Whether this is a data mutation operation
466
248
  * @param simpleSQLFallback - Optional simple SQL to use for loggers with logRecordChangeMetadata=false
467
249
  */
468
- async _logSqlStatement(query, parameters, description, ignoreLogging = false, isMutation = false, simpleSQLFallback) {
250
+ async _logSqlStatement(query, parameters, description, ignoreLogging = false, isMutation = false, simpleSQLFallback, contextUser) {
469
251
  if (ignoreLogging || this._sqlLoggingSessions.size === 0) {
470
252
  return;
471
253
  }
472
- // Log to all active sessions in parallel
473
- const logPromises = Array.from(this._sqlLoggingSessions.values()).map((session) => session.logSqlStatement(query, parameters, description, isMutation, simpleSQLFallback));
254
+ // Check if any session has verbose output enabled for debug logging
255
+ const allSessions = Array.from(this._sqlLoggingSessions.values());
256
+ const hasVerboseSession = allSessions.some(s => s.options.verboseOutput === true);
257
+ if (hasVerboseSession) {
258
+ console.log('=== SQL LOGGING DEBUG ===');
259
+ console.log(`Query to log: ${query.substring(0, 100)}...`);
260
+ console.log(`Context user email: ${contextUser?.Email || 'NOT_PROVIDED'}`);
261
+ console.log(`Active sessions count: ${this._sqlLoggingSessions.size}`);
262
+ console.log(`All sessions:`, allSessions.map(s => ({
263
+ id: s.id,
264
+ filterByUserId: s.options.filterByUserId,
265
+ sessionName: s.options.sessionName
266
+ })));
267
+ }
268
+ const filteredSessions = allSessions.filter((session) => {
269
+ // If session has user filter, only log if contextUser matches AND contextUser is provided
270
+ if (session.options.filterByUserId) {
271
+ if (!contextUser?.Email) {
272
+ if (hasVerboseSession) {
273
+ console.log(`Session ${session.id}: Has user filter but no contextUser provided - SKIPPING`);
274
+ }
275
+ return false; // Don't log if filtering requested but no user context provided
276
+ }
277
+ const matches = session.options.filterByUserId === contextUser.ID;
278
+ if (hasVerboseSession) {
279
+ console.log(`Session ${session.id} filter check:`, {
280
+ filterByUserId: session.options.filterByUserId,
281
+ contextUserEmail: contextUser.Email,
282
+ matches: matches
283
+ });
284
+ }
285
+ return matches;
286
+ }
287
+ // No filter means log for all users (regardless of contextUser)
288
+ if (hasVerboseSession) {
289
+ console.log(`Session ${session.id} has no filter - including`);
290
+ }
291
+ return true;
292
+ });
293
+ if (hasVerboseSession) {
294
+ console.log(`Sessions after filtering: ${filteredSessions.length}`);
295
+ }
296
+ const logPromises = filteredSessions.map((session) => session.logSqlStatement(query, parameters, description, isMutation, simpleSQLFallback));
474
297
  await Promise.all(logPromises);
298
+ if (hasVerboseSession) {
299
+ console.log('=== SQL LOGGING DEBUG END ===');
300
+ }
475
301
  }
476
302
  /**
477
303
  * Static method to log SQL statements from external sources like transaction groups
@@ -482,11 +308,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
482
308
  * @param isMutation - Whether this is a data mutation operation
483
309
  * @param simpleSQLFallback - Optional simple SQL to use for loggers with logRecordChangeMetadata=false
484
310
  */
485
- static async LogSQLStatement(query, parameters, description, isMutation = false, simpleSQLFallback) {
311
+ static async LogSQLStatement(query, parameters, description, isMutation = false, simpleSQLFallback, contextUser) {
486
312
  // Get the current provider instance
487
313
  const provider = core_1.Metadata.Provider;
488
314
  if (provider && provider._sqlLoggingSessions.size > 0) {
489
- await provider._logSqlStatement(query, parameters, description, false, isMutation, simpleSQLFallback);
315
+ await provider._logSqlStatement(query, parameters, description, false, isMutation, simpleSQLFallback, contextUser);
490
316
  }
491
317
  }
492
318
  /**************************************************************************/
@@ -499,11 +325,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
499
325
  const ReportID = params.ReportID;
500
326
  // run the sql and return the data
501
327
  const sqlReport = `SELECT ReportSQL FROM [${this.MJCoreSchemaName}].vwReports WHERE ID =${ReportID}`;
502
- const reportInfo = await this.ExecuteSQL(sqlReport);
328
+ const reportInfo = await this.ExecuteSQL(sqlReport, undefined, undefined, contextUser);
503
329
  if (reportInfo && reportInfo.length > 0) {
504
330
  const start = new Date().getTime();
505
331
  const sql = reportInfo[0].ReportSQL;
506
- const result = await this.ExecuteSQL(sql);
332
+ const result = await this.ExecuteSQL(sql, undefined, undefined, contextUser);
507
333
  const end = new Date().getTime();
508
334
  if (result)
509
335
  return {
@@ -545,11 +371,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
545
371
  filter += ` AND Category = '${params.CategoryName}'`; /* if CategoryName is provided, we add it to the filter */
546
372
  }
547
373
  const sqlQuery = `SELECT ID, Name, SQL FROM [${this.MJCoreSchemaName}].vwQueries WHERE ${filter}`;
548
- const queryInfo = await this.ExecuteSQL(sqlQuery);
374
+ const queryInfo = await this.ExecuteSQL(sqlQuery, undefined, undefined, contextUser);
549
375
  if (queryInfo && queryInfo.length > 0) {
550
376
  const start = new Date().getTime();
551
377
  const sql = queryInfo[0].SQL;
552
- const result = await this.ExecuteSQL(sql);
378
+ const result = await this.ExecuteSQL(sql, undefined, undefined, contextUser);
553
379
  const end = new Date().getTime();
554
380
  if (result)
555
381
  return {
@@ -653,7 +479,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
653
479
  if (params) {
654
480
  const user = contextUser ? contextUser : this.CurrentUser;
655
481
  if (!user)
656
- throw new Error(`User ${this._currentUserEmail} not found in metadata and no contextUser provided to RunView()`);
482
+ throw new Error(`User not found in metadata and no contextUser provided to RunView()`);
657
483
  let viewEntity = null, entityInfo = null;
658
484
  if (params.ViewEntity)
659
485
  viewEntity = params.ViewEntity;
@@ -803,13 +629,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
803
629
  viewSQL += ` OFFSET ${params.StartRow} ROWS FETCH NEXT ${params.MaxRows} ROWS ONLY`;
804
630
  }
805
631
  // now we can run the viewSQL, but only do this if the ResultType !== 'count_only', otherwise we don't need to run the viewSQL
806
- const retData = params.ResultType === 'count_only' ? [] : await this.ExecuteSQL(viewSQL);
632
+ const retData = params.ResultType === 'count_only' ? [] : await this.ExecuteSQL(viewSQL, undefined, undefined, contextUser);
807
633
  // finally, if we have a countSQL, we need to run that to get the row count
808
634
  // but only do that if the # of rows returned is equal to the max rows, otherwise we know we have all the rows
809
635
  // OR do that if we are doing a count_only
810
636
  let rowCount = null;
811
637
  if (countSQL && (params.ResultType === 'count_only' || retData.length === entityInfo.UserViewMaxRows)) {
812
- const countResult = await this.ExecuteSQL(countSQL);
638
+ const countResult = await this.ExecuteSQL(countSQL, undefined, undefined, contextUser);
813
639
  if (countResult && countResult.length > 0) {
814
640
  rowCount = countResult[0].TotalRowCount;
815
641
  }
@@ -820,7 +646,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
820
646
  // ONLY LOG TOP LEVEL VIEW EXECUTION - this would be for views with an ID, and don't have ExtraFilter as ExtraFilter
821
647
  // is only used in the system on a tab or just for ad hoc view execution
822
648
  // we do NOT want to wait for this, so no await,
823
- this.createAuditLogRecord(user, 'Run View', 'Run View', 'Success', JSON.stringify({
649
+ this.CreateAuditLogRecord(user, 'Run View', 'Run View', 'Success', JSON.stringify({
824
650
  ViewID: viewEntity?.ID,
825
651
  ViewName: viewEntity?.Name,
826
652
  Description: params.AuditLogDescription,
@@ -967,7 +793,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
967
793
  INSERT INTO @ViewIDList (ID) (SELECT ${entityInfo.FirstPrimaryKey.Name} FROM [${entityInfo.SchemaName}].${entityBaseView} WHERE (${whereSQL}))
968
794
  EXEC [${this.MJCoreSchemaName}].spCreateUserViewRunWithDetail(${viewId},${user.Email}, @ViewIDLIst)
969
795
  `;
970
- const runIDResult = await this.ExecuteSQL(sSQL);
796
+ const runIDResult = await this.ExecuteSQL(sSQL, undefined, undefined, user);
971
797
  const runID = runIDResult[0].UserViewRunID;
972
798
  const sRetSQL = `SELECT * FROM [${entityInfo.SchemaName}].${entityBaseView} WHERE ${entityInfo.FirstPrimaryKey.Name} IN
973
799
  (SELECT RecordID FROM [${this.MJCoreSchemaName}].vwUserViewRunDetails WHERE UserViewRunID=${runID})
@@ -1032,7 +858,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
1032
858
  }
1033
859
  return sUserSearchSQL;
1034
860
  }
1035
- async createAuditLogRecord(user, authorizationName, auditLogTypeName, status, details, entityId, recordId, auditLogDescription) {
861
+ async CreateAuditLogRecord(user, authorizationName, auditLogTypeName, status, details, entityId, recordId, auditLogDescription) {
1036
862
  try {
1037
863
  const authorization = authorizationName
1038
864
  ? this.Authorizations.find((a) => a?.Name?.trim().toLowerCase() === authorizationName.trim().toLowerCase())
@@ -1090,14 +916,14 @@ class SQLServerDataProvider extends core_1.ProviderBase {
1090
916
  get ProviderType() {
1091
917
  return core_1.ProviderType.Database;
1092
918
  }
1093
- async GetRecordFavoriteStatus(userId, entityName, CompositeKey) {
1094
- const id = await this.GetRecordFavoriteID(userId, entityName, CompositeKey);
919
+ async GetRecordFavoriteStatus(userId, entityName, CompositeKey, contextUser) {
920
+ const id = await this.GetRecordFavoriteID(userId, entityName, CompositeKey, contextUser);
1095
921
  return id !== null;
1096
922
  }
1097
- async GetRecordFavoriteID(userId, entityName, CompositeKey) {
923
+ async GetRecordFavoriteID(userId, entityName, CompositeKey, contextUser) {
1098
924
  try {
1099
925
  const sSQL = `SELECT ID FROM [${this.MJCoreSchemaName}].vwUserFavorites WHERE UserID='${userId}' AND Entity='${entityName}' AND RecordID='${CompositeKey.Values()}'`;
1100
- const result = await this.ExecuteSQL(sSQL);
926
+ const result = await this.ExecuteSQL(sSQL, null, undefined, contextUser);
1101
927
  if (result && result.length > 0)
1102
928
  return result[0].ID;
1103
929
  else
@@ -1141,10 +967,10 @@ class SQLServerDataProvider extends core_1.ProviderBase {
1141
967
  throw e;
1142
968
  }
1143
969
  }
1144
- async GetRecordChanges(entityName, compositeKey) {
970
+ async GetRecordChanges(entityName, compositeKey, contextUser) {
1145
971
  try {
1146
972
  const sSQL = `SELECT * FROM [${this.MJCoreSchemaName}].vwRecordChanges WHERE Entity='${entityName}' AND RecordID='${compositeKey.ToConcatenatedString()}' ORDER BY ChangedAt DESC`;
1147
- return this.ExecuteSQL(sSQL);
973
+ return this.ExecuteSQL(sSQL, undefined, undefined, contextUser);
1148
974
  }
1149
975
  catch (e) {
1150
976
  (0, core_1.LogError)(e);
@@ -1219,7 +1045,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
1219
1045
  * @param entityName the name of the entity to check
1220
1046
  * @param KeyValuePairs the primary key(s) to check - only send multiple if you have an entity with a composite primary key
1221
1047
  */
1222
- async GetRecordDependencies(entityName, compositeKey) {
1048
+ async GetRecordDependencies(entityName, compositeKey, contextUser) {
1223
1049
  try {
1224
1050
  const recordDependencies = [];
1225
1051
  // first, get the entity dependencies for this entity
@@ -1231,7 +1057,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
1231
1057
  // now, we have to construct a query that will return the dependencies for this record, both hard and soft links
1232
1058
  const sSQL = this.GetHardLinkDependencySQL(entityDependencies, compositeKey) + '\n' + this.GetSoftLinkDependencySQL(entityName, compositeKey);
1233
1059
  // now, execute the query
1234
- const result = await this.ExecuteSQL(sSQL);
1060
+ const result = await this.ExecuteSQL(sSQL, null, undefined, contextUser);
1235
1061
  if (!result || result.length === 0) {
1236
1062
  return recordDependencies;
1237
1063
  }
@@ -1610,7 +1436,14 @@ class SQLServerDataProvider extends core_1.ProviderBase {
1610
1436
  else {
1611
1437
  // just add a task and move on, we are doing 'after save' so we don't wait
1612
1438
  try {
1613
- queue_1.QueueManager.AddTask('Entity AI Action', p, null, user);
1439
+ if (this.isTransactionActive) {
1440
+ // Defer the task until after the transaction completes
1441
+ this._deferredTasks.push({ type: 'Entity AI Action', data: p, options: null, user });
1442
+ }
1443
+ else {
1444
+ // No transaction active, add the task immediately
1445
+ queue_1.QueueManager.AddTask('Entity AI Action', p, null, user);
1446
+ }
1614
1447
  }
1615
1448
  catch (e) {
1616
1449
  (0, core_1.LogError)(e.message);
@@ -1734,7 +1567,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
1734
1567
  isMutation: true,
1735
1568
  description: `Save ${entity.EntityInfo.Name}`,
1736
1569
  simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined
1737
- });
1570
+ }, user);
1738
1571
  result = await this.ProcessEntityRows(rawResult, entity.EntityInfo);
1739
1572
  }
1740
1573
  this._bAllowRefresh = true; // allow refreshes now
@@ -1925,7 +1758,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
1925
1758
  async LogRecordChange(newData, oldData, entityName, recordID, entityInfo, type, user) {
1926
1759
  const sSQL = this.GetLogRecordChangeSQL(newData, oldData, entityName, recordID, entityInfo, type, user, true);
1927
1760
  if (sSQL) {
1928
- const result = await this.ExecuteSQL(sSQL);
1761
+ const result = await this.ExecuteSQL(sSQL, undefined, undefined, user);
1929
1762
  return result;
1930
1763
  }
1931
1764
  }
@@ -2035,7 +1868,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2035
1868
  return `[${pk.CodeName}]=${quotes}${val.Value}${quotes}`;
2036
1869
  }).join(' AND ');
2037
1870
  const sql = `SELECT * FROM [${entity.EntityInfo.SchemaName}].${entity.EntityInfo.BaseView} WHERE ${where}`;
2038
- const rawData = await this.ExecuteSQL(sql);
1871
+ const rawData = await this.ExecuteSQL(sql, undefined, undefined, user);
2039
1872
  const d = await this.ProcessEntityRows(rawData, entity.EntityInfo);
2040
1873
  if (d && d.length > 0) {
2041
1874
  // got the record, now process the relationships if there are any
@@ -2076,7 +1909,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2076
1909
  [${relEntitySchemaName}].[${relInfo.JoinView}] _jv ON _theview.[${relInfo.RelatedEntityJoinField}] = _jv.[${relInfo.JoinEntityInverseJoinField}]
2077
1910
  WHERE
2078
1911
  _jv.${relInfo.JoinEntityJoinField} = ${quotes}${ret[entity.FirstPrimaryKey.Name]}${quotes}`; // don't yet support composite foreign keys
2079
- const rawRelData = await this.ExecuteSQL(relSql);
1912
+ const rawRelData = await this.ExecuteSQL(relSql, undefined, undefined, user);
2080
1913
  if (rawRelData && rawRelData.length > 0) {
2081
1914
  // Find the related entity info to process datetime fields correctly
2082
1915
  const relEntityInfo = this.Entities.find((e) => e.Name.trim().toLowerCase() === relInfo.RelatedEntity.trim().toLowerCase());
@@ -2252,7 +2085,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2252
2085
  isMutation: true,
2253
2086
  description: `Delete ${entity.EntityInfo.Name}`,
2254
2087
  simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined
2255
- });
2088
+ }, user);
2256
2089
  }
2257
2090
  if (d && d[0]) {
2258
2091
  // SP executed, now make sure the return value matches up as that is how we know the SP was succesfully internally
@@ -2288,7 +2121,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2288
2121
  /**************************************************************************/
2289
2122
  // START ---- IMetadataProvider
2290
2123
  /**************************************************************************/
2291
- async GetDatasetByName(datasetName, itemFilters) {
2124
+ async GetDatasetByName(datasetName, itemFilters, contextUser) {
2292
2125
  const sSQL = `SELECT
2293
2126
  di.*,
2294
2127
  e.BaseView EntityBaseView,
@@ -2307,7 +2140,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2307
2140
  di.EntityID = e.ID
2308
2141
  WHERE
2309
2142
  d.Name = @p0`;
2310
- const items = await this.ExecuteSQL(sSQL, [datasetName]);
2143
+ const items = await this.ExecuteSQL(sSQL, [datasetName], undefined, contextUser);
2311
2144
  // now we have the dataset and the items, we need to get the update date from the items underlying entities
2312
2145
  if (items && items.length > 0) {
2313
2146
  // Optimization: Use batch SQL execution for multiple items
@@ -2326,7 +2159,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2326
2159
  }
2327
2160
  }
2328
2161
  // Execute all queries in a single batch
2329
- const batchResults = await this.ExecuteSQLBatch(queries);
2162
+ const batchResults = await this.ExecuteSQLBatch(queries, undefined, undefined, contextUser);
2330
2163
  // Process results for each item
2331
2164
  const results = [];
2332
2165
  let queryIndex = 0;
@@ -2425,7 +2258,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2425
2258
  }
2426
2259
  return `SELECT ${columns} FROM [${item.EntitySchemaName}].[${item.EntityBaseView}] ${item.WhereClause ? 'WHERE ' + item.WhereClause : ''}${filterSQL}`;
2427
2260
  }
2428
- async GetDatasetItem(item, itemFilters, datasetName) {
2261
+ async GetDatasetItem(item, itemFilters, datasetName, contextUser) {
2429
2262
  const itemUpdatedAt = new Date(item.DatasetItemUpdatedAt);
2430
2263
  const datasetUpdatedAt = new Date(item.DatasetUpdatedAt);
2431
2264
  const datasetMaxUpdatedAt = new Date(Math.max(itemUpdatedAt.getTime(), datasetUpdatedAt.getTime()));
@@ -2442,7 +2275,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2442
2275
  Success: false,
2443
2276
  };
2444
2277
  }
2445
- const itemData = await this.ExecuteSQL(itemSQL);
2278
+ const itemData = await this.ExecuteSQL(itemSQL, undefined, undefined, contextUser);
2446
2279
  // get the latest update date
2447
2280
  let latestUpdateDate = new Date(1900, 1, 1);
2448
2281
  if (itemData && itemData.length > 0) {
@@ -2511,7 +2344,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2511
2344
  }
2512
2345
  return specifiedColumns.length > 0 ? specifiedColumns.map((colName) => `[${colName.trim()}]`).join(',') : '*';
2513
2346
  }
2514
- async GetDatasetStatusByName(datasetName, itemFilters) {
2347
+ async GetDatasetStatusByName(datasetName, itemFilters, contextUser) {
2515
2348
  const sSQL = `
2516
2349
  SELECT
2517
2350
  di.*,
@@ -2531,7 +2364,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2531
2364
  di.EntityID = e.ID
2532
2365
  WHERE
2533
2366
  d.Name = @p0`;
2534
- const items = await this.ExecuteSQL(sSQL, [datasetName]);
2367
+ const items = await this.ExecuteSQL(sSQL, [datasetName], undefined, contextUser);
2535
2368
  // now we have the dataset and the items, we need to get the update date from the items underlying entities
2536
2369
  if (items && items.length > 0) {
2537
2370
  // loop through each of the items and get the update date from the underlying entity by building a combined UNION ALL SQL statement
@@ -2562,7 +2395,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2562
2395
  combinedSQL += ' UNION ALL ';
2563
2396
  }
2564
2397
  });
2565
- const itemUpdateDates = await this.ExecuteSQL(combinedSQL);
2398
+ const itemUpdateDates = await this.ExecuteSQL(combinedSQL, null, undefined, contextUser);
2566
2399
  if (itemUpdateDates && itemUpdateDates.length > 0) {
2567
2400
  let latestUpdateDate = new Date(1900, 1, 1);
2568
2401
  itemUpdateDates.forEach((itemUpdate) => {
@@ -2608,9 +2441,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2608
2441
  };
2609
2442
  }
2610
2443
  }
2611
- async GetApplicationMetadata() {
2612
- const apps = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwApplications`, null);
2613
- const appEntities = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwApplicationEntities ORDER BY ApplicationName`);
2444
+ async GetApplicationMetadata(contextUser) {
2445
+ const apps = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwApplications`, null, undefined, contextUser);
2446
+ const appEntities = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwApplicationEntities ORDER BY ApplicationName`, undefined, undefined, contextUser);
2614
2447
  const ret = [];
2615
2448
  for (let i = 0; i < apps.length; i++) {
2616
2449
  ret.push(new core_1.ApplicationInfo(this, {
@@ -2620,8 +2453,8 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2620
2453
  }
2621
2454
  return ret;
2622
2455
  }
2623
- async GetAuditLogTypeMetadata() {
2624
- const alts = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwAuditLogTypes`, null);
2456
+ async GetAuditLogTypeMetadata(contextUser) {
2457
+ const alts = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwAuditLogTypes`, null, undefined, contextUser);
2625
2458
  const ret = [];
2626
2459
  for (let i = 0; i < alts.length; i++) {
2627
2460
  const alt = new core_1.AuditLogTypeInfo(alts[i]);
@@ -2629,9 +2462,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2629
2462
  }
2630
2463
  return ret;
2631
2464
  }
2632
- async GetUserMetadata() {
2633
- const users = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUsers`, null);
2634
- const userRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUserRoles ORDER BY UserID`);
2465
+ async GetUserMetadata(contextUser) {
2466
+ const users = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUsers`, null, undefined, contextUser);
2467
+ const userRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUserRoles ORDER BY UserID`, undefined, undefined, contextUser);
2635
2468
  const ret = [];
2636
2469
  for (let i = 0; i < users.length; i++) {
2637
2470
  ret.push(new core_1.UserInfo(this, {
@@ -2641,9 +2474,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2641
2474
  }
2642
2475
  return ret;
2643
2476
  }
2644
- async GetAuthorizationMetadata() {
2645
- const auths = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwAuthorizations`, null);
2646
- const authRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwAuthorizationRoles ORDER BY AuthorizationName`);
2477
+ async GetAuthorizationMetadata(contextUser) {
2478
+ const auths = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwAuthorizations`, null, undefined, contextUser);
2479
+ const authRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwAuthorizationRoles ORDER BY AuthorizationName`, undefined, undefined, contextUser);
2647
2480
  const ret = [];
2648
2481
  for (let i = 0; i < auths.length; i++) {
2649
2482
  ret.push(new core_1.AuthorizationInfo(this, {
@@ -2653,56 +2486,6 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2653
2486
  }
2654
2487
  return ret;
2655
2488
  }
2656
- async GetCurrentUser() {
2657
- if (this.CurrentUser)
2658
- return this.CurrentUser;
2659
- else if (this._currentUserEmail && this._currentUserEmail.length > 0) {
2660
- // attempt to lookup current user from email since this.CurrentUser is null for some reason (unexpected)
2661
- if (UserCache_1.UserCache && UserCache_1.UserCache.Users)
2662
- return UserCache_1.UserCache.Users.find((u) => u.Email.trim().toLowerCase() === this._currentUserEmail.trim().toLowerCase());
2663
- }
2664
- // if we get here we can't get the current user
2665
- return null;
2666
- }
2667
- async GetCurrentUserMetadata() {
2668
- const user = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUsers WHERE Email='${this._currentUserEmail}'`);
2669
- if (user && user.length === 1) {
2670
- const userRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUserRoles WHERE UserID='${user[0].ID}'`);
2671
- return new core_1.UserInfo(this, {
2672
- ...user[0],
2673
- UserRoles: userRoles ? userRoles : [],
2674
- });
2675
- }
2676
- else
2677
- return null;
2678
- }
2679
- async GetRoleMetadata() {
2680
- const roles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwRoles`, null);
2681
- const ret = [];
2682
- for (let i = 0; i < roles.length; i++) {
2683
- const ri = new core_1.RoleInfo(roles[i]);
2684
- ret.push(ri);
2685
- }
2686
- return ret;
2687
- }
2688
- async GetUserRoleMetadata() {
2689
- const userRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUserRoles`, null);
2690
- const ret = [];
2691
- for (let i = 0; i < userRoles.length; i++) {
2692
- const uri = new core_1.UserRoleInfo(userRoles[i]);
2693
- ret.push(uri);
2694
- }
2695
- return ret;
2696
- }
2697
- async GetRowLevelSecurityFilterMetadata() {
2698
- const filters = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwRowLevelSecurityFilters`, null);
2699
- const ret = [];
2700
- for (let i = 0; i < filters.length; i++) {
2701
- const rlsfi = new core_1.RowLevelSecurityFilterInfo(filters[i]);
2702
- ret.push(rlsfi);
2703
- }
2704
- return ret;
2705
- }
2706
2489
  /**
2707
2490
  * Processes entity rows returned from SQL Server to handle timezone conversions for datetime fields.
2708
2491
  * This method specifically handles the conversion of datetime2 fields (which SQL Server returns without timezone info)
@@ -2788,38 +2571,72 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2788
2571
  });
2789
2572
  }
2790
2573
  /**
2791
- * This method can be used to execute raw SQL statements outside of the MJ infrastructure.
2792
- * *CAUTION* - use this method with great care.
2793
- * @param query
2794
- * @param parameters
2795
- * @returns
2574
+ * Static method for executing SQL with proper handling of connections and logging.
2575
+ * This is the single point where all SQL execution happens in the entire class.
2576
+ *
2577
+ * @param query - SQL query to execute
2578
+ * @param parameters - Query parameters
2579
+ * @param context - Execution context containing pool, transaction, and logging functions
2580
+ * @param options - Options for SQL execution
2581
+ * @returns Promise<sql.IResult<any>> - Query result
2582
+ * @private
2796
2583
  */
2797
- async ExecuteSQL(query, parameters = null, options) {
2584
+ static async _internalExecuteSQLStatic(query, parameters, context, options) {
2585
+ // Determine which connection source to use
2586
+ let connectionSource;
2587
+ if (context.transaction) {
2588
+ // Try to use the transaction if provided
2589
+ try {
2590
+ // Test if the transaction is still valid by creating a request
2591
+ const testRequest = new sql.Request(context.transaction);
2592
+ connectionSource = context.transaction;
2593
+ }
2594
+ catch (error) {
2595
+ // Transaction is no longer valid, clear it and use the pool
2596
+ if (context.clearTransaction) {
2597
+ context.clearTransaction();
2598
+ }
2599
+ connectionSource = context.pool;
2600
+ }
2601
+ }
2602
+ else {
2603
+ connectionSource = context.pool;
2604
+ }
2605
+ // Check if the pool is connected before attempting to execute
2606
+ if (connectionSource === context.pool && !context.pool.connected) {
2607
+ const errorMessage = 'Connection pool is closed. Cannot execute SQL query.';
2608
+ const error = new Error(errorMessage);
2609
+ error.code = 'POOL_CLOSED';
2610
+ throw error;
2611
+ }
2612
+ // Handle logging
2613
+ let logPromise;
2614
+ if (options && !options.ignoreLogging && context.logSqlStatement) {
2615
+ logPromise = context.logSqlStatement(query, parameters, options.description, options.ignoreLogging, options.isMutation, options.simpleSQLFallback, options.contextUser);
2616
+ }
2617
+ else {
2618
+ logPromise = Promise.resolve();
2619
+ }
2798
2620
  try {
2621
+ // Create a new request object for this query
2799
2622
  let request;
2800
- if (this._transaction && this._transactionRequest) {
2801
- // Use transaction request if in a transaction
2802
- request = this._transactionRequest;
2803
- }
2804
- else if (this._transaction) {
2805
- // Create a new request for this transaction if we don't have one
2806
- request = new sql.Request(this._transaction);
2623
+ if (connectionSource instanceof sql.Transaction) {
2624
+ request = new sql.Request(connectionSource);
2807
2625
  }
2808
2626
  else {
2809
- // Use pool request for non-transactional queries
2810
- request = new sql.Request(this._pool);
2627
+ request = new sql.Request(connectionSource);
2811
2628
  }
2812
2629
  // Add parameters if provided
2630
+ let processedQuery = query;
2813
2631
  if (parameters) {
2814
2632
  if (Array.isArray(parameters)) {
2815
2633
  // Handle positional parameters (legacy TypeORM style)
2816
- // Convert to named parameters for mssql
2817
2634
  parameters.forEach((value, index) => {
2818
2635
  request.input(`p${index}`, value);
2819
2636
  });
2820
2637
  // Replace ? with @p0, @p1, etc. in the query
2821
2638
  let paramIndex = 0;
2822
- query = query.replace(/\?/g, () => `@p${paramIndex++}`);
2639
+ processedQuery = query.replace(/\?/g, () => `@p${paramIndex++}`);
2823
2640
  }
2824
2641
  else if (typeof parameters === 'object') {
2825
2642
  // Handle named parameters
@@ -2828,16 +2645,111 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2828
2645
  }
2829
2646
  }
2830
2647
  }
2831
- // Log SQL statement to all active logging sessions (runs in parallel with execution)
2832
- const loggingPromise = this._logSqlStatement(query, parameters, options?.description, options?.ignoreLogging, options?.isMutation, options?.simpleSQLFallback);
2833
- // Execute SQL and logging in parallel, but wait for both to complete
2834
- const [result] = await Promise.all([request.query(query), loggingPromise]);
2648
+ // Execute query and logging in parallel
2649
+ const [result] = await Promise.all([
2650
+ request.query(processedQuery),
2651
+ logPromise
2652
+ ]);
2653
+ return result;
2654
+ }
2655
+ catch (error) {
2656
+ // If we get an EREQINPROG error and we were using a transaction, retry with the pool
2657
+ if (error?.code === 'EREQINPROG' && connectionSource === context.transaction) {
2658
+ // Silently retry with pool connection - this is expected behavior during concurrent operations
2659
+ // LogDebug('Transaction connection busy (EREQINPROG) - retrying with pool connection');
2660
+ // Clear the transaction reference
2661
+ if (context.clearTransaction) {
2662
+ context.clearTransaction();
2663
+ }
2664
+ // Retry using the pool connection
2665
+ return SQLServerDataProvider._internalExecuteSQLStatic(query, parameters, {
2666
+ ...context,
2667
+ transaction: null // Force use of pool
2668
+ }, options ? {
2669
+ ...options,
2670
+ description: options.description + ' (retry with pool)'
2671
+ } : undefined);
2672
+ }
2673
+ // Log other errors
2674
+ (0, core_1.LogError)(error);
2675
+ // Re-throw all errors
2676
+ throw error;
2677
+ }
2678
+ }
2679
+ /**
2680
+ * Internal centralized method for executing SQL queries with consistent transaction and connection handling.
2681
+ * This method ensures proper request object creation and management to avoid concurrency issues,
2682
+ * particularly when using transactions where multiple operations may execute in parallel.
2683
+ *
2684
+ * @private
2685
+ * @param query - The SQL query to execute
2686
+ * @param parameters - Optional parameters for the query (array for positional, object for named)
2687
+ * @param connectionSource - Optional specific connection source (pool, transaction, or request)
2688
+ * @param loggingOptions - Optional logging configuration
2689
+ * @returns Promise<sql.IResult<any>> - The raw mssql result object
2690
+ *
2691
+ * @remarks
2692
+ * - Always creates a new Request object for each query to avoid "EREQINPROG" errors
2693
+ * - Handles both positional (?) and named (@param) parameter styles
2694
+ * - Automatically uses active transaction if one exists, otherwise uses connection pool
2695
+ * - Handles SQL logging in parallel with query execution
2696
+ * - Provides automatic retry with pool connection if transaction fails
2697
+ *
2698
+ * @throws {Error} Rethrows any SQL execution errors after logging
2699
+ */
2700
+ async _internalExecuteSQL(query, parameters, connectionSource, loggingOptions) {
2701
+ // Handle the connectionSource parameter for backwards compatibility
2702
+ // If a specific source is provided, we'll pass it as the transaction (if it's a transaction)
2703
+ // or ignore it if it's a pool/request (since we'll use our own pool)
2704
+ let transaction = null;
2705
+ if (connectionSource instanceof sql.Transaction) {
2706
+ transaction = connectionSource;
2707
+ }
2708
+ else if (!connectionSource) {
2709
+ // Use our transaction if available
2710
+ transaction = this._transaction;
2711
+ }
2712
+ // Create the execution context
2713
+ const context = {
2714
+ pool: this._pool,
2715
+ transaction: transaction,
2716
+ logSqlStatement: this._logSqlStatement.bind(this),
2717
+ clearTransaction: () => { this._transaction = null; }
2718
+ };
2719
+ // Convert logging options to internal format
2720
+ const options = loggingOptions ? {
2721
+ description: loggingOptions.description,
2722
+ ignoreLogging: loggingOptions.ignoreLogging,
2723
+ isMutation: loggingOptions.isMutation,
2724
+ simpleSQLFallback: loggingOptions.simpleSQLFallback,
2725
+ contextUser: loggingOptions.contextUser
2726
+ } : undefined;
2727
+ // Delegate to static method
2728
+ return SQLServerDataProvider._internalExecuteSQLStatic(query, parameters, context, options);
2729
+ }
2730
+ /**
2731
+ * This method can be used to execute raw SQL statements outside of the MJ infrastructure.
2732
+ * *CAUTION* - use this method with great care.
2733
+ * @param query
2734
+ * @param parameters
2735
+ * @returns
2736
+ */
2737
+ async ExecuteSQL(query, parameters = null, options, contextUser) {
2738
+ try {
2739
+ // Use internal method with logging options
2740
+ const result = await this._internalExecuteSQL(query, parameters, undefined, {
2741
+ description: options?.description,
2742
+ ignoreLogging: options?.ignoreLogging,
2743
+ isMutation: options?.isMutation,
2744
+ simpleSQLFallback: options?.simpleSQLFallback,
2745
+ contextUser: contextUser
2746
+ });
2835
2747
  // Return recordset for consistency with TypeORM behavior
2836
2748
  // If multiple recordsets, return recordsets array
2837
2749
  return result.recordsets && Array.isArray(result.recordsets) && result.recordsets.length > 1 ? result.recordsets : result.recordset;
2838
2750
  }
2839
2751
  catch (e) {
2840
- (0, core_1.LogError)(e);
2752
+ // Error already logged by _internalExecuteSQL
2841
2753
  throw e; // force caller to handle
2842
2754
  }
2843
2755
  }
@@ -2852,33 +2764,31 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2852
2764
  * @param parameters - Optional parameters for the query
2853
2765
  * @returns Promise<any[]> - Array of results (empty array if no results)
2854
2766
  */
2855
- static async ExecuteSQLWithPool(pool, query, parameters) {
2767
+ static async ExecuteSQLWithPool(pool, query, parameters, contextUser) {
2856
2768
  try {
2857
- const request = new sql.Request(pool);
2858
- // Add parameters if provided
2859
- if (parameters) {
2860
- if (Array.isArray(parameters)) {
2861
- // Handle positional parameters
2862
- parameters.forEach((value, index) => {
2863
- request.input(`p${index}`, value);
2864
- });
2865
- // Replace ? with @p0, @p1, etc. in the query
2866
- let paramIndex = 0;
2867
- query = query.replace(/\?/g, () => `@p${paramIndex++}`);
2769
+ // Create the execution context for static method
2770
+ const context = {
2771
+ pool: pool,
2772
+ transaction: null,
2773
+ logSqlStatement: async (q, p, d, i, m, s, u) => {
2774
+ // Use static logging method
2775
+ await SQLServerDataProvider.LogSQLStatement(q, p, d || 'ExecuteSQLWithPool', m || false, s, u);
2868
2776
  }
2869
- else if (typeof parameters === 'object') {
2870
- // Handle named parameters
2871
- for (const [key, value] of Object.entries(parameters)) {
2872
- request.input(key, value);
2873
- }
2874
- }
2875
- }
2876
- const result = await request.query(query);
2777
+ };
2778
+ // Create options
2779
+ const options = {
2780
+ description: 'ExecuteSQLWithPool',
2781
+ ignoreLogging: false,
2782
+ isMutation: false,
2783
+ contextUser: contextUser
2784
+ };
2785
+ // Use the static execution method
2786
+ const result = await SQLServerDataProvider._internalExecuteSQLStatic(query, parameters, context, options);
2877
2787
  // Always return array for consistency
2878
2788
  return result.recordset || [];
2879
2789
  }
2880
2790
  catch (e) {
2881
- (0, core_1.LogError)(e);
2791
+ // Error already logged by _internalExecuteSQLStatic
2882
2792
  throw e;
2883
2793
  }
2884
2794
  }
@@ -2893,54 +2803,68 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2893
2803
  * @param parameters - Optional array of parameter arrays, one for each query
2894
2804
  * @returns Promise<any[][]> - Array of result arrays, one for each query
2895
2805
  */
2896
- static async ExecuteSQLBatchStatic(connectionSource, queries, parameters) {
2806
+ static async ExecuteSQLBatchStatic(connectionSource, queries, parameters, contextUser) {
2897
2807
  try {
2898
- let request;
2899
- // Determine the request to use based on connection source type
2900
- if (connectionSource instanceof sql.Request) {
2901
- request = connectionSource;
2902
- }
2903
- else if (connectionSource instanceof sql.Transaction) {
2904
- request = new sql.Request(connectionSource);
2905
- }
2906
- else {
2907
- // Assume it's a ConnectionPool
2908
- request = new sql.Request(connectionSource);
2909
- }
2910
- // Build combined batch SQL
2808
+ // Build combined batch SQL and parameters
2911
2809
  let batchSQL = '';
2912
- let paramIndex = 0;
2810
+ const batchParameters = {};
2811
+ let globalParamIndex = 0;
2913
2812
  queries.forEach((query, queryIndex) => {
2813
+ let processedQuery = query;
2914
2814
  // Add parameters for this query if provided
2915
2815
  if (parameters && parameters[queryIndex]) {
2916
2816
  const queryParams = parameters[queryIndex];
2917
2817
  if (Array.isArray(queryParams)) {
2918
2818
  // Handle positional parameters
2919
- queryParams.forEach((value) => {
2920
- request.input(`p${paramIndex}`, value);
2921
- paramIndex++;
2819
+ queryParams.forEach((value, localIndex) => {
2820
+ const paramName = `p${globalParamIndex}`;
2821
+ batchParameters[paramName] = value;
2822
+ globalParamIndex++;
2922
2823
  });
2923
- // Replace @p0, @p1, etc. with actual parameter names
2924
- let localParamIndex = paramIndex - queryParams.length;
2925
- query = query.replace(/@p(\d+)/g, () => `@p${localParamIndex++}`);
2824
+ // Replace ? placeholders with parameter names
2825
+ let localParamIndex = globalParamIndex - queryParams.length;
2826
+ processedQuery = processedQuery.replace(/\?/g, () => `@p${localParamIndex++}`);
2926
2827
  }
2927
2828
  else if (typeof queryParams === 'object') {
2928
2829
  // Handle named parameters - prefix with query index to avoid conflicts
2929
2830
  for (const [key, value] of Object.entries(queryParams)) {
2930
2831
  const paramName = `q${queryIndex}_${key}`;
2931
- request.input(paramName, value);
2832
+ batchParameters[paramName] = value;
2932
2833
  // Replace parameter references in query
2933
- query = query.replace(new RegExp(`@${key}\\b`, 'g'), `@${paramName}`);
2834
+ processedQuery = processedQuery.replace(new RegExp(`@${key}\\b`, 'g'), `@${paramName}`);
2934
2835
  }
2935
2836
  }
2936
2837
  }
2937
- batchSQL += query;
2838
+ batchSQL += processedQuery;
2938
2839
  if (queryIndex < queries.length - 1) {
2939
2840
  batchSQL += ';\n';
2940
2841
  }
2941
2842
  });
2942
- // Execute the batch SQL
2943
- const result = await request.query(batchSQL);
2843
+ // Execute the batch SQL directly (static method can't use instance _internalExecuteSQL)
2844
+ let request;
2845
+ if (connectionSource instanceof sql.Request) {
2846
+ request = connectionSource;
2847
+ }
2848
+ else if (connectionSource instanceof sql.Transaction) {
2849
+ request = new sql.Request(connectionSource);
2850
+ }
2851
+ else if (connectionSource instanceof sql.ConnectionPool) {
2852
+ request = new sql.Request(connectionSource);
2853
+ }
2854
+ else {
2855
+ throw new Error('Invalid connection source type');
2856
+ }
2857
+ // Add all batch parameters to the request
2858
+ for (const [key, value] of Object.entries(batchParameters)) {
2859
+ request.input(key, value);
2860
+ }
2861
+ // Log the batch SQL
2862
+ const logPromise = SQLServerDataProvider.LogSQLStatement(batchSQL, batchParameters, 'Batch execution', false, undefined, contextUser);
2863
+ // Execute batch SQL and logging in parallel
2864
+ const [result] = await Promise.all([
2865
+ request.query(batchSQL),
2866
+ logPromise
2867
+ ]);
2944
2868
  // Return array of recordsets - one for each query
2945
2869
  // Handle both single and multiple recordsets
2946
2870
  if (result.recordsets && Array.isArray(result.recordsets)) {
@@ -2954,7 +2878,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2954
2878
  }
2955
2879
  }
2956
2880
  catch (e) {
2957
- (0, core_1.LogError)(e);
2881
+ // Error already logged by _internalExecuteSQLStatic
2958
2882
  throw e;
2959
2883
  }
2960
2884
  }
@@ -2969,28 +2893,19 @@ class SQLServerDataProvider extends core_1.ProviderBase {
2969
2893
  * @param options - Optional execution options for logging and description
2970
2894
  * @returns Promise<any[][]> - Array of result arrays, one for each query
2971
2895
  */
2972
- async ExecuteSQLBatch(queries, parameters, options) {
2896
+ async ExecuteSQLBatch(queries, parameters, options, contextUser) {
2973
2897
  try {
2974
2898
  let connectionSource;
2975
- if (this._transaction && this._transactionRequest) {
2976
- // Use transaction request if in a transaction
2977
- connectionSource = this._transactionRequest;
2978
- }
2979
- else if (this._transaction) {
2899
+ if (this._transaction) {
2980
2900
  // Use transaction if we have one
2981
2901
  connectionSource = this._transaction;
2982
2902
  }
2983
2903
  else {
2984
- // Use pool request for non-transactional queries
2904
+ // Use pool for non-transactional queries
2985
2905
  connectionSource = this._pool;
2986
2906
  }
2987
- // Log batch SQL statement to all active logging sessions
2988
- const description = options?.description ? `${options.description} (Batch: ${queries.length} queries)` : `Batch execution: ${queries.length} queries`;
2989
- const batchSQL = queries.join(';\n');
2990
- const loggingPromise = this._logSqlStatement(batchSQL, parameters, description, options?.ignoreLogging, options?.isMutation);
2991
- // Execute SQL and logging in parallel, but wait for both to complete
2992
- const [result] = await Promise.all([SQLServerDataProvider.ExecuteSQLBatchStatic(connectionSource, queries, parameters), loggingPromise]);
2993
- return result;
2907
+ // ExecuteSQLBatchStatic handles its own logging, so we don't need to duplicate it here
2908
+ return await SQLServerDataProvider.ExecuteSQLBatchStatic(connectionSource, queries, parameters, contextUser);
2994
2909
  }
2995
2910
  catch (e) {
2996
2911
  (0, core_1.LogError)(e);
@@ -3083,8 +2998,10 @@ class SQLServerDataProvider extends core_1.ProviderBase {
3083
2998
  // Create a new transaction from the pool
3084
2999
  this._transaction = new sql.Transaction(this._pool);
3085
3000
  await this._transaction.begin();
3086
- // Create a request for this transaction that can be reused
3087
- this._transactionRequest = new sql.Request(this._transaction);
3001
+ // Transaction created successfully
3002
+ // Note: We create new Request objects for each query to avoid concurrency issues
3003
+ // Emit transaction state change
3004
+ this._transactionState$.next(true);
3088
3005
  }
3089
3006
  catch (e) {
3090
3007
  (0, core_1.LogError)(e);
@@ -3096,7 +3013,10 @@ class SQLServerDataProvider extends core_1.ProviderBase {
3096
3013
  if (this._transaction) {
3097
3014
  await this._transaction.commit();
3098
3015
  this._transaction = null;
3099
- this._transactionRequest = null;
3016
+ // Emit transaction state change
3017
+ this._transactionState$.next(false);
3018
+ // Process any deferred tasks after successful commit
3019
+ await this.processDeferredTasks();
3100
3020
  }
3101
3021
  }
3102
3022
  catch (e) {
@@ -3109,7 +3029,14 @@ class SQLServerDataProvider extends core_1.ProviderBase {
3109
3029
  if (this._transaction) {
3110
3030
  await this._transaction.rollback();
3111
3031
  this._transaction = null;
3112
- this._transactionRequest = null;
3032
+ // Emit transaction state change
3033
+ this._transactionState$.next(false);
3034
+ // Clear deferred tasks after rollback (don't process them)
3035
+ const deferredCount = this._deferredTasks.length;
3036
+ this._deferredTasks = [];
3037
+ if (deferredCount > 0) {
3038
+ (0, core_1.LogStatus)(`Cleared ${deferredCount} deferred tasks after transaction rollback`);
3039
+ }
3113
3040
  }
3114
3041
  }
3115
3042
  catch (e) {
@@ -3117,14 +3044,56 @@ class SQLServerDataProvider extends core_1.ProviderBase {
3117
3044
  throw e; // force caller to handle
3118
3045
  }
3119
3046
  }
3047
+ /**
3048
+ * Override RefreshIfNeeded to skip refresh when a transaction is active
3049
+ * This prevents conflicts between metadata refresh operations and active transactions
3050
+ * @returns Promise<boolean> - true if refresh was performed, false if skipped or no refresh needed
3051
+ */
3052
+ async RefreshIfNeeded() {
3053
+ // Skip refresh if a transaction is active
3054
+ if (this.isTransactionActive) {
3055
+ (0, core_1.LogStatus)('Skipping metadata refresh - transaction is active');
3056
+ return false;
3057
+ }
3058
+ // Call parent implementation if no transaction
3059
+ return super.RefreshIfNeeded();
3060
+ }
3061
+ /**
3062
+ * Process any deferred tasks that were queued during a transaction
3063
+ * This is called after a successful transaction commit
3064
+ * @private
3065
+ */
3066
+ async processDeferredTasks() {
3067
+ if (this._deferredTasks.length === 0)
3068
+ return;
3069
+ (0, core_1.LogStatus)(`Processing ${this._deferredTasks.length} deferred tasks after transaction commit`);
3070
+ // Copy and clear the deferred tasks array
3071
+ const tasksToProcess = [...this._deferredTasks];
3072
+ this._deferredTasks = [];
3073
+ // Process each deferred task
3074
+ for (const task of tasksToProcess) {
3075
+ try {
3076
+ if (task.type === 'Entity AI Action') {
3077
+ // Process the AI action now that we're outside the transaction
3078
+ await queue_1.QueueManager.AddTask('Entity AI Action', task.data, task.options, task.user);
3079
+ }
3080
+ // Add other task types here as needed
3081
+ }
3082
+ catch (error) {
3083
+ (0, core_1.LogError)(`Failed to process deferred ${task.type} task: ${error}`);
3084
+ // Continue processing other tasks even if one fails
3085
+ }
3086
+ }
3087
+ (0, core_1.LogStatus)(`Completed processing deferred tasks`);
3088
+ }
3120
3089
  get LocalStorageProvider() {
3121
3090
  if (!this._localStorageProvider)
3122
3091
  this._localStorageProvider = new NodeLocalStorageProvider();
3123
3092
  return this._localStorageProvider;
3124
3093
  }
3125
- async GetEntityRecordNames(info) {
3094
+ async GetEntityRecordNames(info, contextUser) {
3126
3095
  const promises = info.map(async (item) => {
3127
- const r = await this.GetEntityRecordName(item.EntityName, item.CompositeKey);
3096
+ const r = await this.GetEntityRecordName(item.EntityName, item.CompositeKey, contextUser);
3128
3097
  return {
3129
3098
  EntityName: item.EntityName,
3130
3099
  CompositeKey: item.CompositeKey,
@@ -3135,11 +3104,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
3135
3104
  });
3136
3105
  return Promise.all(promises);
3137
3106
  }
3138
- async GetEntityRecordName(entityName, CompositeKey) {
3107
+ async GetEntityRecordName(entityName, CompositeKey, contextUser) {
3139
3108
  try {
3140
3109
  const sql = this.GetEntityRecordNameSQL(entityName, CompositeKey);
3141
3110
  if (sql) {
3142
- const data = await this.ExecuteSQL(sql);
3111
+ const data = await this.ExecuteSQL(sql, null, undefined, contextUser);
3143
3112
  if (data && data.length === 1) {
3144
3113
  const fields = Object.keys(data[0]);
3145
3114
  return data[0][fields[0]]; // return first field
@@ -3198,7 +3167,7 @@ class NodeLocalStorageProvider {
3198
3167
  constructor() {
3199
3168
  this._localStorage = {};
3200
3169
  }
3201
- async getItem(key) {
3170
+ async GetItem(key) {
3202
3171
  return new Promise((resolve) => {
3203
3172
  if (this._localStorage.hasOwnProperty(key))
3204
3173
  resolve(this._localStorage[key]);
@@ -3206,13 +3175,13 @@ class NodeLocalStorageProvider {
3206
3175
  resolve(null);
3207
3176
  });
3208
3177
  }
3209
- async setItem(key, value) {
3178
+ async SetItem(key, value) {
3210
3179
  return new Promise((resolve) => {
3211
3180
  this._localStorage[key] = value;
3212
3181
  resolve();
3213
3182
  });
3214
3183
  }
3215
- async remove(key) {
3184
+ async Remove(key) {
3216
3185
  return new Promise((resolve) => {
3217
3186
  if (this._localStorage.hasOwnProperty(key))
3218
3187
  delete this._localStorage[key];