servicenow-mcp-server 2.1.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/.claude/settings.local.json +70 -0
- package/CLAUDE.md +777 -0
- package/LICENSE +21 -0
- package/README.md +562 -0
- package/assets/logo.svg +385 -0
- package/config/servicenow-instances.json.example +28 -0
- package/docs/403_TROUBLESHOOTING.md +329 -0
- package/docs/API_REFERENCE.md +1142 -0
- package/docs/APPLICATION_SCOPE_VALIDATION.md +681 -0
- package/docs/CLAUDE_DESKTOP_SETUP.md +373 -0
- package/docs/CONVENIENCE_TOOLS.md +601 -0
- package/docs/CONVENIENCE_TOOLS_SUMMARY.md +371 -0
- package/docs/FLOW_DESIGNER_GUIDE.md +1021 -0
- package/docs/IMPLEMENTATION_COMPLETE.md +165 -0
- package/docs/INSTANCE_SWITCHING_GUIDE.md +219 -0
- package/docs/MULTI_INSTANCE_CONFIGURATION.md +185 -0
- package/docs/NATURAL_LANGUAGE_SEARCH_IMPLEMENTATION.md +221 -0
- package/docs/PUPPETEER_INTEGRATION_PROPOSAL.md +1322 -0
- package/docs/QUICK_REFERENCE.md +395 -0
- package/docs/README.md +75 -0
- package/docs/RESOURCES_ARCHITECTURE.md +392 -0
- package/docs/RESOURCES_IMPLEMENTATION.md +276 -0
- package/docs/RESOURCES_SUMMARY.md +104 -0
- package/docs/SETUP_GUIDE.md +104 -0
- package/docs/UI_OPERATIONS_ARCHITECTURE.md +1219 -0
- package/docs/UI_OPERATIONS_DECISION_MATRIX.md +542 -0
- package/docs/UI_OPERATIONS_SUMMARY.md +507 -0
- package/docs/UPDATE_SET_VALIDATION.md +598 -0
- package/docs/UPDATE_SET_VALIDATION_SUMMARY.md +209 -0
- package/docs/VALIDATION_SUMMARY.md +479 -0
- package/jest.config.js +24 -0
- package/package.json +61 -0
- package/scripts/background_script_2025-09-29T20-19-35-101Z.js +23 -0
- package/scripts/link_ui_policy_actions_2025-09-29T20-17-15-218Z.js +90 -0
- package/scripts/set_update_set_Integration_Governance_Framework_2025-09-29T19-47-06-790Z.js +30 -0
- package/scripts/set_update_set_Integration_Governance_Framework_2025-09-29T19-59-33-152Z.js +30 -0
- package/scripts/set_update_set_current_2025-09-29T20-16-59-675Z.js +24 -0
- package/scripts/test_sys_dictionary_403.js +85 -0
- package/setup/setup-report.json +5313 -0
- package/src/config/comprehensive-table-definitions.json +2575 -0
- package/src/config/instance-config.json +4693 -0
- package/src/config/prompts.md +59 -0
- package/src/config/table-definitions.json +4681 -0
- package/src/config-manager.js +146 -0
- package/src/mcp-server-consolidated.js +2894 -0
- package/src/natural-language.js +472 -0
- package/src/resources.js +326 -0
- package/src/script-sync.js +428 -0
- package/src/server.js +125 -0
- package/src/servicenow-client.js +1625 -0
- package/src/stdio-server.js +52 -0
- package/start-mcp.sh +7 -0
|
@@ -0,0 +1,1625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServiceNow MCP Server - REST API Client
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Happy Technologies LLC
|
|
5
|
+
* Licensed under the MIT License - see LICENSE file for details
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import axios from 'axios';
|
|
9
|
+
|
|
10
|
+
export class ServiceNowClient {
|
|
11
|
+
constructor(instanceUrl, username, password) {
|
|
12
|
+
this.currentInstanceName = 'default';
|
|
13
|
+
this.setInstance(instanceUrl, username, password);
|
|
14
|
+
this.progressCallback = null; // Callback for progress notifications
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set progress callback for notifications
|
|
19
|
+
* @param {Function} callback - Function to call with progress updates
|
|
20
|
+
*/
|
|
21
|
+
setProgressCallback(callback) {
|
|
22
|
+
this.progressCallback = callback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Send progress notification
|
|
27
|
+
* @param {string} message - Progress message
|
|
28
|
+
*/
|
|
29
|
+
notifyProgress(message) {
|
|
30
|
+
if (this.progressCallback) {
|
|
31
|
+
this.progressCallback(message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Switch to a different ServiceNow instance
|
|
37
|
+
* @param {string} instanceUrl - Instance URL
|
|
38
|
+
* @param {string} username - Username
|
|
39
|
+
* @param {string} password - Password
|
|
40
|
+
* @param {string} instanceName - Optional instance name for tracking
|
|
41
|
+
*/
|
|
42
|
+
setInstance(instanceUrl, username, password, instanceName = null) {
|
|
43
|
+
this.instanceUrl = instanceUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
44
|
+
this.auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
45
|
+
|
|
46
|
+
if (instanceName) {
|
|
47
|
+
this.currentInstanceName = instanceName;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.client = axios.create({
|
|
51
|
+
baseURL: this.instanceUrl,
|
|
52
|
+
headers: {
|
|
53
|
+
'Authorization': `Basic ${this.auth}`,
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
'Accept': 'application/json'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get current instance information
|
|
62
|
+
* @returns {object} Current instance details
|
|
63
|
+
*/
|
|
64
|
+
getCurrentInstance() {
|
|
65
|
+
return {
|
|
66
|
+
name: this.currentInstanceName,
|
|
67
|
+
url: this.instanceUrl
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Generic table operations
|
|
72
|
+
async getRecords(table, query = {}) {
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
if (query.sysparm_query) params.append('sysparm_query', query.sysparm_query);
|
|
75
|
+
if (query.sysparm_limit) params.append('sysparm_limit', query.sysparm_limit);
|
|
76
|
+
if (query.sysparm_fields) params.append('sysparm_fields', query.sysparm_fields);
|
|
77
|
+
|
|
78
|
+
const response = await this.client.get(`/api/now/table/${table}?${params}`);
|
|
79
|
+
return response.data.result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getRecord(table, sysId, queryParams = {}) {
|
|
83
|
+
const params = new URLSearchParams();
|
|
84
|
+
if (queryParams.sysparm_fields) params.append('sysparm_fields', queryParams.sysparm_fields);
|
|
85
|
+
if (queryParams.sysparm_display_value) params.append('sysparm_display_value', queryParams.sysparm_display_value);
|
|
86
|
+
if (queryParams.sysparm_exclude_reference_link) params.append('sysparm_exclude_reference_link', queryParams.sysparm_exclude_reference_link);
|
|
87
|
+
|
|
88
|
+
const queryString = params.toString();
|
|
89
|
+
const url = queryString ? `/api/now/table/${table}/${sysId}?${queryString}` : `/api/now/table/${table}/${sysId}`;
|
|
90
|
+
|
|
91
|
+
const response = await this.client.get(url);
|
|
92
|
+
return response.data.result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async createRecord(table, data) {
|
|
96
|
+
const response = await this.client.post(`/api/now/table/${table}`, data);
|
|
97
|
+
return response.data.result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async updateRecord(table, sysId, data) {
|
|
101
|
+
const response = await this.client.put(`/api/now/table/${table}/${sysId}`, data);
|
|
102
|
+
return response.data.result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async deleteRecord(table, sysId) {
|
|
106
|
+
await this.client.delete(`/api/now/table/${table}/${sysId}`);
|
|
107
|
+
return { success: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Update set management via UI API endpoint
|
|
111
|
+
async setCurrentUpdateSet(updateSetSysId) {
|
|
112
|
+
try {
|
|
113
|
+
// First, get the update set name
|
|
114
|
+
const updateSet = await this.getRecord('sys_update_set', updateSetSysId);
|
|
115
|
+
|
|
116
|
+
// Create axios client with UI session
|
|
117
|
+
const axiosWithCookies = axios.create({
|
|
118
|
+
baseURL: this.instanceUrl,
|
|
119
|
+
headers: {
|
|
120
|
+
'Authorization': `Basic ${this.auth}`,
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
'Accept': 'application/json',
|
|
123
|
+
'User-Agent': 'ServiceNow-MCP-Client/2.0'
|
|
124
|
+
},
|
|
125
|
+
withCredentials: true,
|
|
126
|
+
maxRedirects: 5
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Establish session first
|
|
130
|
+
await axiosWithCookies.get('/', {
|
|
131
|
+
headers: { 'Accept': 'text/html' }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Set the update set via UI API
|
|
135
|
+
const response = await axiosWithCookies.put(
|
|
136
|
+
'/api/now/ui/concoursepicker/updateset',
|
|
137
|
+
{
|
|
138
|
+
name: updateSet.name,
|
|
139
|
+
sysId: updateSetSysId
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
success: true,
|
|
145
|
+
update_set: updateSet.name,
|
|
146
|
+
sys_id: updateSetSysId,
|
|
147
|
+
response: response.data
|
|
148
|
+
};
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// If UI API fails, fall back to sys_trigger method
|
|
151
|
+
console.log('UI API failed, falling back to sys_trigger...');
|
|
152
|
+
|
|
153
|
+
const updateSet = await this.getRecord('sys_update_set', updateSetSysId);
|
|
154
|
+
const script = `// Update user preference for current update set
|
|
155
|
+
var updateSetId = '${updateSetSysId}';
|
|
156
|
+
|
|
157
|
+
// Delete existing preference
|
|
158
|
+
var delGR = new GlideRecord('sys_user_preference');
|
|
159
|
+
delGR.addQuery('user', gs.getUserID());
|
|
160
|
+
delGR.addQuery('name', 'sys_update_set');
|
|
161
|
+
delGR.query();
|
|
162
|
+
if (delGR.next()) {
|
|
163
|
+
delGR.deleteRecord();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create new preference
|
|
167
|
+
var gr = new GlideRecord('sys_user_preference');
|
|
168
|
+
gr.initialize();
|
|
169
|
+
gr.user = gs.getUserID();
|
|
170
|
+
gr.name = 'sys_update_set';
|
|
171
|
+
gr.value = updateSetId;
|
|
172
|
+
gr.insert();
|
|
173
|
+
|
|
174
|
+
gs.info('✅ Update set changed to: ${updateSet.name}');`;
|
|
175
|
+
|
|
176
|
+
const result = await this.executeScriptViaTrigger(script, `Set update set to: ${updateSet.name}`, true);
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
update_set: updateSet.name,
|
|
180
|
+
sys_id: updateSetSysId,
|
|
181
|
+
method: 'sys_trigger',
|
|
182
|
+
trigger_details: result
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getCurrentUpdateSet() {
|
|
188
|
+
// Get the current update set preference
|
|
189
|
+
const response = await this.client.get(`/api/now/ui/preferences/sys_update_set`);
|
|
190
|
+
return response.data;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async listUpdateSets(query = {}) {
|
|
194
|
+
// List available update sets
|
|
195
|
+
return this.getRecords('sys_update_set', query);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async setCurrentApplication(appSysId) {
|
|
199
|
+
const startTime = Date.now();
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Validate input
|
|
203
|
+
if (!appSysId) {
|
|
204
|
+
throw new Error('app_sys_id is required');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Validate sys_id format (32-character hex string)
|
|
208
|
+
if (!/^[0-9a-f]{32}$/i.test(appSysId)) {
|
|
209
|
+
throw new Error(`Invalid sys_id format: ${appSysId}. Must be a 32-character hexadecimal string.`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Get previous application scope (for rollback information)
|
|
213
|
+
let previousScope = null;
|
|
214
|
+
try {
|
|
215
|
+
const prefResponse = await this.client.get('/api/now/ui/preferences/apps.current');
|
|
216
|
+
if (prefResponse.data && prefResponse.data.result) {
|
|
217
|
+
previousScope = {
|
|
218
|
+
sys_id: prefResponse.data.result.value || null,
|
|
219
|
+
name: prefResponse.data.result.display_value || null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
} catch (prefError) {
|
|
223
|
+
// Previous scope query failed - not critical, continue
|
|
224
|
+
console.log('Could not retrieve previous scope:', prefError.message);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get application details
|
|
228
|
+
let app;
|
|
229
|
+
try {
|
|
230
|
+
app = await this.getRecord('sys_app', appSysId);
|
|
231
|
+
} catch (appError) {
|
|
232
|
+
if (appError.response && appError.response.status === 404) {
|
|
233
|
+
throw new Error(`Application not found with sys_id: ${appSysId}. Please verify the sys_id is correct.`);
|
|
234
|
+
}
|
|
235
|
+
throw appError;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Create axios client with cookie jar
|
|
239
|
+
const axiosWithCookies = axios.create({
|
|
240
|
+
baseURL: this.instanceUrl,
|
|
241
|
+
headers: {
|
|
242
|
+
'Authorization': `Basic ${this.auth}`,
|
|
243
|
+
'Content-Type': 'application/json',
|
|
244
|
+
'User-Agent': 'ServiceNow-MCP-Client/2.0'
|
|
245
|
+
},
|
|
246
|
+
withCredentials: true,
|
|
247
|
+
maxRedirects: 5
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Establish session first
|
|
251
|
+
await axiosWithCookies.get('/', {
|
|
252
|
+
headers: { 'Accept': 'text/html' }
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Set the application via UI API
|
|
256
|
+
const response = await axiosWithCookies.put(
|
|
257
|
+
'/api/now/ui/concoursepicker/application',
|
|
258
|
+
{
|
|
259
|
+
app_id: appSysId
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Verify the scope was set correctly
|
|
264
|
+
let verified = false;
|
|
265
|
+
let verificationError = null;
|
|
266
|
+
try {
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 500)); // Wait for preference to update
|
|
268
|
+
const verifyResponse = await this.client.get('/api/now/ui/preferences/apps.current');
|
|
269
|
+
if (verifyResponse.data && verifyResponse.data.result) {
|
|
270
|
+
const currentAppId = verifyResponse.data.result.value;
|
|
271
|
+
verified = (currentAppId === appSysId);
|
|
272
|
+
if (!verified) {
|
|
273
|
+
verificationError = `Verification failed: Current app is ${currentAppId}, expected ${appSysId}`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (verifyError) {
|
|
277
|
+
verificationError = `Verification query failed: ${verifyError.message}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const executionTime = Date.now() - startTime;
|
|
281
|
+
|
|
282
|
+
const result = {
|
|
283
|
+
success: true,
|
|
284
|
+
application: app.name,
|
|
285
|
+
scope: app.scope || 'global',
|
|
286
|
+
sys_id: appSysId,
|
|
287
|
+
previous_scope: previousScope,
|
|
288
|
+
verified: verified,
|
|
289
|
+
verification_error: verificationError,
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
execution_time_ms: executionTime,
|
|
292
|
+
method: 'ui_api',
|
|
293
|
+
endpoint: '/api/now/ui/concoursepicker/application',
|
|
294
|
+
response: response.data
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Add warnings if applicable
|
|
298
|
+
result.warnings = [];
|
|
299
|
+
if (!verified) {
|
|
300
|
+
result.warnings.push('Could not verify scope change - please check ServiceNow UI');
|
|
301
|
+
}
|
|
302
|
+
if (verificationError) {
|
|
303
|
+
result.warnings.push(verificationError);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const executionTime = Date.now() - startTime;
|
|
309
|
+
|
|
310
|
+
// Enhanced error messages based on error type
|
|
311
|
+
let errorMessage = error.message;
|
|
312
|
+
|
|
313
|
+
if (error.response) {
|
|
314
|
+
const status = error.response.status;
|
|
315
|
+
if (status === 401) {
|
|
316
|
+
errorMessage = 'Authentication failed. Please check your credentials.';
|
|
317
|
+
} else if (status === 403) {
|
|
318
|
+
errorMessage = `Access denied. Please verify:\n1. You have admin or developer role\n2. You have access to the application\n3. The application is active`;
|
|
319
|
+
} else if (status === 404) {
|
|
320
|
+
errorMessage = `Application not found with sys_id: ${appSysId}`;
|
|
321
|
+
} else if (status >= 500) {
|
|
322
|
+
errorMessage = `ServiceNow server error (${status}). Please try again later.`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.error('Failed to set current application:', errorMessage);
|
|
327
|
+
|
|
328
|
+
const enhancedError = new Error(`Failed to set current application: ${errorMessage}`);
|
|
329
|
+
enhancedError.execution_time_ms = executionTime;
|
|
330
|
+
enhancedError.app_sys_id = appSysId;
|
|
331
|
+
enhancedError.original_error = error;
|
|
332
|
+
|
|
333
|
+
throw enhancedError;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Incident-specific methods
|
|
338
|
+
async getIncidents(query = {}) {
|
|
339
|
+
return this.getRecords('incident', query);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async getIncidentByNumber(number) {
|
|
343
|
+
const incidents = await this.getRecords('incident', {
|
|
344
|
+
sysparm_query: `number=${number}`,
|
|
345
|
+
sysparm_limit: 1
|
|
346
|
+
});
|
|
347
|
+
return incidents[0] || null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async createIncident(data) {
|
|
351
|
+
return this.createRecord('incident', data);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async updateIncident(sysId, data) {
|
|
355
|
+
return this.updateRecord('incident', sysId, data);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// User management
|
|
359
|
+
async getUsers(query = {}) {
|
|
360
|
+
return this.getRecords('sys_user', query);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async createUser(data) {
|
|
364
|
+
return this.createRecord('sys_user', data);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async updateUser(sysId, data) {
|
|
368
|
+
return this.updateRecord('sys_user', sysId, data);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Catalog items
|
|
372
|
+
async getCatalogItems(query = {}) {
|
|
373
|
+
return this.getRecords('sc_cat_item', query);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async createCatalogItem(data) {
|
|
377
|
+
return this.createRecord('sc_cat_item', data);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Change requests
|
|
381
|
+
async getChangeRequests(query = {}) {
|
|
382
|
+
return this.getRecords('change_request', query);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async getChangeRequestByNumber(number) {
|
|
386
|
+
const changes = await this.getRecords('change_request', {
|
|
387
|
+
sysparm_query: `number=${number}`,
|
|
388
|
+
sysparm_limit: 1
|
|
389
|
+
});
|
|
390
|
+
return changes[0] || null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async createChangeRequest(data) {
|
|
394
|
+
return this.createRecord('change_request', data);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async updateChangeRequest(sysId, data) {
|
|
398
|
+
return this.updateRecord('change_request', sysId, data);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Problems
|
|
402
|
+
async getProblems(query = {}) {
|
|
403
|
+
return this.getRecords('problem', query);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async getProblemByNumber(number) {
|
|
407
|
+
const problems = await this.getRecords('problem', {
|
|
408
|
+
sysparm_query: `number=${number}`,
|
|
409
|
+
sysparm_limit: 1
|
|
410
|
+
});
|
|
411
|
+
return problems[0] || null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async createProblem(data) {
|
|
415
|
+
return this.createRecord('problem', data);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async updateProblem(sysId, data) {
|
|
419
|
+
return this.updateRecord('problem', sysId, data);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Service Requests
|
|
423
|
+
async getServiceRequests(query = {}) {
|
|
424
|
+
return this.getRecords('sc_request', query);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async createServiceRequest(data) {
|
|
428
|
+
return this.createRecord('sc_request', data);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async updateServiceRequest(sysId, data) {
|
|
432
|
+
return this.updateRecord('sc_request', sysId, data);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Groups
|
|
436
|
+
async getGroups(query = {}) {
|
|
437
|
+
return this.getRecords('sys_user_group', query);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async createGroup(data) {
|
|
441
|
+
return this.createRecord('sys_user_group', data);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async updateGroup(sysId, data) {
|
|
445
|
+
return this.updateRecord('sys_user_group', sysId, data);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Group membership
|
|
449
|
+
async addUserToGroup(userId, groupId) {
|
|
450
|
+
return this.createRecord('sys_user_grmember', {
|
|
451
|
+
user: userId,
|
|
452
|
+
group: groupId
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async removeUserFromGroup(userId, groupId) {
|
|
457
|
+
const members = await this.getRecords('sys_user_grmember', {
|
|
458
|
+
sysparm_query: `user=${userId}^group=${groupId}`,
|
|
459
|
+
sysparm_limit: 1
|
|
460
|
+
});
|
|
461
|
+
if (members[0]) {
|
|
462
|
+
await this.deleteRecord('sys_user_grmember', members[0].sys_id);
|
|
463
|
+
}
|
|
464
|
+
return { success: true };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Knowledge Base
|
|
468
|
+
async getKnowledgeBases(query = {}) {
|
|
469
|
+
return this.getRecords('kb_knowledge_base', query);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async createKnowledgeBase(data) {
|
|
473
|
+
return this.createRecord('kb_knowledge_base', data);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Knowledge Articles
|
|
477
|
+
async getKnowledgeArticles(query = {}) {
|
|
478
|
+
return this.getRecords('kb_knowledge', query);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async createKnowledgeArticle(data) {
|
|
482
|
+
return this.createRecord('kb_knowledge', data);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async updateKnowledgeArticle(sysId, data) {
|
|
486
|
+
return this.updateRecord('kb_knowledge', sysId, data);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Catalog Categories
|
|
490
|
+
async getCatalogCategories(query = {}) {
|
|
491
|
+
return this.getRecords('sc_category', query);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async createCatalogCategory(data) {
|
|
495
|
+
return this.createRecord('sc_category', data);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async updateCatalogCategory(sysId, data) {
|
|
499
|
+
return this.updateRecord('sc_category', sysId, data);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Add comments/work notes to any table
|
|
503
|
+
async addComment(table, recordId, comment, isWorkNote = false) {
|
|
504
|
+
const field = isWorkNote ? 'work_notes' : 'comments';
|
|
505
|
+
const updateData = { [field]: comment };
|
|
506
|
+
return this.updateRecord(table, recordId, updateData);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Generic search across tables
|
|
510
|
+
async searchRecords(table, searchTerm, fields = [], limit = 10) {
|
|
511
|
+
const searchQuery = fields.length > 0
|
|
512
|
+
? fields.map(field => `${field}CONTAINS${searchTerm}`).join('^OR')
|
|
513
|
+
: `short_descriptionCONTAINS${searchTerm}^ORdescriptionCONTAINS${searchTerm}`;
|
|
514
|
+
|
|
515
|
+
return this.getRecords(table, {
|
|
516
|
+
sysparm_query: searchQuery,
|
|
517
|
+
sysparm_limit: limit
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Configuration Items (CMDB)
|
|
522
|
+
async getConfigurationItems(query = {}) {
|
|
523
|
+
return this.getRecords('cmdb_ci', query);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async getConfigurationItem(sysId) {
|
|
527
|
+
return this.getRecord('cmdb_ci', sysId);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async createConfigurationItem(data) {
|
|
531
|
+
return this.createRecord('cmdb_ci', data);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async updateConfigurationItem(sysId, data) {
|
|
535
|
+
return this.updateRecord('cmdb_ci', sysId, data);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Business Rules
|
|
539
|
+
async getBusinessRules(query = {}) {
|
|
540
|
+
return this.getRecords('sys_script', query);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async createBusinessRule(data) {
|
|
544
|
+
return this.createRecord('sys_script', data);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async updateBusinessRule(sysId, data) {
|
|
548
|
+
return this.updateRecord('sys_script', sysId, data);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Update Sets
|
|
552
|
+
async getUpdateSets(query = {}) {
|
|
553
|
+
return this.getRecords('sys_update_set', query);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async createUpdateSet(data) {
|
|
557
|
+
return this.createRecord('sys_update_set', data);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async updateUpdateSet(sysId, data) {
|
|
561
|
+
return this.updateRecord('sys_update_set', sysId, data);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Workflows
|
|
565
|
+
async getWorkflows(query = {}) {
|
|
566
|
+
return this.getRecords('wf_workflow', query);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async createWorkflow(data) {
|
|
570
|
+
return this.createRecord('wf_workflow', data);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Attachments
|
|
574
|
+
async getAttachments(tableId, recordId) {
|
|
575
|
+
return this.getRecords('sys_attachment', {
|
|
576
|
+
sysparm_query: `table_name=${tableId}^table_sys_id=${recordId}`
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async createAttachment(tableName, recordId, fileName, contentType, data) {
|
|
581
|
+
const formData = new FormData();
|
|
582
|
+
formData.append('table_name', tableName);
|
|
583
|
+
formData.append('table_sys_id', recordId);
|
|
584
|
+
formData.append('file_name', fileName);
|
|
585
|
+
formData.append('content_type', contentType);
|
|
586
|
+
formData.append('content', data);
|
|
587
|
+
|
|
588
|
+
const response = await this.client.post('/api/now/attachment/file', formData, {
|
|
589
|
+
headers: {
|
|
590
|
+
'Content-Type': 'multipart/form-data'
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
return response.data.result;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// SLA Management
|
|
597
|
+
async getSLAs(query = {}) {
|
|
598
|
+
return this.getRecords('contract_sla', query);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async getSLADefinitions(query = {}) {
|
|
602
|
+
return this.getRecords('sla_definition', query);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Task Assignment
|
|
606
|
+
async assignTask(table, recordId, assignedTo, assignmentGroup = null) {
|
|
607
|
+
const updateData = { assigned_to: assignedTo };
|
|
608
|
+
if (assignmentGroup) {
|
|
609
|
+
updateData.assignment_group = assignmentGroup;
|
|
610
|
+
}
|
|
611
|
+
return this.updateRecord(table, recordId, updateData);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Reports
|
|
615
|
+
async getReports(query = {}) {
|
|
616
|
+
return this.getRecords('sys_report', query);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async createReport(data) {
|
|
620
|
+
return this.createRecord('sys_report', data);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Execute script via sys_trigger (scheduled job that runs immediately)
|
|
624
|
+
async executeScriptViaTrigger(script, description = 'MCP Script Execution', autoDelete = true) {
|
|
625
|
+
try {
|
|
626
|
+
// Calculate next action time (1 second from now)
|
|
627
|
+
const now = new Date();
|
|
628
|
+
const nextAction = new Date(now.getTime() + 1000); // 1 second from now
|
|
629
|
+
|
|
630
|
+
// Format: YYYY-MM-DD HH:MM:SS
|
|
631
|
+
const formatDateTime = (date) => {
|
|
632
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
633
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Wrap script with auto-delete logic if requested
|
|
637
|
+
let finalScript = script;
|
|
638
|
+
let triggerSysId = null;
|
|
639
|
+
if (autoDelete) {
|
|
640
|
+
// We'll set the sys_id after creation, then update the script
|
|
641
|
+
finalScript = script; // Use original script for now
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Create sys_trigger record
|
|
645
|
+
const trigger = await this.createRecord('sys_trigger', {
|
|
646
|
+
name: `MCP_Script_${Date.now()}`,
|
|
647
|
+
script: finalScript,
|
|
648
|
+
next_action: formatDateTime(nextAction),
|
|
649
|
+
trigger_type: '0', // Run once
|
|
650
|
+
state: '0', // Ready state
|
|
651
|
+
description: description || 'Automated script execution via MCP'
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// If auto-delete requested, update script with self-delete logic
|
|
655
|
+
if (autoDelete) {
|
|
656
|
+
const scriptWithDelete = `
|
|
657
|
+
// Auto-generated MCP script trigger
|
|
658
|
+
try {
|
|
659
|
+
${script}
|
|
660
|
+
} finally {
|
|
661
|
+
// Auto-delete this trigger after execution
|
|
662
|
+
var triggerGR = new GlideRecord('sys_trigger');
|
|
663
|
+
if (triggerGR.get('${trigger.sys_id}')) {
|
|
664
|
+
triggerGR.deleteRecord();
|
|
665
|
+
gs.info('MCP: Auto-deleted trigger ${trigger.sys_id}');
|
|
666
|
+
}
|
|
667
|
+
}`;
|
|
668
|
+
|
|
669
|
+
await this.updateRecord('sys_trigger', trigger.sys_id, {
|
|
670
|
+
script: scriptWithDelete
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
success: true,
|
|
676
|
+
trigger_sys_id: trigger.sys_id,
|
|
677
|
+
trigger_name: trigger.name,
|
|
678
|
+
next_action: formatDateTime(nextAction),
|
|
679
|
+
auto_delete: autoDelete,
|
|
680
|
+
message: `Script scheduled to run at ${formatDateTime(nextAction)}. ${autoDelete ? 'Trigger will auto-delete after execution.' : 'Trigger will remain after execution.'}`
|
|
681
|
+
};
|
|
682
|
+
} catch (error) {
|
|
683
|
+
throw new Error(`Failed to create script trigger: ${error.message}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Background script execution via UI endpoint (NOT WORKING - requires interactive session)
|
|
688
|
+
// NOTE: /sys.scripts.do endpoint requires interactive browser session with cookies
|
|
689
|
+
// from login.do - Basic Auth is not sufficient. Always fails with X-Is-Logged-In: false
|
|
690
|
+
// Use executeScriptViaTrigger() instead.
|
|
691
|
+
async executeBackgroundScript(script, scope = 'global') {
|
|
692
|
+
throw new Error('Direct UI script execution not supported - use sys_trigger method instead');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Batch operations
|
|
696
|
+
async batchCreate(operations, transaction = true, reportProgress = true) {
|
|
697
|
+
const results = {
|
|
698
|
+
success: true,
|
|
699
|
+
created_count: 0,
|
|
700
|
+
sys_ids: {},
|
|
701
|
+
errors: [],
|
|
702
|
+
execution_time_ms: 0
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const startTime = Date.now();
|
|
706
|
+
const total = operations.length;
|
|
707
|
+
|
|
708
|
+
// Determine progress reporting frequency
|
|
709
|
+
const shouldReport = (index) => {
|
|
710
|
+
if (!reportProgress) return false;
|
|
711
|
+
if (total <= 10) return true; // Report every item for small batches
|
|
712
|
+
if (total <= 50) return (index + 1) % 5 === 0 || index === total - 1; // Every 5 items
|
|
713
|
+
return (index + 1) % Math.ceil(total / 10) === 0 || index === total - 1; // Every 10%
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
for (let i = 0; i < operations.length; i++) {
|
|
718
|
+
const op = operations[i];
|
|
719
|
+
try {
|
|
720
|
+
// Replace variable references from previous operations
|
|
721
|
+
let processedData = JSON.stringify(op.data);
|
|
722
|
+
Object.keys(results.sys_ids).forEach(key => {
|
|
723
|
+
processedData = processedData.replace(`\${${key}}`, results.sys_ids[key]);
|
|
724
|
+
});
|
|
725
|
+
const data = JSON.parse(processedData);
|
|
726
|
+
|
|
727
|
+
const result = await this.createRecord(op.table, data);
|
|
728
|
+
|
|
729
|
+
// Save sys_id with the save_as key or operation index
|
|
730
|
+
const key = op.save_as || `operation_${i}`;
|
|
731
|
+
results.sys_ids[key] = result.sys_id;
|
|
732
|
+
results.created_count++;
|
|
733
|
+
|
|
734
|
+
// Report progress
|
|
735
|
+
if (shouldReport(i)) {
|
|
736
|
+
const percentage = Math.round(((i + 1) / total) * 100);
|
|
737
|
+
this.notifyProgress(`Creating record ${i + 1}/${total} (${percentage}%): ${op.table}`);
|
|
738
|
+
}
|
|
739
|
+
} catch (error) {
|
|
740
|
+
results.errors.push({
|
|
741
|
+
operation_index: i,
|
|
742
|
+
table: op.table,
|
|
743
|
+
error: error.message
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
if (reportProgress) {
|
|
747
|
+
this.notifyProgress(`Failed ${i + 1}/${total}: ${op.table} - ${error.message}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (transaction) {
|
|
751
|
+
results.success = false;
|
|
752
|
+
throw new Error(`Batch create failed at operation ${i}: ${error.message}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Final summary
|
|
758
|
+
if (reportProgress) {
|
|
759
|
+
const failedCount = results.errors.length;
|
|
760
|
+
if (failedCount > 0) {
|
|
761
|
+
this.notifyProgress(`Complete: ${results.created_count}/${total} records created (${failedCount} failed)`);
|
|
762
|
+
} else {
|
|
763
|
+
this.notifyProgress(`Complete: All ${total} records created successfully`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} finally {
|
|
767
|
+
results.execution_time_ms = Date.now() - startTime;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return results;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async batchUpdate(updates, stopOnError = false, reportProgress = true) {
|
|
774
|
+
const results = {
|
|
775
|
+
success: true,
|
|
776
|
+
updated_count: 0,
|
|
777
|
+
errors: [],
|
|
778
|
+
execution_time_ms: 0
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const startTime = Date.now();
|
|
782
|
+
const total = updates.length;
|
|
783
|
+
|
|
784
|
+
// Determine progress reporting frequency
|
|
785
|
+
const shouldReport = (index) => {
|
|
786
|
+
if (!reportProgress) return false;
|
|
787
|
+
if (total <= 10) return true; // Report every item for small batches
|
|
788
|
+
if (total <= 50) return (index + 1) % 5 === 0 || index === total - 1; // Every 5 items
|
|
789
|
+
return (index + 1) % Math.ceil(total / 10) === 0 || index === total - 1; // Every 10%
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
for (let i = 0; i < updates.length; i++) {
|
|
793
|
+
const update = updates[i];
|
|
794
|
+
try {
|
|
795
|
+
await this.updateRecord(update.table, update.sys_id, update.data);
|
|
796
|
+
results.updated_count++;
|
|
797
|
+
|
|
798
|
+
// Report progress
|
|
799
|
+
if (shouldReport(i)) {
|
|
800
|
+
const percentage = Math.round(((i + 1) / total) * 100);
|
|
801
|
+
this.notifyProgress(`Updating record ${i + 1}/${total} (${percentage}%): ${update.table}`);
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
results.errors.push({
|
|
805
|
+
update_index: i,
|
|
806
|
+
table: update.table,
|
|
807
|
+
sys_id: update.sys_id,
|
|
808
|
+
error: error.message
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
if (reportProgress) {
|
|
812
|
+
this.notifyProgress(`Failed ${i + 1}/${total}: ${update.table} - ${error.message}`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (stopOnError) {
|
|
816
|
+
results.success = false;
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Final summary
|
|
823
|
+
if (reportProgress) {
|
|
824
|
+
const failedCount = results.errors.length;
|
|
825
|
+
if (failedCount > 0) {
|
|
826
|
+
this.notifyProgress(`Complete: ${results.updated_count}/${total} records updated (${failedCount} failed)`);
|
|
827
|
+
} else {
|
|
828
|
+
this.notifyProgress(`Complete: All ${total} records updated successfully`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
results.execution_time_ms = Date.now() - startTime;
|
|
833
|
+
return results;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Workflow creation methods
|
|
837
|
+
async createWorkflow(workflowData) {
|
|
838
|
+
// Create base workflow
|
|
839
|
+
const workflow = await this.createRecord('wf_workflow', {
|
|
840
|
+
name: workflowData.name,
|
|
841
|
+
description: workflowData.description || '',
|
|
842
|
+
template: workflowData.template || false,
|
|
843
|
+
access: workflowData.access || 'public'
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
workflow_sys_id: workflow.sys_id,
|
|
848
|
+
name: workflowData.name
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async createWorkflowVersion(versionData) {
|
|
853
|
+
// Create workflow version
|
|
854
|
+
const version = await this.createRecord('wf_workflow_version', {
|
|
855
|
+
name: versionData.name,
|
|
856
|
+
workflow: versionData.workflow_sys_id,
|
|
857
|
+
table: versionData.table,
|
|
858
|
+
description: versionData.description || '',
|
|
859
|
+
active: versionData.active !== undefined ? versionData.active : true,
|
|
860
|
+
published: versionData.published || false,
|
|
861
|
+
condition: versionData.condition || '',
|
|
862
|
+
order: versionData.order || 100,
|
|
863
|
+
run_multiple: versionData.run_multiple || false,
|
|
864
|
+
after_business_rules: versionData.after_business_rules !== undefined ? versionData.after_business_rules : true,
|
|
865
|
+
expected_time: versionData.expected_time || '',
|
|
866
|
+
condition_type: versionData.condition_type || ''
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
return {
|
|
870
|
+
version_sys_id: version.sys_id,
|
|
871
|
+
name: versionData.name
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async createActivity(activityData) {
|
|
876
|
+
// Create activity
|
|
877
|
+
const activity = await this.createRecord('wf_activity', {
|
|
878
|
+
name: activityData.name,
|
|
879
|
+
workflow_version: activityData.workflow_version_sys_id,
|
|
880
|
+
activity_definition: activityData.activity_definition_sys_id || '',
|
|
881
|
+
x: activityData.x || 100,
|
|
882
|
+
y: activityData.y || 100,
|
|
883
|
+
width: activityData.width || 150,
|
|
884
|
+
height: activityData.height || 80,
|
|
885
|
+
input: activityData.script || activityData.input || '',
|
|
886
|
+
vars: activityData.vars || '',
|
|
887
|
+
stage: activityData.stage_sys_id || '',
|
|
888
|
+
parent: activityData.parent_sys_id || '',
|
|
889
|
+
timeout: activityData.timeout || '0 00:00:00'
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
activity_sys_id: activity.sys_id,
|
|
894
|
+
name: activityData.name
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async createCondition(conditionData) {
|
|
899
|
+
// Create condition
|
|
900
|
+
const condition = await this.createRecord('wf_condition', {
|
|
901
|
+
activity: conditionData.activity_sys_id,
|
|
902
|
+
name: conditionData.name,
|
|
903
|
+
short_description: conditionData.description || '',
|
|
904
|
+
condition: conditionData.condition || '',
|
|
905
|
+
order: conditionData.order || 1,
|
|
906
|
+
else_flag: conditionData.else_flag || false,
|
|
907
|
+
event: conditionData.event || false,
|
|
908
|
+
event_name: conditionData.event_name || '',
|
|
909
|
+
condition_type: conditionData.condition_type || 'standard'
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
return {
|
|
913
|
+
condition_sys_id: condition.sys_id,
|
|
914
|
+
name: conditionData.name
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async createTransition(transitionData) {
|
|
919
|
+
// Create transition
|
|
920
|
+
const transition = await this.createRecord('wf_transition', {
|
|
921
|
+
from: transitionData.from_activity_sys_id,
|
|
922
|
+
to: transitionData.to_activity_sys_id,
|
|
923
|
+
condition: transitionData.condition_sys_id || '',
|
|
924
|
+
order: transitionData.order || 1
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
return {
|
|
928
|
+
transition_sys_id: transition.sys_id
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async publishWorkflow(versionSysId, startActivitySysId) {
|
|
933
|
+
// Update workflow version to set start activity and publish
|
|
934
|
+
const updated = await this.updateRecord('wf_workflow_version', versionSysId, {
|
|
935
|
+
start: startActivitySysId,
|
|
936
|
+
published: true
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
version_sys_id: versionSysId,
|
|
941
|
+
published: true,
|
|
942
|
+
start_activity: startActivitySysId
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async createCompleteWorkflow(workflowSpec, reportProgress = true) {
|
|
947
|
+
// Create complete workflow with activities and transitions in one call
|
|
948
|
+
const results = {
|
|
949
|
+
workflow_sys_id: '',
|
|
950
|
+
version_sys_id: '',
|
|
951
|
+
activity_sys_ids: {},
|
|
952
|
+
transition_sys_ids: [],
|
|
953
|
+
published: false
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
try {
|
|
957
|
+
// 1. Create base workflow
|
|
958
|
+
if (reportProgress) this.notifyProgress('Creating workflow base');
|
|
959
|
+
const workflow = await this.createWorkflow({
|
|
960
|
+
name: workflowSpec.name,
|
|
961
|
+
description: workflowSpec.description,
|
|
962
|
+
template: workflowSpec.template,
|
|
963
|
+
access: workflowSpec.access
|
|
964
|
+
});
|
|
965
|
+
results.workflow_sys_id = workflow.workflow_sys_id;
|
|
966
|
+
|
|
967
|
+
// 2. Create workflow version
|
|
968
|
+
if (reportProgress) this.notifyProgress('Creating workflow version');
|
|
969
|
+
const version = await this.createWorkflowVersion({
|
|
970
|
+
name: workflowSpec.name,
|
|
971
|
+
workflow_sys_id: workflow.workflow_sys_id,
|
|
972
|
+
table: workflowSpec.table,
|
|
973
|
+
description: workflowSpec.description,
|
|
974
|
+
active: workflowSpec.active,
|
|
975
|
+
published: false, // Don't publish yet
|
|
976
|
+
condition: workflowSpec.condition,
|
|
977
|
+
order: workflowSpec.order,
|
|
978
|
+
run_multiple: workflowSpec.run_multiple,
|
|
979
|
+
after_business_rules: workflowSpec.after_business_rules
|
|
980
|
+
});
|
|
981
|
+
results.version_sys_id = version.version_sys_id;
|
|
982
|
+
|
|
983
|
+
// 3. Create activities
|
|
984
|
+
const activities = workflowSpec.activities || [];
|
|
985
|
+
const totalActivities = activities.length;
|
|
986
|
+
for (let i = 0; i < activities.length; i++) {
|
|
987
|
+
const actSpec = activities[i];
|
|
988
|
+
|
|
989
|
+
if (reportProgress) {
|
|
990
|
+
this.notifyProgress(`Creating activity ${i + 1}/${totalActivities}: ${actSpec.name}`);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const activity = await this.createActivity({
|
|
994
|
+
name: actSpec.name,
|
|
995
|
+
workflow_version_sys_id: version.version_sys_id,
|
|
996
|
+
activity_definition_sys_id: actSpec.activity_type,
|
|
997
|
+
x: actSpec.x !== undefined ? actSpec.x : (100 + i * 150),
|
|
998
|
+
y: actSpec.y !== undefined ? actSpec.y : 100,
|
|
999
|
+
width: actSpec.width,
|
|
1000
|
+
height: actSpec.height,
|
|
1001
|
+
script: actSpec.script,
|
|
1002
|
+
vars: actSpec.vars,
|
|
1003
|
+
stage_sys_id: actSpec.stage,
|
|
1004
|
+
parent_sys_id: actSpec.parent,
|
|
1005
|
+
timeout: actSpec.timeout
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
const key = actSpec.id || `activity_${i}`;
|
|
1009
|
+
results.activity_sys_ids[key] = activity.activity_sys_id;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// 4. Create transitions
|
|
1013
|
+
const transitions = workflowSpec.transitions || [];
|
|
1014
|
+
const totalTransitions = transitions.length;
|
|
1015
|
+
for (let i = 0; i < transitions.length; i++) {
|
|
1016
|
+
const transSpec = transitions[i];
|
|
1017
|
+
|
|
1018
|
+
if (reportProgress) {
|
|
1019
|
+
this.notifyProgress(`Creating transition ${i + 1}/${totalTransitions}`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Resolve activity references
|
|
1023
|
+
const fromId = typeof transSpec.from === 'number'
|
|
1024
|
+
? results.activity_sys_ids[`activity_${transSpec.from}`]
|
|
1025
|
+
: results.activity_sys_ids[transSpec.from] || transSpec.from;
|
|
1026
|
+
|
|
1027
|
+
const toId = typeof transSpec.to === 'number'
|
|
1028
|
+
? results.activity_sys_ids[`activity_${transSpec.to}`]
|
|
1029
|
+
: results.activity_sys_ids[transSpec.to] || transSpec.to;
|
|
1030
|
+
|
|
1031
|
+
// Create condition if specified
|
|
1032
|
+
let conditionId = transSpec.condition_sys_id;
|
|
1033
|
+
if (transSpec.condition && !conditionId) {
|
|
1034
|
+
const condition = await this.createCondition({
|
|
1035
|
+
activity_sys_id: fromId,
|
|
1036
|
+
name: transSpec.condition_name || 'Condition',
|
|
1037
|
+
description: transSpec.condition_description,
|
|
1038
|
+
condition: transSpec.condition,
|
|
1039
|
+
order: transSpec.order,
|
|
1040
|
+
else_flag: transSpec.else_flag
|
|
1041
|
+
});
|
|
1042
|
+
conditionId = condition.condition_sys_id;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Create transition
|
|
1046
|
+
const transition = await this.createTransition({
|
|
1047
|
+
from_activity_sys_id: fromId,
|
|
1048
|
+
to_activity_sys_id: toId,
|
|
1049
|
+
condition_sys_id: conditionId,
|
|
1050
|
+
order: transSpec.order
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
results.transition_sys_ids.push(transition.transition_sys_id);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// 5. Publish if requested
|
|
1057
|
+
if (workflowSpec.publish && activities.length > 0) {
|
|
1058
|
+
if (reportProgress) this.notifyProgress('Publishing workflow');
|
|
1059
|
+
|
|
1060
|
+
const startActivityId = workflowSpec.start_activity
|
|
1061
|
+
? (results.activity_sys_ids[workflowSpec.start_activity] || workflowSpec.start_activity)
|
|
1062
|
+
: results.activity_sys_ids['activity_0'] || results.activity_sys_ids[Object.keys(results.activity_sys_ids)[0]];
|
|
1063
|
+
|
|
1064
|
+
await this.publishWorkflow(version.version_sys_id, startActivityId);
|
|
1065
|
+
results.published = true;
|
|
1066
|
+
results.start_activity = startActivityId;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (reportProgress) {
|
|
1070
|
+
this.notifyProgress(`Complete: Workflow created with ${totalActivities} activities and ${totalTransitions} transitions`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return results;
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
throw new Error(`Failed to create workflow: ${error.message}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Move records to update set
|
|
1080
|
+
async moveRecordsToUpdateSet(updateSetId, options = {}) {
|
|
1081
|
+
const {
|
|
1082
|
+
record_sys_ids = [],
|
|
1083
|
+
time_range = null,
|
|
1084
|
+
source_update_set = null,
|
|
1085
|
+
table = 'sys_update_xml',
|
|
1086
|
+
reportProgress = true
|
|
1087
|
+
} = options;
|
|
1088
|
+
|
|
1089
|
+
try {
|
|
1090
|
+
const results = {
|
|
1091
|
+
moved: 0,
|
|
1092
|
+
failed: 0,
|
|
1093
|
+
records: [],
|
|
1094
|
+
errors: []
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
let recordsToMove = [];
|
|
1098
|
+
|
|
1099
|
+
// Get records by sys_ids
|
|
1100
|
+
if (record_sys_ids.length > 0) {
|
|
1101
|
+
if (reportProgress) this.notifyProgress(`Fetching ${record_sys_ids.length} records to move`);
|
|
1102
|
+
const sysIdsQuery = record_sys_ids.map(id => `sys_id=${id}`).join('^OR');
|
|
1103
|
+
recordsToMove = await this.getRecords(table, {
|
|
1104
|
+
sysparm_query: sysIdsQuery,
|
|
1105
|
+
sysparm_limit: 1000
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
// Get records by time range
|
|
1109
|
+
else if (time_range) {
|
|
1110
|
+
if (reportProgress) this.notifyProgress('Fetching records by time range');
|
|
1111
|
+
let query = `sys_created_on>=${time_range.start}^sys_created_on<=${time_range.end}`;
|
|
1112
|
+
if (source_update_set) {
|
|
1113
|
+
query += `^update_set.name=${source_update_set}`;
|
|
1114
|
+
}
|
|
1115
|
+
recordsToMove = await this.getRecords(table, {
|
|
1116
|
+
sysparm_query: query,
|
|
1117
|
+
sysparm_limit: 1000
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const total = recordsToMove.length;
|
|
1122
|
+
if (total === 0) {
|
|
1123
|
+
if (reportProgress) this.notifyProgress('No records found to move');
|
|
1124
|
+
return results;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (reportProgress) this.notifyProgress(`Moving ${total} records to update set`);
|
|
1128
|
+
|
|
1129
|
+
// Determine progress reporting frequency
|
|
1130
|
+
const shouldReport = (index) => {
|
|
1131
|
+
if (!reportProgress) return false;
|
|
1132
|
+
if (total <= 10) return true;
|
|
1133
|
+
if (total <= 50) return (index + 1) % 5 === 0 || index === total - 1;
|
|
1134
|
+
return (index + 1) % Math.ceil(total / 10) === 0 || index === total - 1;
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
// Move each record
|
|
1138
|
+
for (let i = 0; i < recordsToMove.length; i++) {
|
|
1139
|
+
const record = recordsToMove[i];
|
|
1140
|
+
try {
|
|
1141
|
+
await this.updateRecord(table, record.sys_id, {
|
|
1142
|
+
update_set: updateSetId
|
|
1143
|
+
});
|
|
1144
|
+
results.moved++;
|
|
1145
|
+
results.records.push({
|
|
1146
|
+
sys_id: record.sys_id,
|
|
1147
|
+
name: record.name,
|
|
1148
|
+
type: record.type,
|
|
1149
|
+
status: 'moved'
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
if (shouldReport(i)) {
|
|
1153
|
+
const percentage = Math.round(((i + 1) / total) * 100);
|
|
1154
|
+
this.notifyProgress(`Moving record ${i + 1}/${total} (${percentage}%): ${record.type || 'unknown'}`);
|
|
1155
|
+
}
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
results.failed++;
|
|
1158
|
+
results.errors.push({
|
|
1159
|
+
sys_id: record.sys_id,
|
|
1160
|
+
error: error.message
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
if (reportProgress) {
|
|
1164
|
+
this.notifyProgress(`Failed ${i + 1}/${total}: ${record.sys_id} - ${error.message}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (reportProgress) {
|
|
1170
|
+
if (results.failed > 0) {
|
|
1171
|
+
this.notifyProgress(`Complete: ${results.moved}/${total} records moved (${results.failed} failed)`);
|
|
1172
|
+
} else {
|
|
1173
|
+
this.notifyProgress(`Complete: All ${total} records moved successfully`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
return results;
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
throw new Error(`Failed to move records to update set: ${error.message}`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Clone update set
|
|
1184
|
+
async cloneUpdateSet(sourceUpdateSetId, newName, reportProgress = true) {
|
|
1185
|
+
try {
|
|
1186
|
+
// Get source update set
|
|
1187
|
+
if (reportProgress) this.notifyProgress('Fetching source update set');
|
|
1188
|
+
const sourceSet = await this.getRecord('sys_update_set', sourceUpdateSetId);
|
|
1189
|
+
|
|
1190
|
+
// Create new update set
|
|
1191
|
+
if (reportProgress) this.notifyProgress(`Creating new update set: ${newName}`);
|
|
1192
|
+
const newSet = await this.createRecord('sys_update_set', {
|
|
1193
|
+
name: newName,
|
|
1194
|
+
description: `Clone of: ${sourceSet.name}\n\n${sourceSet.description || ''}`,
|
|
1195
|
+
application: sourceSet.application,
|
|
1196
|
+
state: 'in progress'
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Get all update XML records from source
|
|
1200
|
+
if (reportProgress) this.notifyProgress('Fetching update records from source');
|
|
1201
|
+
const updateRecords = await this.getRecords('sys_update_xml', {
|
|
1202
|
+
sysparm_query: `update_set=${sourceUpdateSetId}`,
|
|
1203
|
+
sysparm_limit: 5000
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
const total = updateRecords.length;
|
|
1207
|
+
if (reportProgress) this.notifyProgress(`Cloning ${total} update records`);
|
|
1208
|
+
|
|
1209
|
+
// Determine progress reporting frequency
|
|
1210
|
+
const shouldReport = (index) => {
|
|
1211
|
+
if (!reportProgress) return false;
|
|
1212
|
+
if (total <= 10) return true;
|
|
1213
|
+
if (total <= 50) return (index + 1) % 5 === 0 || index === total - 1;
|
|
1214
|
+
return (index + 1) % Math.ceil(total / 10) === 0 || index === total - 1;
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
// Clone each update record (create new records pointing to new set)
|
|
1218
|
+
const clonedRecords = [];
|
|
1219
|
+
let failedCount = 0;
|
|
1220
|
+
for (let i = 0; i < updateRecords.length; i++) {
|
|
1221
|
+
const record = updateRecords[i];
|
|
1222
|
+
try {
|
|
1223
|
+
const cloned = await this.createRecord('sys_update_xml', {
|
|
1224
|
+
update_set: newSet.sys_id,
|
|
1225
|
+
name: record.name,
|
|
1226
|
+
type: record.type,
|
|
1227
|
+
target_name: record.target_name,
|
|
1228
|
+
payload: record.payload,
|
|
1229
|
+
category: record.category
|
|
1230
|
+
});
|
|
1231
|
+
clonedRecords.push(cloned);
|
|
1232
|
+
|
|
1233
|
+
if (shouldReport(i)) {
|
|
1234
|
+
const percentage = Math.round(((i + 1) / total) * 100);
|
|
1235
|
+
this.notifyProgress(`Cloning record ${i + 1}/${total} (${percentage}%): ${record.type || 'unknown'}`);
|
|
1236
|
+
}
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
failedCount++;
|
|
1239
|
+
console.error(`Failed to clone record ${record.sys_id}: ${error.message}`);
|
|
1240
|
+
if (reportProgress && failedCount <= 5) { // Only report first 5 failures to avoid spam
|
|
1241
|
+
this.notifyProgress(`Failed to clone record ${i + 1}/${total}: ${error.message}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (reportProgress) {
|
|
1247
|
+
if (failedCount > 0) {
|
|
1248
|
+
this.notifyProgress(`Complete: ${clonedRecords.length}/${total} records cloned (${failedCount} failed)`);
|
|
1249
|
+
} else {
|
|
1250
|
+
this.notifyProgress(`Complete: All ${total} records cloned successfully`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return {
|
|
1255
|
+
new_update_set_id: newSet.sys_id,
|
|
1256
|
+
new_update_set_name: newSet.name,
|
|
1257
|
+
source_update_set_id: sourceUpdateSetId,
|
|
1258
|
+
source_update_set_name: sourceSet.name,
|
|
1259
|
+
records_cloned: clonedRecords.length,
|
|
1260
|
+
total_source_records: updateRecords.length
|
|
1261
|
+
};
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
throw new Error(`Failed to clone update set: ${error.message}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Enhanced schema discovery
|
|
1268
|
+
async discoverTableSchema(tableName, options = {}) {
|
|
1269
|
+
const {
|
|
1270
|
+
include_type_codes = false,
|
|
1271
|
+
include_choice_tables = false,
|
|
1272
|
+
include_relationships = false,
|
|
1273
|
+
include_ui_policies = false,
|
|
1274
|
+
include_business_rules = false,
|
|
1275
|
+
include_field_constraints = false
|
|
1276
|
+
} = options;
|
|
1277
|
+
|
|
1278
|
+
const schema = {
|
|
1279
|
+
table: tableName,
|
|
1280
|
+
label: null,
|
|
1281
|
+
fields: []
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
// Get table metadata
|
|
1286
|
+
const tables = await this.getRecords('sys_db_object', {
|
|
1287
|
+
sysparm_query: `name=${tableName}`,
|
|
1288
|
+
sysparm_limit: 1
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
if (tables.length > 0) {
|
|
1292
|
+
schema.label = tables[0].label;
|
|
1293
|
+
schema.super_class = tables[0].super_class?.value;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Get field definitions
|
|
1297
|
+
const fields = await this.getRecords('sys_dictionary', {
|
|
1298
|
+
sysparm_query: `name=${tableName}`,
|
|
1299
|
+
sysparm_limit: 1000
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
for (const field of fields) {
|
|
1303
|
+
const fieldInfo = {
|
|
1304
|
+
name: field.element,
|
|
1305
|
+
label: field.column_label,
|
|
1306
|
+
internal_type: field.internal_type?.value || field.internal_type,
|
|
1307
|
+
max_length: field.max_length,
|
|
1308
|
+
mandatory: field.mandatory === 'true' || field.mandatory === true,
|
|
1309
|
+
read_only: field.read_only === 'true' || field.read_only === true
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
// Add type codes for integer fields (like variable types)
|
|
1313
|
+
if (include_type_codes && fieldInfo.internal_type === 'integer' && field.element === 'type') {
|
|
1314
|
+
const choices = await this.getRecords('sys_choice', {
|
|
1315
|
+
sysparm_query: `name=${tableName}^element=type`,
|
|
1316
|
+
sysparm_limit: 100
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
if (choices.length > 0) {
|
|
1320
|
+
fieldInfo.type_codes = {};
|
|
1321
|
+
choices.forEach(choice => {
|
|
1322
|
+
fieldInfo.type_codes[choice.value] = choice.label;
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Add reference information
|
|
1328
|
+
if (fieldInfo.internal_type === 'reference' && field.reference) {
|
|
1329
|
+
fieldInfo.reference_table = field.reference.value || field.reference;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (include_field_constraints && field.default_value) {
|
|
1333
|
+
fieldInfo.default_value = field.default_value;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
schema.fields.push(fieldInfo);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Choice tables
|
|
1340
|
+
if (include_choice_tables) {
|
|
1341
|
+
schema.choice_tables = {
|
|
1342
|
+
sys_choice: 'For table field choices',
|
|
1343
|
+
question_choice: 'For catalog variable choices'
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Relationships
|
|
1348
|
+
if (include_relationships) {
|
|
1349
|
+
schema.relationships = {};
|
|
1350
|
+
const refFields = schema.fields.filter(f => f.internal_type === 'reference');
|
|
1351
|
+
for (const field of refFields) {
|
|
1352
|
+
if (field.reference_table) {
|
|
1353
|
+
schema.relationships[field.name] = {
|
|
1354
|
+
type: 'reference',
|
|
1355
|
+
table: field.reference_table,
|
|
1356
|
+
description: field.label
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// UI Policies
|
|
1363
|
+
if (include_ui_policies) {
|
|
1364
|
+
const policies = await this.getRecords('sys_ui_policy', {
|
|
1365
|
+
sysparm_query: `table=${tableName}^active=true`,
|
|
1366
|
+
sysparm_fields: 'sys_id,short_description',
|
|
1367
|
+
sysparm_limit: 100
|
|
1368
|
+
});
|
|
1369
|
+
schema.ui_policies = policies.map(p => ({
|
|
1370
|
+
sys_id: p.sys_id,
|
|
1371
|
+
description: p.short_description
|
|
1372
|
+
}));
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Business Rules
|
|
1376
|
+
if (include_business_rules) {
|
|
1377
|
+
const rules = await this.getRecords('sys_script', {
|
|
1378
|
+
sysparm_query: `collection=${tableName}^active=true`,
|
|
1379
|
+
sysparm_fields: 'sys_id,name,when',
|
|
1380
|
+
sysparm_limit: 100
|
|
1381
|
+
});
|
|
1382
|
+
schema.business_rules = rules.map(r => ({
|
|
1383
|
+
sys_id: r.sys_id,
|
|
1384
|
+
name: r.name,
|
|
1385
|
+
when: r.when
|
|
1386
|
+
}));
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
throw new Error(`Failed to discover schema for ${tableName}: ${error.message}`);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
return schema;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Field explanation
|
|
1397
|
+
async explainField(tableName, fieldName, includeExamples = true) {
|
|
1398
|
+
try {
|
|
1399
|
+
const fields = await this.getRecords('sys_dictionary', {
|
|
1400
|
+
sysparm_query: `name=${tableName}^element=${fieldName}`,
|
|
1401
|
+
sysparm_limit: 1
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
if (fields.length === 0) {
|
|
1405
|
+
throw new Error(`Field ${fieldName} not found in table ${tableName}`);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const field = fields[0];
|
|
1409
|
+
const explanation = {
|
|
1410
|
+
field: fieldName,
|
|
1411
|
+
table: tableName,
|
|
1412
|
+
label: field.column_label,
|
|
1413
|
+
type: field.internal_type?.value || field.internal_type,
|
|
1414
|
+
max_length: field.max_length,
|
|
1415
|
+
mandatory: field.mandatory === 'true' || field.mandatory === true,
|
|
1416
|
+
read_only: field.read_only === 'true' || field.read_only === true,
|
|
1417
|
+
comments: field.comments,
|
|
1418
|
+
help: field.help
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
// Get reference info
|
|
1422
|
+
if (field.reference) {
|
|
1423
|
+
explanation.reference_table = field.reference.value || field.reference;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Get choices for choice fields
|
|
1427
|
+
if (field.internal_type === 'choice' || field.internal_type === 'integer') {
|
|
1428
|
+
const choices = await this.getRecords('sys_choice', {
|
|
1429
|
+
sysparm_query: `name=${tableName}^element=${fieldName}`,
|
|
1430
|
+
sysparm_limit: 100
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
if (choices.length > 0) {
|
|
1434
|
+
explanation.choices = choices.map(c => ({
|
|
1435
|
+
value: c.value,
|
|
1436
|
+
label: c.label
|
|
1437
|
+
}));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Known issues for specific fields
|
|
1442
|
+
if (tableName === 'catalog_ui_policy_action' && (fieldName === 'ui_policy' || fieldName === 'catalog_variable')) {
|
|
1443
|
+
explanation.known_issues = [
|
|
1444
|
+
'Cannot be set via REST API - use background script with setValue()',
|
|
1445
|
+
fieldName === 'catalog_variable' ? 'Must include IO: prefix or linkage will fail' : null
|
|
1446
|
+
].filter(Boolean);
|
|
1447
|
+
|
|
1448
|
+
if (fieldName === 'catalog_variable') {
|
|
1449
|
+
explanation.special_format = 'IO:<variable_sys_id>';
|
|
1450
|
+
if (includeExamples) {
|
|
1451
|
+
explanation.example = 'IO:94ababd1c35432101fcbbd43e40131bf';
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return explanation;
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
throw new Error(`Failed to explain field: ${error.message}`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Validate catalog configuration
|
|
1463
|
+
async validateCatalogConfiguration(catalogItemSysId, checks = {}) {
|
|
1464
|
+
const results = {
|
|
1465
|
+
valid: true,
|
|
1466
|
+
issues: [],
|
|
1467
|
+
warnings: 0,
|
|
1468
|
+
errors: 0
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
try {
|
|
1472
|
+
// Validate variables
|
|
1473
|
+
if (checks.variables) {
|
|
1474
|
+
const variables = await this.getRecords('item_option_new', {
|
|
1475
|
+
sysparm_query: `cat_item=${catalogItemSysId}`,
|
|
1476
|
+
sysparm_limit: 1000
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
for (const variable of variables) {
|
|
1480
|
+
// Check if linked
|
|
1481
|
+
if (checks.variables.check_linked && !variable.cat_item) {
|
|
1482
|
+
results.issues.push({
|
|
1483
|
+
severity: 'error',
|
|
1484
|
+
component: 'variable',
|
|
1485
|
+
sys_id: variable.sys_id,
|
|
1486
|
+
issue: `Variable ${variable.name} is not linked to catalog item`,
|
|
1487
|
+
fix: 'Update cat_item field'
|
|
1488
|
+
});
|
|
1489
|
+
results.errors++;
|
|
1490
|
+
results.valid = false;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Check tooltip length
|
|
1494
|
+
if (variable.tooltip && variable.tooltip.length > 40) {
|
|
1495
|
+
results.issues.push({
|
|
1496
|
+
severity: 'warning',
|
|
1497
|
+
component: 'variable',
|
|
1498
|
+
sys_id: variable.sys_id,
|
|
1499
|
+
issue: `Tooltip exceeds 40 characters and will be truncated (${variable.tooltip.length} chars)`,
|
|
1500
|
+
fix: 'Move detailed help to help_text field'
|
|
1501
|
+
});
|
|
1502
|
+
results.warnings++;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Check for choices
|
|
1506
|
+
if (checks.variables.check_choices && (variable.type === '1' || variable.type === '5')) {
|
|
1507
|
+
const choices = await this.getRecords('question_choice', {
|
|
1508
|
+
sysparm_query: `question=${variable.sys_id}`,
|
|
1509
|
+
sysparm_limit: 1
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
if (choices.length === 0) {
|
|
1513
|
+
results.issues.push({
|
|
1514
|
+
severity: 'error',
|
|
1515
|
+
component: 'variable',
|
|
1516
|
+
sys_id: variable.sys_id,
|
|
1517
|
+
issue: `Variable ${variable.name} is type ${variable.type === '1' ? 'Choice' : 'Select Box'} but has no choices defined`,
|
|
1518
|
+
fix: 'Add choices via question_choice table'
|
|
1519
|
+
});
|
|
1520
|
+
results.errors++;
|
|
1521
|
+
results.valid = false;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Validate UI policies
|
|
1528
|
+
if (checks.ui_policies) {
|
|
1529
|
+
const policies = await this.getRecords('catalog_ui_policy', {
|
|
1530
|
+
sysparm_query: `catalog_item=${catalogItemSysId}`,
|
|
1531
|
+
sysparm_limit: 1000
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
for (const policy of policies) {
|
|
1535
|
+
if (checks.ui_policies.check_actions_linked) {
|
|
1536
|
+
const actions = await this.getRecords('catalog_ui_policy_action', {
|
|
1537
|
+
sysparm_query: `ui_policy=${policy.sys_id}`,
|
|
1538
|
+
sysparm_limit: 1000
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
for (const action of actions) {
|
|
1542
|
+
if (!action.catalog_variable || action.catalog_variable === '') {
|
|
1543
|
+
results.issues.push({
|
|
1544
|
+
severity: 'error',
|
|
1545
|
+
component: 'ui_policy_action',
|
|
1546
|
+
sys_id: action.sys_id,
|
|
1547
|
+
issue: 'catalog_variable field is empty - action not linked to policy',
|
|
1548
|
+
fix: 'Run background script to set catalog_variable value'
|
|
1549
|
+
});
|
|
1550
|
+
results.errors++;
|
|
1551
|
+
results.valid = false;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
} catch (error) {
|
|
1559
|
+
results.issues.push({
|
|
1560
|
+
severity: 'error',
|
|
1561
|
+
component: 'validation',
|
|
1562
|
+
issue: `Validation failed: ${error.message}`
|
|
1563
|
+
});
|
|
1564
|
+
results.errors++;
|
|
1565
|
+
results.valid = false;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return results;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Inspect update set
|
|
1572
|
+
async inspectUpdateSet(updateSetSysId, options = {}) {
|
|
1573
|
+
const {
|
|
1574
|
+
show_components = true,
|
|
1575
|
+
show_dependencies = false
|
|
1576
|
+
} = options;
|
|
1577
|
+
|
|
1578
|
+
try {
|
|
1579
|
+
const updateSet = await this.getRecord('sys_update_set', updateSetSysId);
|
|
1580
|
+
|
|
1581
|
+
const result = {
|
|
1582
|
+
update_set: {
|
|
1583
|
+
sys_id: updateSet.sys_id,
|
|
1584
|
+
name: updateSet.name,
|
|
1585
|
+
state: updateSet.state,
|
|
1586
|
+
description: updateSet.description
|
|
1587
|
+
},
|
|
1588
|
+
total_records: 0,
|
|
1589
|
+
components: []
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
if (show_components) {
|
|
1593
|
+
const updates = await this.getRecords('sys_update_xml', {
|
|
1594
|
+
sysparm_query: `update_set=${updateSetSysId}`,
|
|
1595
|
+
sysparm_fields: 'type,name,target_name',
|
|
1596
|
+
sysparm_limit: 10000
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
result.total_records = updates.length;
|
|
1600
|
+
|
|
1601
|
+
// Group by type
|
|
1602
|
+
const typeGroups = {};
|
|
1603
|
+
updates.forEach(update => {
|
|
1604
|
+
const type = update.type || 'unknown';
|
|
1605
|
+
if (!typeGroups[type]) {
|
|
1606
|
+
typeGroups[type] = [];
|
|
1607
|
+
}
|
|
1608
|
+
typeGroups[type].push(update);
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
result.components = Object.keys(typeGroups).map(type => ({
|
|
1612
|
+
type,
|
|
1613
|
+
count: typeGroups[type].length,
|
|
1614
|
+
items: typeGroups[type].slice(0, 10).map(u => u.name || u.target_name)
|
|
1615
|
+
}));
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
result.ready_to_deploy = result.update_set.state === 'complete';
|
|
1619
|
+
|
|
1620
|
+
return result;
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
throw new Error(`Failed to inspect update set: ${error.message}`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|