@mcp-consultant-tools/azure-sql 28.0.0-beta.6 → 28.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/build/cli/commands/connection-commands.d.ts +7 -0
  2. package/build/cli/commands/connection-commands.d.ts.map +1 -0
  3. package/build/cli/commands/connection-commands.js +64 -0
  4. package/build/cli/commands/connection-commands.js.map +1 -0
  5. package/build/cli/commands/crud-commands.d.ts +7 -0
  6. package/build/cli/commands/crud-commands.d.ts.map +1 -0
  7. package/build/cli/commands/crud-commands.js +63 -0
  8. package/build/cli/commands/crud-commands.js.map +1 -0
  9. package/build/cli/commands/index.d.ts +13 -0
  10. package/build/cli/commands/index.d.ts.map +1 -0
  11. package/build/cli/commands/index.js +24 -0
  12. package/build/cli/commands/index.js.map +1 -0
  13. package/build/cli/commands/query-commands.d.ts +7 -0
  14. package/build/cli/commands/query-commands.d.ts.map +1 -0
  15. package/build/cli/commands/query-commands.js +149 -0
  16. package/build/cli/commands/query-commands.js.map +1 -0
  17. package/build/cli/commands/sproc-commands.d.ts +7 -0
  18. package/build/cli/commands/sproc-commands.d.ts.map +1 -0
  19. package/build/cli/commands/sproc-commands.js +87 -0
  20. package/build/cli/commands/sproc-commands.js.map +1 -0
  21. package/build/cli/commands/unrestricted-commands.d.ts +7 -0
  22. package/build/cli/commands/unrestricted-commands.d.ts.map +1 -0
  23. package/build/cli/commands/unrestricted-commands.js +31 -0
  24. package/build/cli/commands/unrestricted-commands.js.map +1 -0
  25. package/build/cli/commands/view-commands.d.ts +7 -0
  26. package/build/cli/commands/view-commands.d.ts.map +1 -0
  27. package/build/cli/commands/view-commands.js +66 -0
  28. package/build/cli/commands/view-commands.js.map +1 -0
  29. package/build/cli/output.d.ts +11 -0
  30. package/build/cli/output.d.ts.map +1 -0
  31. package/build/cli/output.js +10 -0
  32. package/build/cli/output.js.map +1 -0
  33. package/build/cli.d.ts +9 -0
  34. package/build/cli.d.ts.map +1 -0
  35. package/build/cli.js +27 -0
  36. package/build/cli.js.map +1 -0
  37. package/build/context-factory.d.ts +10 -0
  38. package/build/context-factory.d.ts.map +1 -0
  39. package/build/context-factory.js +104 -0
  40. package/build/context-factory.js.map +1 -0
  41. package/build/index.d.ts +1 -0
  42. package/build/index.d.ts.map +1 -1
  43. package/build/index.js +44 -0
  44. package/build/index.js.map +1 -1
  45. package/build/services/index.d.ts +2 -0
  46. package/build/services/index.d.ts.map +1 -1
  47. package/build/services/index.js +1 -0
  48. package/build/services/index.js.map +1 -1
  49. package/build/services/write-service.d.ts +109 -0
  50. package/build/services/write-service.d.ts.map +1 -0
  51. package/build/services/write-service.js +735 -0
  52. package/build/services/write-service.js.map +1 -0
  53. package/build/tool-examples.d.ts +32 -0
  54. package/build/tool-examples.d.ts.map +1 -1
  55. package/build/tool-examples.js +33 -0
  56. package/build/tool-examples.js.map +1 -1
  57. package/build/tools/crud-tools.d.ts +3 -0
  58. package/build/tools/crud-tools.d.ts.map +1 -0
  59. package/build/tools/crud-tools.js +115 -0
  60. package/build/tools/crud-tools.js.map +1 -0
  61. package/build/tools/index.d.ts +4 -0
  62. package/build/tools/index.d.ts.map +1 -1
  63. package/build/tools/index.js +12 -0
  64. package/build/tools/index.js.map +1 -1
  65. package/build/tools/sproc-tools.d.ts +3 -0
  66. package/build/tools/sproc-tools.d.ts.map +1 -0
  67. package/build/tools/sproc-tools.js +172 -0
  68. package/build/tools/sproc-tools.js.map +1 -0
  69. package/build/tools/unrestricted-tools.d.ts +7 -0
  70. package/build/tools/unrestricted-tools.d.ts.map +1 -0
  71. package/build/tools/unrestricted-tools.js +63 -0
  72. package/build/tools/unrestricted-tools.js.map +1 -0
  73. package/build/tools/view-tools.d.ts +3 -0
  74. package/build/tools/view-tools.d.ts.map +1 -0
  75. package/build/tools/view-tools.js +121 -0
  76. package/build/tools/view-tools.js.map +1 -0
  77. package/build/types.d.ts +10 -0
  78. package/build/types.d.ts.map +1 -1
  79. package/package.json +6 -4
@@ -0,0 +1,735 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { auditLogger } from '@mcp-consultant-tools/core';
4
+ /**
5
+ * Regex for valid SQL identifiers — prevents SQL injection on object names.
6
+ */
7
+ const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
8
+ /**
9
+ * Patterns that are forbidden in user-provided DML queries.
10
+ */
11
+ const DANGEROUS_PATTERNS = [
12
+ { pattern: /\b(drop|create|alter|truncate)\b/i, name: 'schema modifications' },
13
+ { pattern: /\b(exec|execute|sp_executesql)\b/i, name: 'command execution' },
14
+ { pattern: /\bxp_\w+/i, name: 'xp_ system procedures' },
15
+ { pattern: /\bsp_\w+/i, name: 'sp_ system procedures' },
16
+ { pattern: /\b(grant|revoke|deny)\b/i, name: 'permission changes' },
17
+ { pattern: /\b(openquery|openrowset|opendatasource)\b/i, name: 'linked server queries' },
18
+ ];
19
+ /**
20
+ * WriteService handles all write operations against Azure SQL databases:
21
+ * view management, stored procedure management/execution, and DML (INSERT/UPDATE/DELETE).
22
+ *
23
+ * Depends on ConnectionService for connection pooling.
24
+ */
25
+ export class WriteService {
26
+ connectionService;
27
+ constructor(connectionService) {
28
+ this.connectionService = connectionService;
29
+ }
30
+ /**
31
+ * Validate that a string is a safe SQL identifier (schema name, table name, etc.).
32
+ * Prevents SQL injection on object names.
33
+ */
34
+ validateIdentifier(name, label) {
35
+ if (!VALID_IDENTIFIER.test(name)) {
36
+ throw new Error(`Invalid ${label}: '${name}'. ` +
37
+ `Only alphanumeric characters and underscores are allowed, ` +
38
+ `and it must start with a letter or underscore.`);
39
+ }
40
+ }
41
+ /**
42
+ * Strip SQL comments and normalize whitespace for validation.
43
+ */
44
+ cleanSql(query) {
45
+ return query
46
+ .replace(/--.*$/gm, '')
47
+ .replace(/\/\*[\s\S]*?\*\//g, '')
48
+ .replace(/\s+/g, ' ')
49
+ .trim();
50
+ }
51
+ /**
52
+ * Validate a DML query starts with the expected keyword and contains no dangerous patterns.
53
+ */
54
+ validateDmlQuery(query, expectedKeyword) {
55
+ const cleaned = this.cleanSql(query).toLowerCase();
56
+ if (!cleaned.startsWith(expectedKeyword.toLowerCase())) {
57
+ throw new Error(`Query must start with ${expectedKeyword}. ` +
58
+ `Got: '${cleaned.substring(0, 50)}...'`);
59
+ }
60
+ for (const { pattern, name } of DANGEROUS_PATTERNS) {
61
+ if (pattern.test(cleaned)) {
62
+ throw new Error(`Query contains forbidden pattern (${name}). ` +
63
+ `Only ${expectedKeyword} statements are allowed in this operation.`);
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Create or alter a view.
69
+ */
70
+ async manageView(serverId, database, schemaName, viewName, selectBody) {
71
+ this.validateIdentifier(schemaName, 'schema name');
72
+ this.validateIdentifier(viewName, 'view name');
73
+ const timer = auditLogger.startTimer();
74
+ const fullName = `[${schemaName}].[${viewName}]`;
75
+ try {
76
+ const pool = await this.connectionService.getPool(serverId, database);
77
+ const sql = `CREATE OR ALTER VIEW ${fullName} AS ${selectBody}`;
78
+ await pool.request().query(sql);
79
+ const result = {
80
+ success: true,
81
+ message: `View ${fullName} created or updated successfully.`,
82
+ objectName: `${schemaName}.${viewName}`,
83
+ };
84
+ auditLogger.log({
85
+ operation: 'manage-view',
86
+ operationType: 'CREATE',
87
+ componentType: 'View',
88
+ componentName: `${serverId}/${database}/${fullName}`,
89
+ success: true,
90
+ parameters: { schemaName, viewName, selectBodyLength: selectBody.length },
91
+ executionTimeMs: timer(),
92
+ });
93
+ return result;
94
+ }
95
+ catch (error) {
96
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
97
+ auditLogger.log({
98
+ operation: 'manage-view',
99
+ operationType: 'CREATE',
100
+ componentType: 'View',
101
+ componentName: `${serverId}/${database}/${fullName}`,
102
+ success: false,
103
+ error: errorMsg,
104
+ parameters: { schemaName, viewName },
105
+ executionTimeMs: timer(),
106
+ });
107
+ throw new Error(`Failed to create/alter view ${fullName}: ${errorMsg}`);
108
+ }
109
+ }
110
+ /**
111
+ * Deploy a view from a local .sql file.
112
+ * Reads the file and executes its contents as-is against the database.
113
+ * The file must contain a valid CREATE OR ALTER VIEW statement.
114
+ */
115
+ async deployViewFromFile(serverId, database, filePath) {
116
+ const timer = auditLogger.startTimer();
117
+ const normalizedPath = filePath.replace(/\\/g, '/');
118
+ const resolvedPath = path.resolve(normalizedPath);
119
+ if (!resolvedPath.toLowerCase().endsWith('.sql')) {
120
+ throw new Error(`File must have a .sql extension. Got: '${path.basename(resolvedPath)}'`);
121
+ }
122
+ let sql;
123
+ try {
124
+ sql = await fs.readFile(resolvedPath, 'utf-8');
125
+ }
126
+ catch (error) {
127
+ if (error.code === 'ENOENT') {
128
+ throw new Error(`File not found: '${resolvedPath}'`);
129
+ }
130
+ throw new Error(`Failed to read file '${resolvedPath}': ${error.message}`);
131
+ }
132
+ const trimmedSql = sql.trim();
133
+ if (!trimmedSql) {
134
+ throw new Error(`File is empty: '${resolvedPath}'`);
135
+ }
136
+ const cleanedForValidation = this.cleanSql(trimmedSql).toLowerCase();
137
+ if (!cleanedForValidation.startsWith('create or alter view')) {
138
+ throw new Error(`File must contain a CREATE OR ALTER VIEW statement. ` +
139
+ `Got: '${cleanedForValidation.substring(0, 60)}...'`);
140
+ }
141
+ let viewDisplayName = 'unknown';
142
+ const nameMatch = cleanedForValidation.match(/create\s+or\s+alter\s+view\s+(?:\[?(\w+)\]?\.)?\[?(\w+)\]?/i);
143
+ if (nameMatch) {
144
+ const schema = nameMatch[1] || 'dbo';
145
+ const name = nameMatch[2];
146
+ viewDisplayName = `[${schema}].[${name}]`;
147
+ }
148
+ try {
149
+ const pool = await this.connectionService.getPool(serverId, database);
150
+ await pool.request().query(trimmedSql);
151
+ const result = {
152
+ success: true,
153
+ message: `View ${viewDisplayName} deployed successfully from file '${path.basename(resolvedPath)}'.`,
154
+ objectName: viewDisplayName,
155
+ };
156
+ auditLogger.log({
157
+ operation: 'deploy-view-file',
158
+ operationType: 'CREATE',
159
+ componentType: 'View',
160
+ componentName: `${serverId}/${database}/${viewDisplayName}`,
161
+ success: true,
162
+ parameters: { filePath: resolvedPath, fileSize: trimmedSql.length, viewName: viewDisplayName },
163
+ executionTimeMs: timer(),
164
+ });
165
+ return result;
166
+ }
167
+ catch (error) {
168
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
169
+ auditLogger.log({
170
+ operation: 'deploy-view-file',
171
+ operationType: 'CREATE',
172
+ componentType: 'View',
173
+ componentName: `${serverId}/${database}/${viewDisplayName}`,
174
+ success: false,
175
+ error: errorMsg,
176
+ parameters: { filePath: resolvedPath, viewName: viewDisplayName },
177
+ executionTimeMs: timer(),
178
+ });
179
+ throw new Error(`Failed to deploy view from file '${path.basename(resolvedPath)}': ${errorMsg}`);
180
+ }
181
+ }
182
+ /**
183
+ * Drop a view if it exists.
184
+ */
185
+ async dropView(serverId, database, schemaName, viewName) {
186
+ this.validateIdentifier(schemaName, 'schema name');
187
+ this.validateIdentifier(viewName, 'view name');
188
+ const timer = auditLogger.startTimer();
189
+ const fullName = `[${schemaName}].[${viewName}]`;
190
+ try {
191
+ const pool = await this.connectionService.getPool(serverId, database);
192
+ await pool.request().query(`DROP VIEW IF EXISTS ${fullName}`);
193
+ const result = {
194
+ success: true,
195
+ message: `View ${fullName} dropped successfully (if it existed).`,
196
+ objectName: `${schemaName}.${viewName}`,
197
+ };
198
+ auditLogger.log({
199
+ operation: 'drop-view',
200
+ operationType: 'DELETE',
201
+ componentType: 'View',
202
+ componentName: `${serverId}/${database}/${fullName}`,
203
+ success: true,
204
+ parameters: { schemaName, viewName },
205
+ executionTimeMs: timer(),
206
+ });
207
+ return result;
208
+ }
209
+ catch (error) {
210
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
211
+ auditLogger.log({
212
+ operation: 'drop-view',
213
+ operationType: 'DELETE',
214
+ componentType: 'View',
215
+ componentName: `${serverId}/${database}/${fullName}`,
216
+ success: false,
217
+ error: errorMsg,
218
+ parameters: { schemaName, viewName },
219
+ executionTimeMs: timer(),
220
+ });
221
+ throw new Error(`Failed to drop view ${fullName}: ${errorMsg}`);
222
+ }
223
+ }
224
+ /**
225
+ * Create or alter a stored procedure.
226
+ */
227
+ async manageSproc(serverId, database, schemaName, sprocName, definition) {
228
+ this.validateIdentifier(schemaName, 'schema name');
229
+ this.validateIdentifier(sprocName, 'procedure name');
230
+ const timer = auditLogger.startTimer();
231
+ const fullName = `[${schemaName}].[${sprocName}]`;
232
+ try {
233
+ const pool = await this.connectionService.getPool(serverId, database);
234
+ const sql = `CREATE OR ALTER PROCEDURE ${fullName} ${definition}`;
235
+ await pool.request().query(sql);
236
+ const result = {
237
+ success: true,
238
+ message: `Stored procedure ${fullName} created or updated successfully.`,
239
+ objectName: `${schemaName}.${sprocName}`,
240
+ };
241
+ auditLogger.log({
242
+ operation: 'manage-sproc',
243
+ operationType: 'CREATE',
244
+ componentType: 'StoredProcedure',
245
+ componentName: `${serverId}/${database}/${fullName}`,
246
+ success: true,
247
+ parameters: { schemaName, sprocName, definitionLength: definition.length },
248
+ executionTimeMs: timer(),
249
+ });
250
+ return result;
251
+ }
252
+ catch (error) {
253
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
254
+ auditLogger.log({
255
+ operation: 'manage-sproc',
256
+ operationType: 'CREATE',
257
+ componentType: 'StoredProcedure',
258
+ componentName: `${serverId}/${database}/${fullName}`,
259
+ success: false,
260
+ error: errorMsg,
261
+ parameters: { schemaName, sprocName },
262
+ executionTimeMs: timer(),
263
+ });
264
+ throw new Error(`Failed to create/alter procedure ${fullName}: ${errorMsg}`);
265
+ }
266
+ }
267
+ /**
268
+ * Deploy a stored procedure from a local .sql file.
269
+ * Reads the file and executes its contents as-is against the database.
270
+ * The file must contain a valid CREATE OR ALTER PROCEDURE statement.
271
+ */
272
+ async deploySprocFromFile(serverId, database, filePath) {
273
+ const timer = auditLogger.startTimer();
274
+ const normalizedPath = filePath.replace(/\\/g, '/');
275
+ const resolvedPath = path.resolve(normalizedPath);
276
+ // Validate file extension
277
+ if (!resolvedPath.toLowerCase().endsWith('.sql')) {
278
+ throw new Error(`File must have a .sql extension. Got: '${path.basename(resolvedPath)}'`);
279
+ }
280
+ // Read file
281
+ let sql;
282
+ try {
283
+ sql = await fs.readFile(resolvedPath, 'utf-8');
284
+ }
285
+ catch (error) {
286
+ if (error.code === 'ENOENT') {
287
+ throw new Error(`File not found: '${resolvedPath}'`);
288
+ }
289
+ throw new Error(`Failed to read file '${resolvedPath}': ${error.message}`);
290
+ }
291
+ const trimmedSql = sql.trim();
292
+ if (!trimmedSql) {
293
+ throw new Error(`File is empty: '${resolvedPath}'`);
294
+ }
295
+ // Validate content starts with CREATE OR ALTER PROCEDURE (ignoring leading comments)
296
+ const cleanedForValidation = this.cleanSql(trimmedSql).toLowerCase();
297
+ if (!cleanedForValidation.startsWith('create or alter procedure')) {
298
+ throw new Error(`File must contain a CREATE OR ALTER PROCEDURE statement. ` +
299
+ `Got: '${cleanedForValidation.substring(0, 60)}...'`);
300
+ }
301
+ // Extract procedure name for audit logging (lightweight regex on cleaned SQL)
302
+ let procDisplayName = 'unknown';
303
+ const nameMatch = cleanedForValidation.match(/create\s+or\s+alter\s+procedure\s+(?:\[?(\w+)\]?\.)?\[?(\w+)\]?/i);
304
+ if (nameMatch) {
305
+ const schema = nameMatch[1] || 'dbo';
306
+ const name = nameMatch[2];
307
+ procDisplayName = `[${schema}].[${name}]`;
308
+ }
309
+ try {
310
+ const pool = await this.connectionService.getPool(serverId, database);
311
+ await pool.request().query(trimmedSql);
312
+ const result = {
313
+ success: true,
314
+ message: `Stored procedure ${procDisplayName} deployed successfully from file '${path.basename(resolvedPath)}'.`,
315
+ objectName: procDisplayName,
316
+ };
317
+ auditLogger.log({
318
+ operation: 'deploy-sproc-file',
319
+ operationType: 'CREATE',
320
+ componentType: 'StoredProcedure',
321
+ componentName: `${serverId}/${database}/${procDisplayName}`,
322
+ success: true,
323
+ parameters: { filePath: resolvedPath, fileSize: trimmedSql.length, procName: procDisplayName },
324
+ executionTimeMs: timer(),
325
+ });
326
+ return result;
327
+ }
328
+ catch (error) {
329
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
330
+ auditLogger.log({
331
+ operation: 'deploy-sproc-file',
332
+ operationType: 'CREATE',
333
+ componentType: 'StoredProcedure',
334
+ componentName: `${serverId}/${database}/${procDisplayName}`,
335
+ success: false,
336
+ error: errorMsg,
337
+ parameters: { filePath: resolvedPath, procName: procDisplayName },
338
+ executionTimeMs: timer(),
339
+ });
340
+ throw new Error(`Failed to deploy procedure from file '${path.basename(resolvedPath)}': ${errorMsg}`);
341
+ }
342
+ }
343
+ /**
344
+ * Drop a stored procedure if it exists.
345
+ */
346
+ async dropSproc(serverId, database, schemaName, sprocName) {
347
+ this.validateIdentifier(schemaName, 'schema name');
348
+ this.validateIdentifier(sprocName, 'procedure name');
349
+ const timer = auditLogger.startTimer();
350
+ const fullName = `[${schemaName}].[${sprocName}]`;
351
+ try {
352
+ const pool = await this.connectionService.getPool(serverId, database);
353
+ await pool.request().query(`DROP PROCEDURE IF EXISTS ${fullName}`);
354
+ const result = {
355
+ success: true,
356
+ message: `Stored procedure ${fullName} dropped successfully (if it existed).`,
357
+ objectName: `${schemaName}.${sprocName}`,
358
+ };
359
+ auditLogger.log({
360
+ operation: 'drop-sproc',
361
+ operationType: 'DELETE',
362
+ componentType: 'StoredProcedure',
363
+ componentName: `${serverId}/${database}/${fullName}`,
364
+ success: true,
365
+ parameters: { schemaName, sprocName },
366
+ executionTimeMs: timer(),
367
+ });
368
+ return result;
369
+ }
370
+ catch (error) {
371
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
372
+ auditLogger.log({
373
+ operation: 'drop-sproc',
374
+ operationType: 'DELETE',
375
+ componentType: 'StoredProcedure',
376
+ componentName: `${serverId}/${database}/${fullName}`,
377
+ success: false,
378
+ error: errorMsg,
379
+ parameters: { schemaName, sprocName },
380
+ executionTimeMs: timer(),
381
+ });
382
+ throw new Error(`Failed to drop procedure ${fullName}: ${errorMsg}`);
383
+ }
384
+ }
385
+ /**
386
+ * Execute a stored procedure using mssql request.execute() (not raw SQL).
387
+ * Parameters are passed via request.input() for safety.
388
+ */
389
+ async executeSproc(serverId, database, schemaName, sprocName, parameters) {
390
+ this.validateIdentifier(schemaName, 'schema name');
391
+ this.validateIdentifier(sprocName, 'procedure name');
392
+ const timer = auditLogger.startTimer();
393
+ const fullName = `${schemaName}.${sprocName}`;
394
+ try {
395
+ const pool = await this.connectionService.getPool(serverId, database);
396
+ const request = pool.request();
397
+ if (parameters) {
398
+ for (const [key, value] of Object.entries(parameters)) {
399
+ request.input(key, value);
400
+ }
401
+ }
402
+ const result = await request.execute(fullName);
403
+ const rows = result.recordset || [];
404
+ const sprocResult = {
405
+ rows,
406
+ rowCount: rows.length,
407
+ returnValue: result.returnValue,
408
+ };
409
+ auditLogger.log({
410
+ operation: 'execute-sproc',
411
+ operationType: 'READ',
412
+ componentType: 'StoredProcedure',
413
+ componentName: `${serverId}/${database}/${fullName}`,
414
+ success: true,
415
+ parameters: {
416
+ schemaName,
417
+ sprocName,
418
+ parameterCount: parameters ? Object.keys(parameters).length : 0,
419
+ rowCount: sprocResult.rowCount,
420
+ },
421
+ executionTimeMs: timer(),
422
+ });
423
+ return sprocResult;
424
+ }
425
+ catch (error) {
426
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
427
+ auditLogger.log({
428
+ operation: 'execute-sproc',
429
+ operationType: 'READ',
430
+ componentType: 'StoredProcedure',
431
+ componentName: `${serverId}/${database}/${fullName}`,
432
+ success: false,
433
+ error: errorMsg,
434
+ parameters: { schemaName, sprocName },
435
+ executionTimeMs: timer(),
436
+ });
437
+ throw new Error(`Failed to execute procedure ${fullName}: ${errorMsg}`);
438
+ }
439
+ }
440
+ /**
441
+ * Execute an INSERT query with safety validation.
442
+ */
443
+ async executeInsert(serverId, database, query) {
444
+ const timer = auditLogger.startTimer();
445
+ try {
446
+ this.validateDmlQuery(query, 'INSERT');
447
+ }
448
+ catch (error) {
449
+ auditLogger.log({
450
+ operation: 'execute-insert',
451
+ operationType: 'CREATE',
452
+ componentType: 'Query',
453
+ componentName: `${serverId}/${database}`,
454
+ success: false,
455
+ error: error.message,
456
+ parameters: { query: query.substring(0, 500) },
457
+ executionTimeMs: timer(),
458
+ });
459
+ throw error;
460
+ }
461
+ try {
462
+ const pool = await this.connectionService.getPool(serverId, database);
463
+ const result = await pool.request().query(query);
464
+ const writeResult = {
465
+ success: true,
466
+ message: `INSERT executed successfully. Rows affected: ${result.rowsAffected?.[0] ?? 0}.`,
467
+ rowsAffected: result.rowsAffected?.[0] ?? 0,
468
+ };
469
+ auditLogger.log({
470
+ operation: 'execute-insert',
471
+ operationType: 'CREATE',
472
+ componentType: 'Query',
473
+ componentName: `${serverId}/${database}`,
474
+ success: true,
475
+ parameters: {
476
+ query: query.substring(0, 500),
477
+ rowsAffected: writeResult.rowsAffected,
478
+ },
479
+ executionTimeMs: timer(),
480
+ });
481
+ return writeResult;
482
+ }
483
+ catch (error) {
484
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
485
+ auditLogger.log({
486
+ operation: 'execute-insert',
487
+ operationType: 'CREATE',
488
+ componentType: 'Query',
489
+ componentName: `${serverId}/${database}`,
490
+ success: false,
491
+ error: errorMsg,
492
+ parameters: { query: query.substring(0, 500) },
493
+ executionTimeMs: timer(),
494
+ });
495
+ throw new Error(`INSERT execution failed: ${errorMsg}`);
496
+ }
497
+ }
498
+ /**
499
+ * Execute an UPDATE query with safety validation.
500
+ */
501
+ async executeUpdate(serverId, database, query) {
502
+ const timer = auditLogger.startTimer();
503
+ try {
504
+ this.validateDmlQuery(query, 'UPDATE');
505
+ }
506
+ catch (error) {
507
+ auditLogger.log({
508
+ operation: 'execute-update',
509
+ operationType: 'UPDATE',
510
+ componentType: 'Query',
511
+ componentName: `${serverId}/${database}`,
512
+ success: false,
513
+ error: error.message,
514
+ parameters: { query: query.substring(0, 500) },
515
+ executionTimeMs: timer(),
516
+ });
517
+ throw error;
518
+ }
519
+ try {
520
+ const pool = await this.connectionService.getPool(serverId, database);
521
+ const result = await pool.request().query(query);
522
+ const writeResult = {
523
+ success: true,
524
+ message: `UPDATE executed successfully. Rows affected: ${result.rowsAffected?.[0] ?? 0}.`,
525
+ rowsAffected: result.rowsAffected?.[0] ?? 0,
526
+ };
527
+ auditLogger.log({
528
+ operation: 'execute-update',
529
+ operationType: 'UPDATE',
530
+ componentType: 'Query',
531
+ componentName: `${serverId}/${database}`,
532
+ success: true,
533
+ parameters: {
534
+ query: query.substring(0, 500),
535
+ rowsAffected: writeResult.rowsAffected,
536
+ },
537
+ executionTimeMs: timer(),
538
+ });
539
+ return writeResult;
540
+ }
541
+ catch (error) {
542
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
543
+ auditLogger.log({
544
+ operation: 'execute-update',
545
+ operationType: 'UPDATE',
546
+ componentType: 'Query',
547
+ componentName: `${serverId}/${database}`,
548
+ success: false,
549
+ error: errorMsg,
550
+ parameters: { query: query.substring(0, 500) },
551
+ executionTimeMs: timer(),
552
+ });
553
+ throw new Error(`UPDATE execution failed: ${errorMsg}`);
554
+ }
555
+ }
556
+ /**
557
+ * Execute a DELETE query with safety validation.
558
+ * REQUIRES a WHERE clause to prevent accidental full-table deletes.
559
+ */
560
+ async executeDelete(serverId, database, query) {
561
+ const timer = auditLogger.startTimer();
562
+ try {
563
+ this.validateDmlQuery(query, 'DELETE');
564
+ }
565
+ catch (error) {
566
+ auditLogger.log({
567
+ operation: 'execute-delete',
568
+ operationType: 'DELETE',
569
+ componentType: 'Query',
570
+ componentName: `${serverId}/${database}`,
571
+ success: false,
572
+ error: error.message,
573
+ parameters: { query: query.substring(0, 500) },
574
+ executionTimeMs: timer(),
575
+ });
576
+ throw error;
577
+ }
578
+ // Require WHERE clause for DELETE operations
579
+ const cleaned = this.cleanSql(query).toLowerCase();
580
+ if (!cleaned.includes('where')) {
581
+ const error = 'DELETE queries must include a WHERE clause to prevent accidental full-table deletion. ' +
582
+ 'If you truly need to delete all rows, use DELETE FROM table WHERE 1=1.';
583
+ auditLogger.log({
584
+ operation: 'execute-delete',
585
+ operationType: 'DELETE',
586
+ componentType: 'Query',
587
+ componentName: `${serverId}/${database}`,
588
+ success: false,
589
+ error,
590
+ parameters: { query: query.substring(0, 500) },
591
+ executionTimeMs: timer(),
592
+ });
593
+ throw new Error(error);
594
+ }
595
+ try {
596
+ const pool = await this.connectionService.getPool(serverId, database);
597
+ const result = await pool.request().query(query);
598
+ const writeResult = {
599
+ success: true,
600
+ message: `DELETE executed successfully. Rows affected: ${result.rowsAffected?.[0] ?? 0}.`,
601
+ rowsAffected: result.rowsAffected?.[0] ?? 0,
602
+ };
603
+ auditLogger.log({
604
+ operation: 'execute-delete',
605
+ operationType: 'DELETE',
606
+ componentType: 'Query',
607
+ componentName: `${serverId}/${database}`,
608
+ success: true,
609
+ parameters: {
610
+ query: query.substring(0, 500),
611
+ rowsAffected: writeResult.rowsAffected,
612
+ },
613
+ executionTimeMs: timer(),
614
+ });
615
+ return writeResult;
616
+ }
617
+ catch (error) {
618
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
619
+ auditLogger.log({
620
+ operation: 'execute-delete',
621
+ operationType: 'DELETE',
622
+ componentType: 'Query',
623
+ componentName: `${serverId}/${database}`,
624
+ success: false,
625
+ error: errorMsg,
626
+ parameters: { query: query.substring(0, 500) },
627
+ executionTimeMs: timer(),
628
+ });
629
+ throw new Error(`DELETE execution failed: ${errorMsg}`);
630
+ }
631
+ }
632
+ /**
633
+ * Split T-SQL on GO batch separators.
634
+ * Matches lines containing only GO (case-insensitive, optional whitespace).
635
+ */
636
+ splitBatches(sql) {
637
+ return sql
638
+ .split(/^\s*GO\s*$/im)
639
+ .map(batch => batch.trim())
640
+ .filter(batch => batch.length > 0);
641
+ }
642
+ /**
643
+ * Detect the primary operation type from a SQL batch for audit logging.
644
+ */
645
+ detectOperationType(sql) {
646
+ const cleaned = this.cleanSql(sql).toLowerCase();
647
+ if (cleaned.startsWith('select'))
648
+ return 'READ';
649
+ if (cleaned.startsWith('insert') || cleaned.startsWith('create'))
650
+ return 'CREATE';
651
+ if (cleaned.startsWith('update') || cleaned.startsWith('alter'))
652
+ return 'UPDATE';
653
+ if (cleaned.startsWith('delete') || cleaned.startsWith('drop') || cleaned.startsWith('truncate'))
654
+ return 'DELETE';
655
+ return 'UPDATE'; // default for EXEC, etc.
656
+ }
657
+ /**
658
+ * Execute any T-SQL without restrictions. Supports multi-batch scripts with GO separators.
659
+ * This is the "break glass" method — no validation, no keyword restrictions.
660
+ * The caller (MCP tool / CLI) is responsible for gating behind SQL_ENABLE_UNRESTRICTED.
661
+ */
662
+ async executeUnrestricted(serverId, database, sql) {
663
+ const batches = this.splitBatches(sql);
664
+ const results = [];
665
+ if (batches.length === 0) {
666
+ throw new Error('No SQL batches to execute. The input was empty or contained only GO separators.');
667
+ }
668
+ const pool = await this.connectionService.getPool(serverId, database);
669
+ for (let i = 0; i < batches.length; i++) {
670
+ const batchSql = batches[i];
671
+ const timer = auditLogger.startTimer();
672
+ const opType = this.detectOperationType(batchSql);
673
+ try {
674
+ const request = pool.request();
675
+ const result = await request.query(batchSql);
676
+ const batchResult = {
677
+ batchIndex: i,
678
+ sql: batchSql.substring(0, 200),
679
+ success: true,
680
+ rowsAffected: result.rowsAffected?.reduce((a, b) => a + b, 0) ?? 0,
681
+ };
682
+ // Include result set if there are rows (SELECT-like statements)
683
+ if (result.recordset && result.recordset.length > 0) {
684
+ batchResult.resultSet = result.recordset;
685
+ }
686
+ results.push(batchResult);
687
+ auditLogger.log({
688
+ operation: 'execute-unrestricted',
689
+ operationType: opType,
690
+ componentType: 'Query',
691
+ componentName: `${serverId}/${database}`,
692
+ success: true,
693
+ parameters: {
694
+ batchIndex: i,
695
+ totalBatches: batches.length,
696
+ sql: batchSql.substring(0, 500),
697
+ rowsAffected: batchResult.rowsAffected,
698
+ },
699
+ executionTimeMs: timer(),
700
+ });
701
+ }
702
+ catch (error) {
703
+ const errorMsg = this.connectionService.sanitizeErrorMessage(error.message);
704
+ results.push({
705
+ batchIndex: i,
706
+ sql: batchSql.substring(0, 200),
707
+ success: false,
708
+ error: errorMsg,
709
+ });
710
+ auditLogger.log({
711
+ operation: 'execute-unrestricted',
712
+ operationType: opType,
713
+ componentType: 'Query',
714
+ componentName: `${serverId}/${database}`,
715
+ success: false,
716
+ error: errorMsg,
717
+ parameters: {
718
+ batchIndex: i,
719
+ totalBatches: batches.length,
720
+ sql: batchSql.substring(0, 500),
721
+ },
722
+ executionTimeMs: timer(),
723
+ });
724
+ // Stop on first failure
725
+ break;
726
+ }
727
+ }
728
+ return {
729
+ batches: results,
730
+ totalBatches: batches.length,
731
+ completedBatches: results.filter(r => r.success).length,
732
+ };
733
+ }
734
+ }
735
+ //# sourceMappingURL=write-service.js.map