snow-flow 10.0.1-dev.480 → 10.0.1-dev.482

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-dev.480",
3
+ "version": "10.0.1-dev.482",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -896,8 +896,8 @@ async function addTriggerViaGraphQL(
896
896
  steps.insert = { success: !!triggerId, triggerId, triggerUiId };
897
897
  if (!triggerId) return { success: false, steps, error: 'GraphQL trigger INSERT returned no trigger ID' };
898
898
 
899
- // Step 2: UPDATE with actual table and condition values (matching UI behavior from trigger-query-2.txt)
900
- // The UI sends: table with displayField+displayValue+value, condition with just displayValue
899
+ // Step 2: UPDATE with actual table and condition values (matching UI behavior)
900
+ // The UI sends: table with displayField+displayValue+value, condition with displayField+displayValue(empty)+value, metadata with predicates
901
901
  if (table) {
902
902
  try {
903
903
  var tableDisplayName = '';
@@ -911,6 +911,30 @@ async function addTriggerViaGraphQL(
911
911
  tableDisplayName = table.charAt(0).toUpperCase() + table.slice(1).replace(/_/g, ' ');
912
912
  }
913
913
 
914
+ // Build condition predicates via query_parse API (same as UI)
915
+ var conditionValue = condition || '^EQ';
916
+ var predicatesJson = '[]';
917
+ if (conditionValue && conditionValue !== '^EQ') {
918
+ try {
919
+ var qpResp = await client.get('/api/now/ui/query_parse/' + table + '/map', {
920
+ params: { table: table, sysparm_query: conditionValue }
921
+ });
922
+ var qpResult = qpResp.data?.result;
923
+ if (qpResult) {
924
+ predicatesJson = typeof qpResult === 'string' ? qpResult : JSON.stringify(qpResult);
925
+ }
926
+ } catch (_) {
927
+ // Fallback: build minimal predicates from parsed condition
928
+ var parsedClauses = parseEncodedQuery(conditionValue);
929
+ if (parsedClauses.length > 0) {
930
+ var minPredicates = parsedClauses.map(function (c) {
931
+ return { field: c.field, operator: c.operator, value: c.value };
932
+ });
933
+ predicatesJson = JSON.stringify(minPredicates);
934
+ }
935
+ }
936
+ }
937
+
914
938
  var trigUpdateInputs: any[] = [
915
939
  {
916
940
  name: 'table',
@@ -920,7 +944,9 @@ async function addTriggerViaGraphQL(
920
944
  },
921
945
  {
922
946
  name: 'condition',
923
- displayValue: { schemaless: false, schemalessValue: '', value: condition || '^EQ' }
947
+ displayField: '',
948
+ displayValue: { value: '' },
949
+ value: { schemaless: false, schemalessValue: '', value: conditionValue }
924
950
  }
925
951
  ];
926
952
 
@@ -929,11 +955,12 @@ async function addTriggerViaGraphQL(
929
955
  triggerInstances: {
930
956
  update: [{
931
957
  id: triggerId,
932
- inputs: trigUpdateInputs
958
+ inputs: trigUpdateInputs,
959
+ metadata: '{"predicates":' + predicatesJson + '}'
933
960
  }]
934
961
  }
935
962
  }, triggerResponseFields);
936
- steps.trigger_update = { success: true, table: table, tableDisplay: tableDisplayName };
963
+ steps.trigger_update = { success: true, table: table, tableDisplay: tableDisplayName, predicates: predicatesJson };
937
964
  } catch (updateErr: any) {
938
965
  steps.trigger_update = { success: false, error: updateErr.message };
939
966
  // Trigger was created — update failure is non-fatal
@@ -1222,9 +1249,9 @@ async function getFlowTriggerInfo(
1222
1249
  // This API returns XML (not JSON). We parse trigger info from the XML string.
1223
1250
  try {
1224
1251
  debug.processflow_api = 'attempting';
1225
- var pfResp = await client.get('/api/now/processflow/flow/' + flowId, {
1226
- headers: { 'Accept': 'application/xml, application/json' }
1227
- });
1252
+ // Note: do NOT pass custom headers in config — some Axios interceptors freeze the config
1253
+ // object, causing "Attempted to assign to readonly property" errors.
1254
+ var pfResp = await client.get('/api/now/processflow/flow/' + flowId);
1228
1255
  var pfRaw = pfResp.data;
1229
1256
  debug.processflow_api = 'success';
1230
1257
  debug.processflow_type = typeof pfRaw;
@@ -1259,6 +1286,14 @@ async function getFlowTriggerInfo(
1259
1286
  var pfData = pfRaw.result || pfRaw;
1260
1287
  debug.processflow_keys = pfData && typeof pfData === 'object' ? Object.keys(pfData).slice(0, 20) : null;
1261
1288
 
1289
+ // ProcessFlow API wraps actual flow data inside a "data" property
1290
+ // e.g. {data: {triggerInstances: [...]}, errorMessage, errorCode, integrationsPluginActive}
1291
+ if (pfData.data && typeof pfData.data === 'object' && !pfData.triggerInstances) {
1292
+ pfData = pfData.data;
1293
+ debug.processflow_unwrapped = true;
1294
+ debug.processflow_data_keys = typeof pfData === 'object' ? Object.keys(pfData).slice(0, 20) : null;
1295
+ }
1296
+
1262
1297
  var pfTriggers = pfData?.triggerInstances || pfData?.trigger_instances || pfData?.triggers || [];
1263
1298
  if (!Array.isArray(pfTriggers) && pfData?.model?.triggerInstances) {
1264
1299
  pfTriggers = pfData.model.triggerInstances;
@@ -1300,16 +1335,28 @@ async function getFlowTriggerInfo(
1300
1335
  var parsed = typeof payload === 'string' ? JSON.parse(payload) : payload;
1301
1336
  debug.version_payload_keys = Object.keys(parsed);
1302
1337
  var trigInst = parsed.triggerInstances || parsed.trigger_instances || [];
1338
+ debug.version_trigger_count = Array.isArray(trigInst) ? trigInst.length : typeof trigInst;
1303
1339
  if (Array.isArray(trigInst) && trigInst.length > 0) {
1304
- if (!triggerName) triggerName = trigInst[0].name || trigInst[0].triggerName || '';
1305
- if (!table && trigInst[0].inputs) {
1306
- for (var vi = 0; vi < trigInst[0].inputs.length; vi++) {
1307
- if (trigInst[0].inputs[vi].name === 'table') {
1308
- table = trigInst[0].inputs[vi].value?.value || '';
1340
+ var t0 = trigInst[0];
1341
+ debug.version_trigger_keys = Object.keys(t0);
1342
+ debug.version_trigger_name_field = t0.name || t0.triggerName || t0.triggerDefinitionName || '';
1343
+ if (!triggerName) triggerName = t0.name || t0.triggerName || t0.triggerDefinitionName || '';
1344
+ // Also try getting the trigger definition name (e.g. "Created or Updated")
1345
+ if (!triggerName && t0.triggerDefinition) {
1346
+ triggerName = t0.triggerDefinition.name || '';
1347
+ }
1348
+ if (!table && t0.inputs) {
1349
+ var t0Inputs = Array.isArray(t0.inputs) ? t0.inputs : [];
1350
+ for (var vi = 0; vi < t0Inputs.length; vi++) {
1351
+ if (t0Inputs[vi].name === 'table') {
1352
+ table = t0Inputs[vi].value?.value || str(t0Inputs[vi].value) || '';
1353
+ debug.version_table_input = t0Inputs[vi];
1309
1354
  break;
1310
1355
  }
1311
1356
  }
1312
1357
  }
1358
+ // Fallback: try reading table directly from trigger object
1359
+ if (!table && t0.table) table = str(t0.table);
1313
1360
  }
1314
1361
  }
1315
1362
  } catch (_) {}
@@ -1494,14 +1541,20 @@ function transformConditionToDataPills(conditionValue: string, dataPillBase: str
1494
1541
  }
1495
1542
 
1496
1543
  /**
1497
- * Build labelCache entries for field-level data pills used in flow logic conditions.
1544
+ * Build labelCache INSERT entries for field-level data pills used in flow logic conditions.
1498
1545
  *
1499
- * Returns { inserts, updates }:
1500
- * - inserts: record-level pill with full metadata (processflow XML format)
1501
- * - updates: field-level pills with minimal fields (name + usedInstances, matching UI mutation)
1546
+ * Returns an array of labelCache INSERT entries with full metadata, matching the UI's exact
1547
+ * mutation format (captured from Flow Designer network tab when editing a programmatic flow):
1502
1548
  *
1503
- * The UI uses labelCache.update for field-level condition pills (captured from network tab).
1504
- * The record-level pill is INSERTed to ensure it exists when triggers are created programmatically.
1549
+ * labelCache: { insert: [{
1550
+ * name: "Created or Updated_1.current.category",
1551
+ * label: "Trigger - Record Created or Updated➛Incident Record➛Category",
1552
+ * reference: "", reference_display: "Category",
1553
+ * type: "choice", base_type: "choice",
1554
+ * parent_table_name: "incident", column_name: "category",
1555
+ * usedInstances: [{uiUniqueIdentifier: "...", inputName: "condition"}],
1556
+ * choices: {}
1557
+ * }] }
1505
1558
  */
1506
1559
  async function buildConditionLabelCache(
1507
1560
  client: any,
@@ -1535,27 +1588,54 @@ async function buildConditionLabelCache(
1535
1588
  }
1536
1589
  if (uniqueFields.length === 0) return [];
1537
1590
 
1538
- // Build labelCache UPDATE entries for condition pills.
1539
- // Matches the UI's exact mutation format (from if-statement-update.txt):
1540
- // labelCache: { update: [{ name: "Created or Updated_1.current.category", usedInstances: [...] }] }
1541
- //
1542
- // The record-level pill (e.g. "Created or Updated_1.current") is NOT included here because:
1543
- // - The ServiceNow backend automatically creates it when the trigger is saved (INSERT → UPDATE)
1544
- // - Sending a duplicate labelCache.insert for it can cause the entire labelCache block to fail
1545
- // - The UI's condition mutation never sends a record-level pill insert (confirmed via network capture)
1546
- var updates: any[] = [];
1591
+ // Batch-query sys_dictionary for field metadata (type, label, reference)
1592
+ var fieldMeta: Record<string, { type: string; label: string; reference: string }> = {};
1593
+ try {
1594
+ var dictResp = await client.get('/api/now/table/sys_dictionary', {
1595
+ params: {
1596
+ sysparm_query: 'name=' + table + '^elementIN' + uniqueFields.join(','),
1597
+ sysparm_fields: 'element,column_label,internal_type,reference',
1598
+ sysparm_display_value: 'false',
1599
+ sysparm_limit: uniqueFields.length + 5
1600
+ }
1601
+ });
1602
+ var dictResults = dictResp.data.result || [];
1603
+ for (var d = 0; d < dictResults.length; d++) {
1604
+ var rec = dictResults[d];
1605
+ var elName = str(rec.element);
1606
+ var intType = str(rec.internal_type?.value || rec.internal_type || 'string');
1607
+ var colLabel = str(rec.column_label);
1608
+ var refTable = str(rec.reference?.value || rec.reference || '');
1609
+ if (elName) fieldMeta[elName] = { type: intType, label: colLabel, reference: refTable };
1610
+ }
1611
+ } catch (_) {
1612
+ // Fallback: use "string" type and generated labels if dictionary lookup fails
1613
+ }
1614
+
1615
+ // Build labelCache INSERT entries with full metadata for each field-level pill.
1616
+ // This matches the UI's mutation format when editing a programmatically-created flow.
1617
+ var inserts: any[] = [];
1547
1618
 
1548
1619
  for (var j = 0; j < uniqueFields.length; j++) {
1549
1620
  var f = uniqueFields[j];
1550
1621
  var pillName = dataPillBase + '.' + f;
1622
+ var meta = fieldMeta[f] || { type: 'string', label: f.replace(/_/g, ' ').replace(/\b\w/g, function (c: string) { return c.toUpperCase(); }), reference: '' };
1551
1623
 
1552
- updates.push({
1624
+ inserts.push({
1553
1625
  name: pillName,
1554
- usedInstances: [{ uiUniqueIdentifier: logicUiId, inputName: 'condition' }]
1626
+ label: 'Trigger - Record ' + triggerName + '\u279b' + tableLabel + ' Record\u279b' + meta.label,
1627
+ reference: meta.reference,
1628
+ reference_display: meta.label,
1629
+ type: meta.type,
1630
+ base_type: meta.type,
1631
+ parent_table_name: table,
1632
+ column_name: f,
1633
+ usedInstances: [{ uiUniqueIdentifier: logicUiId, inputName: 'condition' }],
1634
+ choices: {}
1555
1635
  });
1556
1636
  }
1557
1637
 
1558
- return updates;
1638
+ return inserts;
1559
1639
  }
1560
1640
 
1561
1641
  // ── DATA PILL SUPPORT FOR RECORD ACTIONS (Update/Create Record) ──────
@@ -1886,22 +1966,41 @@ async function addFlowLogicViaGraphQL(
1886
1966
  var needsConditionUpdate = false;
1887
1967
  var conditionTriggerInfo: any = null;
1888
1968
 
1889
- // Pre-process: detect JS-style dot notation conditions and convert to shorthand pill format.
1890
- // Users may write conditions like:
1891
- // "trigger.current.category = software" (single =)
1892
- // "trigger.current.category == 'software'" (JS equality)
1893
- // "current.priority = 1" (short prefix)
1894
- // These are converted to pill shorthand format that the existing logic can handle:
1895
- // "{{trigger.current.category}}=software"
1896
- var DOT_NOTATION_RE = /((?:trigger\.)?current)\.(\w+)\s*(===?|!==?|>=|<=|>|<|=)\s*(?:'([^']*)'|"([^"]*)"|(\S+))/g;
1897
- if (DOT_NOTATION_RE.test(rawCondition)) {
1969
+ // Pre-process: detect dot notation conditions and convert to shorthand pill format.
1970
+ // Supports both symbol operators and word operators:
1971
+ // "trigger.current.category = software" "{{trigger.current.category}}=software"
1972
+ // "trigger.current.category == 'software'" "{{trigger.current.category}}=software"
1973
+ // "trigger.current.category equals software" "{{trigger.current.category}}=software"
1974
+ // "trigger.current.priority != 1" → "{{trigger.current.priority}}!=1"
1975
+ // "current.active is true" → "{{current.active}}=true"
1976
+ var WORD_OP_MAP: Record<string, string> = {
1977
+ 'equals': '=', 'is': '=', 'eq': '=',
1978
+ 'not_equals': '!=', 'is not': '!=', 'neq': '!=', 'not equals': '!=',
1979
+ 'greater than': '>', 'gt': '>', 'less than': '<', 'lt': '<',
1980
+ 'greater or equals': '>=', 'gte': '>=', 'less or equals': '<=', 'lte': '<=',
1981
+ 'contains': 'LIKE', 'starts with': 'STARTSWITH', 'ends with': 'ENDSWITH',
1982
+ 'not contains': 'NOT LIKE', 'is empty': 'ISEMPTY', 'is not empty': 'ISNOTEMPTY'
1983
+ };
1984
+ // First replace word operators with symbols so the regex can match uniformly
1985
+ var dotOriginal = rawCondition;
1986
+ var dotProcessed = rawCondition;
1987
+ var WORD_OPS_SORTED = Object.keys(WORD_OP_MAP).sort(function (a, b) { return b.length - a.length; }); // longest first
1988
+ for (var wi = 0; wi < WORD_OPS_SORTED.length; wi++) {
1989
+ var wordOp = WORD_OPS_SORTED[wi];
1990
+ // Only replace word operators that appear between a dot-notation field and a value
1991
+ var wordRe = new RegExp('((?:trigger\\.)?current\\.\\w+)\\s+' + wordOp.replace(/ /g, '\\s+') + '\\s+', 'gi');
1992
+ dotProcessed = dotProcessed.replace(wordRe, function (m: string, prefix: string) {
1993
+ return prefix + WORD_OP_MAP[wordOp];
1994
+ });
1995
+ }
1996
+ var DOT_NOTATION_RE = /((?:trigger\.)?current)\.(\w+)(===?|!==?|>=|<=|>|<|=|LIKE|STARTSWITH|ENDSWITH|NOT LIKE|ISEMPTY|ISNOTEMPTY)\s*(?:'([^']*)'|"([^"]*)"|(\S*))/g;
1997
+ if (DOT_NOTATION_RE.test(dotProcessed)) {
1898
1998
  DOT_NOTATION_RE.lastIndex = 0;
1899
- var dotOriginal = rawCondition;
1900
- rawCondition = rawCondition.replace(DOT_NOTATION_RE, function (_m: string, prefix: string, field: string, op: string, qv1: string, qv2: string, uv: string) {
1999
+ rawCondition = dotProcessed.replace(DOT_NOTATION_RE, function (_m: string, prefix: string, field: string, op: string, qv1: string, qv2: string, uv: string) {
1901
2000
  var snOp = op;
1902
2001
  if (op === '==' || op === '===') snOp = '=';
1903
2002
  else if (op === '!=' || op === '!==') snOp = '!=';
1904
- var val = qv1 !== undefined ? qv1 : (qv2 !== undefined ? qv2 : uv);
2003
+ var val = qv1 !== undefined ? qv1 : (qv2 !== undefined ? qv2 : (uv || ''));
1905
2004
  return '{{' + prefix + '.' + field + '}}' + snOp + val;
1906
2005
  });
1907
2006
  // Replace JS && with ServiceNow ^ (AND separator)
@@ -1909,6 +2008,36 @@ async function addFlowLogicViaGraphQL(
1909
2008
  steps.dot_notation_rewrite = { original: dotOriginal, rewritten: rawCondition };
1910
2009
  }
1911
2010
 
2011
+ // Pre-process: convert word operators in pill-format conditions
2012
+ // e.g. "{{trigger.current.category}} equals software" → "{{trigger.current.category}}=software"
2013
+ if (rawCondition.includes('{{')) {
2014
+ var PILL_WORD_OPS: [RegExp, string][] = [
2015
+ [/(\}\})\s+not\s+equals\s+/gi, '$1!='],
2016
+ [/(\}\})\s+is\s+not\s+/gi, '$1!='],
2017
+ [/(\}\})\s+equals\s+/gi, '$1='],
2018
+ [/(\}\})\s+is\s+/gi, '$1='],
2019
+ [/(\}\})\s+contains\s+/gi, '$1LIKE'],
2020
+ [/(\}\})\s+starts\s+with\s+/gi, '$1STARTSWITH'],
2021
+ [/(\}\})\s+ends\s+with\s+/gi, '$1ENDSWITH'],
2022
+ [/(\}\})\s+greater\s+than\s+/gi, '$1>'],
2023
+ [/(\}\})\s+less\s+than\s+/gi, '$1<'],
2024
+ // Symbol operators: == / === / != / !== / >= / <= / > / < with optional spaces
2025
+ [/(\}\})\s*===?\s*/g, '$1='],
2026
+ [/(\}\})\s*!==?\s*/g, '$1!='],
2027
+ [/(\}\})\s*>=\s*/g, '$1>='],
2028
+ [/(\}\})\s*<=\s*/g, '$1<='],
2029
+ [/(\}\})\s*>\s*/g, '$1>'],
2030
+ [/(\}\})\s*<\s*/g, '$1<'],
2031
+ ];
2032
+ var pillWordOriginal = rawCondition;
2033
+ for (var pwi = 0; pwi < PILL_WORD_OPS.length; pwi++) {
2034
+ rawCondition = rawCondition.replace(PILL_WORD_OPS[pwi][0], PILL_WORD_OPS[pwi][1]);
2035
+ }
2036
+ if (rawCondition !== pillWordOriginal) {
2037
+ steps.pill_word_op_rewrite = { original: pillWordOriginal, rewritten: rawCondition };
2038
+ }
2039
+ }
2040
+
1912
2041
  // Shorthand patterns that need rewriting to the real data pill base
1913
2042
  // e.g. {{trigger.current.category}} → {{Created or Updated_1.current.category}}
1914
2043
  var PILL_SHORTHANDS = ['trigger.current', 'current', 'trigger_record', 'trigger.record'];
@@ -2032,25 +2161,31 @@ async function addFlowLogicViaGraphQL(
2032
2161
  steps.label_cache = labelCacheResult.map(function (e: any) { return e.name; });
2033
2162
 
2034
2163
  try {
2035
- // Match the UI's exact format for condition UPDATE (from if-statement-update.txt):
2036
- // - labelCache.update: field-level pills with minimal name + usedInstances
2037
- // - NO labelCache.insert (record-level pill is auto-created by backend during trigger save)
2038
- // - condition input: only name + value (NO displayValue, NO flowLogicDefinition)
2164
+ // Match the UI's exact format for condition UPDATE (captured from clean Flow Designer network trace):
2165
+ // - labelCache.insert: field-level pills with FULL metadata (type, parent_table_name, etc.)
2166
+ // - Two inputs: condition_name (label) + condition (pill expression)
2167
+ // - No flowLogicDefinition, no displayValue on condition
2039
2168
  var updatePatch: any = {
2040
2169
  flowId: flowId,
2041
2170
  flowLogics: {
2042
2171
  update: [{
2043
2172
  uiUniqueIdentifier: returnedUuid,
2044
2173
  type: 'flowlogic',
2045
- inputs: [{
2046
- name: 'condition',
2047
- value: { schemaless: false, schemalessValue: '', value: transformedCondition }
2048
- }]
2174
+ inputs: [
2175
+ {
2176
+ name: 'condition_name',
2177
+ value: { schemaless: false, schemalessValue: '', value: 'label' }
2178
+ },
2179
+ {
2180
+ name: 'condition',
2181
+ value: { schemaless: false, schemalessValue: '', value: transformedCondition }
2182
+ }
2183
+ ]
2049
2184
  }]
2050
2185
  }
2051
2186
  };
2052
2187
  if (labelCacheResult.length > 0) {
2053
- updatePatch.labelCache = { update: labelCacheResult };
2188
+ updatePatch.labelCache = { insert: labelCacheResult };
2054
2189
  }
2055
2190
  // Log the exact GraphQL mutation for debugging
2056
2191
  steps.condition_update_mutation = jsToGraphQL(updatePatch);