snow-flow 8.41.8 → 8.41.14
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/dist/mcp/servicenow-mcp-unified/tools/business-rules/snow_create_business_rule.js +4 -4
- package/dist/mcp/servicenow-mcp-unified/tools/business-rules/snow_create_business_rule.js.map +1 -1
- package/dist/mcp/servicenow-mcp-unified/tools/deployment/snow_update.d.ts.map +1 -1
- package/dist/mcp/servicenow-mcp-unified/tools/deployment/snow_update.js +13 -1
- package/dist/mcp/servicenow-mcp-unified/tools/deployment/snow_update.js.map +1 -1
- package/dist/mcp/servicenow-mcp-unified/tools/flow-designer/snow_flow_discover.d.ts +15 -0
- package/dist/mcp/servicenow-mcp-unified/tools/flow-designer/snow_flow_discover.d.ts.map +1 -0
- package/dist/mcp/servicenow-mcp-unified/tools/flow-designer/snow_flow_discover.js +273 -0
- package/dist/mcp/servicenow-mcp-unified/tools/flow-designer/snow_flow_discover.js.map +1 -0
- package/dist/mcp/servicenow-mcp-unified/tools/flow-designer/snow_manage_flow.d.ts +125 -0
- package/dist/mcp/servicenow-mcp-unified/tools/flow-designer/snow_manage_flow.d.ts.map +1 -0
- package/dist/mcp/servicenow-mcp-unified/tools/flow-designer/snow_manage_flow.js +2459 -0
- package/dist/mcp/servicenow-mcp-unified/tools/flow-designer/snow_manage_flow.js.map +1 -0
- package/dist/templates/claude-md-template.d.ts +2 -2
- package/dist/templates/claude-md-template.d.ts.map +1 -1
- package/dist/templates/claude-md-template.js +24 -42
- package/dist/templates/claude-md-template.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,2459 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* snow_manage_flow - Unified Flow Designer Management via Direct Table API
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ EXPERIMENTAL: Manages flows by directly manipulating Flow Designer tables.
|
|
6
|
+
* This approach is NOT officially supported by ServiceNow but enables programmatic flow management.
|
|
7
|
+
*
|
|
8
|
+
* Supported Actions:
|
|
9
|
+
* - create: Create a new flow with triggers, actions, conditions, loops, variables
|
|
10
|
+
* - update: Update an existing flow's properties
|
|
11
|
+
* - delete: Delete a flow and all related records
|
|
12
|
+
* - clone: Clone an existing flow
|
|
13
|
+
* - activate: Activate a flow
|
|
14
|
+
* - deactivate: Deactivate a flow
|
|
15
|
+
* - add_action: Add an action to an existing flow
|
|
16
|
+
* - remove_action: Remove an action from a flow
|
|
17
|
+
* - add_condition: Add a conditional logic block (if/then/else)
|
|
18
|
+
* - add_loop: Add a loop construct (for_each/do_until)
|
|
19
|
+
* - get_details: Get detailed flow structure including decompressed values
|
|
20
|
+
*
|
|
21
|
+
* Features:
|
|
22
|
+
* - Xanadu+ compression support (GlideCompressionUtil compatible)
|
|
23
|
+
* - Nested actions within conditions and loops
|
|
24
|
+
* - Parent-child relationship management
|
|
25
|
+
* - Version detection for table compatibility
|
|
26
|
+
*
|
|
27
|
+
* Flow Designer Table Structure:
|
|
28
|
+
* - sys_hub_flow: Main flow record
|
|
29
|
+
* - sys_hub_flow_snapshot: Flow version/snapshot
|
|
30
|
+
* - sys_hub_trigger_instance_v2: Trigger configuration
|
|
31
|
+
* - sys_hub_action_instance_v2: Action instances (with compressed values)
|
|
32
|
+
* - sys_hub_flow_logic_instance_v2: Conditions, loops (with parent_logic_instance)
|
|
33
|
+
* - sys_hub_flow_variable: Input/output variables
|
|
34
|
+
* - sys_hub_sub_flow_instance: Subflow references
|
|
35
|
+
*
|
|
36
|
+
* @version 2.0.0-experimental
|
|
37
|
+
* @author Snow-Flow v8.3.0 - Flow Designer Direct Table API
|
|
38
|
+
*/
|
|
39
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
40
|
+
if (k2 === undefined) k2 = k;
|
|
41
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
42
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
43
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
44
|
+
}
|
|
45
|
+
Object.defineProperty(o, k2, desc);
|
|
46
|
+
}) : (function(o, m, k, k2) {
|
|
47
|
+
if (k2 === undefined) k2 = k;
|
|
48
|
+
o[k2] = m[k];
|
|
49
|
+
}));
|
|
50
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
51
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
52
|
+
}) : function(o, v) {
|
|
53
|
+
o["default"] = v;
|
|
54
|
+
});
|
|
55
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
56
|
+
var ownKeys = function(o) {
|
|
57
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
58
|
+
var ar = [];
|
|
59
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
60
|
+
return ar;
|
|
61
|
+
};
|
|
62
|
+
return ownKeys(o);
|
|
63
|
+
};
|
|
64
|
+
return function (mod) {
|
|
65
|
+
if (mod && mod.__esModule) return mod;
|
|
66
|
+
var result = {};
|
|
67
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
68
|
+
__setModuleDefault(result, mod);
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
})();
|
|
72
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
73
|
+
exports.author = exports.version = exports.toolDefinition = void 0;
|
|
74
|
+
exports.execute = execute;
|
|
75
|
+
const auth_js_1 = require("../../shared/auth.js");
|
|
76
|
+
const error_handler_js_1 = require("../../shared/error-handler.js");
|
|
77
|
+
const crypto = __importStar(require("crypto"));
|
|
78
|
+
const zlib = __importStar(require("zlib"));
|
|
79
|
+
// ==================== CONSTANTS ====================
|
|
80
|
+
/**
|
|
81
|
+
* Compression header used by ServiceNow for V2 values fields
|
|
82
|
+
* Note: ServiceNow may not use a header - values are just gzip + base64
|
|
83
|
+
* We detect on read and write without header for compatibility
|
|
84
|
+
*/
|
|
85
|
+
const COMPRESSED_HEADER = 'COMPRESSED:';
|
|
86
|
+
/**
|
|
87
|
+
* Flow Designer Table Structure
|
|
88
|
+
*
|
|
89
|
+
* CREATE ORDER (parent → children):
|
|
90
|
+
* 1. sys_hub_flow - Main flow record
|
|
91
|
+
* 2. sys_hub_trigger_* - Triggers (ref: flow)
|
|
92
|
+
* 3. sys_hub_flow_variable - Variables (ref: flow)
|
|
93
|
+
* 4. sys_hub_flow_logic_* - Conditions/Loops (ref: flow, parent_logic_instance)
|
|
94
|
+
* 5. sys_hub_action_* - Actions (ref: flow, parent_logic_instance, branch)
|
|
95
|
+
* 6. sys_hub_sub_flow_* - Subflow calls (ref: flow, parent_logic_instance)
|
|
96
|
+
* 7. sys_hub_flow_snapshot - Snapshots (ref: flow)
|
|
97
|
+
*
|
|
98
|
+
* DELETE ORDER (children → parent, hierarchical):
|
|
99
|
+
* 1. Actions - Children of logic
|
|
100
|
+
* 2. Subflow instances - Can be nested in logic
|
|
101
|
+
* 3. Logic instances - Deepest children first, then parents
|
|
102
|
+
* 4. Triggers
|
|
103
|
+
* 5. Variables
|
|
104
|
+
* 6. Snapshots
|
|
105
|
+
* 7. Flow - Main record last
|
|
106
|
+
*
|
|
107
|
+
* VERSION HISTORY:
|
|
108
|
+
* - Pre-Washington DC: V1 tables (no _v2 suffix)
|
|
109
|
+
* - Washington DC+: V2 tables (with _v2 suffix)
|
|
110
|
+
* - Xanadu+: V2 tables + compressed values (GZIP + Base64)
|
|
111
|
+
*/
|
|
112
|
+
const FLOW_TABLES = {
|
|
113
|
+
// === CORE TABLES ===
|
|
114
|
+
FLOW: 'sys_hub_flow', // Main flow/subflow records
|
|
115
|
+
SNAPSHOT: 'sys_hub_flow_snapshot', // Version snapshots
|
|
116
|
+
VARIABLE: 'sys_hub_flow_variable', // Input/output/scratch variables
|
|
117
|
+
// === V2 TABLES (Washington DC+) ===
|
|
118
|
+
TRIGGER_V2: 'sys_hub_trigger_instance_v2', // Trigger instances
|
|
119
|
+
ACTION_V2: 'sys_hub_action_instance_v2', // Action instances (compressed values in Xanadu+)
|
|
120
|
+
LOGIC_V2: 'sys_hub_flow_logic_instance_v2', // Conditions, loops (supports nesting)
|
|
121
|
+
// === SUBFLOW TABLES ===
|
|
122
|
+
SUBFLOW: 'sys_hub_sub_flow_instance', // Subflow call instances
|
|
123
|
+
// === LEGACY V1 TABLES (Pre-Washington DC) ===
|
|
124
|
+
TRIGGER_V1: 'sys_hub_trigger_instance',
|
|
125
|
+
ACTION_V1: 'sys_hub_action_instance',
|
|
126
|
+
LOGIC_V1: 'sys_hub_flow_logic_instance',
|
|
127
|
+
// === DEFINITION TABLES (Read-only, for lookups) ===
|
|
128
|
+
TRIGGER_TYPE: 'sys_hub_trigger_type', // Available trigger types
|
|
129
|
+
ACTION_TYPE: 'sys_hub_action_type_base', // Available action types
|
|
130
|
+
// === ADDITIONAL TABLES (May be needed for complex flows) ===
|
|
131
|
+
STEP_INSTANCE: 'sys_hub_step_instance', // Step configurations
|
|
132
|
+
FLOW_INPUT: 'sys_hub_flow_input', // Flow inputs (legacy?)
|
|
133
|
+
FLOW_OUTPUT: 'sys_hub_flow_output', // Flow outputs (legacy?)
|
|
134
|
+
FLOW_STAGE: 'sys_hub_flow_stage' // Flow stages
|
|
135
|
+
};
|
|
136
|
+
// ==================== TOOL DEFINITION ====================
|
|
137
|
+
exports.toolDefinition = {
|
|
138
|
+
name: 'snow_manage_flow',
|
|
139
|
+
description: `Unified Flow Designer management via Direct Table API (EXPERIMENTAL).
|
|
140
|
+
|
|
141
|
+
⚠️ WARNING: This tool manipulates Flow Designer tables directly.
|
|
142
|
+
This is NOT officially supported by ServiceNow.
|
|
143
|
+
|
|
144
|
+
Actions:
|
|
145
|
+
- create: Create a new flow with all components
|
|
146
|
+
- update: Update flow properties
|
|
147
|
+
- delete: Delete a flow and all related records
|
|
148
|
+
- clone: Clone an existing flow
|
|
149
|
+
- activate/deactivate: Change flow state
|
|
150
|
+
- add_action: Add an action to a flow
|
|
151
|
+
- remove_action: Remove an action
|
|
152
|
+
- add_condition: Add if/then/else logic
|
|
153
|
+
- add_loop: Add for_each/do_until loop
|
|
154
|
+
- add_variable: Add flow variable
|
|
155
|
+
- get_details: Get flow structure with decompressed values
|
|
156
|
+
- publish: Publish flow changes
|
|
157
|
+
|
|
158
|
+
Features:
|
|
159
|
+
- Xanadu+ compression support
|
|
160
|
+
- Nested actions in conditions/loops
|
|
161
|
+
- Parent-child relationship management
|
|
162
|
+
- Automatic version detection`,
|
|
163
|
+
category: 'automation',
|
|
164
|
+
subcategory: 'flow-designer',
|
|
165
|
+
use_cases: ['flow-management', 'automation', 'workflow'],
|
|
166
|
+
complexity: 'advanced',
|
|
167
|
+
frequency: 'medium',
|
|
168
|
+
permission: 'write',
|
|
169
|
+
allowedRoles: ['developer', 'admin'],
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
action: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
enum: ['create', 'update', 'delete', 'clone', 'activate', 'deactivate',
|
|
176
|
+
'add_action', 'remove_action', 'add_condition', 'add_loop',
|
|
177
|
+
'add_variable', 'get_details', 'publish'],
|
|
178
|
+
description: 'Management action to perform'
|
|
179
|
+
},
|
|
180
|
+
flow: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
description: '[create] Flow configuration',
|
|
183
|
+
properties: {
|
|
184
|
+
name: { type: 'string' },
|
|
185
|
+
internal_name: { type: 'string' },
|
|
186
|
+
description: { type: 'string' },
|
|
187
|
+
active: { type: 'boolean', default: false },
|
|
188
|
+
application_scope: { type: 'string' },
|
|
189
|
+
run_as: { type: 'string', enum: ['system', 'user_who_triggers', 'specific_user'] },
|
|
190
|
+
run_as_user: { type: 'string' }
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
trigger: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
description: '[create] Trigger configuration'
|
|
196
|
+
},
|
|
197
|
+
actions: {
|
|
198
|
+
type: 'array',
|
|
199
|
+
description: '[create] Actions to add',
|
|
200
|
+
items: { type: 'object' }
|
|
201
|
+
},
|
|
202
|
+
conditions: {
|
|
203
|
+
type: 'array',
|
|
204
|
+
description: '[create] Conditional logic blocks',
|
|
205
|
+
items: { type: 'object' }
|
|
206
|
+
},
|
|
207
|
+
loops: {
|
|
208
|
+
type: 'array',
|
|
209
|
+
description: '[create] Loop constructs',
|
|
210
|
+
items: { type: 'object' }
|
|
211
|
+
},
|
|
212
|
+
variables: {
|
|
213
|
+
type: 'array',
|
|
214
|
+
description: '[create] Flow variables',
|
|
215
|
+
items: { type: 'object' }
|
|
216
|
+
},
|
|
217
|
+
flow_id: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
description: '[update/delete/clone/activate/deactivate/get_details/add_*] Flow sys_id'
|
|
220
|
+
},
|
|
221
|
+
flow_name: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
description: '[update/delete/clone/activate/deactivate/get_details/add_*] Flow name (alternative to flow_id)'
|
|
224
|
+
},
|
|
225
|
+
updates: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
description: '[update] Properties to update'
|
|
228
|
+
},
|
|
229
|
+
new_name: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
description: '[clone] Name for cloned flow'
|
|
232
|
+
},
|
|
233
|
+
action_config: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
description: '[add_action] Action to add'
|
|
236
|
+
},
|
|
237
|
+
condition_config: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
description: '[add_condition] Condition to add'
|
|
240
|
+
},
|
|
241
|
+
loop_config: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
description: '[add_loop] Loop to add'
|
|
244
|
+
},
|
|
245
|
+
variable_config: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
description: '[add_variable] Variable to add'
|
|
248
|
+
},
|
|
249
|
+
action_id: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: '[remove_action] Action sys_id to remove'
|
|
252
|
+
},
|
|
253
|
+
auto_activate: {
|
|
254
|
+
type: 'boolean',
|
|
255
|
+
description: 'Activate after create/update',
|
|
256
|
+
default: false
|
|
257
|
+
},
|
|
258
|
+
create_snapshot: {
|
|
259
|
+
type: 'boolean',
|
|
260
|
+
description: 'Create snapshot after changes',
|
|
261
|
+
default: true
|
|
262
|
+
},
|
|
263
|
+
use_compression: {
|
|
264
|
+
type: 'boolean',
|
|
265
|
+
description: 'Use compression for values (auto-detected if not specified)'
|
|
266
|
+
},
|
|
267
|
+
validate_only: {
|
|
268
|
+
type: 'boolean',
|
|
269
|
+
description: 'Validate without making changes',
|
|
270
|
+
default: false
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
required: ['action']
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
// ==================== HELPER FUNCTIONS ====================
|
|
277
|
+
/**
|
|
278
|
+
* Generate ServiceNow-compatible sys_id
|
|
279
|
+
*/
|
|
280
|
+
function generateSysId() {
|
|
281
|
+
return crypto.randomBytes(16).toString('hex');
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Generate internal name from display name
|
|
285
|
+
*/
|
|
286
|
+
function generateInternalName(name, prefix = 'x_snfl') {
|
|
287
|
+
const sanitized = name
|
|
288
|
+
.toLowerCase()
|
|
289
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
290
|
+
.replace(/^_+|_+$/g, '')
|
|
291
|
+
.substring(0, 40);
|
|
292
|
+
return `${prefix}_${sanitized}`;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Compress value for Xanadu+ versions
|
|
296
|
+
* Uses gzip compression + base64 encoding
|
|
297
|
+
*
|
|
298
|
+
* ServiceNow stores values as: base64(gzip(JSON))
|
|
299
|
+
* No header is used - the data starts directly with base64
|
|
300
|
+
*/
|
|
301
|
+
function compressValue(value, useHeader = false) {
|
|
302
|
+
const jsonStr = typeof value === 'object' ? JSON.stringify(value) : value;
|
|
303
|
+
try {
|
|
304
|
+
// Compress with gzip
|
|
305
|
+
const compressed = zlib.gzipSync(Buffer.from(jsonStr, 'utf-8'));
|
|
306
|
+
// Encode as base64
|
|
307
|
+
const encoded = compressed.toString('base64');
|
|
308
|
+
// Return with or without header based on detection
|
|
309
|
+
return useHeader ? COMPRESSED_HEADER + encoded : encoded;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
console.error('Compression failed, returning uncompressed:', error);
|
|
313
|
+
return jsonStr;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Decompress value from Xanadu+ format
|
|
318
|
+
* Handles both with and without COMPRESSED: header
|
|
319
|
+
*/
|
|
320
|
+
function decompressValue(value) {
|
|
321
|
+
if (!value) {
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
let encoded = value;
|
|
326
|
+
// Check if it has the COMPRESSED: header
|
|
327
|
+
if (value.startsWith(COMPRESSED_HEADER)) {
|
|
328
|
+
encoded = value.substring(COMPRESSED_HEADER.length);
|
|
329
|
+
}
|
|
330
|
+
// Try to decode base64 and decompress
|
|
331
|
+
const compressed = Buffer.from(encoded, 'base64');
|
|
332
|
+
// Check if it's actually gzip compressed (magic bytes: 1f 8b)
|
|
333
|
+
if (compressed.length >= 2 && compressed[0] === 0x1f && compressed[1] === 0x8b) {
|
|
334
|
+
const decompressed = zlib.gunzipSync(compressed);
|
|
335
|
+
return decompressed.toString('utf-8');
|
|
336
|
+
}
|
|
337
|
+
// Not compressed, return original or try as plain base64
|
|
338
|
+
try {
|
|
339
|
+
return Buffer.from(encoded, 'base64').toString('utf-8');
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
console.error('Decompression failed:', error);
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Check if value is compressed (with or without header)
|
|
352
|
+
*/
|
|
353
|
+
function isCompressed(value) {
|
|
354
|
+
if (!value)
|
|
355
|
+
return false;
|
|
356
|
+
// Check for COMPRESSED: header
|
|
357
|
+
if (value.startsWith(COMPRESSED_HEADER)) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
// Check if it looks like base64 encoded gzip data
|
|
361
|
+
try {
|
|
362
|
+
const decoded = Buffer.from(value, 'base64');
|
|
363
|
+
// Gzip magic bytes: 1f 8b
|
|
364
|
+
return decoded.length >= 2 && decoded[0] === 0x1f && decoded[1] === 0x8b;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Check if value uses the COMPRESSED: header format
|
|
372
|
+
*/
|
|
373
|
+
function usesCompressionHeader(value) {
|
|
374
|
+
return value && value.startsWith(COMPRESSED_HEADER);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Detect ServiceNow version and capabilities
|
|
378
|
+
*/
|
|
379
|
+
async function detectVersion(client) {
|
|
380
|
+
try {
|
|
381
|
+
// Check if V2 tables exist by querying them
|
|
382
|
+
const v2Response = await client.get('/api/now/table/sys_hub_action_instance_v2', {
|
|
383
|
+
params: { sysparm_limit: 1 }
|
|
384
|
+
});
|
|
385
|
+
// Check if values are compressed by looking at a sample
|
|
386
|
+
let useCompression = false;
|
|
387
|
+
let useCompressionHeader = false;
|
|
388
|
+
if (v2Response.data.result && v2Response.data.result.length > 0) {
|
|
389
|
+
const sample = v2Response.data.result[0];
|
|
390
|
+
if (sample.values) {
|
|
391
|
+
useCompression = isCompressed(sample.values);
|
|
392
|
+
useCompressionHeader = usesCompressionHeader(sample.values);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
useV2Tables: true,
|
|
397
|
+
useCompression,
|
|
398
|
+
useCompressionHeader,
|
|
399
|
+
version: useCompression ? 'Xanadu+' : 'Washington DC+'
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
// V2 tables don't exist, use V1
|
|
404
|
+
return {
|
|
405
|
+
useV2Tables: false,
|
|
406
|
+
useCompression: false,
|
|
407
|
+
useCompressionHeader: false,
|
|
408
|
+
version: 'Pre-Washington DC'
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Detect if a value is a data pill reference
|
|
414
|
+
* Data pills use {{action_name.field}} or {{trigger.field}} syntax
|
|
415
|
+
*/
|
|
416
|
+
function isDataPill(value) {
|
|
417
|
+
if (typeof value !== 'string')
|
|
418
|
+
return false;
|
|
419
|
+
return /^\{\{[\w.]+\}\}$/.test(value) || /^\$\{[\w.]+\}$/.test(value);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Build a data pill reference
|
|
423
|
+
*/
|
|
424
|
+
function buildDataPill(reference, displayValue) {
|
|
425
|
+
return {
|
|
426
|
+
value: reference,
|
|
427
|
+
displayValue: displayValue || reference,
|
|
428
|
+
valueType: 'pill'
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Build a reference field value
|
|
433
|
+
*/
|
|
434
|
+
function buildReferenceValue(sysId, displayValue, table) {
|
|
435
|
+
return {
|
|
436
|
+
value: sysId,
|
|
437
|
+
displayValue: displayValue,
|
|
438
|
+
valueType: 'reference',
|
|
439
|
+
parameter: {
|
|
440
|
+
type: 'reference',
|
|
441
|
+
label: displayValue,
|
|
442
|
+
mandatory: false,
|
|
443
|
+
reference: table
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Build a script value (for Run Script action)
|
|
449
|
+
*/
|
|
450
|
+
function buildScriptValue(script) {
|
|
451
|
+
return {
|
|
452
|
+
value: script,
|
|
453
|
+
valueType: 'script'
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Build a condition value (for If/Loop conditions)
|
|
458
|
+
*/
|
|
459
|
+
function buildConditionValue(condition) {
|
|
460
|
+
return {
|
|
461
|
+
value: condition,
|
|
462
|
+
valueType: 'condition'
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Build a glide list value (multi-select)
|
|
467
|
+
*/
|
|
468
|
+
function buildGlideListValue(values) {
|
|
469
|
+
return {
|
|
470
|
+
value: values.join(','),
|
|
471
|
+
valueType: 'glide_list'
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Build values JSON for action instance - ENHANCED VERSION
|
|
476
|
+
* Supports: data pills, references, arrays, scripts, conditions, transforms
|
|
477
|
+
*/
|
|
478
|
+
function buildActionValues(inputs, useCompression, useCompressionHeader = false) {
|
|
479
|
+
if (!inputs || Object.keys(inputs).length === 0) {
|
|
480
|
+
const emptyValue = '{}';
|
|
481
|
+
return useCompression ? compressValue(emptyValue, useCompressionHeader) : emptyValue;
|
|
482
|
+
}
|
|
483
|
+
const values = {};
|
|
484
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
485
|
+
// Already in Flow Designer format
|
|
486
|
+
if (typeof value === 'object' && value !== null && 'value' in value && 'valueType' in value) {
|
|
487
|
+
values[key] = value;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
// Data pill reference ({{...}} or ${...})
|
|
491
|
+
if (isDataPill(value)) {
|
|
492
|
+
values[key] = {
|
|
493
|
+
value: value,
|
|
494
|
+
displayValue: value,
|
|
495
|
+
valueType: 'pill'
|
|
496
|
+
};
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
// Reference object with sys_id and display
|
|
500
|
+
if (typeof value === 'object' && value !== null && 'sys_id' in value) {
|
|
501
|
+
values[key] = {
|
|
502
|
+
value: value.sys_id,
|
|
503
|
+
displayValue: value.display_value || value.name || value.sys_id,
|
|
504
|
+
valueType: 'reference',
|
|
505
|
+
referenceName: value.table || ''
|
|
506
|
+
};
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
// Array value (e.g., for multi-select or list inputs)
|
|
510
|
+
if (Array.isArray(value)) {
|
|
511
|
+
if (value.every(v => typeof v === 'string')) {
|
|
512
|
+
// String array - join as comma-separated
|
|
513
|
+
values[key] = {
|
|
514
|
+
value: value.join(','),
|
|
515
|
+
valueType: 'glide_list'
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
else if (value.every(v => typeof v === 'object' && 'sys_id' in v)) {
|
|
519
|
+
// Array of references
|
|
520
|
+
values[key] = {
|
|
521
|
+
value: value.map(v => v.sys_id).join(','),
|
|
522
|
+
displayValue: value.map(v => v.display_value || v.name || v.sys_id).join(', '),
|
|
523
|
+
valueType: 'reference_list'
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// Complex array - serialize
|
|
528
|
+
values[key] = {
|
|
529
|
+
value: JSON.stringify(value),
|
|
530
|
+
valueType: 'array'
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
// Object with special type hints
|
|
536
|
+
if (typeof value === 'object' && value !== null) {
|
|
537
|
+
if ('_type' in value) {
|
|
538
|
+
// Handle special types
|
|
539
|
+
switch (value._type) {
|
|
540
|
+
case 'pill':
|
|
541
|
+
case 'data_pill':
|
|
542
|
+
values[key] = buildDataPill(value.reference, value.display);
|
|
543
|
+
break;
|
|
544
|
+
case 'reference':
|
|
545
|
+
values[key] = buildReferenceValue(value.sys_id, value.display || value.sys_id, value.table);
|
|
546
|
+
break;
|
|
547
|
+
case 'script':
|
|
548
|
+
values[key] = buildScriptValue(value.script || value.value);
|
|
549
|
+
break;
|
|
550
|
+
case 'condition':
|
|
551
|
+
values[key] = buildConditionValue(value.condition || value.value);
|
|
552
|
+
break;
|
|
553
|
+
case 'glide_list':
|
|
554
|
+
values[key] = buildGlideListValue(value.values || []);
|
|
555
|
+
break;
|
|
556
|
+
case 'transform':
|
|
557
|
+
// Transform function applied to a value
|
|
558
|
+
values[key] = {
|
|
559
|
+
value: value.source,
|
|
560
|
+
displayValue: value.display || value.source,
|
|
561
|
+
valueType: 'transform',
|
|
562
|
+
transform: {
|
|
563
|
+
function: value.function,
|
|
564
|
+
params: value.params || []
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
break;
|
|
568
|
+
default:
|
|
569
|
+
values[key] = {
|
|
570
|
+
value: JSON.stringify(value),
|
|
571
|
+
valueType: 'object'
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
// Generic object
|
|
577
|
+
values[key] = {
|
|
578
|
+
value: JSON.stringify(value),
|
|
579
|
+
valueType: 'object'
|
|
580
|
+
};
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
// Boolean value
|
|
584
|
+
if (typeof value === 'boolean') {
|
|
585
|
+
values[key] = {
|
|
586
|
+
value: String(value),
|
|
587
|
+
valueType: 'boolean'
|
|
588
|
+
};
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
// Number value
|
|
592
|
+
if (typeof value === 'number') {
|
|
593
|
+
values[key] = {
|
|
594
|
+
value: String(value),
|
|
595
|
+
valueType: Number.isInteger(value) ? 'integer' : 'decimal'
|
|
596
|
+
};
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
// String value (check for special patterns)
|
|
600
|
+
if (typeof value === 'string') {
|
|
601
|
+
// Check if it looks like a date/time
|
|
602
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
603
|
+
values[key] = {
|
|
604
|
+
value: value,
|
|
605
|
+
valueType: value.includes('T') ? 'glide_date_time' : 'glide_date'
|
|
606
|
+
};
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
// Check if it looks like a duration (e.g., "1d 2h 30m")
|
|
610
|
+
if (/^\d+[dhms]\s*/.test(value)) {
|
|
611
|
+
values[key] = {
|
|
612
|
+
value: value,
|
|
613
|
+
valueType: 'duration'
|
|
614
|
+
};
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
// Regular string
|
|
618
|
+
values[key] = {
|
|
619
|
+
value: value,
|
|
620
|
+
valueType: 'string'
|
|
621
|
+
};
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
// Fallback - convert to string
|
|
625
|
+
values[key] = {
|
|
626
|
+
value: String(value),
|
|
627
|
+
valueType: 'string'
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
const jsonStr = JSON.stringify(values);
|
|
631
|
+
return useCompression ? compressValue(jsonStr, useCompressionHeader) : jsonStr;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Remap sys_ids in values JSON when cloning
|
|
635
|
+
* Updates data pill references from old flow to new flow
|
|
636
|
+
*/
|
|
637
|
+
function remapValuesForClone(valuesJson, sysIdMap, oldFlowId, newFlowId) {
|
|
638
|
+
if (!valuesJson)
|
|
639
|
+
return valuesJson;
|
|
640
|
+
let values;
|
|
641
|
+
try {
|
|
642
|
+
values = JSON.parse(valuesJson);
|
|
643
|
+
}
|
|
644
|
+
catch (e) {
|
|
645
|
+
return valuesJson;
|
|
646
|
+
}
|
|
647
|
+
// Recursively update sys_ids and references
|
|
648
|
+
const updateRefs = (obj) => {
|
|
649
|
+
if (!obj || typeof obj !== 'object')
|
|
650
|
+
return obj;
|
|
651
|
+
if (Array.isArray(obj)) {
|
|
652
|
+
return obj.map(item => updateRefs(item));
|
|
653
|
+
}
|
|
654
|
+
const updated = {};
|
|
655
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
656
|
+
if (typeof value === 'string') {
|
|
657
|
+
// Check if it's a sys_id that needs remapping
|
|
658
|
+
if (sysIdMap.has(value)) {
|
|
659
|
+
updated[key] = sysIdMap.get(value);
|
|
660
|
+
}
|
|
661
|
+
// Check if it's a data pill reference
|
|
662
|
+
else if (value.includes(oldFlowId)) {
|
|
663
|
+
updated[key] = value.replace(oldFlowId, newFlowId);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
updated[key] = value;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else if (typeof value === 'object') {
|
|
670
|
+
updated[key] = updateRefs(value);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
updated[key] = value;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return updated;
|
|
677
|
+
};
|
|
678
|
+
const remapped = updateRefs(values);
|
|
679
|
+
return JSON.stringify(remapped);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Get trigger type sys_id from name
|
|
683
|
+
*/
|
|
684
|
+
async function getTriggerTypeSysId(client, triggerType, when) {
|
|
685
|
+
const triggerMap = {
|
|
686
|
+
'record_created': 'Created',
|
|
687
|
+
'record_updated': 'Updated',
|
|
688
|
+
'record_deleted': 'Deleted',
|
|
689
|
+
'record_created_or_updated': 'Created or Updated',
|
|
690
|
+
'scheduled': 'Scheduled',
|
|
691
|
+
'api': 'REST API',
|
|
692
|
+
'inbound_email': 'Inbound Email',
|
|
693
|
+
'service_catalog': 'Service Catalog'
|
|
694
|
+
};
|
|
695
|
+
let searchName = triggerType;
|
|
696
|
+
if (triggerType === 'record' && when) {
|
|
697
|
+
searchName = triggerMap[`record_${when}`] || when;
|
|
698
|
+
}
|
|
699
|
+
else if (triggerMap[triggerType]) {
|
|
700
|
+
searchName = triggerMap[triggerType];
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
// Note: sys_hub_trigger_type doesn't have 'label' or 'active' fields
|
|
704
|
+
const response = await client.get('/api/now/table/sys_hub_trigger_type', {
|
|
705
|
+
params: {
|
|
706
|
+
sysparm_query: `nameLIKE${searchName}`,
|
|
707
|
+
sysparm_limit: 1,
|
|
708
|
+
sysparm_fields: 'sys_id,name,description'
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
if (response.data.result && response.data.result.length > 0) {
|
|
712
|
+
return response.data.result[0].sys_id;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
console.error('Failed to get trigger type:', error);
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Get action type sys_id from name
|
|
722
|
+
*/
|
|
723
|
+
async function getActionTypeSysId(client, actionType) {
|
|
724
|
+
if (/^[0-9a-f]{32}$/i.test(actionType)) {
|
|
725
|
+
return { sys_id: actionType, name: actionType };
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
// Note: sys_hub_action_type_base doesn't have 'label' or 'active' fields
|
|
729
|
+
const response = await client.get('/api/now/table/sys_hub_action_type_base', {
|
|
730
|
+
params: {
|
|
731
|
+
sysparm_query: `nameLIKE${actionType}`,
|
|
732
|
+
sysparm_limit: 1,
|
|
733
|
+
sysparm_fields: 'sys_id,name,description'
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
if (response.data.result && response.data.result.length > 0) {
|
|
737
|
+
const result = response.data.result[0];
|
|
738
|
+
return { sys_id: result.sys_id, name: result.name };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
console.error('Failed to get action type:', error);
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Find flow by ID or name
|
|
748
|
+
*/
|
|
749
|
+
async function findFlow(client, flowId, flowName) {
|
|
750
|
+
if (!flowId && !flowName) {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
let query = '';
|
|
755
|
+
if (flowId) {
|
|
756
|
+
query = `sys_id=${flowId}`;
|
|
757
|
+
}
|
|
758
|
+
else if (flowName) {
|
|
759
|
+
query = `name=${flowName}^ORinternal_name=${flowName}`;
|
|
760
|
+
}
|
|
761
|
+
const response = await client.get('/api/now/table/sys_hub_flow', {
|
|
762
|
+
params: {
|
|
763
|
+
sysparm_query: query,
|
|
764
|
+
sysparm_limit: 1
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
if (response.data.result && response.data.result.length > 0) {
|
|
768
|
+
return response.data.result[0];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
console.error('Failed to find flow:', error);
|
|
773
|
+
}
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Get maximum order number for actions/logic in a flow
|
|
778
|
+
*/
|
|
779
|
+
async function getMaxOrder(client, flowId, table) {
|
|
780
|
+
try {
|
|
781
|
+
const response = await client.get(`/api/now/table/${table}`, {
|
|
782
|
+
params: {
|
|
783
|
+
sysparm_query: `flow=${flowId}`,
|
|
784
|
+
sysparm_fields: 'order',
|
|
785
|
+
sysparm_orderby: 'DESCorder',
|
|
786
|
+
sysparm_limit: 1
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
if (response.data.result && response.data.result.length > 0) {
|
|
790
|
+
return parseInt(response.data.result[0].order || '0', 10);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
console.error('Failed to get max order:', error);
|
|
795
|
+
}
|
|
796
|
+
return 0;
|
|
797
|
+
}
|
|
798
|
+
// ==================== ACTION HANDLERS ====================
|
|
799
|
+
/**
|
|
800
|
+
* Create a new flow
|
|
801
|
+
*/
|
|
802
|
+
async function handleCreate(args, client, versionInfo) {
|
|
803
|
+
const { flow: flowConfig, trigger: triggerConfig, actions: actionConfigs = [], conditions = [], loops = [], variables = [], auto_activate = false, create_snapshot = true, use_compression, validate_only = false } = args;
|
|
804
|
+
if (!flowConfig?.name) {
|
|
805
|
+
return (0, error_handler_js_1.createErrorResult)('flow.name is required for create action');
|
|
806
|
+
}
|
|
807
|
+
const useCompression = use_compression ?? versionInfo.useCompression;
|
|
808
|
+
const flowSysId = generateSysId();
|
|
809
|
+
const snapshotSysId = generateSysId();
|
|
810
|
+
const triggerSysId = generateSysId();
|
|
811
|
+
const internalName = flowConfig.internal_name || generateInternalName(flowConfig.name);
|
|
812
|
+
const timestamp = new Date().toISOString();
|
|
813
|
+
if (validate_only) {
|
|
814
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
815
|
+
valid: true,
|
|
816
|
+
would_create: {
|
|
817
|
+
flow: { sys_id: flowSysId, name: flowConfig.name, internal_name: internalName },
|
|
818
|
+
snapshot: create_snapshot ? { sys_id: snapshotSysId } : null,
|
|
819
|
+
trigger: triggerConfig ? { sys_id: triggerSysId, type: triggerConfig.type } : null,
|
|
820
|
+
actions: actionConfigs.length,
|
|
821
|
+
conditions: conditions.length,
|
|
822
|
+
loops: loops.length,
|
|
823
|
+
variables: variables.length
|
|
824
|
+
},
|
|
825
|
+
version_info: versionInfo,
|
|
826
|
+
compression: useCompression
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
const createdRecords = [];
|
|
830
|
+
const errors = [];
|
|
831
|
+
// 1. CREATE FLOW RECORD
|
|
832
|
+
console.log(`Creating flow: ${flowConfig.name}`);
|
|
833
|
+
const flowData = {
|
|
834
|
+
sys_id: flowSysId,
|
|
835
|
+
name: flowConfig.name,
|
|
836
|
+
internal_name: internalName,
|
|
837
|
+
description: flowConfig.description || '',
|
|
838
|
+
active: false,
|
|
839
|
+
sys_class_name: 'sys_hub_flow',
|
|
840
|
+
type: 'flow',
|
|
841
|
+
flow_type: 'flow',
|
|
842
|
+
status: 'draft',
|
|
843
|
+
source_ui: 'flow_designer',
|
|
844
|
+
access: 'public',
|
|
845
|
+
run_as: flowConfig.run_as || 'system'
|
|
846
|
+
};
|
|
847
|
+
if (flowConfig.application_scope && flowConfig.application_scope !== 'global') {
|
|
848
|
+
flowData.sys_scope = flowConfig.application_scope;
|
|
849
|
+
}
|
|
850
|
+
if (flowConfig.run_as_user) {
|
|
851
|
+
flowData.run_as_user = flowConfig.run_as_user;
|
|
852
|
+
}
|
|
853
|
+
try {
|
|
854
|
+
const flowResponse = await client.post(`/api/now/table/${FLOW_TABLES.FLOW}`, flowData);
|
|
855
|
+
createdRecords.push({ table: FLOW_TABLES.FLOW, sys_id: flowSysId, name: flowConfig.name });
|
|
856
|
+
console.log(`✅ Created flow: ${flowSysId}`);
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to create flow: ${error.response?.data?.error?.message || error.message}`);
|
|
860
|
+
}
|
|
861
|
+
// 2. CREATE TRIGGER
|
|
862
|
+
if (triggerConfig) {
|
|
863
|
+
console.log(`Creating trigger: ${triggerConfig.type}`);
|
|
864
|
+
const triggerTypeSysId = await getTriggerTypeSysId(client, triggerConfig.type, triggerConfig.when);
|
|
865
|
+
if (!triggerTypeSysId) {
|
|
866
|
+
errors.push(`Trigger type not found: ${triggerConfig.type}`);
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
const triggerTable = versionInfo.useV2Tables ? FLOW_TABLES.TRIGGER_V2 : FLOW_TABLES.TRIGGER_V1;
|
|
870
|
+
const triggerData = {
|
|
871
|
+
sys_id: triggerSysId,
|
|
872
|
+
flow: flowSysId,
|
|
873
|
+
trigger_type: triggerTypeSysId,
|
|
874
|
+
active: true,
|
|
875
|
+
order: 0
|
|
876
|
+
};
|
|
877
|
+
if (triggerConfig.type === 'record') {
|
|
878
|
+
if (triggerConfig.table)
|
|
879
|
+
triggerData.table = triggerConfig.table;
|
|
880
|
+
if (triggerConfig.condition) {
|
|
881
|
+
const conditionValue = { filter_condition: { value: triggerConfig.condition } };
|
|
882
|
+
triggerData.values = useCompression ? compressValue(conditionValue) : JSON.stringify(conditionValue);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (triggerConfig.type === 'scheduled' && triggerConfig.schedule) {
|
|
886
|
+
const scheduleValues = {};
|
|
887
|
+
if (triggerConfig.schedule.frequency)
|
|
888
|
+
scheduleValues.frequency = { value: triggerConfig.schedule.frequency };
|
|
889
|
+
if (triggerConfig.schedule.time)
|
|
890
|
+
scheduleValues.time_of_day = { value: triggerConfig.schedule.time };
|
|
891
|
+
if (triggerConfig.schedule.cron)
|
|
892
|
+
scheduleValues.cron_expression = { value: triggerConfig.schedule.cron };
|
|
893
|
+
triggerData.values = useCompression ? compressValue(scheduleValues) : JSON.stringify(scheduleValues);
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
await client.post(`/api/now/table/${triggerTable}`, triggerData);
|
|
897
|
+
createdRecords.push({ table: triggerTable, sys_id: triggerSysId, type: triggerConfig.type });
|
|
898
|
+
console.log(`✅ Created trigger: ${triggerSysId}`);
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
errors.push(`Failed to create trigger: ${error.response?.data?.error?.message || error.message}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// 3. CREATE VARIABLES
|
|
906
|
+
for (let i = 0; i < variables.length; i++) {
|
|
907
|
+
const variable = variables[i];
|
|
908
|
+
const variableSysId = generateSysId();
|
|
909
|
+
console.log(`Creating variable: ${variable.name}`);
|
|
910
|
+
const typeMap = {
|
|
911
|
+
'string': 'string', 'integer': 'integer', 'boolean': 'boolean',
|
|
912
|
+
'reference': 'reference', 'object': 'object', 'array': 'array',
|
|
913
|
+
'glide_date_time': 'glide_date_time'
|
|
914
|
+
};
|
|
915
|
+
// Note: sys_hub_flow_variable uses 'model' field (not 'flow') to reference the parent flow
|
|
916
|
+
const variableData = {
|
|
917
|
+
sys_id: variableSysId,
|
|
918
|
+
model: flowSysId,
|
|
919
|
+
name: variable.name,
|
|
920
|
+
label: variable.label || variable.name,
|
|
921
|
+
description: variable.description || '',
|
|
922
|
+
type: typeMap[variable.type] || 'string',
|
|
923
|
+
direction: variable.direction,
|
|
924
|
+
mandatory: variable.mandatory || false,
|
|
925
|
+
order: i * 100
|
|
926
|
+
};
|
|
927
|
+
if (variable.default_value)
|
|
928
|
+
variableData.default_value = variable.default_value;
|
|
929
|
+
if (variable.reference_table)
|
|
930
|
+
variableData.reference_table = variable.reference_table;
|
|
931
|
+
try {
|
|
932
|
+
await client.post(`/api/now/table/${FLOW_TABLES.VARIABLE}`, variableData);
|
|
933
|
+
createdRecords.push({ table: FLOW_TABLES.VARIABLE, sys_id: variableSysId, name: variable.name });
|
|
934
|
+
console.log(`✅ Created variable: ${variable.name}`);
|
|
935
|
+
}
|
|
936
|
+
catch (error) {
|
|
937
|
+
errors.push(`Failed to create variable ${variable.name}: ${error.response?.data?.error?.message || error.message}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// 4. CREATE CONDITIONS (with nested actions)
|
|
941
|
+
const logicTable = versionInfo.useV2Tables ? FLOW_TABLES.LOGIC_V2 : FLOW_TABLES.LOGIC_V1;
|
|
942
|
+
const actionTable = versionInfo.useV2Tables ? FLOW_TABLES.ACTION_V2 : FLOW_TABLES.ACTION_V1;
|
|
943
|
+
let currentOrder = 100;
|
|
944
|
+
for (const condition of conditions) {
|
|
945
|
+
const conditionSysId = generateSysId();
|
|
946
|
+
console.log(`Creating condition: ${condition.name || 'Condition'}`);
|
|
947
|
+
const conditionData = {
|
|
948
|
+
sys_id: conditionSysId,
|
|
949
|
+
flow: flowSysId,
|
|
950
|
+
name: condition.name || 'If',
|
|
951
|
+
logic_type: 'if',
|
|
952
|
+
order: condition.order ?? currentOrder,
|
|
953
|
+
active: true
|
|
954
|
+
};
|
|
955
|
+
// Build condition values
|
|
956
|
+
const conditionValues = {
|
|
957
|
+
condition: { value: condition.condition, valueType: 'string' }
|
|
958
|
+
};
|
|
959
|
+
conditionData.values = useCompression ? compressValue(conditionValues) : JSON.stringify(conditionValues);
|
|
960
|
+
if (condition.parent_logic_instance) {
|
|
961
|
+
conditionData.parent_logic_instance = condition.parent_logic_instance;
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
await client.post(`/api/now/table/${logicTable}`, conditionData);
|
|
965
|
+
createdRecords.push({ table: logicTable, sys_id: conditionSysId, name: condition.name || 'If', type: 'condition' });
|
|
966
|
+
console.log(`✅ Created condition: ${conditionSysId}`);
|
|
967
|
+
// Create THEN branch actions
|
|
968
|
+
if (condition.then_actions && condition.then_actions.length > 0) {
|
|
969
|
+
let thenOrder = 100;
|
|
970
|
+
for (const thenAction of condition.then_actions) {
|
|
971
|
+
const actionSysId = generateSysId();
|
|
972
|
+
const actionType = await getActionTypeSysId(client, thenAction.type);
|
|
973
|
+
if (actionType) {
|
|
974
|
+
const actionData = {
|
|
975
|
+
sys_id: actionSysId,
|
|
976
|
+
flow: flowSysId,
|
|
977
|
+
action_type: actionType.sys_id,
|
|
978
|
+
name: thenAction.name || actionType.name,
|
|
979
|
+
description: thenAction.description || '',
|
|
980
|
+
order: thenOrder,
|
|
981
|
+
active: true,
|
|
982
|
+
parent_logic_instance: conditionSysId,
|
|
983
|
+
branch: 'then'
|
|
984
|
+
};
|
|
985
|
+
if (thenAction.inputs) {
|
|
986
|
+
actionData.values = buildActionValues(thenAction.inputs, useCompression, versionInfo.useCompressionHeader);
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
await client.post(`/api/now/table/${actionTable}`, actionData);
|
|
990
|
+
createdRecords.push({
|
|
991
|
+
table: actionTable,
|
|
992
|
+
sys_id: actionSysId,
|
|
993
|
+
name: thenAction.name || actionType.name,
|
|
994
|
+
parent: conditionSysId,
|
|
995
|
+
branch: 'then'
|
|
996
|
+
});
|
|
997
|
+
console.log(`✅ Created then-action: ${actionSysId}`);
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
errors.push(`Failed to create then-action: ${error.response?.data?.error?.message || error.message}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
thenOrder += 100;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
// Create ELSE branch actions
|
|
1007
|
+
if (condition.else_actions && condition.else_actions.length > 0) {
|
|
1008
|
+
let elseOrder = 100;
|
|
1009
|
+
for (const elseAction of condition.else_actions) {
|
|
1010
|
+
const actionSysId = generateSysId();
|
|
1011
|
+
const actionType = await getActionTypeSysId(client, elseAction.type);
|
|
1012
|
+
if (actionType) {
|
|
1013
|
+
const actionData = {
|
|
1014
|
+
sys_id: actionSysId,
|
|
1015
|
+
flow: flowSysId,
|
|
1016
|
+
action_type: actionType.sys_id,
|
|
1017
|
+
name: elseAction.name || actionType.name,
|
|
1018
|
+
description: elseAction.description || '',
|
|
1019
|
+
order: elseOrder,
|
|
1020
|
+
active: true,
|
|
1021
|
+
parent_logic_instance: conditionSysId,
|
|
1022
|
+
branch: 'else'
|
|
1023
|
+
};
|
|
1024
|
+
if (elseAction.inputs) {
|
|
1025
|
+
actionData.values = buildActionValues(elseAction.inputs, useCompression, versionInfo.useCompressionHeader);
|
|
1026
|
+
}
|
|
1027
|
+
try {
|
|
1028
|
+
await client.post(`/api/now/table/${actionTable}`, actionData);
|
|
1029
|
+
createdRecords.push({
|
|
1030
|
+
table: actionTable,
|
|
1031
|
+
sys_id: actionSysId,
|
|
1032
|
+
name: elseAction.name || actionType.name,
|
|
1033
|
+
parent: conditionSysId,
|
|
1034
|
+
branch: 'else'
|
|
1035
|
+
});
|
|
1036
|
+
console.log(`✅ Created else-action: ${actionSysId}`);
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
errors.push(`Failed to create else-action: ${error.response?.data?.error?.message || error.message}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
elseOrder += 100;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
errors.push(`Failed to create condition: ${error.response?.data?.error?.message || error.message}`);
|
|
1048
|
+
}
|
|
1049
|
+
currentOrder += 100;
|
|
1050
|
+
}
|
|
1051
|
+
// 5. CREATE LOOPS (with nested actions)
|
|
1052
|
+
for (const loop of loops) {
|
|
1053
|
+
const loopSysId = generateSysId();
|
|
1054
|
+
console.log(`Creating loop: ${loop.name || loop.type}`);
|
|
1055
|
+
const loopData = {
|
|
1056
|
+
sys_id: loopSysId,
|
|
1057
|
+
flow: flowSysId,
|
|
1058
|
+
name: loop.name || (loop.type === 'for_each' ? 'For Each' : 'Do Until'),
|
|
1059
|
+
logic_type: loop.type,
|
|
1060
|
+
order: loop.order ?? currentOrder,
|
|
1061
|
+
active: true
|
|
1062
|
+
};
|
|
1063
|
+
// Build loop values
|
|
1064
|
+
const loopValues = {};
|
|
1065
|
+
if (loop.data_source)
|
|
1066
|
+
loopValues.data_source = { value: loop.data_source, valueType: 'reference' };
|
|
1067
|
+
if (loop.condition)
|
|
1068
|
+
loopValues.condition = { value: loop.condition, valueType: 'string' };
|
|
1069
|
+
if (loop.max_iterations)
|
|
1070
|
+
loopValues.max_iterations = { value: String(loop.max_iterations), valueType: 'integer' };
|
|
1071
|
+
if (Object.keys(loopValues).length > 0) {
|
|
1072
|
+
loopData.values = useCompression ? compressValue(loopValues) : JSON.stringify(loopValues);
|
|
1073
|
+
}
|
|
1074
|
+
if (loop.parent_logic_instance) {
|
|
1075
|
+
loopData.parent_logic_instance = loop.parent_logic_instance;
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
await client.post(`/api/now/table/${logicTable}`, loopData);
|
|
1079
|
+
createdRecords.push({ table: logicTable, sys_id: loopSysId, name: loop.name || loop.type, type: 'loop' });
|
|
1080
|
+
console.log(`✅ Created loop: ${loopSysId}`);
|
|
1081
|
+
// Create nested actions within loop
|
|
1082
|
+
if (loop.actions && loop.actions.length > 0) {
|
|
1083
|
+
let loopActionOrder = 100;
|
|
1084
|
+
for (const loopAction of loop.actions) {
|
|
1085
|
+
const actionSysId = generateSysId();
|
|
1086
|
+
const actionType = await getActionTypeSysId(client, loopAction.type);
|
|
1087
|
+
if (actionType) {
|
|
1088
|
+
const actionData = {
|
|
1089
|
+
sys_id: actionSysId,
|
|
1090
|
+
flow: flowSysId,
|
|
1091
|
+
action_type: actionType.sys_id,
|
|
1092
|
+
name: loopAction.name || actionType.name,
|
|
1093
|
+
description: loopAction.description || '',
|
|
1094
|
+
order: loopActionOrder,
|
|
1095
|
+
active: true,
|
|
1096
|
+
parent_logic_instance: loopSysId
|
|
1097
|
+
};
|
|
1098
|
+
if (loopAction.inputs) {
|
|
1099
|
+
actionData.values = buildActionValues(loopAction.inputs, useCompression, versionInfo.useCompressionHeader);
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
await client.post(`/api/now/table/${actionTable}`, actionData);
|
|
1103
|
+
createdRecords.push({
|
|
1104
|
+
table: actionTable,
|
|
1105
|
+
sys_id: actionSysId,
|
|
1106
|
+
name: loopAction.name || actionType.name,
|
|
1107
|
+
parent: loopSysId
|
|
1108
|
+
});
|
|
1109
|
+
console.log(`✅ Created loop-action: ${actionSysId}`);
|
|
1110
|
+
}
|
|
1111
|
+
catch (error) {
|
|
1112
|
+
errors.push(`Failed to create loop-action: ${error.response?.data?.error?.message || error.message}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
loopActionOrder += 100;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
errors.push(`Failed to create loop: ${error.response?.data?.error?.message || error.message}`);
|
|
1121
|
+
}
|
|
1122
|
+
currentOrder += 100;
|
|
1123
|
+
}
|
|
1124
|
+
// 6. CREATE TOP-LEVEL ACTIONS (not nested in conditions/loops)
|
|
1125
|
+
for (let i = 0; i < actionConfigs.length; i++) {
|
|
1126
|
+
const actionConfig = actionConfigs[i];
|
|
1127
|
+
// Skip if this action is meant to be nested (has parent)
|
|
1128
|
+
if (actionConfig.parent_logic_instance)
|
|
1129
|
+
continue;
|
|
1130
|
+
const actionSysId = generateSysId();
|
|
1131
|
+
console.log(`Creating action: ${actionConfig.type}`);
|
|
1132
|
+
const actionType = await getActionTypeSysId(client, actionConfig.type);
|
|
1133
|
+
if (!actionType) {
|
|
1134
|
+
errors.push(`Action type not found: ${actionConfig.type}`);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const actionData = {
|
|
1138
|
+
sys_id: actionSysId,
|
|
1139
|
+
flow: flowSysId,
|
|
1140
|
+
action_type: actionType.sys_id,
|
|
1141
|
+
name: actionConfig.name || actionType.name,
|
|
1142
|
+
description: actionConfig.description || '',
|
|
1143
|
+
order: actionConfig.order ?? currentOrder,
|
|
1144
|
+
active: true
|
|
1145
|
+
};
|
|
1146
|
+
if (actionConfig.inputs) {
|
|
1147
|
+
actionData.values = buildActionValues(actionConfig.inputs, useCompression, versionInfo.useCompressionHeader);
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
await client.post(`/api/now/table/${actionTable}`, actionData);
|
|
1151
|
+
createdRecords.push({ table: actionTable, sys_id: actionSysId, name: actionConfig.name || actionType.name });
|
|
1152
|
+
console.log(`✅ Created action: ${actionSysId}`);
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
errors.push(`Failed to create action ${actionConfig.type}: ${error.response?.data?.error?.message || error.message}`);
|
|
1156
|
+
}
|
|
1157
|
+
currentOrder += 100;
|
|
1158
|
+
}
|
|
1159
|
+
// 7. CREATE SNAPSHOT WITH FLOW DEFINITION
|
|
1160
|
+
// This is CRITICAL - Flow Designer requires snapshots with main_snapshot/latest_snapshot references
|
|
1161
|
+
if (create_snapshot) {
|
|
1162
|
+
console.log('Creating snapshot with flow definition...');
|
|
1163
|
+
// Build the flow definition structure that Flow Designer expects
|
|
1164
|
+
const flowDefinition = {
|
|
1165
|
+
sys_id: flowSysId,
|
|
1166
|
+
name: flowConfig.name,
|
|
1167
|
+
internal_name: internalName,
|
|
1168
|
+
description: flowConfig.description || '',
|
|
1169
|
+
flow_type: 'flow',
|
|
1170
|
+
run_as: flowConfig.run_as || 'system',
|
|
1171
|
+
// Include references to created components
|
|
1172
|
+
trigger: triggerConfig ? {
|
|
1173
|
+
sys_id: triggerSysId,
|
|
1174
|
+
type: triggerConfig.type,
|
|
1175
|
+
table: triggerConfig.table,
|
|
1176
|
+
when: triggerConfig.when
|
|
1177
|
+
} : null,
|
|
1178
|
+
actions: createdRecords
|
|
1179
|
+
.filter(r => r.table === actionTable)
|
|
1180
|
+
.map(r => ({ sys_id: r.sys_id, name: r.name })),
|
|
1181
|
+
logic: createdRecords
|
|
1182
|
+
.filter(r => r.table === logicTable)
|
|
1183
|
+
.map(r => ({ sys_id: r.sys_id, name: r.name, type: r.type })),
|
|
1184
|
+
variables: createdRecords
|
|
1185
|
+
.filter(r => r.table === FLOW_TABLES.VARIABLE)
|
|
1186
|
+
.map(r => ({ sys_id: r.sys_id, name: r.name }))
|
|
1187
|
+
};
|
|
1188
|
+
const snapshotData = {
|
|
1189
|
+
sys_id: snapshotSysId,
|
|
1190
|
+
flow: flowSysId,
|
|
1191
|
+
name: `${flowConfig.name} - Initial`,
|
|
1192
|
+
version: '1.0.0',
|
|
1193
|
+
active: true,
|
|
1194
|
+
published: false,
|
|
1195
|
+
// Store the flow definition - this is what Flow Designer reads
|
|
1196
|
+
definition: useCompression
|
|
1197
|
+
? compressValue(flowDefinition, versionInfo.useCompressionHeader)
|
|
1198
|
+
: JSON.stringify(flowDefinition)
|
|
1199
|
+
};
|
|
1200
|
+
try {
|
|
1201
|
+
await client.post(`/api/now/table/${FLOW_TABLES.SNAPSHOT}`, snapshotData);
|
|
1202
|
+
createdRecords.push({ table: FLOW_TABLES.SNAPSHOT, sys_id: snapshotSysId, name: `${flowConfig.name} - Initial` });
|
|
1203
|
+
console.log(`✅ Created snapshot: ${snapshotSysId}`);
|
|
1204
|
+
// *** CRITICAL FIX ***
|
|
1205
|
+
// Update the flow record with snapshot references
|
|
1206
|
+
// Without this, Flow Designer shows an empty flow!
|
|
1207
|
+
console.log('Updating flow with snapshot references...');
|
|
1208
|
+
try {
|
|
1209
|
+
await client.patch(`/api/now/table/${FLOW_TABLES.FLOW}/${flowSysId}`, {
|
|
1210
|
+
main_snapshot: snapshotSysId,
|
|
1211
|
+
latest_snapshot: snapshotSysId
|
|
1212
|
+
});
|
|
1213
|
+
console.log(`✅ Flow updated with main_snapshot and latest_snapshot`);
|
|
1214
|
+
}
|
|
1215
|
+
catch (updateError) {
|
|
1216
|
+
errors.push(`Failed to update flow with snapshot references: ${updateError.response?.data?.error?.message || updateError.message}`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
errors.push(`Failed to create snapshot: ${error.response?.data?.error?.message || error.message}`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
// 8. ACTIVATE IF REQUESTED
|
|
1224
|
+
if (auto_activate && errors.length === 0) {
|
|
1225
|
+
console.log('Activating flow...');
|
|
1226
|
+
try {
|
|
1227
|
+
await client.patch(`/api/now/table/${FLOW_TABLES.FLOW}/${flowSysId}`, {
|
|
1228
|
+
active: true,
|
|
1229
|
+
status: 'published'
|
|
1230
|
+
});
|
|
1231
|
+
console.log(`✅ Flow activated`);
|
|
1232
|
+
}
|
|
1233
|
+
catch (error) {
|
|
1234
|
+
errors.push(`Failed to activate: ${error.response?.data?.error?.message || error.message}`);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const instanceUrl = (client.defaults?.baseURL || '').replace('/api/now', '');
|
|
1238
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
1239
|
+
action: 'create',
|
|
1240
|
+
flow: {
|
|
1241
|
+
sys_id: flowSysId,
|
|
1242
|
+
name: flowConfig.name,
|
|
1243
|
+
internal_name: internalName,
|
|
1244
|
+
active: auto_activate && errors.length === 0,
|
|
1245
|
+
url: `${instanceUrl}/sys_hub_flow.do?sys_id=${flowSysId}`,
|
|
1246
|
+
flow_designer_url: `${instanceUrl}/nav_to.do?uri=/flow-designer.do%23/flow/${flowSysId}`,
|
|
1247
|
+
main_snapshot: create_snapshot ? snapshotSysId : undefined,
|
|
1248
|
+
latest_snapshot: create_snapshot ? snapshotSysId : undefined
|
|
1249
|
+
},
|
|
1250
|
+
created_records: createdRecords,
|
|
1251
|
+
version_info: versionInfo,
|
|
1252
|
+
compression_used: useCompression,
|
|
1253
|
+
errors: errors.length > 0 ? errors : undefined
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Update an existing flow
|
|
1258
|
+
*/
|
|
1259
|
+
async function handleUpdate(args, client, versionInfo) {
|
|
1260
|
+
const { flow_id, flow_name, updates } = args;
|
|
1261
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
1262
|
+
if (!flow) {
|
|
1263
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
1264
|
+
}
|
|
1265
|
+
if (!updates || Object.keys(updates).length === 0) {
|
|
1266
|
+
return (0, error_handler_js_1.createErrorResult)('No updates provided');
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
await client.patch(`/api/now/table/${FLOW_TABLES.FLOW}/${flow.sys_id}`, updates);
|
|
1270
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
1271
|
+
action: 'update',
|
|
1272
|
+
flow_id: flow.sys_id,
|
|
1273
|
+
updated_fields: Object.keys(updates)
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to update flow: ${error.response?.data?.error?.message || error.message}`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Delete a flow and all related records
|
|
1282
|
+
*
|
|
1283
|
+
* IMPORTANT: Delete order respects parent-child hierarchy:
|
|
1284
|
+
* 1. Actions (children of logic)
|
|
1285
|
+
* 2. Subflow instances (can be nested in logic)
|
|
1286
|
+
* 3. Logic instances - hierarchically (children before parents)
|
|
1287
|
+
* 4. Triggers
|
|
1288
|
+
* 5. Variables
|
|
1289
|
+
* 6. Snapshots
|
|
1290
|
+
* 7. Flow record (last)
|
|
1291
|
+
*/
|
|
1292
|
+
async function handleDelete(args, client, versionInfo) {
|
|
1293
|
+
const { flow_id, flow_name } = args;
|
|
1294
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
1295
|
+
if (!flow) {
|
|
1296
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
1297
|
+
}
|
|
1298
|
+
const deletedRecords = [];
|
|
1299
|
+
const errors = [];
|
|
1300
|
+
const actionTable = versionInfo.useV2Tables ? FLOW_TABLES.ACTION_V2 : FLOW_TABLES.ACTION_V1;
|
|
1301
|
+
const logicTable = versionInfo.useV2Tables ? FLOW_TABLES.LOGIC_V2 : FLOW_TABLES.LOGIC_V1;
|
|
1302
|
+
const triggerTable = versionInfo.useV2Tables ? FLOW_TABLES.TRIGGER_V2 : FLOW_TABLES.TRIGGER_V1;
|
|
1303
|
+
// Helper function to delete records from a table
|
|
1304
|
+
const deleteFromTable = async (table, query) => {
|
|
1305
|
+
try {
|
|
1306
|
+
const response = await client.get(`/api/now/table/${table}`, {
|
|
1307
|
+
params: { sysparm_query: query, sysparm_fields: 'sys_id' }
|
|
1308
|
+
});
|
|
1309
|
+
if (response.data.result) {
|
|
1310
|
+
for (const record of response.data.result) {
|
|
1311
|
+
try {
|
|
1312
|
+
await client.delete(`/api/now/table/${table}/${record.sys_id}`);
|
|
1313
|
+
deletedRecords.push({ table, sys_id: record.sys_id });
|
|
1314
|
+
}
|
|
1315
|
+
catch (e) {
|
|
1316
|
+
errors.push(`Failed to delete ${table}/${record.sys_id}: ${e.message}`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
catch (e) {
|
|
1322
|
+
// Table might not exist or be empty - this is OK
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
console.log(`Deleting flow: ${flow.name} (${flow.sys_id})`);
|
|
1326
|
+
// 1. DELETE ACTIONS (children of logic instances)
|
|
1327
|
+
console.log('Deleting actions...');
|
|
1328
|
+
await deleteFromTable(actionTable, `flow=${flow.sys_id}`);
|
|
1329
|
+
// 2. DELETE SUBFLOW INSTANCES (can be nested in logic)
|
|
1330
|
+
console.log('Deleting subflow instances...');
|
|
1331
|
+
await deleteFromTable(FLOW_TABLES.SUBFLOW, `flow=${flow.sys_id}`);
|
|
1332
|
+
// 3. DELETE LOGIC INSTANCES - HIERARCHICALLY
|
|
1333
|
+
// First delete children (items WITH parent_logic_instance), then parents
|
|
1334
|
+
console.log('Deleting logic instances (hierarchically)...');
|
|
1335
|
+
try {
|
|
1336
|
+
const logicResponse = await client.get(`/api/now/table/${logicTable}`, {
|
|
1337
|
+
params: {
|
|
1338
|
+
sysparm_query: `flow=${flow.sys_id}`,
|
|
1339
|
+
sysparm_fields: 'sys_id,parent_logic_instance'
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
if (logicResponse.data.result) {
|
|
1343
|
+
const logicItems = logicResponse.data.result;
|
|
1344
|
+
// Separate into children (with parent) and parents (without parent)
|
|
1345
|
+
const children = logicItems.filter((item) => item.parent_logic_instance);
|
|
1346
|
+
const parents = logicItems.filter((item) => !item.parent_logic_instance);
|
|
1347
|
+
// Build hierarchy depth map for proper ordering
|
|
1348
|
+
const depthMap = new Map();
|
|
1349
|
+
const getDepth = (item, visited = new Set()) => {
|
|
1350
|
+
if (!item.parent_logic_instance)
|
|
1351
|
+
return 0;
|
|
1352
|
+
if (visited.has(item.sys_id))
|
|
1353
|
+
return 0; // Prevent infinite loops
|
|
1354
|
+
visited.add(item.sys_id);
|
|
1355
|
+
const parent = logicItems.find((p) => p.sys_id === item.parent_logic_instance);
|
|
1356
|
+
if (!parent)
|
|
1357
|
+
return 1;
|
|
1358
|
+
return 1 + getDepth(parent, visited);
|
|
1359
|
+
};
|
|
1360
|
+
// Calculate depth for each item
|
|
1361
|
+
for (const item of logicItems) {
|
|
1362
|
+
depthMap.set(item.sys_id, getDepth(item));
|
|
1363
|
+
}
|
|
1364
|
+
// Sort by depth descending (deepest children first)
|
|
1365
|
+
const sortedLogic = [...logicItems].sort((a, b) => {
|
|
1366
|
+
const depthA = depthMap.get(a.sys_id) || 0;
|
|
1367
|
+
const depthB = depthMap.get(b.sys_id) || 0;
|
|
1368
|
+
return depthB - depthA; // Deepest first
|
|
1369
|
+
});
|
|
1370
|
+
// Delete in sorted order (deepest children first)
|
|
1371
|
+
for (const logic of sortedLogic) {
|
|
1372
|
+
try {
|
|
1373
|
+
await client.delete(`/api/now/table/${logicTable}/${logic.sys_id}`);
|
|
1374
|
+
deletedRecords.push({ table: logicTable, sys_id: logic.sys_id });
|
|
1375
|
+
}
|
|
1376
|
+
catch (e) {
|
|
1377
|
+
errors.push(`Failed to delete logic ${logic.sys_id}: ${e.message}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
catch (e) {
|
|
1383
|
+
errors.push(`Failed to fetch logic instances: ${e.message}`);
|
|
1384
|
+
}
|
|
1385
|
+
// 4. DELETE TRIGGERS
|
|
1386
|
+
console.log('Deleting triggers...');
|
|
1387
|
+
await deleteFromTable(triggerTable, `flow=${flow.sys_id}`);
|
|
1388
|
+
// 5. DELETE VARIABLES
|
|
1389
|
+
// Note: sys_hub_flow_variable uses 'model' field (not 'flow')
|
|
1390
|
+
console.log('Deleting variables...');
|
|
1391
|
+
await deleteFromTable(FLOW_TABLES.VARIABLE, `model=${flow.sys_id}`);
|
|
1392
|
+
// 6. DELETE SNAPSHOTS
|
|
1393
|
+
console.log('Deleting snapshots...');
|
|
1394
|
+
await deleteFromTable(FLOW_TABLES.SNAPSHOT, `flow=${flow.sys_id}`);
|
|
1395
|
+
// 7. DELETE FLOW RECORD (last)
|
|
1396
|
+
console.log('Deleting flow record...');
|
|
1397
|
+
try {
|
|
1398
|
+
await client.delete(`/api/now/table/${FLOW_TABLES.FLOW}/${flow.sys_id}`);
|
|
1399
|
+
deletedRecords.push({ table: FLOW_TABLES.FLOW, sys_id: flow.sys_id });
|
|
1400
|
+
console.log(`✅ Flow deleted: ${flow.sys_id}`);
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to delete flow: ${error.response?.data?.error?.message || error.message}`);
|
|
1404
|
+
}
|
|
1405
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
1406
|
+
action: 'delete',
|
|
1407
|
+
flow_id: flow.sys_id,
|
|
1408
|
+
flow_name: flow.name,
|
|
1409
|
+
deleted_records: deletedRecords,
|
|
1410
|
+
delete_order: [
|
|
1411
|
+
'actions',
|
|
1412
|
+
'subflow_instances',
|
|
1413
|
+
'logic_instances (hierarchical)',
|
|
1414
|
+
'triggers',
|
|
1415
|
+
'variables',
|
|
1416
|
+
'snapshots',
|
|
1417
|
+
'flow'
|
|
1418
|
+
],
|
|
1419
|
+
errors: errors.length > 0 ? errors : undefined
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Clone a flow - FULL IMPLEMENTATION
|
|
1424
|
+
* Copies ALL components: flow, triggers, actions, logic, variables, subflows
|
|
1425
|
+
* Maintains parent-child relationships and remaps sys_ids
|
|
1426
|
+
*/
|
|
1427
|
+
async function handleClone(args, client, versionInfo) {
|
|
1428
|
+
const { flow_id, flow_name, new_name, use_compression, create_snapshot = true } = args;
|
|
1429
|
+
const sourceFlow = await findFlow(client, flow_id, flow_name);
|
|
1430
|
+
if (!sourceFlow) {
|
|
1431
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
1432
|
+
}
|
|
1433
|
+
if (!new_name) {
|
|
1434
|
+
return (0, error_handler_js_1.createErrorResult)('new_name is required for clone action');
|
|
1435
|
+
}
|
|
1436
|
+
const useCompression = use_compression ?? versionInfo.useCompression;
|
|
1437
|
+
const triggerTable = versionInfo.useV2Tables ? FLOW_TABLES.TRIGGER_V2 : FLOW_TABLES.TRIGGER_V1;
|
|
1438
|
+
const actionTable = versionInfo.useV2Tables ? FLOW_TABLES.ACTION_V2 : FLOW_TABLES.ACTION_V1;
|
|
1439
|
+
const logicTable = versionInfo.useV2Tables ? FLOW_TABLES.LOGIC_V2 : FLOW_TABLES.LOGIC_V1;
|
|
1440
|
+
// Map old sys_ids to new sys_ids for remapping references
|
|
1441
|
+
const sysIdMap = new Map();
|
|
1442
|
+
const createdRecords = [];
|
|
1443
|
+
const errors = [];
|
|
1444
|
+
// Generate new IDs
|
|
1445
|
+
const newFlowId = generateSysId();
|
|
1446
|
+
const newInternalName = generateInternalName(new_name);
|
|
1447
|
+
sysIdMap.set(sourceFlow.sys_id, newFlowId);
|
|
1448
|
+
console.log(`Cloning flow: ${sourceFlow.name} -> ${new_name}`);
|
|
1449
|
+
// ==================== 1. CLONE FLOW RECORD ====================
|
|
1450
|
+
const newFlowData = {
|
|
1451
|
+
sys_id: newFlowId,
|
|
1452
|
+
name: new_name,
|
|
1453
|
+
internal_name: newInternalName,
|
|
1454
|
+
description: `Cloned from: ${sourceFlow.name}\n\n${sourceFlow.description || ''}`,
|
|
1455
|
+
active: false,
|
|
1456
|
+
sys_class_name: 'sys_hub_flow',
|
|
1457
|
+
type: sourceFlow.type || 'flow',
|
|
1458
|
+
flow_type: sourceFlow.flow_type || 'flow',
|
|
1459
|
+
status: 'draft',
|
|
1460
|
+
source_ui: 'flow_designer',
|
|
1461
|
+
access: sourceFlow.access || 'public',
|
|
1462
|
+
run_as: sourceFlow.run_as || 'system'
|
|
1463
|
+
};
|
|
1464
|
+
if (sourceFlow.sys_scope) {
|
|
1465
|
+
newFlowData.sys_scope = sourceFlow.sys_scope;
|
|
1466
|
+
}
|
|
1467
|
+
if (sourceFlow.run_as_user) {
|
|
1468
|
+
newFlowData.run_as_user = sourceFlow.run_as_user;
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
await client.post(`/api/now/table/${FLOW_TABLES.FLOW}`, newFlowData);
|
|
1472
|
+
createdRecords.push({ table: FLOW_TABLES.FLOW, sys_id: newFlowId, name: new_name, type: 'flow' });
|
|
1473
|
+
console.log(`✅ Cloned flow record: ${newFlowId}`);
|
|
1474
|
+
}
|
|
1475
|
+
catch (error) {
|
|
1476
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to create cloned flow: ${error.response?.data?.error?.message || error.message}`);
|
|
1477
|
+
}
|
|
1478
|
+
// ==================== 2. CLONE VARIABLES ====================
|
|
1479
|
+
// Note: sys_hub_flow_variable uses 'model' field (not 'flow')
|
|
1480
|
+
try {
|
|
1481
|
+
const variablesResponse = await client.get(`/api/now/table/${FLOW_TABLES.VARIABLE}`, {
|
|
1482
|
+
params: { sysparm_query: `model=${sourceFlow.sys_id}`, sysparm_orderby: 'order' }
|
|
1483
|
+
});
|
|
1484
|
+
if (variablesResponse.data.result) {
|
|
1485
|
+
for (const variable of variablesResponse.data.result) {
|
|
1486
|
+
const newVariableId = generateSysId();
|
|
1487
|
+
sysIdMap.set(variable.sys_id, newVariableId);
|
|
1488
|
+
// Note: sys_hub_flow_variable uses 'model' field (not 'flow')
|
|
1489
|
+
const newVariable = {
|
|
1490
|
+
sys_id: newVariableId,
|
|
1491
|
+
model: newFlowId,
|
|
1492
|
+
name: variable.name,
|
|
1493
|
+
label: variable.label,
|
|
1494
|
+
description: variable.description,
|
|
1495
|
+
type: variable.type,
|
|
1496
|
+
direction: variable.direction,
|
|
1497
|
+
mandatory: variable.mandatory,
|
|
1498
|
+
order: variable.order,
|
|
1499
|
+
default_value: variable.default_value,
|
|
1500
|
+
reference_table: variable.reference_table
|
|
1501
|
+
};
|
|
1502
|
+
try {
|
|
1503
|
+
await client.post(`/api/now/table/${FLOW_TABLES.VARIABLE}`, newVariable);
|
|
1504
|
+
createdRecords.push({
|
|
1505
|
+
table: FLOW_TABLES.VARIABLE,
|
|
1506
|
+
sys_id: newVariableId,
|
|
1507
|
+
name: variable.name,
|
|
1508
|
+
type: 'variable'
|
|
1509
|
+
});
|
|
1510
|
+
console.log(`✅ Cloned variable: ${variable.name}`);
|
|
1511
|
+
}
|
|
1512
|
+
catch (e) {
|
|
1513
|
+
errors.push(`Failed to clone variable ${variable.name}: ${e.message}`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
catch (e) {
|
|
1519
|
+
errors.push(`Failed to fetch variables: ${e.message}`);
|
|
1520
|
+
}
|
|
1521
|
+
// ==================== 3. CLONE TRIGGERS ====================
|
|
1522
|
+
try {
|
|
1523
|
+
const triggersResponse = await client.get(`/api/now/table/${triggerTable}`, {
|
|
1524
|
+
params: { sysparm_query: `flow=${sourceFlow.sys_id}` }
|
|
1525
|
+
});
|
|
1526
|
+
if (triggersResponse.data.result) {
|
|
1527
|
+
for (const trigger of triggersResponse.data.result) {
|
|
1528
|
+
const newTriggerId = generateSysId();
|
|
1529
|
+
sysIdMap.set(trigger.sys_id, newTriggerId);
|
|
1530
|
+
// Decompress and remap values if needed
|
|
1531
|
+
let newValues = trigger.values;
|
|
1532
|
+
if (newValues) {
|
|
1533
|
+
if (isCompressed(newValues)) {
|
|
1534
|
+
const decompressed = decompressValue(newValues);
|
|
1535
|
+
const remapped = remapValuesForClone(decompressed, sysIdMap, sourceFlow.sys_id, newFlowId);
|
|
1536
|
+
newValues = useCompression ? compressValue(remapped) : remapped;
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
newValues = remapValuesForClone(newValues, sysIdMap, sourceFlow.sys_id, newFlowId);
|
|
1540
|
+
if (useCompression)
|
|
1541
|
+
newValues = compressValue(newValues);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
const newTrigger = {
|
|
1545
|
+
sys_id: newTriggerId,
|
|
1546
|
+
flow: newFlowId,
|
|
1547
|
+
trigger_type: trigger.trigger_type,
|
|
1548
|
+
table: trigger.table,
|
|
1549
|
+
active: trigger.active,
|
|
1550
|
+
order: trigger.order,
|
|
1551
|
+
values: newValues
|
|
1552
|
+
};
|
|
1553
|
+
try {
|
|
1554
|
+
await client.post(`/api/now/table/${triggerTable}`, newTrigger);
|
|
1555
|
+
createdRecords.push({
|
|
1556
|
+
table: triggerTable,
|
|
1557
|
+
sys_id: newTriggerId,
|
|
1558
|
+
type: 'trigger',
|
|
1559
|
+
trigger_type: trigger.trigger_type
|
|
1560
|
+
});
|
|
1561
|
+
console.log(`✅ Cloned trigger: ${newTriggerId}`);
|
|
1562
|
+
}
|
|
1563
|
+
catch (e) {
|
|
1564
|
+
errors.push(`Failed to clone trigger: ${e.message}`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
catch (e) {
|
|
1570
|
+
errors.push(`Failed to fetch triggers: ${e.message}`);
|
|
1571
|
+
}
|
|
1572
|
+
// ==================== 4. CLONE LOGIC (CONDITIONS/LOOPS) ====================
|
|
1573
|
+
// First pass: create all logic instances and map sys_ids
|
|
1574
|
+
const logicItems = [];
|
|
1575
|
+
try {
|
|
1576
|
+
const logicResponse = await client.get(`/api/now/table/${logicTable}`, {
|
|
1577
|
+
params: { sysparm_query: `flow=${sourceFlow.sys_id}`, sysparm_orderby: 'order' }
|
|
1578
|
+
});
|
|
1579
|
+
if (logicResponse.data.result) {
|
|
1580
|
+
// First pass: generate new IDs for all logic items
|
|
1581
|
+
for (const logic of logicResponse.data.result) {
|
|
1582
|
+
const newLogicId = generateSysId();
|
|
1583
|
+
sysIdMap.set(logic.sys_id, newLogicId);
|
|
1584
|
+
logicItems.push({ ...logic, newSysId: newLogicId });
|
|
1585
|
+
}
|
|
1586
|
+
// Second pass: create logic items with remapped parent references
|
|
1587
|
+
for (const logic of logicItems) {
|
|
1588
|
+
// Decompress and remap values
|
|
1589
|
+
let newValues = logic.values;
|
|
1590
|
+
if (newValues) {
|
|
1591
|
+
if (isCompressed(newValues)) {
|
|
1592
|
+
const decompressed = decompressValue(newValues);
|
|
1593
|
+
const remapped = remapValuesForClone(decompressed, sysIdMap, sourceFlow.sys_id, newFlowId);
|
|
1594
|
+
newValues = useCompression ? compressValue(remapped) : remapped;
|
|
1595
|
+
}
|
|
1596
|
+
else {
|
|
1597
|
+
newValues = remapValuesForClone(newValues, sysIdMap, sourceFlow.sys_id, newFlowId);
|
|
1598
|
+
if (useCompression)
|
|
1599
|
+
newValues = compressValue(newValues);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
// Remap parent_logic_instance if it exists
|
|
1603
|
+
let newParentLogic = null;
|
|
1604
|
+
if (logic.parent_logic_instance) {
|
|
1605
|
+
newParentLogic = sysIdMap.get(logic.parent_logic_instance) || logic.parent_logic_instance;
|
|
1606
|
+
}
|
|
1607
|
+
const newLogic = {
|
|
1608
|
+
sys_id: logic.newSysId,
|
|
1609
|
+
flow: newFlowId,
|
|
1610
|
+
name: logic.name,
|
|
1611
|
+
logic_type: logic.logic_type,
|
|
1612
|
+
order: logic.order,
|
|
1613
|
+
active: logic.active,
|
|
1614
|
+
values: newValues,
|
|
1615
|
+
condition: logic.condition
|
|
1616
|
+
};
|
|
1617
|
+
if (newParentLogic) {
|
|
1618
|
+
newLogic.parent_logic_instance = newParentLogic;
|
|
1619
|
+
}
|
|
1620
|
+
if (logic.branch) {
|
|
1621
|
+
newLogic.branch = logic.branch;
|
|
1622
|
+
}
|
|
1623
|
+
try {
|
|
1624
|
+
await client.post(`/api/now/table/${logicTable}`, newLogic);
|
|
1625
|
+
createdRecords.push({
|
|
1626
|
+
table: logicTable,
|
|
1627
|
+
sys_id: logic.newSysId,
|
|
1628
|
+
name: logic.name,
|
|
1629
|
+
type: 'logic',
|
|
1630
|
+
logic_type: logic.logic_type
|
|
1631
|
+
});
|
|
1632
|
+
console.log(`✅ Cloned logic: ${logic.name} (${logic.logic_type})`);
|
|
1633
|
+
}
|
|
1634
|
+
catch (e) {
|
|
1635
|
+
errors.push(`Failed to clone logic ${logic.name}: ${e.message}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
catch (e) {
|
|
1641
|
+
errors.push(`Failed to fetch logic: ${e.message}`);
|
|
1642
|
+
}
|
|
1643
|
+
// ==================== 5. CLONE ACTIONS ====================
|
|
1644
|
+
try {
|
|
1645
|
+
const actionsResponse = await client.get(`/api/now/table/${actionTable}`, {
|
|
1646
|
+
params: { sysparm_query: `flow=${sourceFlow.sys_id}`, sysparm_orderby: 'order' }
|
|
1647
|
+
});
|
|
1648
|
+
if (actionsResponse.data.result) {
|
|
1649
|
+
// First pass: generate new IDs
|
|
1650
|
+
const actionItems = [];
|
|
1651
|
+
for (const action of actionsResponse.data.result) {
|
|
1652
|
+
const newActionId = generateSysId();
|
|
1653
|
+
sysIdMap.set(action.sys_id, newActionId);
|
|
1654
|
+
actionItems.push({ ...action, newSysId: newActionId });
|
|
1655
|
+
}
|
|
1656
|
+
// Second pass: create actions with remapped references
|
|
1657
|
+
for (const action of actionItems) {
|
|
1658
|
+
// Decompress and remap values
|
|
1659
|
+
let newValues = action.values;
|
|
1660
|
+
if (newValues) {
|
|
1661
|
+
if (isCompressed(newValues)) {
|
|
1662
|
+
const decompressed = decompressValue(newValues);
|
|
1663
|
+
const remapped = remapValuesForClone(decompressed, sysIdMap, sourceFlow.sys_id, newFlowId);
|
|
1664
|
+
newValues = useCompression ? compressValue(remapped) : remapped;
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
newValues = remapValuesForClone(newValues, sysIdMap, sourceFlow.sys_id, newFlowId);
|
|
1668
|
+
if (useCompression)
|
|
1669
|
+
newValues = compressValue(newValues);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
// Remap parent_logic_instance if it exists
|
|
1673
|
+
let newParentLogic = null;
|
|
1674
|
+
if (action.parent_logic_instance) {
|
|
1675
|
+
newParentLogic = sysIdMap.get(action.parent_logic_instance) || action.parent_logic_instance;
|
|
1676
|
+
}
|
|
1677
|
+
const newAction = {
|
|
1678
|
+
sys_id: action.newSysId,
|
|
1679
|
+
flow: newFlowId,
|
|
1680
|
+
action_type: action.action_type,
|
|
1681
|
+
name: action.name,
|
|
1682
|
+
description: action.description,
|
|
1683
|
+
order: action.order,
|
|
1684
|
+
active: action.active,
|
|
1685
|
+
values: newValues
|
|
1686
|
+
};
|
|
1687
|
+
if (newParentLogic) {
|
|
1688
|
+
newAction.parent_logic_instance = newParentLogic;
|
|
1689
|
+
}
|
|
1690
|
+
if (action.branch) {
|
|
1691
|
+
newAction.branch = action.branch;
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
await client.post(`/api/now/table/${actionTable}`, newAction);
|
|
1695
|
+
createdRecords.push({
|
|
1696
|
+
table: actionTable,
|
|
1697
|
+
sys_id: action.newSysId,
|
|
1698
|
+
name: action.name,
|
|
1699
|
+
type: 'action',
|
|
1700
|
+
action_type: action.action_type
|
|
1701
|
+
});
|
|
1702
|
+
console.log(`✅ Cloned action: ${action.name}`);
|
|
1703
|
+
}
|
|
1704
|
+
catch (e) {
|
|
1705
|
+
errors.push(`Failed to clone action ${action.name}: ${e.message}`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
catch (e) {
|
|
1711
|
+
errors.push(`Failed to fetch actions: ${e.message}`);
|
|
1712
|
+
}
|
|
1713
|
+
// ==================== 6. CLONE SUBFLOW INSTANCES ====================
|
|
1714
|
+
try {
|
|
1715
|
+
const subflowsResponse = await client.get(`/api/now/table/${FLOW_TABLES.SUBFLOW}`, {
|
|
1716
|
+
params: { sysparm_query: `flow=${sourceFlow.sys_id}` }
|
|
1717
|
+
});
|
|
1718
|
+
if (subflowsResponse.data.result) {
|
|
1719
|
+
for (const subflow of subflowsResponse.data.result) {
|
|
1720
|
+
const newSubflowId = generateSysId();
|
|
1721
|
+
sysIdMap.set(subflow.sys_id, newSubflowId);
|
|
1722
|
+
// Decompress and remap values
|
|
1723
|
+
let newValues = subflow.values;
|
|
1724
|
+
if (newValues) {
|
|
1725
|
+
if (isCompressed(newValues)) {
|
|
1726
|
+
const decompressed = decompressValue(newValues);
|
|
1727
|
+
const remapped = remapValuesForClone(decompressed, sysIdMap, sourceFlow.sys_id, newFlowId);
|
|
1728
|
+
newValues = useCompression ? compressValue(remapped) : remapped;
|
|
1729
|
+
}
|
|
1730
|
+
else {
|
|
1731
|
+
newValues = remapValuesForClone(newValues, sysIdMap, sourceFlow.sys_id, newFlowId);
|
|
1732
|
+
if (useCompression)
|
|
1733
|
+
newValues = compressValue(newValues);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
// Remap parent_logic_instance if it exists
|
|
1737
|
+
let newParentLogic = null;
|
|
1738
|
+
if (subflow.parent_logic_instance) {
|
|
1739
|
+
newParentLogic = sysIdMap.get(subflow.parent_logic_instance) || subflow.parent_logic_instance;
|
|
1740
|
+
}
|
|
1741
|
+
const newSubflow = {
|
|
1742
|
+
sys_id: newSubflowId,
|
|
1743
|
+
flow: newFlowId,
|
|
1744
|
+
sub_flow: subflow.sub_flow, // Reference to the actual subflow definition
|
|
1745
|
+
name: subflow.name,
|
|
1746
|
+
description: subflow.description,
|
|
1747
|
+
order: subflow.order,
|
|
1748
|
+
active: subflow.active,
|
|
1749
|
+
values: newValues
|
|
1750
|
+
};
|
|
1751
|
+
if (newParentLogic) {
|
|
1752
|
+
newSubflow.parent_logic_instance = newParentLogic;
|
|
1753
|
+
}
|
|
1754
|
+
if (subflow.branch) {
|
|
1755
|
+
newSubflow.branch = subflow.branch;
|
|
1756
|
+
}
|
|
1757
|
+
try {
|
|
1758
|
+
await client.post(`/api/now/table/${FLOW_TABLES.SUBFLOW}`, newSubflow);
|
|
1759
|
+
createdRecords.push({
|
|
1760
|
+
table: FLOW_TABLES.SUBFLOW,
|
|
1761
|
+
sys_id: newSubflowId,
|
|
1762
|
+
name: subflow.name,
|
|
1763
|
+
type: 'subflow_instance',
|
|
1764
|
+
sub_flow: subflow.sub_flow
|
|
1765
|
+
});
|
|
1766
|
+
console.log(`✅ Cloned subflow instance: ${subflow.name}`);
|
|
1767
|
+
}
|
|
1768
|
+
catch (e) {
|
|
1769
|
+
errors.push(`Failed to clone subflow instance ${subflow.name}: ${e.message}`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
catch (e) {
|
|
1775
|
+
// Subflow table might not exist or be empty
|
|
1776
|
+
}
|
|
1777
|
+
// ==================== 7. CREATE SNAPSHOT ====================
|
|
1778
|
+
let clonedSnapshotId;
|
|
1779
|
+
if (create_snapshot) {
|
|
1780
|
+
clonedSnapshotId = generateSysId();
|
|
1781
|
+
// Build flow definition for snapshot
|
|
1782
|
+
const flowDefinition = {
|
|
1783
|
+
sys_id: newFlowId,
|
|
1784
|
+
name: new_name,
|
|
1785
|
+
internal_name: newInternalName,
|
|
1786
|
+
cloned_from: sourceFlow.sys_id,
|
|
1787
|
+
// Include created components
|
|
1788
|
+
components: createdRecords.map(r => ({
|
|
1789
|
+
table: r.table,
|
|
1790
|
+
sys_id: r.sys_id,
|
|
1791
|
+
name: r.name,
|
|
1792
|
+
type: r.type
|
|
1793
|
+
}))
|
|
1794
|
+
};
|
|
1795
|
+
const snapshotData = {
|
|
1796
|
+
sys_id: clonedSnapshotId,
|
|
1797
|
+
flow: newFlowId,
|
|
1798
|
+
name: `${new_name} - Initial (Cloned)`,
|
|
1799
|
+
version: '1.0.0',
|
|
1800
|
+
active: true,
|
|
1801
|
+
published: false,
|
|
1802
|
+
// Include flow definition
|
|
1803
|
+
definition: useCompression
|
|
1804
|
+
? compressValue(flowDefinition, versionInfo.useCompressionHeader)
|
|
1805
|
+
: JSON.stringify(flowDefinition)
|
|
1806
|
+
};
|
|
1807
|
+
try {
|
|
1808
|
+
await client.post(`/api/now/table/${FLOW_TABLES.SNAPSHOT}`, snapshotData);
|
|
1809
|
+
createdRecords.push({
|
|
1810
|
+
table: FLOW_TABLES.SNAPSHOT,
|
|
1811
|
+
sys_id: clonedSnapshotId,
|
|
1812
|
+
name: snapshotData.name,
|
|
1813
|
+
type: 'snapshot'
|
|
1814
|
+
});
|
|
1815
|
+
console.log(`✅ Created snapshot: ${clonedSnapshotId}`);
|
|
1816
|
+
// *** CRITICAL FIX ***
|
|
1817
|
+
// Update the cloned flow with snapshot references
|
|
1818
|
+
console.log('Updating cloned flow with snapshot references...');
|
|
1819
|
+
try {
|
|
1820
|
+
await client.patch(`/api/now/table/${FLOW_TABLES.FLOW}/${newFlowId}`, {
|
|
1821
|
+
main_snapshot: clonedSnapshotId,
|
|
1822
|
+
latest_snapshot: clonedSnapshotId
|
|
1823
|
+
});
|
|
1824
|
+
console.log(`✅ Cloned flow updated with main_snapshot and latest_snapshot`);
|
|
1825
|
+
}
|
|
1826
|
+
catch (updateError) {
|
|
1827
|
+
errors.push(`Failed to update cloned flow with snapshot references: ${updateError.message}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
catch (e) {
|
|
1831
|
+
errors.push(`Failed to create snapshot: ${e.message}`);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
// ==================== BUILD RESULT ====================
|
|
1835
|
+
const instanceUrl = (client.defaults?.baseURL || '').replace('/api/now', '');
|
|
1836
|
+
// Summary statistics
|
|
1837
|
+
const summary = {
|
|
1838
|
+
flow: 1,
|
|
1839
|
+
variables: createdRecords.filter(r => r.type === 'variable').length,
|
|
1840
|
+
triggers: createdRecords.filter(r => r.type === 'trigger').length,
|
|
1841
|
+
logic: createdRecords.filter(r => r.type === 'logic').length,
|
|
1842
|
+
actions: createdRecords.filter(r => r.type === 'action').length,
|
|
1843
|
+
subflow_instances: createdRecords.filter(r => r.type === 'subflow_instance').length,
|
|
1844
|
+
snapshots: createdRecords.filter(r => r.type === 'snapshot').length,
|
|
1845
|
+
total: createdRecords.length
|
|
1846
|
+
};
|
|
1847
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
1848
|
+
action: 'clone',
|
|
1849
|
+
source_flow: {
|
|
1850
|
+
sys_id: sourceFlow.sys_id,
|
|
1851
|
+
name: sourceFlow.name
|
|
1852
|
+
},
|
|
1853
|
+
cloned_flow: {
|
|
1854
|
+
sys_id: newFlowId,
|
|
1855
|
+
name: new_name,
|
|
1856
|
+
internal_name: newInternalName,
|
|
1857
|
+
active: false,
|
|
1858
|
+
url: `${instanceUrl}/sys_hub_flow.do?sys_id=${newFlowId}`,
|
|
1859
|
+
flow_designer_url: `${instanceUrl}/nav_to.do?uri=/flow-designer.do%23/flow/${newFlowId}`,
|
|
1860
|
+
main_snapshot: clonedSnapshotId,
|
|
1861
|
+
latest_snapshot: clonedSnapshotId
|
|
1862
|
+
},
|
|
1863
|
+
summary,
|
|
1864
|
+
created_records: createdRecords,
|
|
1865
|
+
sys_id_mapping: Object.fromEntries(sysIdMap),
|
|
1866
|
+
version_info: versionInfo,
|
|
1867
|
+
compression_used: useCompression,
|
|
1868
|
+
errors: errors.length > 0 ? errors : undefined
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Activate a flow
|
|
1873
|
+
*/
|
|
1874
|
+
async function handleActivate(args, client) {
|
|
1875
|
+
const { flow_id, flow_name } = args;
|
|
1876
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
1877
|
+
if (!flow) {
|
|
1878
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
1879
|
+
}
|
|
1880
|
+
try {
|
|
1881
|
+
await client.patch(`/api/now/table/${FLOW_TABLES.FLOW}/${flow.sys_id}`, {
|
|
1882
|
+
active: true,
|
|
1883
|
+
status: 'published'
|
|
1884
|
+
});
|
|
1885
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
1886
|
+
action: 'activate',
|
|
1887
|
+
flow_id: flow.sys_id,
|
|
1888
|
+
flow_name: flow.name,
|
|
1889
|
+
active: true
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
catch (error) {
|
|
1893
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to activate: ${error.response?.data?.error?.message || error.message}`);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Deactivate a flow
|
|
1898
|
+
*/
|
|
1899
|
+
async function handleDeactivate(args, client) {
|
|
1900
|
+
const { flow_id, flow_name } = args;
|
|
1901
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
1902
|
+
if (!flow) {
|
|
1903
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
1904
|
+
}
|
|
1905
|
+
try {
|
|
1906
|
+
await client.patch(`/api/now/table/${FLOW_TABLES.FLOW}/${flow.sys_id}`, {
|
|
1907
|
+
active: false
|
|
1908
|
+
});
|
|
1909
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
1910
|
+
action: 'deactivate',
|
|
1911
|
+
flow_id: flow.sys_id,
|
|
1912
|
+
flow_name: flow.name,
|
|
1913
|
+
active: false
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
catch (error) {
|
|
1917
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to deactivate: ${error.response?.data?.error?.message || error.message}`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Add an action to an existing flow
|
|
1922
|
+
*/
|
|
1923
|
+
async function handleAddAction(args, client, versionInfo) {
|
|
1924
|
+
const { flow_id, flow_name, action_config, use_compression } = args;
|
|
1925
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
1926
|
+
if (!flow) {
|
|
1927
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
1928
|
+
}
|
|
1929
|
+
if (!action_config?.type) {
|
|
1930
|
+
return (0, error_handler_js_1.createErrorResult)('action_config.type is required');
|
|
1931
|
+
}
|
|
1932
|
+
const useCompression = use_compression ?? versionInfo.useCompression;
|
|
1933
|
+
const actionTable = versionInfo.useV2Tables ? FLOW_TABLES.ACTION_V2 : FLOW_TABLES.ACTION_V1;
|
|
1934
|
+
const actionSysId = generateSysId();
|
|
1935
|
+
const actionType = await getActionTypeSysId(client, action_config.type);
|
|
1936
|
+
if (!actionType) {
|
|
1937
|
+
return (0, error_handler_js_1.createErrorResult)(`Action type not found: ${action_config.type}`);
|
|
1938
|
+
}
|
|
1939
|
+
const maxOrder = await getMaxOrder(client, flow.sys_id, actionTable);
|
|
1940
|
+
const actionData = {
|
|
1941
|
+
sys_id: actionSysId,
|
|
1942
|
+
flow: flow.sys_id,
|
|
1943
|
+
action_type: actionType.sys_id,
|
|
1944
|
+
name: action_config.name || actionType.name,
|
|
1945
|
+
description: action_config.description || '',
|
|
1946
|
+
order: action_config.order ?? (maxOrder + 100),
|
|
1947
|
+
active: true
|
|
1948
|
+
};
|
|
1949
|
+
if (action_config.parent_logic_instance) {
|
|
1950
|
+
actionData.parent_logic_instance = action_config.parent_logic_instance;
|
|
1951
|
+
}
|
|
1952
|
+
if (action_config.branch) {
|
|
1953
|
+
actionData.branch = action_config.branch;
|
|
1954
|
+
}
|
|
1955
|
+
if (action_config.inputs) {
|
|
1956
|
+
actionData.values = buildActionValues(action_config.inputs, useCompression, versionInfo.useCompressionHeader);
|
|
1957
|
+
}
|
|
1958
|
+
try {
|
|
1959
|
+
await client.post(`/api/now/table/${actionTable}`, actionData);
|
|
1960
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
1961
|
+
action: 'add_action',
|
|
1962
|
+
flow_id: flow.sys_id,
|
|
1963
|
+
added_action: {
|
|
1964
|
+
sys_id: actionSysId,
|
|
1965
|
+
name: action_config.name || actionType.name,
|
|
1966
|
+
type: action_config.type,
|
|
1967
|
+
order: actionData.order
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
catch (error) {
|
|
1972
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to add action: ${error.response?.data?.error?.message || error.message}`);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Remove an action from a flow
|
|
1977
|
+
*/
|
|
1978
|
+
async function handleRemoveAction(args, client, versionInfo) {
|
|
1979
|
+
const { action_id } = args;
|
|
1980
|
+
if (!action_id) {
|
|
1981
|
+
return (0, error_handler_js_1.createErrorResult)('action_id is required');
|
|
1982
|
+
}
|
|
1983
|
+
const actionTable = versionInfo.useV2Tables ? FLOW_TABLES.ACTION_V2 : FLOW_TABLES.ACTION_V1;
|
|
1984
|
+
try {
|
|
1985
|
+
await client.delete(`/api/now/table/${actionTable}/${action_id}`);
|
|
1986
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
1987
|
+
action: 'remove_action',
|
|
1988
|
+
action_id,
|
|
1989
|
+
removed: true
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
catch (error) {
|
|
1993
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to remove action: ${error.response?.data?.error?.message || error.message}`);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Add a condition to an existing flow
|
|
1998
|
+
*/
|
|
1999
|
+
async function handleAddCondition(args, client, versionInfo) {
|
|
2000
|
+
const { flow_id, flow_name, condition_config, use_compression } = args;
|
|
2001
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
2002
|
+
if (!flow) {
|
|
2003
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
2004
|
+
}
|
|
2005
|
+
if (!condition_config?.condition) {
|
|
2006
|
+
return (0, error_handler_js_1.createErrorResult)('condition_config.condition is required');
|
|
2007
|
+
}
|
|
2008
|
+
const useCompression = use_compression ?? versionInfo.useCompression;
|
|
2009
|
+
const logicTable = versionInfo.useV2Tables ? FLOW_TABLES.LOGIC_V2 : FLOW_TABLES.LOGIC_V1;
|
|
2010
|
+
const conditionSysId = generateSysId();
|
|
2011
|
+
const maxOrder = await getMaxOrder(client, flow.sys_id, logicTable);
|
|
2012
|
+
const conditionData = {
|
|
2013
|
+
sys_id: conditionSysId,
|
|
2014
|
+
flow: flow.sys_id,
|
|
2015
|
+
name: condition_config.name || 'If',
|
|
2016
|
+
logic_type: 'if',
|
|
2017
|
+
order: condition_config.order ?? (maxOrder + 100),
|
|
2018
|
+
active: true
|
|
2019
|
+
};
|
|
2020
|
+
const conditionValues = { condition: { value: condition_config.condition, valueType: 'string' } };
|
|
2021
|
+
conditionData.values = useCompression ? compressValue(conditionValues) : JSON.stringify(conditionValues);
|
|
2022
|
+
if (condition_config.parent_logic_instance) {
|
|
2023
|
+
conditionData.parent_logic_instance = condition_config.parent_logic_instance;
|
|
2024
|
+
}
|
|
2025
|
+
try {
|
|
2026
|
+
await client.post(`/api/now/table/${logicTable}`, conditionData);
|
|
2027
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
2028
|
+
action: 'add_condition',
|
|
2029
|
+
flow_id: flow.sys_id,
|
|
2030
|
+
condition: {
|
|
2031
|
+
sys_id: conditionSysId,
|
|
2032
|
+
name: condition_config.name || 'If',
|
|
2033
|
+
order: conditionData.order
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
catch (error) {
|
|
2038
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to add condition: ${error.response?.data?.error?.message || error.message}`);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Add a loop to an existing flow
|
|
2043
|
+
*/
|
|
2044
|
+
async function handleAddLoop(args, client, versionInfo) {
|
|
2045
|
+
const { flow_id, flow_name, loop_config, use_compression } = args;
|
|
2046
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
2047
|
+
if (!flow) {
|
|
2048
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
2049
|
+
}
|
|
2050
|
+
if (!loop_config?.type) {
|
|
2051
|
+
return (0, error_handler_js_1.createErrorResult)('loop_config.type is required');
|
|
2052
|
+
}
|
|
2053
|
+
const useCompression = use_compression ?? versionInfo.useCompression;
|
|
2054
|
+
const logicTable = versionInfo.useV2Tables ? FLOW_TABLES.LOGIC_V2 : FLOW_TABLES.LOGIC_V1;
|
|
2055
|
+
const loopSysId = generateSysId();
|
|
2056
|
+
const maxOrder = await getMaxOrder(client, flow.sys_id, logicTable);
|
|
2057
|
+
const loopData = {
|
|
2058
|
+
sys_id: loopSysId,
|
|
2059
|
+
flow: flow.sys_id,
|
|
2060
|
+
name: loop_config.name || (loop_config.type === 'for_each' ? 'For Each' : 'Do Until'),
|
|
2061
|
+
logic_type: loop_config.type,
|
|
2062
|
+
order: loop_config.order ?? (maxOrder + 100),
|
|
2063
|
+
active: true
|
|
2064
|
+
};
|
|
2065
|
+
const loopValues = {};
|
|
2066
|
+
if (loop_config.data_source)
|
|
2067
|
+
loopValues.data_source = { value: loop_config.data_source, valueType: 'reference' };
|
|
2068
|
+
if (loop_config.condition)
|
|
2069
|
+
loopValues.condition = { value: loop_config.condition, valueType: 'string' };
|
|
2070
|
+
if (loop_config.max_iterations)
|
|
2071
|
+
loopValues.max_iterations = { value: String(loop_config.max_iterations), valueType: 'integer' };
|
|
2072
|
+
if (Object.keys(loopValues).length > 0) {
|
|
2073
|
+
loopData.values = useCompression ? compressValue(loopValues) : JSON.stringify(loopValues);
|
|
2074
|
+
}
|
|
2075
|
+
if (loop_config.parent_logic_instance) {
|
|
2076
|
+
loopData.parent_logic_instance = loop_config.parent_logic_instance;
|
|
2077
|
+
}
|
|
2078
|
+
try {
|
|
2079
|
+
await client.post(`/api/now/table/${logicTable}`, loopData);
|
|
2080
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
2081
|
+
action: 'add_loop',
|
|
2082
|
+
flow_id: flow.sys_id,
|
|
2083
|
+
loop: {
|
|
2084
|
+
sys_id: loopSysId,
|
|
2085
|
+
name: loopData.name,
|
|
2086
|
+
type: loop_config.type,
|
|
2087
|
+
order: loopData.order
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
catch (error) {
|
|
2092
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to add loop: ${error.response?.data?.error?.message || error.message}`);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Add a variable to an existing flow
|
|
2097
|
+
*/
|
|
2098
|
+
async function handleAddVariable(args, client) {
|
|
2099
|
+
const { flow_id, flow_name, variable_config } = args;
|
|
2100
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
2101
|
+
if (!flow) {
|
|
2102
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
2103
|
+
}
|
|
2104
|
+
if (!variable_config?.name || !variable_config?.type || !variable_config?.direction) {
|
|
2105
|
+
return (0, error_handler_js_1.createErrorResult)('variable_config requires name, type, and direction');
|
|
2106
|
+
}
|
|
2107
|
+
const variableSysId = generateSysId();
|
|
2108
|
+
// Get max order for variables
|
|
2109
|
+
// Note: sys_hub_flow_variable uses 'model' field (not 'flow')
|
|
2110
|
+
let maxOrder = 0;
|
|
2111
|
+
try {
|
|
2112
|
+
const response = await client.get(`/api/now/table/${FLOW_TABLES.VARIABLE}`, {
|
|
2113
|
+
params: {
|
|
2114
|
+
sysparm_query: `model=${flow.sys_id}`,
|
|
2115
|
+
sysparm_fields: 'order',
|
|
2116
|
+
sysparm_orderby: 'DESCorder',
|
|
2117
|
+
sysparm_limit: 1
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
if (response.data.result && response.data.result.length > 0) {
|
|
2121
|
+
maxOrder = parseInt(response.data.result[0].order || '0', 10);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
catch (e) { }
|
|
2125
|
+
const variableData = {
|
|
2126
|
+
sys_id: variableSysId,
|
|
2127
|
+
model: flow.sys_id,
|
|
2128
|
+
name: variable_config.name,
|
|
2129
|
+
label: variable_config.label || variable_config.name,
|
|
2130
|
+
description: variable_config.description || '',
|
|
2131
|
+
type: variable_config.type,
|
|
2132
|
+
direction: variable_config.direction,
|
|
2133
|
+
mandatory: variable_config.mandatory || false,
|
|
2134
|
+
order: maxOrder + 100
|
|
2135
|
+
};
|
|
2136
|
+
if (variable_config.default_value)
|
|
2137
|
+
variableData.default_value = variable_config.default_value;
|
|
2138
|
+
if (variable_config.reference_table)
|
|
2139
|
+
variableData.reference_table = variable_config.reference_table;
|
|
2140
|
+
try {
|
|
2141
|
+
await client.post(`/api/now/table/${FLOW_TABLES.VARIABLE}`, variableData);
|
|
2142
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
2143
|
+
action: 'add_variable',
|
|
2144
|
+
flow_id: flow.sys_id,
|
|
2145
|
+
variable: {
|
|
2146
|
+
sys_id: variableSysId,
|
|
2147
|
+
name: variable_config.name,
|
|
2148
|
+
type: variable_config.type,
|
|
2149
|
+
direction: variable_config.direction
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
catch (error) {
|
|
2154
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to add variable: ${error.response?.data?.error?.message || error.message}`);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Get detailed flow structure with decompressed values
|
|
2159
|
+
*/
|
|
2160
|
+
async function handleGetDetails(args, client, versionInfo) {
|
|
2161
|
+
const { flow_id, flow_name } = args;
|
|
2162
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
2163
|
+
if (!flow) {
|
|
2164
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
2165
|
+
}
|
|
2166
|
+
const details = {
|
|
2167
|
+
flow: flow,
|
|
2168
|
+
trigger: null,
|
|
2169
|
+
actions: [],
|
|
2170
|
+
logic: [],
|
|
2171
|
+
variables: [],
|
|
2172
|
+
snapshots: []
|
|
2173
|
+
};
|
|
2174
|
+
// Get trigger
|
|
2175
|
+
const triggerTable = versionInfo.useV2Tables ? FLOW_TABLES.TRIGGER_V2 : FLOW_TABLES.TRIGGER_V1;
|
|
2176
|
+
try {
|
|
2177
|
+
const response = await client.get(`/api/now/table/${triggerTable}`, {
|
|
2178
|
+
params: { sysparm_query: `flow=${flow.sys_id}` }
|
|
2179
|
+
});
|
|
2180
|
+
if (response.data.result && response.data.result.length > 0) {
|
|
2181
|
+
const trigger = response.data.result[0];
|
|
2182
|
+
if (trigger.values && isCompressed(trigger.values)) {
|
|
2183
|
+
trigger.values_decompressed = JSON.parse(decompressValue(trigger.values));
|
|
2184
|
+
}
|
|
2185
|
+
else if (trigger.values) {
|
|
2186
|
+
try {
|
|
2187
|
+
trigger.values_parsed = JSON.parse(trigger.values);
|
|
2188
|
+
}
|
|
2189
|
+
catch (e) { }
|
|
2190
|
+
}
|
|
2191
|
+
details.trigger = trigger;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
catch (e) { }
|
|
2195
|
+
// Get actions
|
|
2196
|
+
const actionTable = versionInfo.useV2Tables ? FLOW_TABLES.ACTION_V2 : FLOW_TABLES.ACTION_V1;
|
|
2197
|
+
try {
|
|
2198
|
+
const response = await client.get(`/api/now/table/${actionTable}`, {
|
|
2199
|
+
params: { sysparm_query: `flow=${flow.sys_id}`, sysparm_orderby: 'order' }
|
|
2200
|
+
});
|
|
2201
|
+
if (response.data.result) {
|
|
2202
|
+
details.actions = response.data.result.map((action) => {
|
|
2203
|
+
if (action.values && isCompressed(action.values)) {
|
|
2204
|
+
action.values_decompressed = JSON.parse(decompressValue(action.values));
|
|
2205
|
+
}
|
|
2206
|
+
else if (action.values) {
|
|
2207
|
+
try {
|
|
2208
|
+
action.values_parsed = JSON.parse(action.values);
|
|
2209
|
+
}
|
|
2210
|
+
catch (e) { }
|
|
2211
|
+
}
|
|
2212
|
+
return action;
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
catch (e) { }
|
|
2217
|
+
// Get logic (conditions, loops)
|
|
2218
|
+
const logicTable = versionInfo.useV2Tables ? FLOW_TABLES.LOGIC_V2 : FLOW_TABLES.LOGIC_V1;
|
|
2219
|
+
try {
|
|
2220
|
+
const response = await client.get(`/api/now/table/${logicTable}`, {
|
|
2221
|
+
params: { sysparm_query: `flow=${flow.sys_id}`, sysparm_orderby: 'order' }
|
|
2222
|
+
});
|
|
2223
|
+
if (response.data.result) {
|
|
2224
|
+
details.logic = response.data.result.map((logic) => {
|
|
2225
|
+
if (logic.values && isCompressed(logic.values)) {
|
|
2226
|
+
logic.values_decompressed = JSON.parse(decompressValue(logic.values));
|
|
2227
|
+
}
|
|
2228
|
+
else if (logic.values) {
|
|
2229
|
+
try {
|
|
2230
|
+
logic.values_parsed = JSON.parse(logic.values);
|
|
2231
|
+
}
|
|
2232
|
+
catch (e) { }
|
|
2233
|
+
}
|
|
2234
|
+
return logic;
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
catch (e) { }
|
|
2239
|
+
// Get variables
|
|
2240
|
+
// Note: sys_hub_flow_variable uses 'model' field (not 'flow')
|
|
2241
|
+
try {
|
|
2242
|
+
const response = await client.get(`/api/now/table/${FLOW_TABLES.VARIABLE}`, {
|
|
2243
|
+
params: { sysparm_query: `model=${flow.sys_id}`, sysparm_orderby: 'order' }
|
|
2244
|
+
});
|
|
2245
|
+
if (response.data.result) {
|
|
2246
|
+
details.variables = response.data.result;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
catch (e) { }
|
|
2250
|
+
// Get snapshots
|
|
2251
|
+
try {
|
|
2252
|
+
const response = await client.get(`/api/now/table/${FLOW_TABLES.SNAPSHOT}`, {
|
|
2253
|
+
params: { sysparm_query: `flow=${flow.sys_id}`, sysparm_orderby: 'DESCsys_created_on', sysparm_limit: 5 }
|
|
2254
|
+
});
|
|
2255
|
+
if (response.data.result) {
|
|
2256
|
+
details.snapshots = response.data.result;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
catch (e) { }
|
|
2260
|
+
// Build tree structure for nested actions
|
|
2261
|
+
const buildTree = () => {
|
|
2262
|
+
const tree = [];
|
|
2263
|
+
const itemMap = new Map();
|
|
2264
|
+
// Index all items
|
|
2265
|
+
for (const action of details.actions) {
|
|
2266
|
+
itemMap.set(action.sys_id, { ...action, _type: 'action', _children: [] });
|
|
2267
|
+
}
|
|
2268
|
+
for (const logic of details.logic) {
|
|
2269
|
+
itemMap.set(logic.sys_id, { ...logic, _type: 'logic', _children: [] });
|
|
2270
|
+
}
|
|
2271
|
+
// Build hierarchy
|
|
2272
|
+
itemMap.forEach((item, id) => {
|
|
2273
|
+
if (item.parent_logic_instance) {
|
|
2274
|
+
const parent = itemMap.get(item.parent_logic_instance);
|
|
2275
|
+
if (parent) {
|
|
2276
|
+
parent._children.push(item);
|
|
2277
|
+
}
|
|
2278
|
+
else {
|
|
2279
|
+
tree.push(item);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
else {
|
|
2283
|
+
tree.push(item);
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
// Sort by order
|
|
2287
|
+
const sortByOrder = (items) => {
|
|
2288
|
+
items.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
2289
|
+
for (const item of items) {
|
|
2290
|
+
if (item._children && item._children.length > 0) {
|
|
2291
|
+
sortByOrder(item._children);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2295
|
+
sortByOrder(tree);
|
|
2296
|
+
return tree;
|
|
2297
|
+
};
|
|
2298
|
+
details.tree_structure = buildTree();
|
|
2299
|
+
details.version_info = versionInfo;
|
|
2300
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
2301
|
+
action: 'get_details',
|
|
2302
|
+
...details
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Publish flow changes (create new snapshot and activate)
|
|
2307
|
+
*/
|
|
2308
|
+
async function handlePublish(args, client, versionInfo) {
|
|
2309
|
+
const { flow_id, flow_name } = args;
|
|
2310
|
+
const flow = await findFlow(client, flow_id, flow_name);
|
|
2311
|
+
if (!flow) {
|
|
2312
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow not found: ${flow_id || flow_name}`);
|
|
2313
|
+
}
|
|
2314
|
+
const snapshotSysId = generateSysId();
|
|
2315
|
+
const useCompression = versionInfo.useCompression;
|
|
2316
|
+
// Get existing flow components for the definition
|
|
2317
|
+
const actionTable = versionInfo.useV2Tables ? FLOW_TABLES.ACTION_V2 : FLOW_TABLES.ACTION_V1;
|
|
2318
|
+
const triggerTable = versionInfo.useV2Tables ? FLOW_TABLES.TRIGGER_V2 : FLOW_TABLES.TRIGGER_V1;
|
|
2319
|
+
const logicTable = versionInfo.useV2Tables ? FLOW_TABLES.LOGIC_V2 : FLOW_TABLES.LOGIC_V1;
|
|
2320
|
+
// Fetch existing components
|
|
2321
|
+
let actions = [];
|
|
2322
|
+
let triggers = [];
|
|
2323
|
+
let logic = [];
|
|
2324
|
+
let variables = [];
|
|
2325
|
+
try {
|
|
2326
|
+
const actionsResp = await client.get(`/api/now/table/${actionTable}`, {
|
|
2327
|
+
params: { sysparm_query: `flow=${flow.sys_id}`, sysparm_fields: 'sys_id,name,order' }
|
|
2328
|
+
});
|
|
2329
|
+
actions = actionsResp.data.result || [];
|
|
2330
|
+
}
|
|
2331
|
+
catch (e) { }
|
|
2332
|
+
try {
|
|
2333
|
+
const triggersResp = await client.get(`/api/now/table/${triggerTable}`, {
|
|
2334
|
+
params: { sysparm_query: `flow=${flow.sys_id}`, sysparm_fields: 'sys_id,trigger_type' }
|
|
2335
|
+
});
|
|
2336
|
+
triggers = triggersResp.data.result || [];
|
|
2337
|
+
}
|
|
2338
|
+
catch (e) { }
|
|
2339
|
+
try {
|
|
2340
|
+
const logicResp = await client.get(`/api/now/table/${logicTable}`, {
|
|
2341
|
+
params: { sysparm_query: `flow=${flow.sys_id}`, sysparm_fields: 'sys_id,name,logic_type' }
|
|
2342
|
+
});
|
|
2343
|
+
logic = logicResp.data.result || [];
|
|
2344
|
+
}
|
|
2345
|
+
catch (e) { }
|
|
2346
|
+
try {
|
|
2347
|
+
const variablesResp = await client.get(`/api/now/table/${FLOW_TABLES.VARIABLE}`, {
|
|
2348
|
+
params: { sysparm_query: `model=${flow.sys_id}`, sysparm_fields: 'sys_id,name,type,direction' }
|
|
2349
|
+
});
|
|
2350
|
+
variables = variablesResp.data.result || [];
|
|
2351
|
+
}
|
|
2352
|
+
catch (e) { }
|
|
2353
|
+
// Build flow definition
|
|
2354
|
+
const flowDefinition = {
|
|
2355
|
+
sys_id: flow.sys_id,
|
|
2356
|
+
name: flow.name,
|
|
2357
|
+
internal_name: flow.internal_name,
|
|
2358
|
+
published_at: new Date().toISOString(),
|
|
2359
|
+
triggers: triggers.map(t => ({ sys_id: t.sys_id, trigger_type: t.trigger_type })),
|
|
2360
|
+
actions: actions.map(a => ({ sys_id: a.sys_id, name: a.name, order: a.order })),
|
|
2361
|
+
logic: logic.map(l => ({ sys_id: l.sys_id, name: l.name, logic_type: l.logic_type })),
|
|
2362
|
+
variables: variables.map(v => ({ sys_id: v.sys_id, name: v.name, type: v.type, direction: v.direction }))
|
|
2363
|
+
};
|
|
2364
|
+
// Create new snapshot with definition
|
|
2365
|
+
const snapshotData = {
|
|
2366
|
+
sys_id: snapshotSysId,
|
|
2367
|
+
flow: flow.sys_id,
|
|
2368
|
+
name: `${flow.name} - Published ${new Date().toISOString()}`,
|
|
2369
|
+
version: '1.0.0',
|
|
2370
|
+
active: true,
|
|
2371
|
+
published: true,
|
|
2372
|
+
definition: useCompression
|
|
2373
|
+
? compressValue(flowDefinition, versionInfo.useCompressionHeader)
|
|
2374
|
+
: JSON.stringify(flowDefinition)
|
|
2375
|
+
};
|
|
2376
|
+
try {
|
|
2377
|
+
await client.post(`/api/now/table/${FLOW_TABLES.SNAPSHOT}`, snapshotData);
|
|
2378
|
+
}
|
|
2379
|
+
catch (error) {
|
|
2380
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to create snapshot: ${error.response?.data?.error?.message || error.message}`);
|
|
2381
|
+
}
|
|
2382
|
+
// *** CRITICAL FIX ***
|
|
2383
|
+
// Update flow with snapshot references AND activate
|
|
2384
|
+
try {
|
|
2385
|
+
await client.patch(`/api/now/table/${FLOW_TABLES.FLOW}/${flow.sys_id}`, {
|
|
2386
|
+
active: true,
|
|
2387
|
+
status: 'published',
|
|
2388
|
+
main_snapshot: snapshotSysId,
|
|
2389
|
+
latest_snapshot: snapshotSysId
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
catch (error) {
|
|
2393
|
+
return (0, error_handler_js_1.createErrorResult)(`Failed to activate flow: ${error.response?.data?.error?.message || error.message}`);
|
|
2394
|
+
}
|
|
2395
|
+
return (0, error_handler_js_1.createSuccessResult)({
|
|
2396
|
+
action: 'publish',
|
|
2397
|
+
flow_id: flow.sys_id,
|
|
2398
|
+
flow_name: flow.name,
|
|
2399
|
+
snapshot_id: snapshotSysId,
|
|
2400
|
+
main_snapshot: snapshotSysId,
|
|
2401
|
+
latest_snapshot: snapshotSysId,
|
|
2402
|
+
active: true,
|
|
2403
|
+
components_included: {
|
|
2404
|
+
triggers: triggers.length,
|
|
2405
|
+
actions: actions.length,
|
|
2406
|
+
logic: logic.length,
|
|
2407
|
+
variables: variables.length
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
// ==================== MAIN EXECUTOR ====================
|
|
2412
|
+
async function execute(args, context) {
|
|
2413
|
+
const { action } = args;
|
|
2414
|
+
if (!action) {
|
|
2415
|
+
return (0, error_handler_js_1.createErrorResult)('action is required');
|
|
2416
|
+
}
|
|
2417
|
+
try {
|
|
2418
|
+
const client = await (0, auth_js_1.getAuthenticatedClient)(context);
|
|
2419
|
+
const versionInfo = await detectVersion(client);
|
|
2420
|
+
console.log(`Executing flow management action: ${action}`);
|
|
2421
|
+
console.log(`Detected version: ${versionInfo.version}, compression: ${versionInfo.useCompression}`);
|
|
2422
|
+
switch (action) {
|
|
2423
|
+
case 'create':
|
|
2424
|
+
return handleCreate(args, client, versionInfo);
|
|
2425
|
+
case 'update':
|
|
2426
|
+
return handleUpdate(args, client, versionInfo);
|
|
2427
|
+
case 'delete':
|
|
2428
|
+
return handleDelete(args, client, versionInfo);
|
|
2429
|
+
case 'clone':
|
|
2430
|
+
return handleClone(args, client, versionInfo);
|
|
2431
|
+
case 'activate':
|
|
2432
|
+
return handleActivate(args, client);
|
|
2433
|
+
case 'deactivate':
|
|
2434
|
+
return handleDeactivate(args, client);
|
|
2435
|
+
case 'add_action':
|
|
2436
|
+
return handleAddAction(args, client, versionInfo);
|
|
2437
|
+
case 'remove_action':
|
|
2438
|
+
return handleRemoveAction(args, client, versionInfo);
|
|
2439
|
+
case 'add_condition':
|
|
2440
|
+
return handleAddCondition(args, client, versionInfo);
|
|
2441
|
+
case 'add_loop':
|
|
2442
|
+
return handleAddLoop(args, client, versionInfo);
|
|
2443
|
+
case 'add_variable':
|
|
2444
|
+
return handleAddVariable(args, client);
|
|
2445
|
+
case 'get_details':
|
|
2446
|
+
return handleGetDetails(args, client, versionInfo);
|
|
2447
|
+
case 'publish':
|
|
2448
|
+
return handlePublish(args, client, versionInfo);
|
|
2449
|
+
default:
|
|
2450
|
+
return (0, error_handler_js_1.createErrorResult)(`Unknown action: ${action}`);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
catch (error) {
|
|
2454
|
+
return (0, error_handler_js_1.createErrorResult)(`Flow management failed: ${error.message}`);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
exports.version = '2.1.0';
|
|
2458
|
+
exports.author = 'Snow-Flow v8.41.13 - Flow Designer Management (snapshot fix)';
|
|
2459
|
+
//# sourceMappingURL=snow_manage_flow.js.map
|