snow-flow 1.3.28 → 1.3.30

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.
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Update Set Importer
5
+ *
6
+ * Programmatically imports Update Set XML files into ServiceNow
7
+ * via REST API with full error handling and verification
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.UpdateSetImporter = void 0;
44
+ exports.deployFlowXML = deployFlowXML;
45
+ exports.previewFlowXML = previewFlowXML;
46
+ const servicenow_client_js_1 = require("./servicenow-client.js");
47
+ const snow_oauth_js_1 = require("./snow-oauth.js");
48
+ const logger_js_1 = require("./logger.js");
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ class UpdateSetImporter {
52
+ constructor() {
53
+ this.client = new servicenow_client_js_1.ServiceNowClient();
54
+ this.oauth = new snow_oauth_js_1.ServiceNowOAuth();
55
+ this.logger = new logger_js_1.Logger('UpdateSetImporter');
56
+ }
57
+ /**
58
+ * Import Update Set XML file to ServiceNow
59
+ */
60
+ async importUpdateSet(xmlFilePath, options = {}) {
61
+ try {
62
+ // Check authentication
63
+ const isAuth = await this.oauth.isAuthenticated();
64
+ if (!isAuth) {
65
+ throw new Error('Not authenticated. Run: snow-flow auth login');
66
+ }
67
+ // Read XML file
68
+ const xmlContent = await fs.promises.readFile(xmlFilePath, 'utf-8');
69
+ // Validate XML if requested
70
+ if (options.validateFirst) {
71
+ const validation = this.validateXML(xmlContent);
72
+ if (!validation.valid) {
73
+ throw new Error(`XML validation failed: ${validation.errors.join(', ')}`);
74
+ }
75
+ }
76
+ // Step 1: Import as remote update set
77
+ this.logger.info('Importing XML as remote update set...');
78
+ const remoteUpdateSetId = await this.importRemoteUpdateSet(xmlContent);
79
+ // Step 2: Load the remote update set
80
+ this.logger.info('Loading remote update set...');
81
+ const localUpdateSetId = await this.loadRemoteUpdateSet(remoteUpdateSetId);
82
+ // Step 3: Preview if requested
83
+ let previewStatus = 'clean';
84
+ let previewProblems = [];
85
+ if (options.autoPreview !== false) {
86
+ this.logger.info('Previewing update set...');
87
+ const preview = await this.previewUpdateSet(localUpdateSetId);
88
+ previewStatus = preview.status;
89
+ previewProblems = preview.problems;
90
+ if (previewStatus !== 'clean' && options.skipOnConflict) {
91
+ return {
92
+ success: false,
93
+ remoteUpdateSetId,
94
+ localUpdateSetId,
95
+ previewStatus,
96
+ previewProblems,
97
+ commitStatus: 'skipped',
98
+ error: 'Preview found conflicts/errors, skipping commit'
99
+ };
100
+ }
101
+ }
102
+ // Step 4: Backup if requested
103
+ let backupPath;
104
+ if (options.backupBeforeCommit && localUpdateSetId) {
105
+ this.logger.info('Creating backup...');
106
+ backupPath = await this.backupUpdateSet(localUpdateSetId);
107
+ }
108
+ // Step 5: Commit if requested and preview is clean
109
+ let commitStatus = 'skipped';
110
+ let flowSysId;
111
+ let flowUrl;
112
+ if (options.autoCommit && previewStatus === 'clean') {
113
+ this.logger.info('Committing update set...');
114
+ const commit = await this.commitUpdateSet(localUpdateSetId);
115
+ commitStatus = commit.success ? 'success' : 'failed';
116
+ if (commit.success) {
117
+ // Try to find the flow that was deployed
118
+ const flowInfo = await this.findDeployedFlow(localUpdateSetId);
119
+ flowSysId = flowInfo?.sys_id;
120
+ flowUrl = flowInfo?.url;
121
+ }
122
+ }
123
+ return {
124
+ success: commitStatus === 'success' || (previewStatus === 'clean' && !options.autoCommit),
125
+ remoteUpdateSetId,
126
+ localUpdateSetId,
127
+ previewStatus,
128
+ previewProblems,
129
+ commitStatus,
130
+ backupPath,
131
+ flowSysId,
132
+ flowUrl
133
+ };
134
+ }
135
+ catch (error) {
136
+ this.logger.error('Import failed:', error);
137
+ return {
138
+ success: false,
139
+ error: error instanceof Error ? error.message : String(error)
140
+ };
141
+ }
142
+ }
143
+ /**
144
+ * Import XML as remote update set
145
+ */
146
+ async importRemoteUpdateSet(xmlContent) {
147
+ const response = await this.client.makeRequest({
148
+ method: 'POST',
149
+ url: '/api/now/v2/table/sys_remote_update_set/import',
150
+ headers: {
151
+ 'Content-Type': 'application/xml',
152
+ 'Accept': 'application/json'
153
+ },
154
+ data: xmlContent
155
+ });
156
+ if (!response.success || !response.result) {
157
+ throw new Error('Failed to import remote update set');
158
+ }
159
+ // Handle different response formats
160
+ const sysId = response.result.sys_id ||
161
+ response.result.result?.sys_id ||
162
+ response.result[0]?.sys_id;
163
+ if (!sysId) {
164
+ throw new Error('Failed to get remote update set sys_id from response');
165
+ }
166
+ return sysId;
167
+ }
168
+ /**
169
+ * Load remote update set to create local update set
170
+ */
171
+ async loadRemoteUpdateSet(remoteUpdateSetId) {
172
+ // First, update state to loaded
173
+ await this.client.makeRequest({
174
+ method: 'PATCH',
175
+ url: `/api/now/table/sys_remote_update_set/${remoteUpdateSetId}`,
176
+ data: {
177
+ state: 'loaded'
178
+ }
179
+ });
180
+ // Wait a moment for processing
181
+ await new Promise(resolve => setTimeout(resolve, 2000));
182
+ // Find the loaded update set
183
+ const response = await this.client.makeRequest({
184
+ method: 'GET',
185
+ url: '/api/now/table/sys_update_set',
186
+ params: {
187
+ sysparm_query: `origin_sys_id=${remoteUpdateSetId}^ORremote_sys_id=${remoteUpdateSetId}`,
188
+ sysparm_limit: 1,
189
+ sysparm_fields: 'sys_id,name,state'
190
+ }
191
+ });
192
+ if (!response.success || !response.result || response.result.length === 0) {
193
+ throw new Error('Failed to find loaded update set');
194
+ }
195
+ return response.result[0].sys_id;
196
+ }
197
+ /**
198
+ * Preview update set
199
+ */
200
+ async previewUpdateSet(updateSetId) {
201
+ // Trigger preview
202
+ await this.client.makeRequest({
203
+ method: 'POST',
204
+ url: `/api/now/table/sys_update_set/${updateSetId}/preview`,
205
+ data: {}
206
+ });
207
+ // Wait for preview to complete
208
+ let attempts = 0;
209
+ const maxAttempts = 30;
210
+ while (attempts < maxAttempts) {
211
+ await new Promise(resolve => setTimeout(resolve, 2000));
212
+ const response = await this.client.makeRequest({
213
+ method: 'GET',
214
+ url: `/api/now/table/sys_update_set/${updateSetId}`,
215
+ params: {
216
+ sysparm_fields: 'state,preview_state'
217
+ }
218
+ });
219
+ if (response.result?.preview_state === 'complete') {
220
+ break;
221
+ }
222
+ attempts++;
223
+ }
224
+ // Check for preview problems
225
+ const problemsResponse = await this.client.makeRequest({
226
+ method: 'GET',
227
+ url: '/api/now/table/sys_update_preview_problem',
228
+ params: {
229
+ sysparm_query: `update_set=${updateSetId}`,
230
+ sysparm_limit: 100
231
+ }
232
+ });
233
+ const problems = problemsResponse.result || [];
234
+ let status = 'clean';
235
+ if (problems.length > 0) {
236
+ const hasErrors = problems.some((p) => p.type === 'error');
237
+ status = hasErrors ? 'errors' : 'conflicts';
238
+ }
239
+ return { status, problems };
240
+ }
241
+ /**
242
+ * Commit update set
243
+ */
244
+ async commitUpdateSet(updateSetId) {
245
+ try {
246
+ await this.client.makeRequest({
247
+ method: 'POST',
248
+ url: `/api/now/table/sys_update_set/${updateSetId}/commit`,
249
+ data: {}
250
+ });
251
+ // Verify commit completed
252
+ await new Promise(resolve => setTimeout(resolve, 3000));
253
+ const response = await this.client.makeRequest({
254
+ method: 'GET',
255
+ url: `/api/now/table/sys_update_set/${updateSetId}`,
256
+ params: {
257
+ sysparm_fields: 'state'
258
+ }
259
+ });
260
+ const isCommitted = response.result?.state === 'complete' ||
261
+ response.result?.state === 'committed';
262
+ return { success: isCommitted };
263
+ }
264
+ catch (error) {
265
+ return {
266
+ success: false,
267
+ error: error instanceof Error ? error.message : String(error)
268
+ };
269
+ }
270
+ }
271
+ /**
272
+ * Backup update set before commit
273
+ */
274
+ async backupUpdateSet(updateSetId) {
275
+ const response = await this.client.makeRequest({
276
+ method: 'GET',
277
+ url: `/api/now/v2/table/sys_update_set/${updateSetId}/export`
278
+ });
279
+ const backupDir = path.join(process.cwd(), 'update-set-backups');
280
+ if (!fs.existsSync(backupDir)) {
281
+ fs.mkdirSync(backupDir, { recursive: true });
282
+ }
283
+ const backupPath = path.join(backupDir, `backup_${updateSetId}_${Date.now()}.xml`);
284
+ fs.writeFileSync(backupPath, response.result || response);
285
+ return backupPath;
286
+ }
287
+ /**
288
+ * Find deployed flow from update set
289
+ */
290
+ async findDeployedFlow(updateSetId) {
291
+ try {
292
+ // Look for flow in update set
293
+ const response = await this.client.makeRequest({
294
+ method: 'GET',
295
+ url: '/api/now/table/sys_update_xml',
296
+ params: {
297
+ sysparm_query: `update_set=${updateSetId}^name^STARTSWITHsys_hub_flow_`,
298
+ sysparm_limit: 1,
299
+ sysparm_fields: 'name'
300
+ }
301
+ });
302
+ if (response.result && response.result.length > 0) {
303
+ const flowSysId = response.result[0].name.replace('sys_hub_flow_', '');
304
+ const credentials = await this.oauth.getCredentials();
305
+ const flowUrl = `https://${credentials?.instance}/flow_designer/${flowSysId}`;
306
+ return { sys_id: flowSysId, url: flowUrl };
307
+ }
308
+ }
309
+ catch (error) {
310
+ this.logger.warn('Could not find deployed flow:', error);
311
+ }
312
+ return null;
313
+ }
314
+ /**
315
+ * Validate XML structure
316
+ */
317
+ validateXML(xml) {
318
+ const errors = [];
319
+ if (!xml.includes('<?xml')) {
320
+ errors.push('Missing XML declaration');
321
+ }
322
+ if (!xml.includes('<sys_remote_update_set')) {
323
+ errors.push('Missing sys_remote_update_set element');
324
+ }
325
+ if (!xml.includes('<unload')) {
326
+ errors.push('Missing unload root element');
327
+ }
328
+ return {
329
+ valid: errors.length === 0,
330
+ errors
331
+ };
332
+ }
333
+ /**
334
+ * Static helper to import with default options
335
+ */
336
+ static async importFlow(xmlFilePath) {
337
+ const importer = new UpdateSetImporter();
338
+ return importer.importUpdateSet(xmlFilePath, {
339
+ autoPreview: true,
340
+ autoCommit: true,
341
+ skipOnConflict: false,
342
+ backupBeforeCommit: true,
343
+ validateFirst: true
344
+ });
345
+ }
346
+ }
347
+ exports.UpdateSetImporter = UpdateSetImporter;
348
+ // Export helper functions
349
+ async function deployFlowXML(xmlFilePath, autoCommit = true) {
350
+ const importer = new UpdateSetImporter();
351
+ return importer.importUpdateSet(xmlFilePath, {
352
+ autoPreview: true,
353
+ autoCommit,
354
+ skipOnConflict: false,
355
+ backupBeforeCommit: true,
356
+ validateFirst: true
357
+ });
358
+ }
359
+ async function previewFlowXML(xmlFilePath) {
360
+ const importer = new UpdateSetImporter();
361
+ return importer.importUpdateSet(xmlFilePath, {
362
+ autoPreview: true,
363
+ autoCommit: false,
364
+ skipOnConflict: false,
365
+ backupBeforeCommit: false,
366
+ validateFirst: true
367
+ });
368
+ }
369
+ exports.default = UpdateSetImporter;