@memberjunction/sqlserver-dataprovider 2.49.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.
- package/README.md +223 -19
- package/dist/SQLServerDataProvider.d.ts +93 -114
- package/dist/SQLServerDataProvider.d.ts.map +1 -1
- package/dist/SQLServerDataProvider.js +413 -444
- package/dist/SQLServerDataProvider.js.map +1 -1
- package/dist/SqlLogger.d.ts +61 -0
- package/dist/SqlLogger.d.ts.map +1 -0
- package/dist/SqlLogger.js +282 -0
- package/dist/SqlLogger.js.map +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/types.js.map +1 -0
- package/package.json +10 -9
|
@@ -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 =
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
473
|
-
const
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
2792
|
-
*
|
|
2793
|
-
*
|
|
2794
|
-
* @param
|
|
2795
|
-
* @
|
|
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
|
|
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 (
|
|
2801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
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
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2921
|
-
|
|
2819
|
+
queryParams.forEach((value, localIndex) => {
|
|
2820
|
+
const paramName = `p${globalParamIndex}`;
|
|
2821
|
+
batchParameters[paramName] = value;
|
|
2822
|
+
globalParamIndex++;
|
|
2922
2823
|
});
|
|
2923
|
-
// Replace
|
|
2924
|
-
let localParamIndex =
|
|
2925
|
-
|
|
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
|
-
|
|
2832
|
+
batchParameters[paramName] = value;
|
|
2932
2833
|
// Replace parameter references in query
|
|
2933
|
-
|
|
2834
|
+
processedQuery = processedQuery.replace(new RegExp(`@${key}\\b`, 'g'), `@${paramName}`);
|
|
2934
2835
|
}
|
|
2935
2836
|
}
|
|
2936
2837
|
}
|
|
2937
|
-
batchSQL +=
|
|
2838
|
+
batchSQL += processedQuery;
|
|
2938
2839
|
if (queryIndex < queries.length - 1) {
|
|
2939
2840
|
batchSQL += ';\n';
|
|
2940
2841
|
}
|
|
2941
2842
|
});
|
|
2942
|
-
// Execute the batch SQL
|
|
2943
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2904
|
+
// Use pool for non-transactional queries
|
|
2985
2905
|
connectionSource = this._pool;
|
|
2986
2906
|
}
|
|
2987
|
-
//
|
|
2988
|
-
|
|
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
|
-
//
|
|
3087
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3184
|
+
async Remove(key) {
|
|
3216
3185
|
return new Promise((resolve) => {
|
|
3217
3186
|
if (this._localStorage.hasOwnProperty(key))
|
|
3218
3187
|
delete this._localStorage[key];
|