snow-flow-test 10.0.1-test.205 → 10.0.1-test.207

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.1-test.205",
3
+ "version": "10.0.1-test.207",
4
4
  "name": "snow-flow-test",
5
5
  "description": "Snow-Flow Test - ServiceNow Multi-Agent Development Framework",
6
6
  "license": "Elastic-2.0",
@@ -115,19 +115,62 @@ async function executeFlowPatchMutation(
115
115
  * The UI calls safeEdit(create: flowId) when opening the editor.
116
116
  * This must be called before GraphQL mutations on existing flows.
117
117
  */
118
- async function acquireFlowEditingLock(client: any, flowId: string): Promise<{ success: boolean; error?: string }> {
118
+ async function acquireFlowEditingLock(client: any, flowId: string): Promise<{ success: boolean; error?: string; debug?: any }> {
119
+ var debug: any = {};
120
+
121
+ // Step 1: Call safeEdit(create) GraphQL mutation (same as UI)
119
122
  try {
120
123
  var mutation = 'mutation { global { snFlowDesigner { safeEdit(safeEditInput: {create: "' + flowId + '"}) { createResult { canEdit id editingUserDisplayName __typename } __typename } __typename } __typename } }';
121
124
  var resp = await client.post('/api/now/graphql', { variables: {}, query: mutation });
122
125
  var result = resp.data?.data?.global?.snFlowDesigner?.safeEdit?.createResult;
123
- if (result?.canEdit === true || result?.canEdit === 'true') {
124
- return { success: true };
126
+ debug.graphql_response = result;
127
+ if (result?.canEdit !== true && result?.canEdit !== 'true') {
128
+ var editingUser = result?.editingUserDisplayName || 'another user';
129
+ return { success: false, error: 'Flow is locked by ' + editingUser, debug };
130
+ }
131
+ debug.graphql_canEdit = true;
132
+ } catch (e: any) {
133
+ debug.graphql_error = e.message;
134
+ }
135
+
136
+ // Step 2: Verify a sys_hub_flow_safe_edit record was actually created
137
+ try {
138
+ var checkResp = await client.get('/api/now/table/sys_hub_flow_safe_edit', {
139
+ params: { sysparm_query: 'document_id=' + flowId, sysparm_fields: 'sys_id,document_id,user', sysparm_limit: 1 }
140
+ });
141
+ var existing = checkResp.data.result?.[0];
142
+ if (existing?.sys_id) {
143
+ debug.safe_edit_record = existing.sys_id;
144
+ return { success: true, debug };
145
+ }
146
+ debug.safe_edit_record = 'not_found_after_graphql';
147
+ } catch (e: any) {
148
+ debug.safe_edit_check_error = e.message;
149
+ }
150
+
151
+ // Step 3: Fallback — create the safe_edit record directly via REST API
152
+ // The GraphQL mutation may return canEdit=true without persisting a record.
153
+ try {
154
+ var createResp = await client.post('/api/now/table/sys_hub_flow_safe_edit', {
155
+ document_id: flowId
156
+ });
157
+ var created = createResp.data.result;
158
+ if (created?.sys_id) {
159
+ debug.rest_created = created.sys_id;
160
+ return { success: true, debug };
125
161
  }
126
- var editingUser = result?.editingUserDisplayName || 'another user';
127
- return { success: false, error: 'Flow is locked by ' + editingUser };
162
+ debug.rest_create_response = created;
128
163
  } catch (e: any) {
129
- return { success: false, error: e.message || 'unknown error' };
164
+ debug.rest_create_error = e.message;
130
165
  }
166
+
167
+ // If GraphQL said canEdit=true, trust it even if we couldn't verify/create the record
168
+ if (debug.graphql_canEdit) {
169
+ debug.fallback = 'trusting_graphql_canEdit';
170
+ return { success: true, debug };
171
+ }
172
+
173
+ return { success: false, error: 'Could not acquire editing lock (GraphQL + REST fallback both failed)', debug };
131
174
  }
132
175
 
133
176
  /**
@@ -1158,6 +1201,62 @@ async function addActionViaGraphQL(
1158
1201
  steps.record_action = recordActionResult.steps;
1159
1202
  var hasRecordPills = (recordActionResult.labelCacheUpdates.length + recordActionResult.labelCacheInserts.length) > 0;
1160
1203
 
1204
+ // ── Rewrite shorthand pills in generic action inputs (e.g. Log "message") ────
1205
+ // Non-record inputs (anything other than record/table_name/values) that contain
1206
+ // {{trigger.current.X}} need rewriting to {{Created or Updated_1.current.X}} + labelCache.
1207
+ var PILL_SHORTHANDS_ACTION = ['trigger.current', 'current', 'trigger_record', 'trigger.record'];
1208
+ var RECORD_INPUTS = ['record', 'table_name', 'values'];
1209
+ var genericPillInputs: { name: string; fields: string[]; isRecordLevel: boolean }[] = [];
1210
+ var actionTriggerInfo: any = null;
1211
+
1212
+ for (var gpi = 0; gpi < recordActionResult.inputs.length; gpi++) {
1213
+ var gpInput = recordActionResult.inputs[gpi];
1214
+ if (RECORD_INPUTS.includes(gpInput.name)) continue;
1215
+ var gpVal = gpInput.value?.value || '';
1216
+ if (!gpVal.includes('{{')) continue;
1217
+
1218
+ var gpHasShorthand = PILL_SHORTHANDS_ACTION.some(function (sh) {
1219
+ return gpVal.includes('{{' + sh + '.') || gpVal.includes('{{' + sh + '}}');
1220
+ });
1221
+ if (!gpHasShorthand) continue;
1222
+
1223
+ // Get trigger info if not already fetched
1224
+ if (!actionTriggerInfo) {
1225
+ actionTriggerInfo = await getFlowTriggerInfo(client, flowId);
1226
+ steps.action_trigger_info = {
1227
+ dataPillBase: actionTriggerInfo.dataPillBase, triggerName: actionTriggerInfo.triggerName,
1228
+ table: actionTriggerInfo.table, tableLabel: actionTriggerInfo.tableLabel
1229
+ };
1230
+ }
1231
+ if (!actionTriggerInfo.dataPillBase) continue;
1232
+
1233
+ var gpPillBase = actionTriggerInfo.dataPillBase;
1234
+ var gpOrigVal = gpVal;
1235
+ for (var gsi = 0; gsi < PILL_SHORTHANDS_ACTION.length; gsi++) {
1236
+ var gsh = PILL_SHORTHANDS_ACTION[gsi];
1237
+ gpVal = gpVal.split('{{' + gsh + '.').join('{{' + gpPillBase + '.');
1238
+ gpVal = gpVal.split('{{' + gsh + '}}').join('{{' + gpPillBase + '}}');
1239
+ }
1240
+ gpInput.value.value = gpVal;
1241
+
1242
+ // Extract field names from pills for labelCache
1243
+ var gpPillFields: string[] = [];
1244
+ var gpIsRecordLevel = false;
1245
+ var gpPillRx = /\{\{([^}]+)\}\}/g;
1246
+ var gpm: RegExpExecArray | null;
1247
+ while ((gpm = gpPillRx.exec(gpVal)) !== null) {
1248
+ var gpParts = gpm[1].split('.');
1249
+ if (gpParts.length > 2) {
1250
+ gpPillFields.push(gpParts[gpParts.length - 1]);
1251
+ } else {
1252
+ gpIsRecordLevel = true;
1253
+ }
1254
+ }
1255
+
1256
+ genericPillInputs.push({ name: gpInput.name, fields: gpPillFields, isRecordLevel: gpIsRecordLevel });
1257
+ steps['pill_rewrite_' + gpInput.name] = { original: gpOrigVal, rewritten: gpVal };
1258
+ }
1259
+
1161
1260
  // For record actions: clear data pill values from INSERT — they'll be set via separate UPDATE
1162
1261
  // (Flow Designer's GraphQL API ignores labelCache during INSERT, it only works with UPDATE)
1163
1262
  var insertInputs = recordActionResult.inputs;
@@ -1270,6 +1369,64 @@ async function addActionViaGraphQL(
1270
1369
  }
1271
1370
  }
1272
1371
 
1372
+ // Step 3: UPDATE labelCache for generic action inputs with data pills (e.g. Log "message")
1373
+ if (genericPillInputs.length > 0 && actionTriggerInfo?.dataPillBase) {
1374
+ try {
1375
+ var gpLabelInserts: any[] = [];
1376
+ var gpBase = actionTriggerInfo.dataPillBase;
1377
+ var gpTrigName = actionTriggerInfo.triggerName;
1378
+ var gpTable = actionTriggerInfo.tableRef || actionTriggerInfo.table;
1379
+ var gpTableLabel = actionTriggerInfo.tableLabel;
1380
+
1381
+ for (var gli = 0; gli < genericPillInputs.length; gli++) {
1382
+ var gpi2 = genericPillInputs[gli];
1383
+
1384
+ // Field-level pills: build labelCache with metadata from sys_dictionary
1385
+ if (gpi2.fields.length > 0) {
1386
+ var gpFieldEntries = await buildConditionLabelCache(
1387
+ client, '', gpBase, gpTrigName, gpTable, gpTableLabel, uuid, gpi2.fields, gpi2.name
1388
+ );
1389
+ gpLabelInserts = gpLabelInserts.concat(gpFieldEntries);
1390
+ }
1391
+
1392
+ // Record-level pill
1393
+ if (gpi2.isRecordLevel) {
1394
+ gpLabelInserts.push({
1395
+ name: gpBase,
1396
+ label: 'Trigger - Record ' + gpTrigName + '\u279b' + gpTableLabel + ' Record\u279b' + gpTableLabel,
1397
+ reference: gpTable,
1398
+ reference_display: gpTableLabel,
1399
+ type: 'reference',
1400
+ base_type: 'reference',
1401
+ parent_table_name: gpTable,
1402
+ column_name: '',
1403
+ attributes: '',
1404
+ choices: {},
1405
+ usedInstances: [{ uiUniqueIdentifier: uuid, inputName: gpi2.name }]
1406
+ });
1407
+ }
1408
+ }
1409
+
1410
+ if (gpLabelInserts.length > 0) {
1411
+ var gpUpdatePatch: any = {
1412
+ flowId: flowId,
1413
+ actions: {
1414
+ update: [{
1415
+ uiUniqueIdentifier: uuid,
1416
+ type: 'action',
1417
+ }]
1418
+ },
1419
+ labelCache: { insert: gpLabelInserts }
1420
+ };
1421
+ steps.generic_pill_label_cache_mutation = jsToGraphQL(gpUpdatePatch);
1422
+ await executeFlowPatchMutation(client, gpUpdatePatch, actionResponseFields);
1423
+ steps.generic_pill_label_cache_update = { success: true, count: gpLabelInserts.length };
1424
+ }
1425
+ } catch (gpe: any) {
1426
+ steps.generic_pill_label_cache_update = { success: false, error: gpe.message };
1427
+ }
1428
+ }
1429
+
1273
1430
  return { success: true, actionId: actionId || undefined, steps };
1274
1431
  } catch (e: any) {
1275
1432
  steps.insert = { success: false, error: e.message };
@@ -1614,7 +1771,8 @@ async function buildConditionLabelCache(
1614
1771
  table: string,
1615
1772
  tableLabel: string,
1616
1773
  logicUiId: string,
1617
- explicitFields?: string[]
1774
+ explicitFields?: string[],
1775
+ inputName?: string
1618
1776
  ): Promise<any[]> {
1619
1777
  if (!dataPillBase) return [];
1620
1778
 
@@ -1680,7 +1838,7 @@ async function buildConditionLabelCache(
1680
1838
  base_type: meta.type,
1681
1839
  parent_table_name: table,
1682
1840
  column_name: f,
1683
- usedInstances: [{ uiUniqueIdentifier: logicUiId, inputName: 'condition' }],
1841
+ usedInstances: [{ uiUniqueIdentifier: logicUiId, inputName: inputName || 'condition' }],
1684
1842
  choices: {}
1685
1843
  });
1686
1844
  }
@@ -2163,6 +2321,60 @@ async function addFlowLogicViaGraphQL(
2163
2321
  steps.condition_not_encoded_query = true;
2164
2322
  }
2165
2323
 
2324
+ // ── Rewrite shorthand pills in non-condition inputs (e.g. FOR_EACH "items") ────
2325
+ // These inputs may contain {{trigger.current}} or {{current.field}} that need rewriting
2326
+ // to the full dataPillBase (e.g. {{Created or Updated_1.current}}) + labelCache for rendering.
2327
+ var nonConditionPillInputs: { name: string; fields: string[]; isRecordLevel: boolean }[] = [];
2328
+ for (var nci = 0; nci < inputResult.inputs.length; nci++) {
2329
+ var ncInput = inputResult.inputs[nci];
2330
+ if (ncInput.name === 'condition' || ncInput.name === 'condition_name') continue;
2331
+ var ncVal = ncInput.value?.value || '';
2332
+ if (!ncVal.includes('{{')) continue;
2333
+
2334
+ var ncHasShorthand = PILL_SHORTHANDS.some(function (sh) {
2335
+ return ncVal.includes('{{' + sh + '.') || ncVal.includes('{{' + sh + '}}');
2336
+ });
2337
+ if (!ncHasShorthand) continue;
2338
+
2339
+ // Get trigger info if not already fetched
2340
+ if (!conditionTriggerInfo) {
2341
+ conditionTriggerInfo = await getFlowTriggerInfo(client, flowId);
2342
+ steps.trigger_info = {
2343
+ dataPillBase: conditionTriggerInfo.dataPillBase, triggerName: conditionTriggerInfo.triggerName,
2344
+ table: conditionTriggerInfo.table, tableLabel: conditionTriggerInfo.tableLabel, error: conditionTriggerInfo.error,
2345
+ debug: conditionTriggerInfo.debug
2346
+ };
2347
+ }
2348
+ if (!conditionTriggerInfo.dataPillBase) continue;
2349
+
2350
+ var ncPillBase = conditionTriggerInfo.dataPillBase;
2351
+ var ncOrigVal = ncVal;
2352
+ for (var si2 = 0; si2 < PILL_SHORTHANDS.length; si2++) {
2353
+ var sh2 = PILL_SHORTHANDS[si2];
2354
+ ncVal = ncVal.split('{{' + sh2 + '.').join('{{' + ncPillBase + '.');
2355
+ ncVal = ncVal.split('{{' + sh2 + '}}').join('{{' + ncPillBase + '}}');
2356
+ }
2357
+ ncInput.value.value = ncVal;
2358
+
2359
+ // Extract field names from pills for labelCache
2360
+ var ncPillFields: string[] = [];
2361
+ var ncIsRecordLevel = false;
2362
+ var ncPillRx = /\{\{([^}]+)\}\}/g;
2363
+ var ncm: RegExpExecArray | null;
2364
+ while ((ncm = ncPillRx.exec(ncVal)) !== null) {
2365
+ var ncParts = ncm[1].split('.');
2366
+ if (ncParts.length > 2) {
2367
+ ncPillFields.push(ncParts[ncParts.length - 1]);
2368
+ } else {
2369
+ // Record-level pill like {{Created or Updated_1.current}}
2370
+ ncIsRecordLevel = true;
2371
+ }
2372
+ }
2373
+
2374
+ nonConditionPillInputs.push({ name: ncInput.name, fields: ncPillFields, isRecordLevel: ncIsRecordLevel });
2375
+ steps['pill_rewrite_' + ncInput.name] = { original: ncOrigVal, rewritten: ncVal };
2376
+ }
2377
+
2166
2378
  // Calculate insertion order
2167
2379
  const resolvedOrder = await calculateInsertOrder(client, flowId, parentUiId, order);
2168
2380
  steps.insert_order = resolvedOrder;
@@ -2282,6 +2494,65 @@ async function addFlowLogicViaGraphQL(
2282
2494
  }
2283
2495
  }
2284
2496
 
2497
+ // Step 3: UPDATE labelCache for non-condition inputs with data pills (e.g. FOR_EACH "items")
2498
+ if (nonConditionPillInputs.length > 0 && conditionTriggerInfo?.dataPillBase) {
2499
+ try {
2500
+ var ncLabelInserts: any[] = [];
2501
+ var dPillBase = conditionTriggerInfo.dataPillBase;
2502
+ var dTriggerName = conditionTriggerInfo.triggerName;
2503
+ var dTable = conditionTriggerInfo.tableRef;
2504
+ var dTableLabel = conditionTriggerInfo.tableLabel;
2505
+
2506
+ for (var nli = 0; nli < nonConditionPillInputs.length; nli++) {
2507
+ var ncpi = nonConditionPillInputs[nli];
2508
+
2509
+ // Field-level pills: reuse buildConditionLabelCache with the correct inputName
2510
+ if (ncpi.fields.length > 0) {
2511
+ var ncFieldEntries = await buildConditionLabelCache(
2512
+ client, '', dPillBase, dTriggerName, dTable, dTableLabel, returnedUuid, ncpi.fields, ncpi.name
2513
+ );
2514
+ ncLabelInserts = ncLabelInserts.concat(ncFieldEntries);
2515
+ }
2516
+
2517
+ // Record-level pill (e.g. {{Created or Updated_1.current}}) — add record-level labelCache entry
2518
+ if (ncpi.isRecordLevel) {
2519
+ ncLabelInserts.push({
2520
+ name: dPillBase,
2521
+ label: 'Trigger - Record ' + dTriggerName + '\u279b' + dTableLabel + ' Record\u279b' + dTableLabel,
2522
+ reference: dTable,
2523
+ reference_display: dTableLabel,
2524
+ type: 'reference',
2525
+ base_type: 'reference',
2526
+ parent_table_name: dTable,
2527
+ column_name: '',
2528
+ attributes: '',
2529
+ choices: {},
2530
+ usedInstances: [{ uiUniqueIdentifier: returnedUuid, inputName: ncpi.name }]
2531
+ });
2532
+ }
2533
+ }
2534
+
2535
+ if (ncLabelInserts.length > 0) {
2536
+ var ncUpdatePatch: any = {
2537
+ flowId: flowId,
2538
+ flowLogics: {
2539
+ update: [{
2540
+ uiUniqueIdentifier: returnedUuid,
2541
+ type: 'flowlogic',
2542
+ }]
2543
+ },
2544
+ labelCache: { insert: ncLabelInserts }
2545
+ };
2546
+ steps.nc_pill_label_cache_mutation = jsToGraphQL(ncUpdatePatch);
2547
+ await executeFlowPatchMutation(client, ncUpdatePatch, logicResponseFields);
2548
+ steps.nc_pill_label_cache_update = { success: true, count: ncLabelInserts.length };
2549
+ }
2550
+ } catch (nce: any) {
2551
+ steps.nc_pill_label_cache_update = { success: false, error: nce.message };
2552
+ // Non-fatal: element was created, just label rendering may be affected
2553
+ }
2554
+ }
2555
+
2285
2556
  return { success: true, logicId, uiUniqueIdentifier: returnedUuid, steps };
2286
2557
  } catch (e: any) {
2287
2558
  steps.insert = { success: false, error: e.message };
@@ -3781,15 +4052,15 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
3781
4052
  await client.get('/api/now/processflow/flow/' + openFlowId);
3782
4053
  } catch (_) { /* best-effort — flow data load is not critical for lock acquisition */ }
3783
4054
 
3784
- // Step 2: Acquire editing lock via safeEdit create mutation (required for GraphQL mutations)
4055
+ // Step 2: Acquire editing lock via safeEdit create mutation + REST fallback
3785
4056
  var lockResult = await acquireFlowEditingLock(client, openFlowId);
3786
4057
  if (lockResult.success) {
3787
4058
  openSummary.success('Flow opened for editing (lock acquired)').field('Flow', openFlowId)
3788
4059
  .line('You can now use add_action, add_flow_logic, etc. Call close_flow when done.');
3789
- return createSuccessResult({ action: 'open_flow', flow_id: openFlowId, editing_session: true }, {}, openSummary.build());
4060
+ return createSuccessResult({ action: 'open_flow', flow_id: openFlowId, editing_session: true, lock_debug: lockResult.debug }, {}, openSummary.build());
3790
4061
  } else {
3791
4062
  openSummary.error('Cannot open flow: ' + (lockResult.error || 'lock acquisition failed')).field('Flow', openFlowId);
3792
- return createErrorResult('Cannot open flow for editing: ' + (lockResult.error || 'lock acquisition failed'));
4063
+ return createErrorResult('Cannot open flow for editing: ' + (lockResult.error || 'lock acquisition failed') + (lockResult.debug ? ' | debug: ' + JSON.stringify(lockResult.debug) : ''));
3793
4064
  }
3794
4065
  }
3795
4066