@memberjunction/sqlserver-dataprovider 2.48.0 → 2.49.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 +58 -0
- package/dist/SQLServerDataProvider.d.ts +281 -6
- package/dist/SQLServerDataProvider.d.ts.map +1 -1
- package/dist/SQLServerDataProvider.js +842 -93
- package/dist/SQLServerDataProvider.js.map +1 -1
- package/dist/SQLServerTransactionGroup.d.ts.map +1 -1
- package/dist/SQLServerTransactionGroup.js +99 -20
- package/dist/SQLServerTransactionGroup.js.map +1 -1
- package/dist/UserCache.d.ts +3 -3
- package/dist/UserCache.d.ts.map +1 -1
- package/dist/UserCache.js +33 -5
- package/dist/UserCache.js.map +1 -1
- package/dist/config.js +10 -10
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
|
@@ -1,25 +1,77 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview SQL Server Data Provider for MemberJunction
|
|
4
|
+
*
|
|
5
|
+
* This module provides a comprehensive SQL Server implementation of the MemberJunction data provider interfaces.
|
|
6
|
+
* It handles all database operations including CRUD operations, metadata management, view execution, and
|
|
7
|
+
* advanced features like SQL logging, transaction management, and record change tracking.
|
|
8
|
+
*
|
|
9
|
+
* @module @memberjunction/sqlserver-dataprovider
|
|
10
|
+
* @author MemberJunction Team
|
|
11
|
+
* @version 2.0
|
|
12
|
+
* @since 1.0
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.SQLServerDataProvider = exports.SQLServerProviderConfigData = void 0;
|
|
2
39
|
/**************************************************************************************************************
|
|
3
40
|
* The SQLServerDataProvider provides a data provider for the entities framework that uses SQL Server directly
|
|
4
41
|
* In practice - this FILE will NOT exist in the entities library, we need to move to its own separate project
|
|
5
42
|
* so it is only included by the consumer of the entities library if they want to use it.
|
|
6
43
|
**************************************************************************************************************/
|
|
7
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.SQLServerDataProvider = exports.SQLServerProviderConfigData = void 0;
|
|
9
44
|
const core_1 = require("@memberjunction/core");
|
|
10
45
|
const core_entities_1 = require("@memberjunction/core-entities");
|
|
11
46
|
const aiengine_1 = require("@memberjunction/aiengine");
|
|
12
47
|
const queue_1 = require("@memberjunction/queue");
|
|
48
|
+
const sql = __importStar(require("mssql"));
|
|
13
49
|
const SQLServerTransactionGroup_1 = require("./SQLServerTransactionGroup");
|
|
14
50
|
const UserCache_1 = require("./UserCache");
|
|
15
51
|
const actions_1 = require("@memberjunction/actions");
|
|
52
|
+
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
|
+
*/
|
|
16
59
|
class SQLServerProviderConfigData extends core_1.ProviderConfigDataBase {
|
|
60
|
+
/**
|
|
61
|
+
* Gets the SQL Server data source configuration
|
|
62
|
+
*/
|
|
17
63
|
get DataSource() {
|
|
18
64
|
return this.Data.DataSource;
|
|
19
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Gets the current user's email address
|
|
68
|
+
*/
|
|
20
69
|
get CurrentUserEmail() {
|
|
21
70
|
return this.Data.CurrentUserEmail;
|
|
22
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Gets the interval in seconds for checking metadata refresh
|
|
74
|
+
*/
|
|
23
75
|
get CheckRefreshIntervalSeconds() {
|
|
24
76
|
return this.Data.CheckRefreshIntervalSeconds;
|
|
25
77
|
}
|
|
@@ -32,20 +84,255 @@ class SQLServerProviderConfigData extends core_1.ProviderConfigDataBase {
|
|
|
32
84
|
}
|
|
33
85
|
}
|
|
34
86
|
exports.SQLServerProviderConfigData = SQLServerProviderConfigData;
|
|
35
|
-
|
|
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
|
+
/**
|
|
295
|
+
* SQL Server implementation of the MemberJunction data provider interfaces.
|
|
296
|
+
*
|
|
297
|
+
* This class provides comprehensive database functionality including:
|
|
298
|
+
* - CRUD operations for entities
|
|
299
|
+
* - Metadata management and caching
|
|
300
|
+
* - View and query execution
|
|
301
|
+
* - Transaction support
|
|
302
|
+
* - SQL logging capabilities
|
|
303
|
+
* - Record change tracking
|
|
304
|
+
* - AI integration hooks
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* const config = new SQLServerProviderConfigData(dataSource, userEmail);
|
|
309
|
+
* const provider = new SQLServerDataProvider(config);
|
|
310
|
+
* await provider.Config();
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
36
313
|
class SQLServerDataProvider extends core_1.ProviderBase {
|
|
37
314
|
constructor() {
|
|
38
315
|
super(...arguments);
|
|
39
316
|
this._bAllowRefresh = true;
|
|
40
317
|
this._needsDatetimeOffsetAdjustment = false;
|
|
41
318
|
this._datetimeOffsetTestComplete = false;
|
|
319
|
+
this._sqlLoggingSessions = new Map();
|
|
42
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Gets the current configuration data for this provider instance
|
|
323
|
+
*/
|
|
43
324
|
get ConfigData() {
|
|
44
325
|
return super.ConfigData;
|
|
45
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Configures the SQL Server data provider with connection settings and initializes the connection pool
|
|
329
|
+
*
|
|
330
|
+
* @param configData - Configuration data including connection string and options
|
|
331
|
+
* @returns Promise<boolean> - True if configuration succeeded
|
|
332
|
+
*/
|
|
46
333
|
async Config(configData) {
|
|
47
334
|
try {
|
|
48
|
-
this.
|
|
335
|
+
this._pool = configData.DataSource; // Now expects a ConnectionPool instead of DataSource
|
|
49
336
|
this._currentUserEmail = configData.CurrentUserEmail;
|
|
50
337
|
return super.Config(configData); // now parent class can do it's config
|
|
51
338
|
}
|
|
@@ -55,34 +342,157 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
55
342
|
}
|
|
56
343
|
}
|
|
57
344
|
/**
|
|
58
|
-
*
|
|
345
|
+
* Gets the underlying SQL Server connection pool
|
|
346
|
+
* @returns The mssql ConnectionPool object
|
|
59
347
|
*/
|
|
60
348
|
get DatabaseConnection() {
|
|
61
|
-
return this.
|
|
349
|
+
return this._pool;
|
|
62
350
|
}
|
|
63
351
|
/**
|
|
64
352
|
* For the SQLServerDataProvider the unique instance connection string which is used to identify, uniquely, a given connection is the following format:
|
|
65
|
-
*
|
|
353
|
+
* mssql://host:port/instanceName?/database
|
|
66
354
|
* instanceName is only inserted if it is provided in the options
|
|
67
355
|
*/
|
|
68
356
|
get InstanceConnectionString() {
|
|
69
|
-
|
|
357
|
+
// For mssql, we need to access the pool's internal connection options
|
|
358
|
+
// Since mssql v11 doesn't expose config directly, we'll construct from what we know
|
|
359
|
+
const pool = this._pool;
|
|
70
360
|
const options = {
|
|
71
|
-
type:
|
|
72
|
-
host:
|
|
73
|
-
port:
|
|
74
|
-
instanceName:
|
|
75
|
-
database:
|
|
361
|
+
type: 'mssql',
|
|
362
|
+
host: pool._config?.server || 'localhost',
|
|
363
|
+
port: pool._config?.port || 1433,
|
|
364
|
+
instanceName: pool._config?.options?.instanceName ? '/' + pool._config.options.instanceName : '',
|
|
365
|
+
database: pool._config?.database || '',
|
|
76
366
|
};
|
|
77
367
|
return options.type + '://' + options.host + ':' + options.port + options.instanceName + '/' + options.database;
|
|
78
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Gets whether metadata refresh is currently allowed
|
|
371
|
+
* @internal
|
|
372
|
+
*/
|
|
79
373
|
get AllowRefresh() {
|
|
80
374
|
return this._bAllowRefresh;
|
|
81
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Gets the MemberJunction core schema name (defaults to __mj if not configured)
|
|
378
|
+
*/
|
|
82
379
|
get MJCoreSchemaName() {
|
|
83
380
|
return this.ConfigData.MJCoreSchemaName;
|
|
84
381
|
}
|
|
85
382
|
/**************************************************************************/
|
|
383
|
+
// START ---- SQL Logging Methods
|
|
384
|
+
/**************************************************************************/
|
|
385
|
+
/**
|
|
386
|
+
* Creates a new SQL logging session that will capture all SQL operations to a file.
|
|
387
|
+
* Returns a disposable session object that must be disposed to stop logging.
|
|
388
|
+
*
|
|
389
|
+
* @param filePath - Full path to the file where SQL statements will be logged
|
|
390
|
+
* @param options - Optional configuration for the logging session
|
|
391
|
+
* @returns Promise<SqlLoggingSession> - Disposable session object
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```typescript
|
|
395
|
+
* // Basic usage
|
|
396
|
+
* const session = await provider.createSqlLogger('./logs/metadata-sync.sql');
|
|
397
|
+
* try {
|
|
398
|
+
* // Perform operations that will be logged
|
|
399
|
+
* await provider.ExecuteSQL('INSERT INTO ...');
|
|
400
|
+
* } finally {
|
|
401
|
+
* await session.dispose(); // Stop logging
|
|
402
|
+
* }
|
|
403
|
+
*
|
|
404
|
+
* // With migration formatting
|
|
405
|
+
* const session = await provider.createSqlLogger('./migrations/changes.sql', {
|
|
406
|
+
* formatAsMigration: true,
|
|
407
|
+
* description: 'MetadataSync push operation'
|
|
408
|
+
* });
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
async createSqlLogger(filePath, options) {
|
|
412
|
+
const sessionId = (0, uuid_1.v4)();
|
|
413
|
+
const session = new SqlLoggingSessionImpl(sessionId, filePath, options);
|
|
414
|
+
// Initialize the session (create file, write header)
|
|
415
|
+
await session.initialize();
|
|
416
|
+
// Store in active sessions map
|
|
417
|
+
this._sqlLoggingSessions.set(sessionId, session);
|
|
418
|
+
// Return a proxy that handles cleanup on dispose
|
|
419
|
+
return {
|
|
420
|
+
id: session.id,
|
|
421
|
+
filePath: session.filePath,
|
|
422
|
+
startTime: session.startTime,
|
|
423
|
+
get statementCount() {
|
|
424
|
+
return session.statementCount;
|
|
425
|
+
},
|
|
426
|
+
options: session.options,
|
|
427
|
+
dispose: async () => {
|
|
428
|
+
await session.dispose();
|
|
429
|
+
this._sqlLoggingSessions.delete(sessionId);
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Gets information about all active SQL logging sessions.
|
|
435
|
+
* Useful for monitoring and debugging.
|
|
436
|
+
*
|
|
437
|
+
* @returns Array of session information objects
|
|
438
|
+
*/
|
|
439
|
+
getActiveSqlLoggingSessions() {
|
|
440
|
+
return Array.from(this._sqlLoggingSessions.values()).map((session) => ({
|
|
441
|
+
id: session.id,
|
|
442
|
+
filePath: session.filePath,
|
|
443
|
+
startTime: session.startTime,
|
|
444
|
+
statementCount: session.statementCount,
|
|
445
|
+
options: session.options,
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Disposes all active SQL logging sessions.
|
|
450
|
+
* Useful for cleanup on provider shutdown.
|
|
451
|
+
*/
|
|
452
|
+
async disposeAllSqlLoggingSessions() {
|
|
453
|
+
const disposePromises = Array.from(this._sqlLoggingSessions.values()).map((session) => session.dispose());
|
|
454
|
+
await Promise.all(disposePromises);
|
|
455
|
+
this._sqlLoggingSessions.clear();
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Internal method to log SQL statement to all active logging sessions.
|
|
459
|
+
* This is called automatically by ExecuteSQL methods.
|
|
460
|
+
*
|
|
461
|
+
* @param query - The SQL query being executed
|
|
462
|
+
* @param parameters - Parameters for the query
|
|
463
|
+
* @param description - Optional description for this operation
|
|
464
|
+
* @param ignoreLogging - If true, this statement will not be logged
|
|
465
|
+
* @param isMutation - Whether this is a data mutation operation
|
|
466
|
+
* @param simpleSQLFallback - Optional simple SQL to use for loggers with logRecordChangeMetadata=false
|
|
467
|
+
*/
|
|
468
|
+
async _logSqlStatement(query, parameters, description, ignoreLogging = false, isMutation = false, simpleSQLFallback) {
|
|
469
|
+
if (ignoreLogging || this._sqlLoggingSessions.size === 0) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
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));
|
|
474
|
+
await Promise.all(logPromises);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Static method to log SQL statements from external sources like transaction groups
|
|
478
|
+
*
|
|
479
|
+
* @param query - The SQL query being executed
|
|
480
|
+
* @param parameters - Parameters for the query
|
|
481
|
+
* @param description - Optional description for this operation
|
|
482
|
+
* @param isMutation - Whether this is a data mutation operation
|
|
483
|
+
* @param simpleSQLFallback - Optional simple SQL to use for loggers with logRecordChangeMetadata=false
|
|
484
|
+
*/
|
|
485
|
+
static async LogSQLStatement(query, parameters, description, isMutation = false, simpleSQLFallback) {
|
|
486
|
+
// Get the current provider instance
|
|
487
|
+
const provider = core_1.Metadata.Provider;
|
|
488
|
+
if (provider && provider._sqlLoggingSessions.size > 0) {
|
|
489
|
+
await provider._logSqlStatement(query, parameters, description, false, isMutation, simpleSQLFallback);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**************************************************************************/
|
|
493
|
+
// END ---- SQL Logging Methods
|
|
494
|
+
/**************************************************************************/
|
|
495
|
+
/**************************************************************************/
|
|
86
496
|
// START ---- IRunReportProvider
|
|
87
497
|
/**************************************************************************/
|
|
88
498
|
async RunReport(params, contextUser) {
|
|
@@ -98,7 +508,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
98
508
|
if (result)
|
|
99
509
|
return {
|
|
100
510
|
Success: true,
|
|
101
|
-
ReportID
|
|
511
|
+
ReportID,
|
|
102
512
|
Results: result,
|
|
103
513
|
RowCount: result.length,
|
|
104
514
|
ExecutionTime: end - start,
|
|
@@ -107,7 +517,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
107
517
|
else
|
|
108
518
|
return {
|
|
109
519
|
Success: false,
|
|
110
|
-
ReportID
|
|
520
|
+
ReportID,
|
|
111
521
|
Results: [],
|
|
112
522
|
RowCount: 0,
|
|
113
523
|
ExecutionTime: end - start,
|
|
@@ -115,7 +525,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
115
525
|
};
|
|
116
526
|
}
|
|
117
527
|
else
|
|
118
|
-
return { Success: false, ReportID
|
|
528
|
+
return { Success: false, ReportID, Results: [], RowCount: 0, ExecutionTime: 0, ErrorMessage: 'Report not found' };
|
|
119
529
|
}
|
|
120
530
|
/**************************************************************************/
|
|
121
531
|
// END ---- IRunReportProvider
|
|
@@ -167,7 +577,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
167
577
|
Results: [],
|
|
168
578
|
RowCount: 0,
|
|
169
579
|
ExecutionTime: 0,
|
|
170
|
-
ErrorMessage: e.message
|
|
580
|
+
ErrorMessage: e.message,
|
|
171
581
|
};
|
|
172
582
|
}
|
|
173
583
|
}
|
|
@@ -393,22 +803,20 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
393
803
|
viewSQL += ` OFFSET ${params.StartRow} ROWS FETCH NEXT ${params.MaxRows} ROWS ONLY`;
|
|
394
804
|
}
|
|
395
805
|
// now we can run the viewSQL, but only do this if the ResultType !== 'count_only', otherwise we don't need to run the viewSQL
|
|
396
|
-
const retData = params.ResultType === 'count_only' ? [] : await this.
|
|
806
|
+
const retData = params.ResultType === 'count_only' ? [] : await this.ExecuteSQL(viewSQL);
|
|
397
807
|
// finally, if we have a countSQL, we need to run that to get the row count
|
|
398
808
|
// but only do that if the # of rows returned is equal to the max rows, otherwise we know we have all the rows
|
|
399
809
|
// OR do that if we are doing a count_only
|
|
400
810
|
let rowCount = null;
|
|
401
811
|
if (countSQL && (params.ResultType === 'count_only' || retData.length === entityInfo.UserViewMaxRows)) {
|
|
402
|
-
const countResult = await this.
|
|
812
|
+
const countResult = await this.ExecuteSQL(countSQL);
|
|
403
813
|
if (countResult && countResult.length > 0) {
|
|
404
814
|
rowCount = countResult[0].TotalRowCount;
|
|
405
815
|
}
|
|
406
816
|
}
|
|
407
817
|
const stopTime = new Date();
|
|
408
818
|
if (params.ForceAuditLog ||
|
|
409
|
-
(viewEntity?.ID &&
|
|
410
|
-
(extraFilter === undefined || extraFilter === null || extraFilter?.trim().length === 0) &&
|
|
411
|
-
entityInfo.AuditViewRuns)) {
|
|
819
|
+
(viewEntity?.ID && (extraFilter === undefined || extraFilter === null || extraFilter?.trim().length === 0) && entityInfo.AuditViewRuns)) {
|
|
412
820
|
// ONLY LOG TOP LEVEL VIEW EXECUTION - this would be for views with an ID, and don't have ExtraFilter as ExtraFilter
|
|
413
821
|
// is only used in the system on a tab or just for ad hoc view execution
|
|
414
822
|
// we do NOT want to wait for this, so no await,
|
|
@@ -450,7 +858,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
450
858
|
}
|
|
451
859
|
}
|
|
452
860
|
async RunViews(params, contextUser) {
|
|
453
|
-
const promises = params.map(p => this.RunView(p, contextUser));
|
|
861
|
+
const promises = params.map((p) => this.RunView(p, contextUser));
|
|
454
862
|
return Promise.all(promises);
|
|
455
863
|
}
|
|
456
864
|
validateUserProvidedSQLClause(clause) {
|
|
@@ -559,12 +967,12 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
559
967
|
INSERT INTO @ViewIDList (ID) (SELECT ${entityInfo.FirstPrimaryKey.Name} FROM [${entityInfo.SchemaName}].${entityBaseView} WHERE (${whereSQL}))
|
|
560
968
|
EXEC [${this.MJCoreSchemaName}].spCreateUserViewRunWithDetail(${viewId},${user.Email}, @ViewIDLIst)
|
|
561
969
|
`;
|
|
562
|
-
const runIDResult = await this.
|
|
970
|
+
const runIDResult = await this.ExecuteSQL(sSQL);
|
|
563
971
|
const runID = runIDResult[0].UserViewRunID;
|
|
564
972
|
const sRetSQL = `SELECT * FROM [${entityInfo.SchemaName}].${entityBaseView} WHERE ${entityInfo.FirstPrimaryKey.Name} IN
|
|
565
973
|
(SELECT RecordID FROM [${this.MJCoreSchemaName}].vwUserViewRunDetails WHERE UserViewRunID=${runID})
|
|
566
974
|
${orderBySQL && orderBySQL.length > 0 ? ' ORDER BY ' + orderBySQL : ''}`;
|
|
567
|
-
return { executeViewSQL: sRetSQL, runID
|
|
975
|
+
return { executeViewSQL: sRetSQL, runID };
|
|
568
976
|
}
|
|
569
977
|
createViewUserSearchSQL(entityInfo, userSearchString) {
|
|
570
978
|
// we have a user search string.
|
|
@@ -629,9 +1037,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
629
1037
|
const authorization = authorizationName
|
|
630
1038
|
? this.Authorizations.find((a) => a?.Name?.trim().toLowerCase() === authorizationName.trim().toLowerCase())
|
|
631
1039
|
: null;
|
|
632
|
-
const auditLogType = auditLogTypeName
|
|
633
|
-
? this.AuditLogTypes.find((a) => a?.Name?.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase())
|
|
634
|
-
: null;
|
|
1040
|
+
const auditLogType = auditLogTypeName ? this.AuditLogTypes.find((a) => a?.Name?.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase()) : null;
|
|
635
1041
|
if (!user)
|
|
636
1042
|
throw new Error(`User is a required parameter`);
|
|
637
1043
|
if (!auditLogType)
|
|
@@ -840,7 +1246,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
840
1246
|
// entityInfo.PrimaryKeys.forEach((pk) => {
|
|
841
1247
|
// pkeyValues.push({FieldName: pk.Name, Value: r[pk.Name]}) // add all of the primary keys, which often is as simple as just "ID", but this is generic way to do it
|
|
842
1248
|
// })
|
|
843
|
-
|
|
1249
|
+
const compositeKey = new core_1.CompositeKey();
|
|
844
1250
|
// the row r will have a PrimaryKeyValue field that is a string that is a concatenation of the primary key field names and values
|
|
845
1251
|
// we need to parse that out so that we can then pass it to the CompositeKey object
|
|
846
1252
|
const pkeys = {};
|
|
@@ -889,11 +1295,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
889
1295
|
}
|
|
890
1296
|
const listEntity = await this.GetEntityObject('Lists');
|
|
891
1297
|
listEntity.ContextCurrentUser = contextUser;
|
|
892
|
-
|
|
1298
|
+
const success = await listEntity.Load(params.ListID);
|
|
893
1299
|
if (!success) {
|
|
894
1300
|
throw new Error(`List with ID ${params.ListID} not found.`);
|
|
895
1301
|
}
|
|
896
|
-
|
|
1302
|
+
const duplicateRun = await this.GetEntityObject('Duplicate Runs');
|
|
897
1303
|
duplicateRun.NewRecord();
|
|
898
1304
|
duplicateRun.EntityID = params.EntityID;
|
|
899
1305
|
duplicateRun.StartedByUserID = contextUser.ID;
|
|
@@ -906,7 +1312,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
906
1312
|
if (!saveResult) {
|
|
907
1313
|
throw new Error(`Failed to save Duplicate Run Entity`);
|
|
908
1314
|
}
|
|
909
|
-
|
|
1315
|
+
const response = {
|
|
910
1316
|
Status: 'Inprogress',
|
|
911
1317
|
PotentialDuplicateResult: [],
|
|
912
1318
|
};
|
|
@@ -1043,7 +1449,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1043
1449
|
if (await recordMergeLog.Save()) {
|
|
1044
1450
|
// top level saved, now let's create the deletion detail records for each of the records that were merged
|
|
1045
1451
|
for (const d of result.RecordStatus) {
|
|
1046
|
-
const recordMergeDeletionLog =
|
|
1452
|
+
const recordMergeDeletionLog = await this.GetEntityObject('Record Merge Deletion Logs', contextUser);
|
|
1047
1453
|
recordMergeDeletionLog.NewRecord();
|
|
1048
1454
|
recordMergeDeletionLog.RecordMergeLogID = recordMergeLog.ID;
|
|
1049
1455
|
recordMergeDeletionLog.DeletedRecordID = d.CompositeKey.Values(); // this would join together all of the primary key values, which is fine as the primary key is a string
|
|
@@ -1067,6 +1473,14 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1067
1473
|
* it is also used by the SQLServerTransactionGroup to regenerate Save SQL if any values were changed by the transaction group due to transaction variables being set into the object.
|
|
1068
1474
|
*/
|
|
1069
1475
|
GetSaveSQL(entity, bNewRecord, spName, user) {
|
|
1476
|
+
const result = this.GetSaveSQLWithDetails(entity, bNewRecord, spName, user);
|
|
1477
|
+
return result.fullSQL;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* This function generates both the full SQL (with record change metadata) and the simple stored procedure call
|
|
1481
|
+
* @returns Object with fullSQL and simpleSQL properties
|
|
1482
|
+
*/
|
|
1483
|
+
GetSaveSQLWithDetails(entity, bNewRecord, spName, user) {
|
|
1070
1484
|
const sSimpleSQL = `EXEC [${entity.EntityInfo.SchemaName}].${spName} ${this.generateSPParams(entity, !bNewRecord)}`;
|
|
1071
1485
|
const recordChangesEntityInfo = this.Entities.find((e) => e.Name === 'Record Changes');
|
|
1072
1486
|
let sSQL = '';
|
|
@@ -1081,34 +1495,54 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1081
1495
|
sSQL = `
|
|
1082
1496
|
DECLARE @ResultTable TABLE (
|
|
1083
1497
|
${this.getAllEntityColumnsSQL(entity.EntityInfo)}
|
|
1084
|
-
)
|
|
1498
|
+
);
|
|
1085
1499
|
|
|
1086
1500
|
INSERT INTO @ResultTable
|
|
1087
|
-
${sSimpleSQL}
|
|
1501
|
+
${sSimpleSQL};
|
|
1088
1502
|
|
|
1089
|
-
DECLARE @ID NVARCHAR(MAX)
|
|
1090
|
-
|
|
1503
|
+
DECLARE @ID NVARCHAR(MAX);
|
|
1504
|
+
|
|
1505
|
+
SELECT @ID = ${concatPKIDString} FROM @ResultTable;
|
|
1506
|
+
|
|
1091
1507
|
IF @ID IS NOT NULL
|
|
1092
1508
|
BEGIN
|
|
1093
1509
|
DECLARE @ResultChangesTable TABLE (
|
|
1094
1510
|
${this.getAllEntityColumnsSQL(recordChangesEntityInfo)}
|
|
1095
|
-
)
|
|
1511
|
+
);
|
|
1096
1512
|
|
|
1097
1513
|
INSERT INTO @ResultChangesTable
|
|
1098
|
-
${this.GetLogRecordChangeSQL(entity.GetAll(false), oldData, entity.EntityInfo.Name, '@ID', entity.EntityInfo, bNewRecord ? 'Create' : 'Update', user, false)}
|
|
1099
|
-
END
|
|
1514
|
+
${this.GetLogRecordChangeSQL(entity.GetAll(false), oldData, entity.EntityInfo.Name, '@ID', entity.EntityInfo, bNewRecord ? 'Create' : 'Update', user, false)};
|
|
1515
|
+
END;
|
|
1100
1516
|
|
|
1101
|
-
SELECT * FROM @ResultTable
|
|
1517
|
+
SELECT * FROM @ResultTable;`; // NOTE - in the above, we call the T-SQL variable @ID for simplicity just as a variable name, even though for each entity the pkey could be something else. Entity pkeys are not always a field called ID could be something else including composite keys.
|
|
1102
1518
|
}
|
|
1103
1519
|
else {
|
|
1104
1520
|
// not doing track changes for this entity, keep it simple
|
|
1105
1521
|
sSQL = sSimpleSQL;
|
|
1106
1522
|
}
|
|
1107
|
-
return sSQL;
|
|
1523
|
+
return { fullSQL: sSQL, simpleSQL: sSimpleSQL };
|
|
1108
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Gets AI actions configured for an entity based on trigger timing
|
|
1527
|
+
*
|
|
1528
|
+
* @param entityInfo - The entity to get AI actions for
|
|
1529
|
+
* @param before - True to get before-save actions, false for after-save
|
|
1530
|
+
* @returns Array of AI action entities
|
|
1531
|
+
* @internal
|
|
1532
|
+
*/
|
|
1109
1533
|
GetEntityAIActions(entityInfo, before) {
|
|
1110
1534
|
return aiengine_1.AIEngine.Instance.EntityAIActions.filter((a) => a.EntityID === entityInfo.ID && a.TriggerEvent.toLowerCase().trim() === (before ? 'before save' : 'after save'));
|
|
1111
1535
|
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Handles entity actions (non-AI) for save, delete, or validate operations
|
|
1538
|
+
*
|
|
1539
|
+
* @param entity - The entity being operated on
|
|
1540
|
+
* @param baseType - The type of operation
|
|
1541
|
+
* @param before - Whether this is before or after the operation
|
|
1542
|
+
* @param user - The user performing the operation
|
|
1543
|
+
* @returns Array of action results
|
|
1544
|
+
* @internal
|
|
1545
|
+
*/
|
|
1112
1546
|
async HandleEntityActions(entity, baseType, before, user) {
|
|
1113
1547
|
// use the EntityActionEngine for this
|
|
1114
1548
|
try {
|
|
@@ -1247,14 +1681,19 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1247
1681
|
// but they are supported (for now)
|
|
1248
1682
|
await this.HandleEntityAIActions(entity, 'save', true, user);
|
|
1249
1683
|
}
|
|
1250
|
-
const
|
|
1684
|
+
const sqlDetails = this.GetSaveSQLWithDetails(entity, bNewRecord, spName, user);
|
|
1685
|
+
const sSQL = sqlDetails.fullSQL;
|
|
1251
1686
|
if (entity.TransactionGroup && !bReplay /*we never participate in a transaction if we're in replay mode*/) {
|
|
1252
1687
|
// we have a transaction group, need to play nice and be part of it
|
|
1253
1688
|
entity.RaiseReadyForTransaction(); // let the entity know we're ready to be part of the transaction
|
|
1254
1689
|
// we are part of a transaction group, so just add our query to the list
|
|
1255
1690
|
// and when the transaction is committed, we will send all the queries at once
|
|
1256
1691
|
this._bAllowRefresh = false; // stop refreshes of metadata while we're doing work
|
|
1257
|
-
entity.TransactionGroup.AddTransaction(new core_1.TransactionItem(entity, entityResult.Type === 'create' ? 'Create' : 'Update', sSQL, null, {
|
|
1692
|
+
entity.TransactionGroup.AddTransaction(new core_1.TransactionItem(entity, entityResult.Type === 'create' ? 'Create' : 'Update', sSQL, null, {
|
|
1693
|
+
dataSource: this._pool,
|
|
1694
|
+
simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined,
|
|
1695
|
+
entityName: entity.EntityInfo.Name
|
|
1696
|
+
}, (transactionResult, success) => {
|
|
1258
1697
|
// we get here whenever the transaction group does gets around to committing
|
|
1259
1698
|
// our query.
|
|
1260
1699
|
this._bAllowRefresh = true; // allow refreshes again
|
|
@@ -1290,7 +1729,12 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1290
1729
|
result = [entity.GetAll()]; // just return the entity as it was before the save as we are NOT saving anything as we are in replay mode
|
|
1291
1730
|
}
|
|
1292
1731
|
else {
|
|
1293
|
-
|
|
1732
|
+
// Execute SQL with optional simple SQL fallback for loggers
|
|
1733
|
+
const rawResult = await this.ExecuteSQL(sSQL, null, {
|
|
1734
|
+
isMutation: true,
|
|
1735
|
+
description: `Save ${entity.EntityInfo.Name}`,
|
|
1736
|
+
simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined
|
|
1737
|
+
});
|
|
1294
1738
|
result = await this.ProcessEntityRows(rawResult, entity.EntityInfo);
|
|
1295
1739
|
}
|
|
1296
1740
|
this._bAllowRefresh = true; // allow refreshes now
|
|
@@ -1324,10 +1768,10 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1324
1768
|
}
|
|
1325
1769
|
}
|
|
1326
1770
|
MapTransactionResultToNewValues(transactionResult) {
|
|
1327
|
-
return Object.keys(transactionResult).map(k => {
|
|
1771
|
+
return Object.keys(transactionResult).map((k) => {
|
|
1328
1772
|
return {
|
|
1329
1773
|
FieldName: k,
|
|
1330
|
-
Value: transactionResult[k]
|
|
1774
|
+
Value: transactionResult[k],
|
|
1331
1775
|
};
|
|
1332
1776
|
}); // transform the result into a list of field/value pairs
|
|
1333
1777
|
}
|
|
@@ -1362,22 +1806,26 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1362
1806
|
let sRet = '', bFirst = true;
|
|
1363
1807
|
for (let i = 0; i < entity.EntityInfo.Fields.length; i++) {
|
|
1364
1808
|
const f = entity.EntityInfo.Fields[i];
|
|
1365
|
-
|
|
1366
|
-
|
|
1809
|
+
// For CREATE operations, include primary keys that are not auto-increment and have actual values
|
|
1810
|
+
const includePrimaryKeyForCreate = !isUpdate && f.IsPrimaryKey && !f.AutoIncrement && entity.Get(f.Name) !== null && entity.Get(f.Name) !== undefined;
|
|
1811
|
+
if (f.AllowUpdateAPI || includePrimaryKeyForCreate) {
|
|
1812
|
+
if (!f.SkipValidation || includePrimaryKeyForCreate) {
|
|
1367
1813
|
// DO NOT INCLUDE any fields where we skip validation, these are fields that are not editable by the user/object
|
|
1368
1814
|
// model/api because they're special fields like ID, CreatedAt, etc. or they're virtual or auto-increment, etc.
|
|
1815
|
+
// EXCEPTION: Include primary keys for CREATE when they have values and are not auto-increment
|
|
1369
1816
|
let value = entity.Get(f.Name);
|
|
1370
1817
|
if (value && f.Type.trim().toLowerCase() === 'datetimeoffset') {
|
|
1371
1818
|
// for non-null datetimeoffset fields, we need to convert the value to ISO format
|
|
1372
1819
|
value = new Date(value).toISOString();
|
|
1373
1820
|
}
|
|
1374
|
-
else if (!isUpdate && f.Type.trim().toLowerCase() === 'uniqueidentifier') {
|
|
1821
|
+
else if (!isUpdate && f.Type.trim().toLowerCase() === 'uniqueidentifier' && !includePrimaryKeyForCreate) {
|
|
1375
1822
|
// in the case of unique identifiers, for CREATE procs only,
|
|
1376
1823
|
// we need to check to see if the value we have in the entity object is a function like newid() or newsquentialid()
|
|
1377
1824
|
// in those cases we should just skip the parameter entirely because that means there is a default value that should be used
|
|
1378
1825
|
// and that will be handled by the database not by us
|
|
1379
1826
|
// instead of just checking for specific functions like newid(), we can instead check for any string that includes ()
|
|
1380
1827
|
// this way we can handle any function that the database might support in the future
|
|
1828
|
+
// EXCEPTION: Don't skip if we're including a primary key for create
|
|
1381
1829
|
if (typeof value === 'string' && value.includes('()')) {
|
|
1382
1830
|
continue; // skip this field entirely by going to the next iteration of the loop
|
|
1383
1831
|
}
|
|
@@ -1390,7 +1838,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1390
1838
|
}
|
|
1391
1839
|
if (isUpdate && bFirst === false) {
|
|
1392
1840
|
// this is an update and we have other fields, so we need to add all of the pkeys to the end of the SP call
|
|
1393
|
-
for (
|
|
1841
|
+
for (const pkey of entity.PrimaryKey.KeyValuePairs) {
|
|
1394
1842
|
const f = entity.EntityInfo.Fields.find((f) => f.Name.trim().toLowerCase() === pkey.FieldName.trim().toLowerCase());
|
|
1395
1843
|
const pkeyQuotes = f.NeedsQuotes ? "'" : '';
|
|
1396
1844
|
sRet += `, @${f.CodeName} = ` + pkeyQuotes + pkey.Value + pkeyQuotes; // add pkey to update SP at end, but only if other fields included
|
|
@@ -1490,7 +1938,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1490
1938
|
*/
|
|
1491
1939
|
CreateUserDescriptionOfChanges(changesObject, maxValueLength = 200, cutOffText = '...') {
|
|
1492
1940
|
let sRet = '';
|
|
1493
|
-
|
|
1941
|
+
const keys = Object.keys(changesObject);
|
|
1494
1942
|
for (let i = 0; i < keys.length; i++) {
|
|
1495
1943
|
const change = changesObject[keys[i]];
|
|
1496
1944
|
if (sRet.length > 0) {
|
|
@@ -1592,6 +2040,15 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1592
2040
|
if (d && d.length > 0) {
|
|
1593
2041
|
// got the record, now process the relationships if there are any
|
|
1594
2042
|
const ret = d[0];
|
|
2043
|
+
// we need to post process the retrieval to see if we have any char or nchar fields and we need to remove their trailing spaces
|
|
2044
|
+
for (const field of entity.EntityInfo.Fields) {
|
|
2045
|
+
if (field.TSType === core_1.EntityFieldTSType.String &&
|
|
2046
|
+
field.Type.toLowerCase().includes('char') &&
|
|
2047
|
+
!field.Type.toLowerCase().includes('varchar')) {
|
|
2048
|
+
// trim trailing spaces for char and nchar fields
|
|
2049
|
+
ret[field.Name] = ret[field.Name] ? ret[field.Name].trimEnd() : ret[field.Name];
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
1595
2052
|
if (EntityRelationshipsToLoad && EntityRelationshipsToLoad.length > 0) {
|
|
1596
2053
|
for (let i = 0; i < EntityRelationshipsToLoad.length; i++) {
|
|
1597
2054
|
const rel = EntityRelationshipsToLoad[i];
|
|
@@ -1622,7 +2079,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1622
2079
|
const rawRelData = await this.ExecuteSQL(relSql);
|
|
1623
2080
|
if (rawRelData && rawRelData.length > 0) {
|
|
1624
2081
|
// Find the related entity info to process datetime fields correctly
|
|
1625
|
-
const relEntityInfo = this.Entities.find(e => e.Name.trim().toLowerCase() === relInfo.RelatedEntity.trim().toLowerCase());
|
|
2082
|
+
const relEntityInfo = this.Entities.find((e) => e.Name.trim().toLowerCase() === relInfo.RelatedEntity.trim().toLowerCase());
|
|
1626
2083
|
if (relEntityInfo) {
|
|
1627
2084
|
ret[rel] = await this.ProcessEntityRows(rawRelData, relEntityInfo);
|
|
1628
2085
|
}
|
|
@@ -1639,7 +2096,23 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1639
2096
|
// if we get here, something didn't go right
|
|
1640
2097
|
return null;
|
|
1641
2098
|
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Generates the SQL statement for deleting an entity record
|
|
2101
|
+
*
|
|
2102
|
+
* @param entity - The entity to delete
|
|
2103
|
+
* @param user - The user performing the delete
|
|
2104
|
+
* @returns The full SQL statement for deletion
|
|
2105
|
+
* @internal
|
|
2106
|
+
*/
|
|
1642
2107
|
GetDeleteSQL(entity, user) {
|
|
2108
|
+
const result = this.GetDeleteSQLWithDetails(entity, user);
|
|
2109
|
+
return result.fullSQL;
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* This function generates both the full SQL (with record change metadata) and the simple stored procedure call for delete
|
|
2113
|
+
* @returns Object with fullSQL and simpleSQL properties
|
|
2114
|
+
*/
|
|
2115
|
+
GetDeleteSQLWithDetails(entity, user) {
|
|
1643
2116
|
let sSQL = '';
|
|
1644
2117
|
const spName = entity.EntityInfo.spDelete ? entity.EntityInfo.spDelete : `spDelete${entity.EntityInfo.ClassName}`;
|
|
1645
2118
|
const sParams = entity.PrimaryKey.KeyValuePairs.map((kv) => {
|
|
@@ -1698,7 +2171,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1698
2171
|
// just delete the record
|
|
1699
2172
|
sSQL = sSimpleSQL;
|
|
1700
2173
|
}
|
|
1701
|
-
return sSQL;
|
|
2174
|
+
return { fullSQL: sSQL, simpleSQL: sSimpleSQL };
|
|
1702
2175
|
}
|
|
1703
2176
|
async Delete(entity, options, user) {
|
|
1704
2177
|
const result = new core_1.BaseEntityResult();
|
|
@@ -1721,7 +2194,8 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1721
2194
|
entity.ResultHistory.push(result); // push the new result as we have started a process
|
|
1722
2195
|
// REMEMBER - this is the provider and the BaseEntity/subclasses handle user-level permission checking already, we just make sure API was turned on for the operation
|
|
1723
2196
|
// if we get here we can delete, so build the SQL and then handle appropriately either as part of TransGroup or directly...
|
|
1724
|
-
const
|
|
2197
|
+
const sqlDetails = this.GetDeleteSQLWithDetails(entity, user);
|
|
2198
|
+
const sSQL = sqlDetails.fullSQL;
|
|
1725
2199
|
// Handle Entity and Entity AI Actions here w/ before and after handling
|
|
1726
2200
|
if (false === options?.SkipEntityActions)
|
|
1727
2201
|
await this.HandleEntityActions(entity, 'delete', true, user);
|
|
@@ -1732,7 +2206,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1732
2206
|
entity.RaiseReadyForTransaction();
|
|
1733
2207
|
// we are part of a transaction group, so just add our query to the list
|
|
1734
2208
|
// and when the transaction is committed, we will send all the queries at once
|
|
1735
|
-
entity.TransactionGroup.AddTransaction(new core_1.TransactionItem(entity, 'Delete', sSQL, null, {
|
|
2209
|
+
entity.TransactionGroup.AddTransaction(new core_1.TransactionItem(entity, 'Delete', sSQL, null, {
|
|
2210
|
+
dataSource: this._pool,
|
|
2211
|
+
simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined,
|
|
2212
|
+
entityName: entity.EntityInfo.Name
|
|
2213
|
+
}, (transactionResult, success) => {
|
|
1736
2214
|
// we get here whenever the transaction group does gets around to committing
|
|
1737
2215
|
// our query.
|
|
1738
2216
|
result.EndedAt = new Date();
|
|
@@ -1745,7 +2223,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1745
2223
|
this.HandleEntityAIActions(entity, 'delete', false, user);
|
|
1746
2224
|
}
|
|
1747
2225
|
// Make sure the return value matches up as that is how we know the SP was succesfully internally
|
|
1748
|
-
for (
|
|
2226
|
+
for (const key of entity.PrimaryKeys) {
|
|
1749
2227
|
if (key.Value !== transactionResult[key.Name]) {
|
|
1750
2228
|
result.Success = false;
|
|
1751
2229
|
result.Message = 'Transaction failed to commit';
|
|
@@ -1769,11 +2247,16 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1769
2247
|
d = [entity.GetAll()]; // just return the entity as it was before the save as we are NOT saving anything as we are in replay mode
|
|
1770
2248
|
}
|
|
1771
2249
|
else {
|
|
1772
|
-
|
|
2250
|
+
// Execute SQL with optional simple SQL fallback for loggers
|
|
2251
|
+
d = await this.ExecuteSQL(sSQL, null, {
|
|
2252
|
+
isMutation: true,
|
|
2253
|
+
description: `Delete ${entity.EntityInfo.Name}`,
|
|
2254
|
+
simpleSQLFallback: entity.EntityInfo.TrackRecordChanges ? sqlDetails.simpleSQL : undefined
|
|
2255
|
+
});
|
|
1773
2256
|
}
|
|
1774
2257
|
if (d && d[0]) {
|
|
1775
2258
|
// SP executed, now make sure the return value matches up as that is how we know the SP was succesfully internally
|
|
1776
|
-
for (
|
|
2259
|
+
for (const key of entity.PrimaryKeys) {
|
|
1777
2260
|
if (key.Value !== d[0][key.Name]) {
|
|
1778
2261
|
return false;
|
|
1779
2262
|
}
|
|
@@ -1823,16 +2306,73 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1823
2306
|
ON
|
|
1824
2307
|
di.EntityID = e.ID
|
|
1825
2308
|
WHERE
|
|
1826
|
-
d.Name = @
|
|
2309
|
+
d.Name = @p0`;
|
|
1827
2310
|
const items = await this.ExecuteSQL(sSQL, [datasetName]);
|
|
1828
2311
|
// now we have the dataset and the items, we need to get the update date from the items underlying entities
|
|
1829
2312
|
if (items && items.length > 0) {
|
|
1830
|
-
//
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2313
|
+
// Optimization: Use batch SQL execution for multiple items
|
|
2314
|
+
// Build SQL queries for all items
|
|
2315
|
+
const queries = [];
|
|
2316
|
+
const itemsWithSQL = [];
|
|
2317
|
+
for (const item of items) {
|
|
2318
|
+
const itemSQL = this.GetDatasetItemSQL(item, itemFilters, datasetName);
|
|
2319
|
+
if (itemSQL) {
|
|
2320
|
+
queries.push(itemSQL);
|
|
2321
|
+
itemsWithSQL.push(item);
|
|
2322
|
+
}
|
|
2323
|
+
else {
|
|
2324
|
+
// Handle invalid SQL case - add to results with error
|
|
2325
|
+
itemsWithSQL.push({ ...item, hasError: true });
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
// Execute all queries in a single batch
|
|
2329
|
+
const batchResults = await this.ExecuteSQLBatch(queries);
|
|
2330
|
+
// Process results for each item
|
|
2331
|
+
const results = [];
|
|
2332
|
+
let queryIndex = 0;
|
|
2333
|
+
for (const item of itemsWithSQL) {
|
|
2334
|
+
if (item.hasError) {
|
|
2335
|
+
// Handle error case for invalid columns
|
|
2336
|
+
results.push({
|
|
2337
|
+
EntityID: item.EntityID,
|
|
2338
|
+
EntityName: item.Entity,
|
|
2339
|
+
Code: item.Code,
|
|
2340
|
+
Results: null,
|
|
2341
|
+
LatestUpdateDate: null,
|
|
2342
|
+
Status: 'Invalid columns specified for dataset item',
|
|
2343
|
+
Success: false,
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
else {
|
|
2347
|
+
// Process successful query result
|
|
2348
|
+
const itemData = batchResults[queryIndex] || [];
|
|
2349
|
+
const itemUpdatedAt = new Date(item.DatasetItemUpdatedAt);
|
|
2350
|
+
const datasetUpdatedAt = new Date(item.DatasetUpdatedAt);
|
|
2351
|
+
const datasetMaxUpdatedAt = new Date(Math.max(itemUpdatedAt.getTime(), datasetUpdatedAt.getTime()));
|
|
2352
|
+
// get the latest update date
|
|
2353
|
+
let latestUpdateDate = new Date(1900, 1, 1);
|
|
2354
|
+
if (itemData && itemData.length > 0) {
|
|
2355
|
+
itemData.forEach((data) => {
|
|
2356
|
+
if (data[item.DateFieldToCheck] && new Date(data[item.DateFieldToCheck]) > latestUpdateDate) {
|
|
2357
|
+
latestUpdateDate = new Date(data[item.DateFieldToCheck]);
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
// finally, compare the latestUpdatedDate to the dataset max date, and use the latter if it is more recent
|
|
2362
|
+
if (datasetMaxUpdatedAt > latestUpdateDate) {
|
|
2363
|
+
latestUpdateDate = datasetMaxUpdatedAt;
|
|
2364
|
+
}
|
|
2365
|
+
results.push({
|
|
2366
|
+
EntityID: item.EntityID,
|
|
2367
|
+
EntityName: item.Entity,
|
|
2368
|
+
Code: item.Code,
|
|
2369
|
+
Results: itemData,
|
|
2370
|
+
LatestUpdateDate: latestUpdateDate,
|
|
2371
|
+
Success: itemData !== null && itemData !== undefined,
|
|
2372
|
+
});
|
|
2373
|
+
queryIndex++;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
1836
2376
|
// determine overall success
|
|
1837
2377
|
const bSuccess = results.every((result) => result.Success);
|
|
1838
2378
|
// get the latest update date from all the results
|
|
@@ -1844,8 +2384,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1844
2384
|
}
|
|
1845
2385
|
}
|
|
1846
2386
|
return acc;
|
|
1847
|
-
}, new Date(0)
|
|
1848
|
-
);
|
|
2387
|
+
}, new Date(0));
|
|
1849
2388
|
return {
|
|
1850
2389
|
DatasetID: items[0].DatasetID,
|
|
1851
2390
|
DatasetName: datasetName,
|
|
@@ -1866,18 +2405,32 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1866
2405
|
};
|
|
1867
2406
|
}
|
|
1868
2407
|
}
|
|
1869
|
-
|
|
2408
|
+
/**
|
|
2409
|
+
* Constructs the SQL query for a dataset item.
|
|
2410
|
+
* @param item - The dataset item metadata
|
|
2411
|
+
* @param itemFilters - Optional filters to apply
|
|
2412
|
+
* @param datasetName - Name of the dataset (for error logging)
|
|
2413
|
+
* @returns The SQL query string, or null if columns are invalid
|
|
2414
|
+
*/
|
|
2415
|
+
GetDatasetItemSQL(item, itemFilters, datasetName) {
|
|
1870
2416
|
let filterSQL = '';
|
|
1871
2417
|
if (itemFilters && itemFilters.length > 0) {
|
|
1872
2418
|
const filter = itemFilters.find((f) => f.ItemCode === item.Code);
|
|
1873
2419
|
if (filter)
|
|
1874
2420
|
filterSQL = (item.WhereClause ? ' AND ' : ' WHERE ') + '(' + filter.Filter + ')';
|
|
1875
2421
|
}
|
|
2422
|
+
const columns = this.GetColumnsForDatasetItem(item, datasetName);
|
|
2423
|
+
if (!columns) {
|
|
2424
|
+
return null; // Invalid columns
|
|
2425
|
+
}
|
|
2426
|
+
return `SELECT ${columns} FROM [${item.EntitySchemaName}].[${item.EntityBaseView}] ${item.WhereClause ? 'WHERE ' + item.WhereClause : ''}${filterSQL}`;
|
|
2427
|
+
}
|
|
2428
|
+
async GetDatasetItem(item, itemFilters, datasetName) {
|
|
1876
2429
|
const itemUpdatedAt = new Date(item.DatasetItemUpdatedAt);
|
|
1877
2430
|
const datasetUpdatedAt = new Date(item.DatasetUpdatedAt);
|
|
1878
2431
|
const datasetMaxUpdatedAt = new Date(Math.max(itemUpdatedAt.getTime(), datasetUpdatedAt.getTime()));
|
|
1879
|
-
const
|
|
1880
|
-
if (!
|
|
2432
|
+
const itemSQL = this.GetDatasetItemSQL(item, itemFilters, datasetName);
|
|
2433
|
+
if (!itemSQL) {
|
|
1881
2434
|
// failure condition within columns, return a failed result
|
|
1882
2435
|
return {
|
|
1883
2436
|
EntityID: item.EntityID,
|
|
@@ -1889,7 +2442,6 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1889
2442
|
Success: false,
|
|
1890
2443
|
};
|
|
1891
2444
|
}
|
|
1892
|
-
const itemSQL = `SELECT ${columns} FROM [${item.EntitySchemaName}].[${item.EntityBaseView}] ${item.WhereClause ? 'WHERE ' + item.WhereClause : ''}${filterSQL}`;
|
|
1893
2445
|
const itemData = await this.ExecuteSQL(itemSQL);
|
|
1894
2446
|
// get the latest update date
|
|
1895
2447
|
let latestUpdateDate = new Date(1900, 1, 1);
|
|
@@ -1978,7 +2530,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1978
2530
|
ON
|
|
1979
2531
|
di.EntityID = e.ID
|
|
1980
2532
|
WHERE
|
|
1981
|
-
d.Name = @
|
|
2533
|
+
d.Name = @p0`;
|
|
1982
2534
|
const items = await this.ExecuteSQL(sSQL, [datasetName]);
|
|
1983
2535
|
// now we have the dataset and the items, we need to get the update date from the items underlying entities
|
|
1984
2536
|
if (items && items.length > 0) {
|
|
@@ -2165,7 +2717,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2165
2717
|
return rows;
|
|
2166
2718
|
}
|
|
2167
2719
|
// Find all datetime fields in the entity
|
|
2168
|
-
const datetimeFields = entityInfo.Fields.filter(field => field.TSType === core_1.EntityFieldTSType.Date);
|
|
2720
|
+
const datetimeFields = entityInfo.Fields.filter((field) => field.TSType === core_1.EntityFieldTSType.Date);
|
|
2169
2721
|
// If there are no datetime fields, return the rows as-is
|
|
2170
2722
|
if (datetimeFields.length === 0) {
|
|
2171
2723
|
return rows;
|
|
@@ -2173,7 +2725,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2173
2725
|
// Check if we need datetimeoffset adjustment (lazy loaded on first use)
|
|
2174
2726
|
const needsAdjustment = await this.NeedsDatetimeOffsetAdjustment();
|
|
2175
2727
|
// Process each row
|
|
2176
|
-
return rows.map(row => {
|
|
2728
|
+
return rows.map((row) => {
|
|
2177
2729
|
const processedRow = { ...row };
|
|
2178
2730
|
for (const field of datetimeFields) {
|
|
2179
2731
|
const fieldValue = processedRow[field.Name];
|
|
@@ -2194,9 +2746,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2194
2746
|
}
|
|
2195
2747
|
}
|
|
2196
2748
|
else if (fieldValue instanceof Date) {
|
|
2197
|
-
//
|
|
2749
|
+
// DB driver has already converted to a Date object using local timezone
|
|
2198
2750
|
// We need to adjust it back to UTC
|
|
2199
|
-
// SQL Server stores datetime2 as UTC, but
|
|
2751
|
+
// SQL Server stores datetime2 as UTC, but DB Driver interprets it as local
|
|
2200
2752
|
const localDate = fieldValue;
|
|
2201
2753
|
const timezoneOffsetMs = localDate.getTimezoneOffset() * 60 * 1000;
|
|
2202
2754
|
const utcDate = new Date(localDate.getTime() + timezoneOffsetMs);
|
|
@@ -2211,7 +2763,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2211
2763
|
}
|
|
2212
2764
|
else if (fieldValue instanceof Date && needsAdjustment) {
|
|
2213
2765
|
// The database driver has incorrectly converted to a Date object using local timezone
|
|
2214
|
-
// For datetimeoffset, SQL Server provides the value with timezone info, but the driver
|
|
2766
|
+
// For datetimeoffset, SQL Server provides the value with timezone info, but the driver
|
|
2215
2767
|
// creates the Date as if it were in local time, ignoring the offset
|
|
2216
2768
|
// We need to adjust it back to the correct UTC time
|
|
2217
2769
|
const localDate = fieldValue;
|
|
@@ -2242,25 +2794,212 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2242
2794
|
* @param parameters
|
|
2243
2795
|
* @returns
|
|
2244
2796
|
*/
|
|
2245
|
-
async ExecuteSQL(query, parameters = null) {
|
|
2797
|
+
async ExecuteSQL(query, parameters = null, options) {
|
|
2246
2798
|
try {
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2799
|
+
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);
|
|
2250
2807
|
}
|
|
2251
2808
|
else {
|
|
2252
|
-
|
|
2253
|
-
|
|
2809
|
+
// Use pool request for non-transactional queries
|
|
2810
|
+
request = new sql.Request(this._pool);
|
|
2811
|
+
}
|
|
2812
|
+
// Add parameters if provided
|
|
2813
|
+
if (parameters) {
|
|
2814
|
+
if (Array.isArray(parameters)) {
|
|
2815
|
+
// Handle positional parameters (legacy TypeORM style)
|
|
2816
|
+
// Convert to named parameters for mssql
|
|
2817
|
+
parameters.forEach((value, index) => {
|
|
2818
|
+
request.input(`p${index}`, value);
|
|
2819
|
+
});
|
|
2820
|
+
// Replace ? with @p0, @p1, etc. in the query
|
|
2821
|
+
let paramIndex = 0;
|
|
2822
|
+
query = query.replace(/\?/g, () => `@p${paramIndex++}`);
|
|
2823
|
+
}
|
|
2824
|
+
else if (typeof parameters === 'object') {
|
|
2825
|
+
// Handle named parameters
|
|
2826
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
2827
|
+
request.input(key, value);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2254
2830
|
}
|
|
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]);
|
|
2835
|
+
// Return recordset for consistency with TypeORM behavior
|
|
2836
|
+
// If multiple recordsets, return recordsets array
|
|
2837
|
+
return result.recordsets && Array.isArray(result.recordsets) && result.recordsets.length > 1 ? result.recordsets : result.recordset;
|
|
2255
2838
|
}
|
|
2256
2839
|
catch (e) {
|
|
2257
2840
|
(0, core_1.LogError)(e);
|
|
2258
2841
|
throw e; // force caller to handle
|
|
2259
2842
|
}
|
|
2260
2843
|
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Static helper method for executing SQL queries on an external connection pool.
|
|
2846
|
+
* This method is designed to be used by generated code where a connection pool
|
|
2847
|
+
* is passed in from the context. It returns results as arrays for consistency
|
|
2848
|
+
* with the expected behavior in generated resolvers.
|
|
2849
|
+
*
|
|
2850
|
+
* @param pool - The mssql ConnectionPool to execute the query on
|
|
2851
|
+
* @param query - The SQL query to execute
|
|
2852
|
+
* @param parameters - Optional parameters for the query
|
|
2853
|
+
* @returns Promise<any[]> - Array of results (empty array if no results)
|
|
2854
|
+
*/
|
|
2855
|
+
static async ExecuteSQLWithPool(pool, query, parameters) {
|
|
2856
|
+
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++}`);
|
|
2868
|
+
}
|
|
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);
|
|
2877
|
+
// Always return array for consistency
|
|
2878
|
+
return result.recordset || [];
|
|
2879
|
+
}
|
|
2880
|
+
catch (e) {
|
|
2881
|
+
(0, core_1.LogError)(e);
|
|
2882
|
+
throw e;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* Static method to execute a batch of SQL queries using a provided connection source.
|
|
2887
|
+
* This allows the batch logic to be reused from external contexts like TransactionGroup.
|
|
2888
|
+
* All queries are combined into a single SQL statement and executed together within
|
|
2889
|
+
* the same connection/transaction context for optimal performance.
|
|
2890
|
+
*
|
|
2891
|
+
* @param connectionSource - Either a sql.ConnectionPool, sql.Transaction, or sql.Request to use for execution
|
|
2892
|
+
* @param queries - Array of SQL queries to execute
|
|
2893
|
+
* @param parameters - Optional array of parameter arrays, one for each query
|
|
2894
|
+
* @returns Promise<any[][]> - Array of result arrays, one for each query
|
|
2895
|
+
*/
|
|
2896
|
+
static async ExecuteSQLBatchStatic(connectionSource, queries, parameters) {
|
|
2897
|
+
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
|
|
2911
|
+
let batchSQL = '';
|
|
2912
|
+
let paramIndex = 0;
|
|
2913
|
+
queries.forEach((query, queryIndex) => {
|
|
2914
|
+
// Add parameters for this query if provided
|
|
2915
|
+
if (parameters && parameters[queryIndex]) {
|
|
2916
|
+
const queryParams = parameters[queryIndex];
|
|
2917
|
+
if (Array.isArray(queryParams)) {
|
|
2918
|
+
// Handle positional parameters
|
|
2919
|
+
queryParams.forEach((value) => {
|
|
2920
|
+
request.input(`p${paramIndex}`, value);
|
|
2921
|
+
paramIndex++;
|
|
2922
|
+
});
|
|
2923
|
+
// Replace @p0, @p1, etc. with actual parameter names
|
|
2924
|
+
let localParamIndex = paramIndex - queryParams.length;
|
|
2925
|
+
query = query.replace(/@p(\d+)/g, () => `@p${localParamIndex++}`);
|
|
2926
|
+
}
|
|
2927
|
+
else if (typeof queryParams === 'object') {
|
|
2928
|
+
// Handle named parameters - prefix with query index to avoid conflicts
|
|
2929
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
2930
|
+
const paramName = `q${queryIndex}_${key}`;
|
|
2931
|
+
request.input(paramName, value);
|
|
2932
|
+
// Replace parameter references in query
|
|
2933
|
+
query = query.replace(new RegExp(`@${key}\\b`, 'g'), `@${paramName}`);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
batchSQL += query;
|
|
2938
|
+
if (queryIndex < queries.length - 1) {
|
|
2939
|
+
batchSQL += ';\n';
|
|
2940
|
+
}
|
|
2941
|
+
});
|
|
2942
|
+
// Execute the batch SQL
|
|
2943
|
+
const result = await request.query(batchSQL);
|
|
2944
|
+
// Return array of recordsets - one for each query
|
|
2945
|
+
// Handle both single and multiple recordsets
|
|
2946
|
+
if (result.recordsets && Array.isArray(result.recordsets)) {
|
|
2947
|
+
return result.recordsets;
|
|
2948
|
+
}
|
|
2949
|
+
else if (result.recordset) {
|
|
2950
|
+
return [result.recordset];
|
|
2951
|
+
}
|
|
2952
|
+
else {
|
|
2953
|
+
return [];
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
catch (e) {
|
|
2957
|
+
(0, core_1.LogError)(e);
|
|
2958
|
+
throw e;
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Executes multiple SQL queries in a single batch for optimal performance.
|
|
2963
|
+
* All queries are combined into a single SQL statement and executed together.
|
|
2964
|
+
* This is particularly useful for bulk operations where you need to execute
|
|
2965
|
+
* many similar queries and want to minimize round trips to the database.
|
|
2966
|
+
*
|
|
2967
|
+
* @param queries - Array of SQL queries to execute
|
|
2968
|
+
* @param parameters - Optional array of parameter arrays, one for each query
|
|
2969
|
+
* @param options - Optional execution options for logging and description
|
|
2970
|
+
* @returns Promise<any[][]> - Array of result arrays, one for each query
|
|
2971
|
+
*/
|
|
2972
|
+
async ExecuteSQLBatch(queries, parameters, options) {
|
|
2973
|
+
try {
|
|
2974
|
+
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) {
|
|
2980
|
+
// Use transaction if we have one
|
|
2981
|
+
connectionSource = this._transaction;
|
|
2982
|
+
}
|
|
2983
|
+
else {
|
|
2984
|
+
// Use pool request for non-transactional queries
|
|
2985
|
+
connectionSource = this._pool;
|
|
2986
|
+
}
|
|
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;
|
|
2994
|
+
}
|
|
2995
|
+
catch (e) {
|
|
2996
|
+
(0, core_1.LogError)(e);
|
|
2997
|
+
throw e;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
2261
3000
|
/**
|
|
2262
3001
|
* Determines whether the database driver requires adjustment for datetimeoffset fields.
|
|
2263
|
-
* This method performs an empirical test on first use to detect if the driver (e.g.,
|
|
3002
|
+
* This method performs an empirical test on first use to detect if the driver (e.g., mssql)
|
|
2264
3003
|
* incorrectly handles timezone information in datetimeoffset columns.
|
|
2265
3004
|
*
|
|
2266
3005
|
* @returns {Promise<boolean>} True if datetimeoffset values need timezone adjustment, false otherwise
|
|
@@ -2306,7 +3045,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2306
3045
|
-- Select and return the row
|
|
2307
3046
|
SELECT TestDateTime FROM @TestTable;
|
|
2308
3047
|
`;
|
|
2309
|
-
const result = await this.ExecuteSQL(testQuery);
|
|
3048
|
+
const result = await this.ExecuteSQL(testQuery, null, { description: 'DatetimeOffset handling test' });
|
|
2310
3049
|
if (result && result.length > 0) {
|
|
2311
3050
|
const testDate = result[0].TestDateTime;
|
|
2312
3051
|
// Expected: January 1, 1900 at 11:00 AM UTC
|
|
@@ -2341,9 +3080,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2341
3080
|
}
|
|
2342
3081
|
async BeginTransaction() {
|
|
2343
3082
|
try {
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
await this.
|
|
3083
|
+
// Create a new transaction from the pool
|
|
3084
|
+
this._transaction = new sql.Transaction(this._pool);
|
|
3085
|
+
await this._transaction.begin();
|
|
3086
|
+
// Create a request for this transaction that can be reused
|
|
3087
|
+
this._transactionRequest = new sql.Request(this._transaction);
|
|
2347
3088
|
}
|
|
2348
3089
|
catch (e) {
|
|
2349
3090
|
(0, core_1.LogError)(e);
|
|
@@ -2352,7 +3093,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2352
3093
|
}
|
|
2353
3094
|
async CommitTransaction() {
|
|
2354
3095
|
try {
|
|
2355
|
-
|
|
3096
|
+
if (this._transaction) {
|
|
3097
|
+
await this._transaction.commit();
|
|
3098
|
+
this._transaction = null;
|
|
3099
|
+
this._transactionRequest = null;
|
|
3100
|
+
}
|
|
2356
3101
|
}
|
|
2357
3102
|
catch (e) {
|
|
2358
3103
|
(0, core_1.LogError)(e);
|
|
@@ -2361,7 +3106,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2361
3106
|
}
|
|
2362
3107
|
async RollbackTransaction() {
|
|
2363
3108
|
try {
|
|
2364
|
-
|
|
3109
|
+
if (this._transaction) {
|
|
3110
|
+
await this._transaction.rollback();
|
|
3111
|
+
this._transaction = null;
|
|
3112
|
+
this._transactionRequest = null;
|
|
3113
|
+
}
|
|
2365
3114
|
}
|
|
2366
3115
|
catch (e) {
|
|
2367
3116
|
(0, core_1.LogError)(e);
|
|
@@ -2420,9 +3169,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2420
3169
|
}
|
|
2421
3170
|
else {
|
|
2422
3171
|
// got our field, create a SQL Query
|
|
2423
|
-
|
|
3172
|
+
const sql = `SELECT [${f.Name}] FROM [${e.SchemaName}].[${e.BaseView}] WHERE `;
|
|
2424
3173
|
let where = '';
|
|
2425
|
-
for (
|
|
3174
|
+
for (const pkv of CompositeKey.KeyValuePairs) {
|
|
2426
3175
|
const pk = e.PrimaryKeys.find((pk) => pk.Name === pkv.FieldName);
|
|
2427
3176
|
const quotes = pk.NeedsQuotes ? "'" : '';
|
|
2428
3177
|
if (where.length > 0)
|