snow-flow-test 10.0.1-test.206 → 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.206",
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 };
@@ -3895,15 +4052,15 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
3895
4052
  await client.get('/api/now/processflow/flow/' + openFlowId);
3896
4053
  } catch (_) { /* best-effort — flow data load is not critical for lock acquisition */ }
3897
4054
 
3898
- // 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
3899
4056
  var lockResult = await acquireFlowEditingLock(client, openFlowId);
3900
4057
  if (lockResult.success) {
3901
4058
  openSummary.success('Flow opened for editing (lock acquired)').field('Flow', openFlowId)
3902
4059
  .line('You can now use add_action, add_flow_logic, etc. Call close_flow when done.');
3903
- 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());
3904
4061
  } else {
3905
4062
  openSummary.error('Cannot open flow: ' + (lockResult.error || 'lock acquisition failed')).field('Flow', openFlowId);
3906
- 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) : ''));
3907
4064
  }
3908
4065
  }
3909
4066